From f4dd9004becefab6ebb48f8852394d88ae1d4741 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 25 Jul 2024 16:47:25 +0200 Subject: [PATCH] Implemented assert keyword --- README.md | 1 + src/build/ast/Assert.go | 10 ++++++++++ src/build/ast/Parse.go | 9 +++++++++ src/build/compiler/Result.go | 7 ++++++- src/build/core/Compile.go | 4 ++++ src/build/core/CompileASTNode.go | 3 +++ src/build/core/CompileAssert.go | 29 +++++++++++++++++++++++++++++ src/build/core/Defer.go | 6 ++++++ src/build/core/Function.go | 2 ++ src/build/errors/CompileErrors.go | 1 + src/build/token/Kind.go | 1 + src/build/token/Tokenize.go | 2 ++ tests/programs/assert.q | 6 ++++++ tests/programs_test.go | 1 + 14 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/build/ast/Assert.go create mode 100644 src/build/core/CompileAssert.go create mode 100644 src/build/core/Defer.go create mode 100644 tests/programs/assert.q diff --git a/README.md b/README.md index 988d4c8..d636314 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ This is what generates expressions from tokens. ### Keywords +- [x] `assert` - [ ] `for` - [x] `if` - [x] `import` diff --git a/src/build/ast/Assert.go b/src/build/ast/Assert.go new file mode 100644 index 0000000..0f537db --- /dev/null +++ b/src/build/ast/Assert.go @@ -0,0 +1,10 @@ +package ast + +import ( + "git.akyoto.dev/cli/q/src/build/expression" +) + +// Assert represents a condition that must be true, otherwise the program stops. +type Assert struct { + Condition *expression.Expression +} diff --git a/src/build/ast/Parse.go b/src/build/ast/Parse.go index 5610e1a..a7c0e0e 100644 --- a/src/build/ast/Parse.go +++ b/src/build/ast/Parse.go @@ -35,6 +35,15 @@ func toASTNode(tokens token.List, buffer []byte) (Node, error) { return &Return{Value: value}, nil } + if tokens[0].Kind == token.Assert { + if len(tokens) == 1 { + return nil, errors.New(errors.MissingExpression, nil, tokens[0].End()) + } + + condition := expression.Parse(tokens[1:]) + return &Assert{Condition: condition}, nil + } + if keywordHasBlock(tokens[0].Kind) { blockStart := tokens.IndexKind(token.BlockStart) blockEnd := tokens.LastIndexKind(token.BlockEnd) diff --git a/src/build/compiler/Result.go b/src/build/compiler/Result.go index 2762d93..ee7c8c1 100644 --- a/src/build/compiler/Result.go +++ b/src/build/compiler/Result.go @@ -26,7 +26,7 @@ func (r *Result) finalize() ([]byte, []byte) { // The reason we call `main` instead of using `main` itself is to place // a return address on the stack, which allows return statements in `main`. final := asm.Assembler{ - Instructions: make([]asm.Instruction, 0, r.InstructionCount+4), + Instructions: make([]asm.Instruction, 0, r.InstructionCount+8), Data: make(map[string][]byte, r.DataCount), } @@ -35,6 +35,11 @@ func (r *Result) finalize() ([]byte, []byte) { final.RegisterNumber(asm.MOVE, x64.SyscallRegisters[1], 0) final.Syscall() + final.Label(asm.LABEL, "_crash") + final.RegisterNumber(asm.MOVE, x64.SyscallRegisters[0], linux.Exit) + final.RegisterNumber(asm.MOVE, x64.SyscallRegisters[1], 1) + final.Syscall() + // This will place the main function immediately after the entry point // and also add everything the main function calls recursively. r.eachFunction(r.Main, map[*core.Function]bool{}, func(f *core.Function) { diff --git a/src/build/core/Compile.go b/src/build/core/Compile.go index 41c079d..4898f69 100644 --- a/src/build/core/Compile.go +++ b/src/build/core/Compile.go @@ -5,4 +5,8 @@ func (f *Function) Compile() { f.AddLabel(f.Name) f.Err = f.CompileTokens(f.Body) f.Return() + + for _, call := range f.deferred { + call() + } } diff --git a/src/build/core/CompileASTNode.go b/src/build/core/CompileASTNode.go index 34ce620..16cb0bd 100644 --- a/src/build/core/CompileASTNode.go +++ b/src/build/core/CompileASTNode.go @@ -7,6 +7,9 @@ import ( // CompileASTNode compiles a node in the AST. func (f *Function) CompileASTNode(node ast.Node) error { switch node := node.(type) { + case *ast.Assert: + return f.CompileAssert(node) + case *ast.Assign: return f.CompileAssign(node) diff --git a/src/build/core/CompileAssert.go b/src/build/core/CompileAssert.go new file mode 100644 index 0000000..2715e44 --- /dev/null +++ b/src/build/core/CompileAssert.go @@ -0,0 +1,29 @@ +package core + +import ( + "fmt" + + "git.akyoto.dev/cli/q/src/build/asm" + "git.akyoto.dev/cli/q/src/build/ast" +) + +// CompileAssert compiles an assertion. +func (f *Function) CompileAssert(assert *ast.Assert) error { + f.count.assert++ + success := fmt.Sprintf("%s_assert_%d_true", f.Name, f.count.assert) + fail := fmt.Sprintf("%s_assert_%d_false", f.Name, f.count.assert) + err := f.CompileCondition(assert.Condition, success, fail) + + if err != nil { + return err + } + + f.AddLabel(success) + + f.Defer(func() { + f.AddLabel(fail) + f.Jump(asm.JUMP, "_crash") + }) + + return err +} diff --git a/src/build/core/Defer.go b/src/build/core/Defer.go new file mode 100644 index 0000000..0313a31 --- /dev/null +++ b/src/build/core/Defer.go @@ -0,0 +1,6 @@ +package core + +// Defer executes the callback at the end of function compilation. +func (f *Function) Defer(call func()) { + f.deferred = append(f.deferred, call) +} diff --git a/src/build/core/Function.go b/src/build/core/Function.go index f926956..29fe007 100644 --- a/src/build/core/Function.go +++ b/src/build/core/Function.go @@ -14,11 +14,13 @@ type Function struct { Body []token.Token Functions map[string]*Function Err error + deferred []func() count counter } // counter stores how often a certain statement appeared so we can generate a unique label from it. type counter struct { + assert int branch int data int loop int diff --git a/src/build/errors/CompileErrors.go b/src/build/errors/CompileErrors.go index 3aab709..5f65804 100644 --- a/src/build/errors/CompileErrors.go +++ b/src/build/errors/CompileErrors.go @@ -5,6 +5,7 @@ var ( InvalidExpression = &Base{"Invalid expression"} InvalidRune = &Base{"Invalid rune"} InvalidStatement = &Base{"Invalid statement"} + MissingExpression = &Base{"Missing expression"} MissingMainFunction = &Base{"Missing main function"} MissingOperand = &Base{"Missing operand"} NotImplemented = &Base{"Not implemented"} diff --git a/src/build/token/Kind.go b/src/build/token/Kind.go index 53997c4..b9490db 100644 --- a/src/build/token/Kind.go +++ b/src/build/token/Kind.go @@ -20,6 +20,7 @@ const ( ArrayStart // [ ArrayEnd // ] _keywords // + Assert // assert If // if Import // import Loop // loop diff --git a/src/build/token/Tokenize.go b/src/build/token/Tokenize.go index a6a34fc..610a36f 100644 --- a/src/build/token/Tokenize.go +++ b/src/build/token/Tokenize.go @@ -95,6 +95,8 @@ func Tokenize(buffer []byte) List { kind := Identifier switch string(identifier) { + case "assert": + kind = Assert case "if": kind = If case "import": diff --git a/tests/programs/assert.q b/tests/programs/assert.q new file mode 100644 index 0000000..9c3123e --- /dev/null +++ b/tests/programs/assert.q @@ -0,0 +1,6 @@ +main() { + assert 1 != 0 + assert 1 == 0 || 1 != 0 + assert 1 != 0 && 2 != 0 + assert 1 == 0 +} \ No newline at end of file diff --git a/tests/programs_test.go b/tests/programs_test.go index 8e8bf54..ee99875 100644 --- a/tests/programs_test.go +++ b/tests/programs_test.go @@ -40,6 +40,7 @@ var programs = []struct { {"jump-near", "", "", 0}, {"loop", "", "", 0}, {"loop-lifetime", "", "", 0}, + {"assert", "", "", 1}, } func TestPrograms(t *testing.T) {