From 20a9ed1de0b4a482e19579beb1f28f10a18303b8 Mon Sep 17 00:00:00 2001 From: MineGame159 Date: Sun, 14 Jan 2024 18:40:57 +0100 Subject: [PATCH] CORE: Add Test attribute CLI: Add test command Tests: Add a tests project for testing language features --- cmd/cmd/test.go | 211 +++++++++++++++++++++++++++++++++++ cmd/lsp/annotator.go | 2 +- cmd/main.go | 1 + core/ast/helpers.go | 14 +++ core/checker/attributes.go | 13 +++ core/checker/declarations.go | 15 ++- core/checker/expressions.go | 7 ++ core/codegen/expressions.go | 2 +- core/codegen/statements.go | 7 +- core/scanner/token.go | 4 +- tests/project.toml | 3 + tests/src/arrays.fb | 26 +++++ tests/src/pointers.fb | 30 +++++ tests/src/statements.fb | 94 ++++++++++++++++ tests/src/structs.fb | 81 ++++++++++++++ 15 files changed, 501 insertions(+), 9 deletions(-) create mode 100644 cmd/cmd/test.go create mode 100644 tests/project.toml create mode 100644 tests/src/arrays.fb create mode 100644 tests/src/pointers.fb create mode 100644 tests/src/statements.fb create mode 100644 tests/src/structs.fb diff --git a/cmd/cmd/test.go b/cmd/cmd/test.go new file mode 100644 index 0000000..ea463a4 --- /dev/null +++ b/cmd/cmd/test.go @@ -0,0 +1,211 @@ +package cmd + +import ( + "bytes" + "errors" + "fireball/cmd/build" + "fireball/core/ast" + "fireball/core/ir" + "fireball/core/workspace" + "fmt" + "github.com/fatih/color" + "github.com/spf13/cobra" + "log" + "os/exec" + "strconv" + "strings" + "time" +) + +func GetTestCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "test", + Short: "Test project.", + Run: testCmd, + } + + return cmd +} + +func testCmd(_ *cobra.Command, _ []string) { + start := time.Now() + + // Create project + project, err := workspace.NewProject(".") + if err != nil { + log.Fatalln(err.Error()) + } + + // Load files + err = project.LoadFiles() + if err != nil { + log.Fatalln(err.Error()) + } + + // Report errors + build.Report(project) + + // Build + tests := getTests(project) + + output, err := build.Build(project, generateTestsEntrypoint(tests), 0, fmt.Sprintf("%s_tests", project.Config.Name)) + if err != nil { + log.Fatalln(err.Error()) + } + + // Run + failed, err := runTests(output, tests) + if err != nil { + log.Fatalln(err.Error()) + } + + // Print info + took := time.Now().Sub(start) + + namespaceStyle := color.New(color.FgWhite) + testStyle := color.New(color.Underline) + + for _, test := range failed { + file := ast.GetParent[*ast.File](test) + + for _, part := range file.Namespace.Name.Parts { + _, _ = namespaceStyle.Printf("%s.", part) + } + + if s := test.Method(); s != nil { + _, _ = namespaceStyle.Printf("%s.", s.Name) + } + + _, _ = testStyle.Print(test.TestName()) + + color.Red(" failed") + fmt.Println() + } + + if len(failed) == 0 { + _, _ = color.New(color.FgGreen).Printf("%d tests succeeded", len(tests)) + } else { + _, _ = color.New(color.FgYellow).Printf("%d out of %d tests failed", len(failed), len(tests)) + } + + fmt.Printf(", took %s\n", took) +} + +// Run tests + +func runTests(path string, tests []*ast.Func) ([]*ast.Func, error) { + // Run + cmd := exec.Command(path) + + stdout := bytes.Buffer{} + cmd.Stdout = &stdout + + err := cmd.Run() + if err != nil || !cmd.ProcessState.Success() { + return nil, errors.New("failed to run: " + path) + } + + // Parse output + var failed []*ast.Func + + for _, line := range strings.Split(stdout.String(), "\n") { + index, err := strconv.ParseInt(line, 10, 32) + if err != nil { + continue + } + + failed = append(failed, tests[index]) + } + + return failed, nil +} + +// Entrypoint generation + +func generateTestsEntrypoint(tests []*ast.Func) *ir.Module { + m := &ir.Module{Path: "__entrypoint"} + + // Run + printf := m.Declare("printf", &ir.FuncType{ + Params: []*ir.Param{{ + Typ: &ir.PointerType{Pointee: ir.I8}, + Name_: "format", + }}, + Variadic: true, + }) + + testType := &ir.FuncType{Returns: ir.I1} + + testParam := &ir.Param{Typ: testType, Name_: "test"} + testIndexParam := &ir.Param{Typ: ir.I32, Name_: "index"} + + run := m.Define("__run_test", &ir.FuncType{Params: []*ir.Param{testParam, testIndexParam}}, 0) + + runBlock := run.Block("entry") + failedBlock := run.Block("failed") + exitBlock := run.Block("exit") + + successful := runBlock.Add(&ir.CallInst{Callee: testParam}) + runBlock.Add(&ir.BrInst{Condition: successful, True: exitBlock, False: failedBlock}) + + failedMsg := m.Constant("failed", &ir.StringConst{Length: 4, Value: []byte("%d\n\000")}) + + failedBlock.Add(&ir.CallInst{ + Callee: printf, + Args: []ir.Value{ + failedMsg, + testIndexParam, + }, + }) + failedBlock.Add(&ir.RetInst{}) + + exitBlock.Add(&ir.RetInst{}) + + // Main + main := m.Define("main", &ir.FuncType{Returns: ir.I32}, 0) + mainBlock := main.Block("entry") + + for i, test := range tests { + mainBlock.Add(&ir.CallInst{ + Callee: run, + Args: []ir.Value{ + m.Declare(test.MangledName(), testType), + &ir.IntConst{Typ: ir.I32, Value: ir.Unsigned(uint64(i))}, + }, + }) + } + + mainBlock.Add(&ir.RetInst{Value: &ir.IntConst{Typ: ir.I32, Value: ir.Unsigned(0)}}) + + return m +} + +// Test collection + +func getTests(project *workspace.Project) []*ast.Func { + collector := testCollector{} + + for _, file := range project.Files { + collector.VisitNode(file.Ast) + } + + return collector.tests +} + +type testCollector struct { + tests []*ast.Func +} + +func (t *testCollector) VisitNode(node ast.Node) { + switch node := node.(type) { + case *ast.Func: + name := node.TestName() + + if name != "" { + t.tests = append(t.tests, node) + } + + case ast.Decl, *ast.File: + node.AcceptChildren(t) + } +} diff --git a/cmd/lsp/annotator.go b/cmd/lsp/annotator.go index b03d4ff..99e1b59 100644 --- a/cmd/lsp/annotator.go +++ b/cmd/lsp/annotator.go @@ -19,7 +19,7 @@ func annotate(node ast.Node) []protocol.InlayHint { } for _, decl := range node.(*ast.File).Decls { - if f, ok := decl.(*ast.Func); ok { + if f, ok := decl.(*ast.Func); ok && f.Name != nil { a.functions[f.Name.String()] = f } } diff --git a/cmd/main.go b/cmd/main.go index 09ea4ec..27bfde2 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,6 +17,7 @@ func main() { root.AddCommand( cmd.GetBuildCmd(), cmd.GetRunCmd(), + cmd.GetTestCmd(), cmd.GetInitCommand(), lsp.GetCmd(), ) diff --git a/core/ast/helpers.go b/core/ast/helpers.go index a745d80..acbc3d9 100644 --- a/core/ast/helpers.go +++ b/core/ast/helpers.go @@ -165,6 +165,20 @@ func (f *Func) IntrinsicName() string { return "" } +func (f *Func) TestName() string { + for _, attribute := range f.Attributes { + if attribute.Name.String() == "Test" { + if len(attribute.Args) > 0 { + return attribute.Args[0].String()[1 : len(attribute.Args[0].String())-1] + } + + return f.Name.String() + } + } + + return "" +} + func (f *Func) HasBody() bool { for _, attribute := range f.Attributes { if attribute.Name.String() == "Extern" || attribute.Name.String() == "Intrinsic" { diff --git a/core/checker/attributes.go b/core/checker/attributes.go index 1a2af46..0479b8f 100644 --- a/core/checker/attributes.go +++ b/core/checker/attributes.go @@ -25,6 +25,19 @@ func (c *checker) visitAttribute(decl *ast.Func, attribute *ast.Attribute) { c.error(attribute.Name, "Inline doesn't have any arguments") } + case "Test": + if len(attribute.Args) > 1 { + c.error(attribute.Name, "Test attribute can only have one argument") + } + + if !ast.IsPrimitive(decl.Returns, ast.Bool) { + c.error(attribute.Name, "Tests need to return a boolean") + } + + if len(decl.Params) != 0 { + c.error(attribute.Name, "Tests can't have any parameters") + } + default: c.error(attribute.Name, "Attribute with this name doesn't exist") } diff --git a/core/checker/declarations.go b/core/checker/declarations.go index a109130..514e38b 100644 --- a/core/checker/declarations.go +++ b/core/checker/declarations.go @@ -185,12 +185,16 @@ func (c *checker) VisitFunc(decl *ast.Func) { isExtern := false isIntrinsic := false + isTest := false for _, attribute := range decl.Attributes { - if attribute.Name.String() == "Extern" { + switch attribute.Name.String() { + case "Extern": isExtern = true - } else if attribute.Name.String() == "Intrinsic" { + case "Intrinsic": isIntrinsic = true + case "Test": + isTest = true } } @@ -200,11 +204,18 @@ func (c *checker) VisitFunc(decl *ast.Func) { if isImpl && isIntrinsic && !decl.IsStatic() { c.error(decl.Name, "Non static methods can't be intrinsics") } + if isImpl && isTest && !decl.IsStatic() { + c.error(decl.Name, "Non static methods can't be a test") + } if decl.IsVariadic() && !isExtern { c.error(decl.Name, "Only extern functions can be variadic") } + if (isExtern && isIntrinsic) || (isExtern && isTest) || (isIntrinsic && isTest) { + c.error(decl.Name, "Invalid combination of attributes") + } + // Push scope c.function = decl c.pushScope() diff --git a/core/checker/expressions.go b/core/checker/expressions.go index 2987814..7fdd2c2 100644 --- a/core/checker/expressions.go +++ b/core/checker/expressions.go @@ -329,6 +329,13 @@ func (c *checker) VisitUnary(expr *ast.Unary) { expr.Result().SetValue(result.Type, 0, nil) case scanner.FuncPtr: + if result.Kind != ast.CallableResultKind { + c.error(expr.Value, "Invalid value") + expr.Result().SetInvalid() + + return + } + if f, ok := result.Callable().(*ast.Func); ok && f.Method() != nil { c.error(expr.Value, "Cannot take address of a non-static method") } diff --git a/core/codegen/expressions.go b/core/codegen/expressions.go index 2aba2d3..b672e13 100644 --- a/core/codegen/expressions.go +++ b/core/codegen/expressions.go @@ -435,7 +435,7 @@ func (c *codegen) VisitLogical(expr *ast.Logical) { startBlock := c.block c.setLocationMeta( - c.block.Add(&ir.BrInst{Condition: left.v, True: end, False: end}), + c.block.Add(&ir.BrInst{Condition: left.v, True: true_, False: end}), expr, ) diff --git a/core/codegen/statements.go b/core/codegen/statements.go index 9989a7e..ce67666 100644 --- a/core/codegen/statements.go +++ b/core/codegen/statements.go @@ -133,10 +133,11 @@ func (c *codegen) VisitFor(stmt *ast.For) { c.beginBlock(body) } - c.acceptStmt(stmt.Body) - c.acceptExpr(stmt.Increment) + if c.acceptStmt(stmt.Body) { + c.acceptExpr(stmt.Increment) - c.block.Add(&ir.BrInst{True: c.loopStart}) + c.block.Add(&ir.BrInst{True: c.loopStart}) + } // End c.scopes.pop() diff --git a/core/scanner/token.go b/core/scanner/token.go index 1978ca1..6ad8769 100644 --- a/core/scanner/token.go +++ b/core/scanner/token.go @@ -56,12 +56,12 @@ const ( FuncPtr Hashtag DotDotDot + And + Or Nil True False - And - Or Var If Else diff --git a/tests/project.toml b/tests/project.toml new file mode 100644 index 0000000..f18fb37 --- /dev/null +++ b/tests/project.toml @@ -0,0 +1,3 @@ +Name = "Tests" +Src = "src" +Namespace = "Tests" \ No newline at end of file diff --git a/tests/src/arrays.fb b/tests/src/arrays.fb new file mode 100644 index 0000000..09a588a --- /dev/null +++ b/tests/src/arrays.fb @@ -0,0 +1,26 @@ +namespace Tests.Arrays; + +#[Test] +func index() bool { + var a = [ 5, 9 ]; + + return a[0] == 5; +} + +#[Test] +func assign() bool { + var a [3]i32; + a[1] = 2; + + return a[1] == 2; +} + +#[Test] +func pointer() bool { + var a = [ 5, 9 ]; + + var ptr = &a[1]; + *ptr = 12; + + return a[1] == 12; +} diff --git a/tests/src/pointers.fb b/tests/src/pointers.fb new file mode 100644 index 0000000..f54f172 --- /dev/null +++ b/tests/src/pointers.fb @@ -0,0 +1,30 @@ +namespace Tests.Pointers; + +#[Test] +func dereference() bool { + var a = 5; + var ptr = &a; + + return *ptr == 5; +} + +#[Test] +func modify() bool { + var a = 5; + + var ptr = &a; + *ptr = 9; + + return a == 9; +} + +func get() i32 { + return 5; +} + +#[Test] +func call() bool { + var ptr = => get; + + return ptr() == 5; +} diff --git a/tests/src/statements.fb b/tests/src/statements.fb new file mode 100644 index 0000000..95b8c08 --- /dev/null +++ b/tests/src/statements.fb @@ -0,0 +1,94 @@ +namespace Tests.Statements; + +#[Test] +func variableShadowing() bool { + var a = 3; + + { + var a = 6; + a = 9; + + if (a != 9) + return false; + } + + return a == 3; +} + +#[Test("if")] +func _if() bool { + if (true) + return true; + + return false; +} + +#[Test("else")] +func _else() bool { + if (false) + return false; + else + return true; + + return false; +} + +#[Test("while")] +func _while() bool { + var a = 0; + + while (a < 5) + a++; + + return a == 5; +} + +#[Test("for-1")] +func _for1() bool { + var a = 0; + + for (var i = 0; i < 4; i++) + a++; + + return a == 4; +} + +#[Test("for-2")] +func _for2() bool { + var a = 0; + + for (var i = 0; i < 4;) { + i++; + a++; + } + + return a == 4; +} + +#[Test("for-3")] +func _for3() bool { + var a = 0; + + for (var _i = 0;;) { + a++; + + if (a == 4) + break; + } + + return a == 4; +} + +#[Test("for-4")] +func _for4() bool { + var a = 0; + + for (;;) { + a++; + + if (a == 4) + break; + } + + return a == 4; +} diff --git a/tests/src/structs.fb b/tests/src/structs.fb new file mode 100644 index 0000000..0329ce5 --- /dev/null +++ b/tests/src/structs.fb @@ -0,0 +1,81 @@ +namespace Tests.Structs; + +struct Vec2 { + static number i32, + + x i32, + y i32, +} + +impl Vec2 { + static func something() i32 { + return 13; + } + + func setX(x i32) { + this.x = x; + } +} + +#[Test] +func initializer() bool { + var a = Vec2 { x: 2, y: 3 }; + + return a.x == 2 && a.y == 3; +} + +#[Test] +func assign() bool { + var a Vec2; + a.x = 9; + + return a.x == 9; +} + +#[Test] +func pointer() bool { + var a Vec2; + + var ptr = &a.y; + *ptr = 4; + + return a.y == 4; +} + +#[Test("sizeof")] +func _sizeof() bool { + return sizeof(Vec2) == 8; +} + +#[Test("alignof")] +func _alignof() bool { + return alignof(Vec2) == 4; +} + +#[Test] +func staticField() bool { + Vec2.number = 8; + + return Vec2.number == 8; +} + +#[Test] +func staticFieldPointer() bool { + var ptr = &Vec2.number; + *ptr = 7; + + return Vec2.number == 7; +} + +#[Test] +func method() bool { + var a Vec2; + a.setX(1); + + return a.x == 1; +} + +#[Test] +func staticMethod() bool { + return Vec2.something() == 13; +}