From feebfe65bb4180ee4aeef81364291df0acd9d13c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 3 Jul 2024 11:39:24 +0200 Subject: [PATCH] Refactored code structure --- README.md | 18 ++- examples/hello/hello.q | 7 +- examples/write/write.q | 2 +- src/build/Assignment.go | 19 --- src/build/Build.go | 29 +++-- src/build/ExpressionToRegister.go | 48 ------- src/build/Instruction.go | 1 - src/build/Write.go | 29 ----- src/build/arch/x64/Registers.go | 2 +- src/build/asm/Assembler.go | 3 + src/build/asm/Instructions.go | 10 ++ src/build/asm/Mnemonic.go | 18 +-- src/build/ast/AST.go | 30 ++++- src/build/ast/Parse.go | 4 +- src/build/{Build_test.go => build_test.go} | 15 ++- src/build/compiler.go | 81 ------------ src/build/config/config.go | 3 +- src/build/{compile.go => core/Compile.go} | 31 ++--- src/build/core/CompileAllFunctions.go | 19 +++ src/build/core/CompileAssign.go | 19 +++ src/build/{Call.go => core/CompileCall.go} | 43 +----- .../CompileDefinition.go} | 53 +++++--- src/build/{Loop.go => core/CompileLoop.go} | 2 +- .../{Return.go => core/CompileReturn.go} | 4 +- src/build/core/Evaluate.go | 123 ++++++++++++++++++ src/build/{ => core}/Execute.go | 17 +-- src/build/{ => core}/Function.go | 61 +++++---- src/build/{ => core}/Result.go | 54 ++++++-- src/build/{ => core}/SaveRegister.go | 15 +-- src/build/{ => core}/TokenToRegister.go | 11 +- src/build/{ => core}/Variable.go | 3 +- src/build/{Math.go => core/opRegister.go} | 12 +- src/build/core/state.go | 58 +++++++++ src/build/cpu/CPU.go | 11 ++ src/{ => build}/errors/Base.go | 0 src/{ => build}/errors/CompileErrors.go | 0 src/{ => build}/errors/Error.go | 0 src/{ => build}/errors/InvalidCharacter.go | 0 src/{ => build}/errors/InvalidInstruction.go | 0 src/{ => build}/errors/InvalidOperator.go | 0 .../errors/KeywordNotImplemented.go | 0 src/{ => build}/errors/ScanErrors.go | 0 src/{ => build}/errors/Stack.go | 0 src/{ => build}/errors/UnknownIdentifier.go | 0 src/{ => build}/errors/UnusedVariable.go | 0 .../errors/VariableAlreadyExists.go | 0 src/build/{scan.go => scanner/Scan.go} | 78 +++++------ src/cli/Build.go | 30 +++-- src/cli/Help.go | 3 +- bench_test.go => tests/benchmarks_test.go | 6 +- errors_test.go => tests/errors_test.go | 6 +- tests/examples_test.go | 23 ++++ tests/programs/successive-calls.q | 7 + examples_test.go => tests/programs_test.go | 25 ++-- 54 files changed, 583 insertions(+), 450 deletions(-) delete mode 100644 src/build/Assignment.go delete mode 100644 src/build/ExpressionToRegister.go delete mode 100644 src/build/Instruction.go delete mode 100644 src/build/Write.go rename src/build/{Build_test.go => build_test.go} (53%) delete mode 100644 src/build/compiler.go rename src/build/{compile.go => core/Compile.go} (59%) create mode 100644 src/build/core/CompileAllFunctions.go create mode 100644 src/build/core/CompileAssign.go rename src/build/{Call.go => core/CompileCall.go} (51%) rename src/build/{VariableDefinition.go => core/CompileDefinition.go} (64%) rename src/build/{Loop.go => core/CompileLoop.go} (95%) rename src/build/{Return.go => core/CompileReturn.go} (75%) create mode 100644 src/build/core/Evaluate.go rename src/build/{ => core}/Execute.go (75%) rename src/build/{ => core}/Function.go (57%) rename src/build/{ => core}/Result.go (59%) rename src/build/{ => core}/SaveRegister.go (69%) rename src/build/{ => core}/TokenToRegister.go (81%) rename src/build/{ => core}/Variable.go (88%) rename src/build/{Math.go => core/opRegister.go} (74%) create mode 100644 src/build/core/state.go rename src/{ => build}/errors/Base.go (100%) rename src/{ => build}/errors/CompileErrors.go (100%) rename src/{ => build}/errors/Error.go (100%) rename src/{ => build}/errors/InvalidCharacter.go (100%) rename src/{ => build}/errors/InvalidInstruction.go (100%) rename src/{ => build}/errors/InvalidOperator.go (100%) rename src/{ => build}/errors/KeywordNotImplemented.go (100%) rename src/{ => build}/errors/ScanErrors.go (100%) rename src/{ => build}/errors/Stack.go (100%) rename src/{ => build}/errors/UnknownIdentifier.go (100%) rename src/{ => build}/errors/UnusedVariable.go (100%) rename src/{ => build}/errors/VariableAlreadyExists.go (100%) rename src/build/{scan.go => scanner/Scan.go} (75%) rename bench_test.go => tests/benchmarks_test.go (72%) rename errors_test.go => tests/errors_test.go (93%) create mode 100644 tests/examples_test.go create mode 100644 tests/programs/successive-calls.q rename examples_test.go => tests/programs_test.go (60%) diff --git a/README.md b/README.md index e5e860f..866b982 100644 --- a/README.md +++ b/README.md @@ -83,22 +83,30 @@ Each function will then be translated to generic assembler instructions. All the functions that are required to run the program will be added to the final assembler. The final assembler resolves label addresses, optimizes the performance and generates the specific x86-64 machine code from the generic instruction set. -### [src/build/Function.go](src/build/Function.go) +### [src/build/core/Function.go](src/build/core/Function.go) This is the "heart" of the compiler. -Each function runs `f.Compile()` which organizes the source code into instructions that are then compiled via `f.CompileInstruction`. -You can think of instructions as the individual lines in your source code, but instructions can also span over multiple lines. +Each function runs `f.Compile` which organizes the source code into an abstract syntax tree that is then compiled via `f.CompileAST`. +You can think of AST nodes as the individual statements in your source code. + +### [src/build/ast/Parse.go](src/build/ast/Parse.go) + +This is what generates the AST from tokens. + +### [src/build/expression/Parse.go](src/build/expression/Parse.go) + +This is what generates expressions from tokens. ## Tests ```shell -go test -coverpkg=./... +go test ./... -v ``` ## Benchmarks ```shell -go test -bench=. -benchmem +go test ./tests -bench=. -benchmem ``` ## License diff --git a/examples/hello/hello.q b/examples/hello/hello.q index aace245..bffc930 100644 --- a/examples/hello/hello.q +++ b/examples/hello/hello.q @@ -1,8 +1,7 @@ main() { - x := f(2, 3) - syscall(60, x) + syscall(60, f(1) + f(2) + f(3)) } -f(x, y) { - return (x + y) * (x + y) +f(x) { + return x + 1 } \ No newline at end of file diff --git a/examples/write/write.q b/examples/write/write.q index c06884e..31b18d6 100644 --- a/examples/write/write.q +++ b/examples/write/write.q @@ -5,7 +5,7 @@ main() { } print(address, length) { - write(length-2, address, length) + write(1, address, length) } write(fd, address, length) { diff --git a/src/build/Assignment.go b/src/build/Assignment.go deleted file mode 100644 index 66703da..0000000 --- a/src/build/Assignment.go +++ /dev/null @@ -1,19 +0,0 @@ -package build - -import ( - "git.akyoto.dev/cli/q/src/build/expression" - "git.akyoto.dev/cli/q/src/errors" -) - -// CompileAssignment compiles an assignment. -func (f *Function) CompileAssignment(expr *expression.Expression) error { - name := expr.Children[0].Token.Text() - variable, exists := f.variables[name] - - if !exists { - return errors.New(&errors.UnknownIdentifier{Name: name}, f.File, expr.Children[0].Token.Position) - } - - defer f.useVariable(variable) - return f.Execute(expr.Token, variable.Register, expr.Children[1]) -} diff --git a/src/build/Build.go b/src/build/Build.go index 2821e2d..bf90d2c 100644 --- a/src/build/Build.go +++ b/src/build/Build.go @@ -3,6 +3,9 @@ package build import ( "path/filepath" "strings" + + "git.akyoto.dev/cli/q/src/build/core" + "git.akyoto.dev/cli/q/src/build/scanner" ) // Build describes a compiler build. @@ -18,18 +21,28 @@ func New(files ...string) *Build { } // Run parses the input files and generates an executable file. -func (build *Build) Run() (Result, error) { - functions, errors := scan(build.Files) - return compile(functions, errors) +func (build *Build) Run() (core.Result, error) { + functions, errors := scanner.Scan(build.Files) + return core.Compile(functions, errors) } // Executable returns the path to the executable. func (build *Build) Executable() string { - directory, _ := filepath.Abs(build.Files[0]) + path, _ := filepath.Abs(build.Files[0]) - if strings.HasSuffix(directory, ".q") { - directory = filepath.Dir(directory) + if strings.HasSuffix(path, ".q") { + return fromFileName(path) + } else { + return fromDirectoryName(path) } - - return filepath.Join(directory, filepath.Base(directory)) +} + +// fromDirectoryName returns the executable path based on the directory name. +func fromDirectoryName(path string) string { + return filepath.Join(path, filepath.Base(path)) +} + +// fromFileName returns the executable path based on the file name. +func fromFileName(path string) string { + return filepath.Join(filepath.Dir(path), strings.TrimSuffix(filepath.Base(path), ".q")) } diff --git a/src/build/ExpressionToRegister.go b/src/build/ExpressionToRegister.go deleted file mode 100644 index cf0a04c..0000000 --- a/src/build/ExpressionToRegister.go +++ /dev/null @@ -1,48 +0,0 @@ -package build - -import ( - "git.akyoto.dev/cli/q/src/build/asm" - "git.akyoto.dev/cli/q/src/build/ast" - "git.akyoto.dev/cli/q/src/build/config" - "git.akyoto.dev/cli/q/src/build/cpu" - "git.akyoto.dev/cli/q/src/build/expression" -) - -// ExpressionToRegister moves the result of an expression into the given register. -func (f *Function) ExpressionToRegister(root *expression.Expression, register cpu.Register) error { - if config.Verbose { - f.Logf("%s to register %s", root, register) - } - - operation := root.Token - - if root.IsLeaf() { - return f.TokenToRegister(operation, register) - } - - if ast.IsFunctionCall(root) { - err := f.CompileFunctionCall(root) - - if err != nil { - return err - } - - if register != f.cpu.Return[0] { - f.assembler.RegisterRegister(asm.MOVE, register, f.cpu.Return[0]) - } - - return nil - } - - left := root.Children[0] - right := root.Children[1] - - err := f.ExpressionToRegister(left, register) - - if err != nil { - return err - } - - f.SaveRegister(register) - return f.Execute(operation, register, right) -} diff --git a/src/build/Instruction.go b/src/build/Instruction.go deleted file mode 100644 index 5c93a93..0000000 --- a/src/build/Instruction.go +++ /dev/null @@ -1 +0,0 @@ -package build diff --git a/src/build/Write.go b/src/build/Write.go deleted file mode 100644 index f6385bb..0000000 --- a/src/build/Write.go +++ /dev/null @@ -1,29 +0,0 @@ -package build - -import ( - "bufio" - "os" - - "git.akyoto.dev/cli/q/src/build/elf" -) - -// Write writes the executable file to disk. -func Write(filePath string, code []byte, data []byte) error { - file, err := os.Create(filePath) - - if err != nil { - return err - } - - buffer := bufio.NewWriter(file) - executable := elf.New(code, data) - executable.Write(buffer) - buffer.Flush() - err = file.Close() - - if err != nil { - return err - } - - return os.Chmod(filePath, 0755) -} diff --git a/src/build/arch/x64/Registers.go b/src/build/arch/x64/Registers.go index 8104f84..fafea37 100644 --- a/src/build/arch/x64/Registers.go +++ b/src/build/arch/x64/Registers.go @@ -23,7 +23,7 @@ const ( var ( CallRegisters = SyscallRegisters - GeneralRegisters = []cpu.Register{RBX, RBP, R12, R13, R14, R15} + GeneralRegisters = []cpu.Register{RCX, RBX, RBP, R11, R12, R13, R14, R15} SyscallRegisters = []cpu.Register{RAX, RDI, RSI, RDX, R10, R8, R9} ReturnValueRegisters = []cpu.Register{RAX, RCX, R11} ) diff --git a/src/build/asm/Assembler.go b/src/build/asm/Assembler.go index 9c6f990..9a9f19e 100644 --- a/src/build/asm/Assembler.go +++ b/src/build/asm/Assembler.go @@ -63,6 +63,9 @@ func (a Assembler) Finalize() ([]byte, []byte) { }, }) + case COMMENT: + continue + case JUMP: code = x64.Jump8(code, 0x00) size := 1 diff --git a/src/build/asm/Instructions.go b/src/build/asm/Instructions.go index 4126bcf..6b547a2 100644 --- a/src/build/asm/Instructions.go +++ b/src/build/asm/Instructions.go @@ -44,6 +44,16 @@ func (a *Assembler) Label(name string) { }) } +// Comment adds a comment at the current position. +func (a *Assembler) Comment(text string) { + a.Instructions = append(a.Instructions, Instruction{ + Mnemonic: COMMENT, + Data: &Label{ + Name: text, + }, + }) +} + // Call calls a function whose position is identified by a label. func (a *Assembler) Call(name string) { a.Instructions = append(a.Instructions, Instruction{ diff --git a/src/build/asm/Mnemonic.go b/src/build/asm/Mnemonic.go index 77dbf81..315af19 100644 --- a/src/build/asm/Mnemonic.go +++ b/src/build/asm/Mnemonic.go @@ -6,6 +6,7 @@ const ( NONE Mnemonic = iota ADD CALL + COMMENT DIV JUMP MUL @@ -23,40 +24,31 @@ func (m Mnemonic) String() string { switch m { case ADD: return "add" - case CALL: return "call" - + case COMMENT: + return "comment" case DIV: return "div" - case JUMP: return "jump" - case LABEL: return "label" - case MOVE: return "move" - case MUL: return "mul" - case POP: return "pop" - case PUSH: return "push" - case RETURN: return "return" - case SUB: return "sub" - case SYSCALL: return "syscall" + default: + return "" } - - return "NONE" } diff --git a/src/build/ast/AST.go b/src/build/ast/AST.go index 2d8314a..d1b991b 100644 --- a/src/build/ast/AST.go +++ b/src/build/ast/AST.go @@ -1,36 +1,54 @@ package ast import ( + "fmt" + "git.akyoto.dev/cli/q/src/build/expression" "git.akyoto.dev/cli/q/src/build/token" ) -type Node interface{} +type Node fmt.Stringer type AST []Node type Assign struct { - Value *expression.Expression - Name token.Token + Value *expression.Expression + Name token.Token + Operator token.Token +} + +func (node *Assign) String() string { + return fmt.Sprintf("(= %s %s)", node.Name.Text(), node.Value) } type Call struct { Expression *expression.Expression } +func (node *Call) String() string { + return node.Expression.String() +} + type Define struct { Value *expression.Expression Name token.Token } -type If struct { - Condition *expression.Expression - Body AST +func (node *Define) String() string { + return fmt.Sprintf("(= %s %s)", node.Name.Text(), node.Value) } type Loop struct { Body AST } +func (node *Loop) String() string { + return fmt.Sprintf("(loop %s)", node.Body) +} + type Return struct { Value *expression.Expression } + +func (node *Return) String() string { + return fmt.Sprintf("(return %s)", node.Value) +} diff --git a/src/build/ast/Parse.go b/src/build/ast/Parse.go index b977704..69e6756 100644 --- a/src/build/ast/Parse.go +++ b/src/build/ast/Parse.go @@ -1,10 +1,10 @@ package ast import ( + "git.akyoto.dev/cli/q/src/build/errors" "git.akyoto.dev/cli/q/src/build/expression" "git.akyoto.dev/cli/q/src/build/keyword" "git.akyoto.dev/cli/q/src/build/token" - "git.akyoto.dev/cli/q/src/errors" ) // Parse generates an AST from a list of tokens. @@ -75,7 +75,7 @@ func toASTNode(tokens token.List) (Node, error) { name := expr.Children[0].Token value := expr.Children[1] - return &Assign{Name: name, Value: value}, nil + return &Assign{Name: name, Value: value, Operator: expr.Token}, nil case IsFunctionCall(expr): return &Call{Expression: expr}, nil diff --git a/src/build/Build_test.go b/src/build/build_test.go similarity index 53% rename from src/build/Build_test.go rename to src/build/build_test.go index 88d8e52..36242d8 100644 --- a/src/build/Build_test.go +++ b/src/build/build_test.go @@ -8,17 +8,28 @@ import ( "git.akyoto.dev/go/assert" ) -func TestBuild(t *testing.T) { +func TestBuildDirectory(t *testing.T) { b := build.New("../../examples/hello") _, err := b.Run() assert.Nil(t, err) } -func TestExecutable(t *testing.T) { +func TestBuildFile(t *testing.T) { + b := build.New("../../examples/hello/hello.q") + _, err := b.Run() + assert.Nil(t, err) +} + +func TestExecutableFromDirectory(t *testing.T) { b := build.New("../../examples/hello") assert.Equal(t, filepath.Base(b.Executable()), "hello") } +func TestExecutableFromFile(t *testing.T) { + b := build.New("../../examples/hello/hello.q") + assert.Equal(t, filepath.Base(b.Executable()), "hello") +} + func TestNonExisting(t *testing.T) { b := build.New("does-not-exist") _, err := b.Run() diff --git a/src/build/compiler.go b/src/build/compiler.go deleted file mode 100644 index ac28b37..0000000 --- a/src/build/compiler.go +++ /dev/null @@ -1,81 +0,0 @@ -package build - -import ( - "fmt" - - "git.akyoto.dev/cli/q/src/build/asm" - "git.akyoto.dev/cli/q/src/build/ast" - "git.akyoto.dev/cli/q/src/build/cpu" - "git.akyoto.dev/go/color/ansi" -) - -// compiler is the data structure we embed in each function to preserve compilation state. -type compiler struct { - err error - definitions map[string]*Definition - variables map[string]*Variable - functions map[string]*Function - finished chan struct{} - assembler asm.Assembler - debug []debug - cpu cpu.CPU - count counter - sideEffects int -} - -// counter stores how often a certain statement appeared so we can generate a unique label from it. -type counter struct { - loop int -} - -// debug is used to look up the source code at the given position. -type debug struct { - source ast.Node - position int -} - -// PrintInstructions shows the assembly instructions. -func (c *compiler) PrintInstructions() { - ansi.Dim.Println("╭──────────────────────────────────────╮") - - for i, x := range c.assembler.Instructions { - instruction := c.sourceAt(i) - - if instruction != nil { - ansi.Dim.Println("├──────────────────────────────────────┤") - } - - ansi.Dim.Print("│ ") - - if x.Mnemonic == asm.LABEL { - ansi.Yellow.Printf("%-36s", x.Data.String()+":") - } else { - ansi.Green.Printf("%-8s", x.Mnemonic.String()) - - if x.Data != nil { - fmt.Printf("%-28s", x.Data.String()) - } else { - fmt.Printf("%-28s", "") - } - } - - ansi.Dim.Print(" │\n") - } - - ansi.Dim.Println("╰──────────────────────────────────────╯") -} - -// sourceAt retrieves the source code at the given position or `nil`. -func (c *compiler) sourceAt(position int) ast.Node { - for _, record := range c.debug { - if record.position == position { - return record.source - } - - if record.position > position { - return nil - } - } - - return nil -} diff --git a/src/build/config/config.go b/src/build/config/config.go index d87cf95..4f97a22 100644 --- a/src/build/config/config.go +++ b/src/build/config/config.go @@ -9,5 +9,6 @@ const ( var ( Assembler = false - Verbose = false + Comments = false + Dry = false ) diff --git a/src/build/compile.go b/src/build/core/Compile.go similarity index 59% rename from src/build/compile.go rename to src/build/core/Compile.go index 0430873..edd8712 100644 --- a/src/build/compile.go +++ b/src/build/core/Compile.go @@ -1,13 +1,11 @@ -package build +package core import ( - "sync" - - "git.akyoto.dev/cli/q/src/errors" + "git.akyoto.dev/cli/q/src/build/errors" ) -// compile waits for the scan to finish and compiles all functions. -func compile(functions <-chan *Function, errs <-chan error) (Result, error) { +// Compile waits for the scan to finish and compiles all functions. +func Compile(functions <-chan *Function, errs <-chan error) (Result, error) { result := Result{} allFunctions := map[string]*Function{} @@ -32,8 +30,10 @@ func compile(functions <-chan *Function, errs <-chan error) (Result, error) { } } - compileFunctions(allFunctions) + // Start parallel compilation + CompileAllFunctions(allFunctions) + // Report errors if any occurred for _, function := range allFunctions { if function.err != nil { return result, function.err @@ -42,6 +42,7 @@ func compile(functions <-chan *Function, errs <-chan error) (Result, error) { result.InstructionCount += len(function.assembler.Instructions) } + // Check for existence of `main` main, exists := allFunctions["main"] if !exists { @@ -52,19 +53,3 @@ func compile(functions <-chan *Function, errs <-chan error) (Result, error) { result.Functions = allFunctions return result, nil } - -// compileFunctions starts a goroutine for each function compilation and waits for completion. -func compileFunctions(functions map[string]*Function) { - wg := sync.WaitGroup{} - - for _, function := range functions { - wg.Add(1) - - go func() { - defer wg.Done() - function.Compile() - }() - } - - wg.Wait() -} diff --git a/src/build/core/CompileAllFunctions.go b/src/build/core/CompileAllFunctions.go new file mode 100644 index 0000000..66d3ef9 --- /dev/null +++ b/src/build/core/CompileAllFunctions.go @@ -0,0 +1,19 @@ +package core + +import "sync" + +// CompileAllFunctions starts a goroutine for each function compilation and waits for completion. +func CompileAllFunctions(functions map[string]*Function) { + wg := sync.WaitGroup{} + + for _, function := range functions { + wg.Add(1) + + go func() { + defer wg.Done() + function.Compile() + }() + } + + wg.Wait() +} diff --git a/src/build/core/CompileAssign.go b/src/build/core/CompileAssign.go new file mode 100644 index 0000000..ce46a34 --- /dev/null +++ b/src/build/core/CompileAssign.go @@ -0,0 +1,19 @@ +package core + +import ( + "git.akyoto.dev/cli/q/src/build/ast" + "git.akyoto.dev/cli/q/src/build/errors" +) + +// CompileAssign compiles an assign statement. +func (f *Function) CompileAssign(node *ast.Assign) error { + name := node.Name.Text() + variable, exists := f.variables[name] + + if !exists { + return errors.New(&errors.UnknownIdentifier{Name: name}, f.File, node.Name.Position) + } + + defer f.useVariable(variable) + return f.Execute(node.Operator, variable.Register, node.Value) +} diff --git a/src/build/Call.go b/src/build/core/CompileCall.go similarity index 51% rename from src/build/Call.go rename to src/build/core/CompileCall.go index 44df7c9..283439c 100644 --- a/src/build/Call.go +++ b/src/build/core/CompileCall.go @@ -1,39 +1,16 @@ -package build +package core import ( - "git.akyoto.dev/cli/q/src/build/config" "git.akyoto.dev/cli/q/src/build/cpu" "git.akyoto.dev/cli/q/src/build/expression" ) -// CompileFunctionCall executes a function call. +// CompileCall executes a function call. // All call registers must hold the correct parameter values before the function invocation. // Registers that are in use must be saved if they are modified by the function. // After the function call, they must be restored in reverse order. -func (f *Function) CompileFunctionCall(expr *expression.Expression) error { - funcName := expr.Children[0].Token.Text() - - if funcName == "syscall" { - return f.CompileSyscall(expr) - } - - function := f.functions[funcName] - - if function != f { - function.Wait() - } - - parameters := expr.Children[1:] - registers := f.cpu.Call[:len(parameters)] - - err := f.ExpressionsToRegisters(parameters, registers) - - if config.Verbose { - f.Logf("call: %s", funcName) - } - - f.assembler.Call(funcName) - f.sideEffects += function.sideEffects +func (f *Function) CompileCall(expr *expression.Expression) error { + _, err := f.EvaluateCall(expr) return err } @@ -49,15 +26,9 @@ func (f *Function) CompileSyscall(expr *expression.Expression) error { // ExpressionsToRegisters moves multiple expressions into the specified registers. func (f *Function) ExpressionsToRegisters(expressions []*expression.Expression, registers []cpu.Register) error { - for _, register := range registers { - f.SaveRegister(register) - } - - for i := len(expressions) - 1; i >= 0; i-- { - expression := expressions[i] - register := registers[i] - - err := f.ExpressionToRegister(expression, register) + for i := len(registers) - 1; i >= 0; i-- { + f.SaveRegister(registers[i]) + err := f.EvaluateTo(expressions[i], registers[i]) if err != nil { return err diff --git a/src/build/VariableDefinition.go b/src/build/core/CompileDefinition.go similarity index 64% rename from src/build/VariableDefinition.go rename to src/build/core/CompileDefinition.go index f385c24..7abd309 100644 --- a/src/build/VariableDefinition.go +++ b/src/build/core/CompileDefinition.go @@ -1,22 +1,24 @@ -package build +package core import ( + "fmt" + "git.akyoto.dev/cli/q/src/build/ast" "git.akyoto.dev/cli/q/src/build/config" + "git.akyoto.dev/cli/q/src/build/errors" "git.akyoto.dev/cli/q/src/build/expression" "git.akyoto.dev/cli/q/src/build/token" - "git.akyoto.dev/cli/q/src/errors" ) -// CompileVariableDefinition compiles a variable definition. -func (f *Function) CompileVariableDefinition(node *ast.Define) error { +// CompileDefinition compiles a variable definition. +func (f *Function) CompileDefinition(node *ast.Define) error { name := node.Name.Text() if f.identifierExists(name) { return errors.New(&errors.VariableAlreadyExists{Name: name}, f.File, node.Name.Position) } - uses := countIdentifier(f.Body, name) - 1 + uses := CountIdentifier(f.Body, name) - 1 if uses == 0 { return errors.New(&errors.UnusedVariable{Name: name}, f.File, node.Name.Position) @@ -48,29 +50,46 @@ func (f *Function) CompileVariableDefinition(node *ast.Define) error { return f.storeVariableInRegister(name, value, uses) } -func (f *Function) addVariable(variable *Variable) { - if config.Verbose { - f.Logf("%s occupies %s (alive: %d)", variable.Name, variable.Register, variable.Alive) +func (f *Function) AddVariable(variable *Variable) { + if config.Comments { + f.assembler.Comment(fmt.Sprintf("%s = %s (%s, %d uses)", variable.Name, variable.Value, variable.Register, variable.Alive)) } f.variables[variable.Name] = variable f.cpu.Use(variable.Register) } +func (f *Function) addTemporary(root *expression.Expression) *Variable { + f.count.tmps++ + name := fmt.Sprintf("t%d", f.count.tmps) + register := f.cpu.MustUseFree(f.cpu.General) + + tmp := &Variable{ + Name: name, + Value: root, + Alive: 1, + Register: register, + } + + f.variables[name] = tmp + + if config.Comments { + f.assembler.Comment(fmt.Sprintf("%s = %s (%s)", name, root, register)) + } + + return tmp +} + func (f *Function) useVariable(variable *Variable) { variable.Alive-- - if config.Verbose { - f.Logf("%s occupying %s was used (alive: %d)", variable.Name, variable.Register, variable.Alive) - } - if variable.Alive < 0 { panic("incorrect number of variable use calls") } if variable.Alive == 0 { - if config.Verbose { - f.Logf("%s is no longer used, free register: %s", variable.Name, variable.Register) + if config.Comments { + f.assembler.Comment(fmt.Sprintf("%s died (%s)", variable.Name, variable.Register)) } f.cpu.Free(variable.Register) @@ -102,9 +121,9 @@ func (f *Function) storeVariableInRegister(name string, value *expression.Expres panic("no free registers") } - err := f.ExpressionToRegister(value, reg) + err := f.EvaluateTo(value, reg) - f.addVariable(&Variable{ + f.AddVariable(&Variable{ Name: name, Register: reg, Alive: uses, @@ -113,7 +132,7 @@ func (f *Function) storeVariableInRegister(name string, value *expression.Expres return err } -func countIdentifier(tokens token.List, name string) int { +func CountIdentifier(tokens token.List, name string) int { count := 0 for _, t := range tokens { diff --git a/src/build/Loop.go b/src/build/core/CompileLoop.go similarity index 95% rename from src/build/Loop.go rename to src/build/core/CompileLoop.go index 2165eca..37a3c94 100644 --- a/src/build/Loop.go +++ b/src/build/core/CompileLoop.go @@ -1,4 +1,4 @@ -package build +package core import ( "fmt" diff --git a/src/build/Return.go b/src/build/core/CompileReturn.go similarity index 75% rename from src/build/Return.go rename to src/build/core/CompileReturn.go index c661fc7..c829a91 100644 --- a/src/build/Return.go +++ b/src/build/core/CompileReturn.go @@ -1,4 +1,4 @@ -package build +package core import ( "git.akyoto.dev/cli/q/src/build/ast" @@ -12,5 +12,5 @@ func (f *Function) CompileReturn(node *ast.Return) error { return nil } - return f.ExpressionToRegister(node.Value, f.cpu.Return[0]) + return f.EvaluateTo(node.Value, f.cpu.Return[0]) } diff --git a/src/build/core/Evaluate.go b/src/build/core/Evaluate.go new file mode 100644 index 0000000..ed65df0 --- /dev/null +++ b/src/build/core/Evaluate.go @@ -0,0 +1,123 @@ +package core + +import ( + "git.akyoto.dev/cli/q/src/build/asm" + "git.akyoto.dev/cli/q/src/build/ast" + "git.akyoto.dev/cli/q/src/build/cpu" + "git.akyoto.dev/cli/q/src/build/expression" + "git.akyoto.dev/cli/q/src/build/token" +) + +// Evaluate evaluates the result of an expression and saves it into a temporary register. +func (f *Function) Evaluate(root *expression.Expression) (*Variable, error) { + if root.IsLeaf() { + return f.EvaluateLeaf(root) + } + + if ast.IsFunctionCall(root) { + return f.EvaluateCall(root) + } + + left := root.Children[0] + right := root.Children[1] + + tmpLeft, err := f.Evaluate(left) + + if err != nil { + return nil, err + } + + tmpLeftExpr := expression.NewLeaf(token.Token{ + Kind: token.Identifier, + Position: left.Token.Position, + Bytes: []byte(tmpLeft.Name), + }) + + tmpLeftExpr.Parent = root + root.Children[0].Parent = nil + root.Children[0] = tmpLeftExpr + + tmpRight, err := f.Evaluate(right) + + if err != nil { + return nil, err + } + + tmpRightExpr := expression.NewLeaf(token.Token{ + Kind: token.Identifier, + Position: left.Token.Position, + Bytes: []byte(tmpRight.Name), + }) + + tmpRightExpr.Parent = root + root.Children[1].Parent = nil + root.Children[1] = tmpRightExpr + + tmp := f.addTemporary(root) + + f.assembler.RegisterRegister(asm.MOVE, tmp.Register, tmpLeft.Register) + f.useVariable(tmpLeft) + + err = f.opRegisterRegister(root.Token, tmp.Register, tmpRight.Register) + f.useVariable(tmpRight) + + return tmp, err +} + +func (f *Function) EvaluateCall(root *expression.Expression) (*Variable, error) { + funcName := root.Children[0].Token.Text() + parameters := root.Children[1:] + registers := f.cpu.Call[:len(parameters)] + isSyscall := funcName == "syscall" + + if isSyscall { + registers = f.cpu.Syscall[:len(parameters)] + } + + err := f.ExpressionsToRegisters(parameters, registers) + + for _, register := range f.cpu.General { + if !f.cpu.IsFree(register) { + f.assembler.Register(asm.PUSH, register) + } + } + + if isSyscall { + f.assembler.Syscall() + } else { + f.assembler.Call(funcName) + } + + for i := len(f.cpu.General) - 1; i >= 0; i-- { + register := f.cpu.General[i] + + if !f.cpu.IsFree(register) { + f.assembler.Register(asm.POP, register) + } + } + + tmp := f.addTemporary(root) + f.assembler.RegisterRegister(asm.MOVE, tmp.Register, f.cpu.Return[0]) + return tmp, err +} + +func (f *Function) EvaluateLeaf(root *expression.Expression) (*Variable, error) { + tmp := f.addTemporary(root) + err := f.TokenToRegister(root.Token, tmp.Register) + return tmp, err +} + +func (f *Function) EvaluateTo(root *expression.Expression, register cpu.Register) error { + tmp, err := f.Evaluate(root) + + if err != nil { + return err + } + + if register != tmp.Register { + f.assembler.RegisterRegister(asm.MOVE, register, tmp.Register) + } + + f.useVariable(tmp) + return nil +} diff --git a/src/build/Execute.go b/src/build/core/Execute.go similarity index 75% rename from src/build/Execute.go rename to src/build/core/Execute.go index e85cd1e..067f854 100644 --- a/src/build/Execute.go +++ b/src/build/core/Execute.go @@ -1,22 +1,17 @@ -package build +package core import ( "strconv" "git.akyoto.dev/cli/q/src/build/ast" - "git.akyoto.dev/cli/q/src/build/config" "git.akyoto.dev/cli/q/src/build/cpu" + "git.akyoto.dev/cli/q/src/build/errors" "git.akyoto.dev/cli/q/src/build/expression" "git.akyoto.dev/cli/q/src/build/token" - "git.akyoto.dev/cli/q/src/errors" ) // Execute executes an operation on a register with a value operand. func (f *Function) Execute(operation token.Token, register cpu.Register, value *expression.Expression) error { - if config.Verbose { - f.Logf("execute: %s on register %s with value %s", operation.Text(), register, value) - } - if value.IsLeaf() { return f.ExecuteLeaf(operation, register, value.Token) } @@ -36,13 +31,13 @@ func (f *Function) Execute(operation token.Token, register cpu.Register, value * f.cpu.Use(temporary) defer f.cpu.Free(temporary) - err := f.ExpressionToRegister(value, temporary) + err := f.EvaluateTo(value, temporary) if err != nil { return err } - return f.ExecuteRegisterRegister(operation, register, temporary) + return f.opRegisterRegister(operation, register, temporary) } // ExecuteLeaf performs an operation on a register with the given leaf operand. @@ -57,7 +52,7 @@ func (f *Function) ExecuteLeaf(operation token.Token, register cpu.Register, ope } defer f.useVariable(variable) - return f.ExecuteRegisterRegister(operation, register, variable.Register) + return f.opRegisterRegister(operation, register, variable.Register) case token.Number: value := operand.Text() @@ -67,7 +62,7 @@ func (f *Function) ExecuteLeaf(operation token.Token, register cpu.Register, ope return err } - return f.ExecuteRegisterNumber(operation, register, number) + return f.opRegisterNumber(operation, register, number) } return errors.New(errors.NotImplemented, f.File, operation.Position) diff --git a/src/build/Function.go b/src/build/core/Function.go similarity index 57% rename from src/build/Function.go rename to src/build/core/Function.go index de87e2f..518affb 100644 --- a/src/build/Function.go +++ b/src/build/core/Function.go @@ -1,21 +1,46 @@ -package build +package core import ( "fmt" + "git.akyoto.dev/cli/q/src/build/arch/x64" + "git.akyoto.dev/cli/q/src/build/asm" "git.akyoto.dev/cli/q/src/build/ast" - "git.akyoto.dev/cli/q/src/build/config" + "git.akyoto.dev/cli/q/src/build/cpu" + "git.akyoto.dev/cli/q/src/build/errors" "git.akyoto.dev/cli/q/src/build/fs" "git.akyoto.dev/cli/q/src/build/token" - "git.akyoto.dev/cli/q/src/errors" ) -// Function represents a function. +// Function represents the smallest unit of code. type Function struct { - File *fs.File Name string + File *fs.File Body token.List - compiler + state +} + +// NewFunction creates a new function. +func NewFunction(name string, file *fs.File, body token.List) *Function { + return &Function{ + Name: name, + File: file, + Body: body, + state: state{ + assembler: asm.Assembler{ + Instructions: make([]asm.Instruction, 0, 32), + }, + cpu: cpu.CPU{ + Call: x64.CallRegisters, + General: x64.GeneralRegisters, + Syscall: x64.SyscallRegisters, + Return: x64.ReturnValueRegisters, + }, + definitions: map[string]*Definition{}, + variables: map[string]*Variable{}, + finished: make(chan struct{}), + }, + } } // Compile turns a function into machine code. @@ -41,18 +66,7 @@ func (f *Function) CompileTokens(tokens token.List) error { // CompileAST compiles an abstract syntax tree. func (f *Function) CompileAST(tree ast.AST) error { for _, node := range tree { - if config.Verbose { - f.Logf("%T %s", node, node) - } - - if config.Assembler { - f.debug = append(f.debug, debug{ - position: len(f.assembler.Instructions), - source: node, - }) - } - - err := f.CompileNode(node) + err := f.CompileASTNode(node) if err != nil { return err @@ -62,14 +76,17 @@ func (f *Function) CompileAST(tree ast.AST) error { return nil } -// CompileNode compiles a node in the AST. -func (f *Function) CompileNode(node ast.Node) error { +// CompileASTNode compiles a node in the AST. +func (f *Function) CompileASTNode(node ast.Node) error { switch node := node.(type) { + case *ast.Assign: + return f.CompileAssign(node) + case *ast.Call: - return f.CompileFunctionCall(node.Expression) + return f.CompileCall(node.Expression) case *ast.Define: - return f.CompileVariableDefinition(node) + return f.CompileDefinition(node) case *ast.Return: return f.CompileReturn(node) diff --git a/src/build/Result.go b/src/build/core/Result.go similarity index 59% rename from src/build/Result.go rename to src/build/core/Result.go index 6a5e94b..9430012 100644 --- a/src/build/Result.go +++ b/src/build/core/Result.go @@ -1,8 +1,12 @@ -package build +package core import ( + "bufio" + "os" + "git.akyoto.dev/cli/q/src/build/arch/x64" "git.akyoto.dev/cli/q/src/build/asm" + "git.akyoto.dev/cli/q/src/build/elf" "git.akyoto.dev/cli/q/src/build/os/linux" ) @@ -13,8 +17,8 @@ type Result struct { InstructionCount int } -// Finalize generates the final machine code. -func (r *Result) Finalize() ([]byte, []byte) { +// finalize generates the final machine code. +func (r *Result) finalize() ([]byte, []byte) { // This will be the entry point of the executable. // The only job of the entry function is to call `main` and exit cleanly. // The reason we call `main` instead of using `main` itself is to place @@ -30,7 +34,7 @@ func (r *Result) Finalize() ([]byte, []byte) { // 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[*Function]bool{}, func(f *Function) { + r.eachFunction(r.Main, map[*Function]bool{}, func(f *Function) { final.Merge(f.assembler) }) @@ -38,9 +42,9 @@ func (r *Result) Finalize() ([]byte, []byte) { return code, data } -// EachFunction recursively finds all the calls to external functions. +// eachFunction recursively finds all the calls to external functions. // It avoids calling the same function twice with the help of a hashmap. -func (r *Result) EachFunction(caller *Function, traversed map[*Function]bool, call func(*Function)) { +func (r *Result) eachFunction(caller *Function, traversed map[*Function]bool, call func(*Function)) { call(caller) traversed[caller] = true @@ -60,12 +64,40 @@ func (r *Result) EachFunction(caller *Function, traversed map[*Function]bool, ca continue } - r.EachFunction(callee, traversed, call) + r.eachFunction(callee, traversed, call) } } -// Write write the final executable to disk. -func (r *Result) Write(path string) error { - code, data := r.Finalize() - return Write(path, code, data) +// PrintInstructions prints out the generated instructions. +func (r *Result) PrintInstructions() { + r.eachFunction(r.Main, map[*Function]bool{}, func(f *Function) { + f.PrintInstructions() + }) +} + +// Write writes an executable file to disk. +func (r *Result) Write(path string) error { + code, data := r.finalize() + return write(path, code, data) +} + +// write writes an executable file to disk. +func write(path string, code []byte, data []byte) error { + file, err := os.Create(path) + + if err != nil { + return err + } + + buffer := bufio.NewWriter(file) + executable := elf.New(code, data) + executable.Write(buffer) + buffer.Flush() + err = file.Close() + + if err != nil { + return err + } + + return os.Chmod(path, 0755) } diff --git a/src/build/SaveRegister.go b/src/build/core/SaveRegister.go similarity index 69% rename from src/build/SaveRegister.go rename to src/build/core/SaveRegister.go index d74c95d..99d5375 100644 --- a/src/build/SaveRegister.go +++ b/src/build/core/SaveRegister.go @@ -1,6 +1,8 @@ -package build +package core import ( + "fmt" + "git.akyoto.dev/cli/q/src/build/asm" "git.akyoto.dev/cli/q/src/build/config" "git.akyoto.dev/cli/q/src/build/cpu" @@ -25,18 +27,13 @@ func (f *Function) SaveRegister(register cpu.Register) { return } - newRegister, exists := f.cpu.FindFree(f.cpu.General) + newRegister := f.cpu.MustUseFree(f.cpu.General) - if !exists { - panic("no free registers") - } - - if config.Verbose { - f.Logf("moving %s from %s to %s (alive: %d)", variable.Name, variable.Register, newRegister, variable.Alive) + if config.Comments { + f.assembler.Comment(fmt.Sprintf("save %s to %s", register, newRegister)) } f.assembler.RegisterRegister(asm.MOVE, newRegister, register) f.cpu.Free(register) - f.cpu.Use(newRegister) variable.Register = newRegister } diff --git a/src/build/TokenToRegister.go b/src/build/core/TokenToRegister.go similarity index 81% rename from src/build/TokenToRegister.go rename to src/build/core/TokenToRegister.go index 6bef4cb..faf4036 100644 --- a/src/build/TokenToRegister.go +++ b/src/build/core/TokenToRegister.go @@ -1,13 +1,12 @@ -package build +package core import ( "strconv" "git.akyoto.dev/cli/q/src/build/asm" - "git.akyoto.dev/cli/q/src/build/config" "git.akyoto.dev/cli/q/src/build/cpu" + "git.akyoto.dev/cli/q/src/build/errors" "git.akyoto.dev/cli/q/src/build/token" - "git.akyoto.dev/cli/q/src/errors" ) // TokenToRegister moves a token into a register. @@ -19,11 +18,7 @@ func (f *Function) TokenToRegister(t token.Token, register cpu.Register) error { constant, exists := f.definitions[name] if exists { - if config.Verbose { - f.Logf("constant %s = %s", constant.Name, constant.Value) - } - - return f.ExpressionToRegister(constant.Value, register) + return f.EvaluateTo(constant.Value, register) } variable, exists := f.variables[name] diff --git a/src/build/Variable.go b/src/build/core/Variable.go similarity index 88% rename from src/build/Variable.go rename to src/build/core/Variable.go index c81b850..8667b35 100644 --- a/src/build/Variable.go +++ b/src/build/core/Variable.go @@ -1,4 +1,4 @@ -package build +package core import ( "git.akyoto.dev/cli/q/src/build/cpu" @@ -7,6 +7,7 @@ import ( // Variable represents a named register. type Variable struct { + Value *expression.Expression Name string Register cpu.Register Alive int diff --git a/src/build/Math.go b/src/build/core/opRegister.go similarity index 74% rename from src/build/Math.go rename to src/build/core/opRegister.go index b02d42d..2cdd81c 100644 --- a/src/build/Math.go +++ b/src/build/core/opRegister.go @@ -1,14 +1,14 @@ -package build +package core import ( "git.akyoto.dev/cli/q/src/build/asm" "git.akyoto.dev/cli/q/src/build/cpu" + "git.akyoto.dev/cli/q/src/build/errors" "git.akyoto.dev/cli/q/src/build/token" - "git.akyoto.dev/cli/q/src/errors" ) -// ExecuteRegisterNumber performs an operation on a register and a number. -func (f *Function) ExecuteRegisterNumber(operation token.Token, register cpu.Register, number int) error { +// opRegisterNumber performs an operation on a register and a number. +func (f *Function) opRegisterNumber(operation token.Token, register cpu.Register, number int) error { switch operation.Text() { case "+", "+=": f.assembler.RegisterNumber(asm.ADD, register, number) @@ -32,8 +32,8 @@ func (f *Function) ExecuteRegisterNumber(operation token.Token, register cpu.Reg return nil } -// ExecuteRegisterRegister performs an operation on two registers. -func (f *Function) ExecuteRegisterRegister(operation token.Token, destination cpu.Register, source cpu.Register) error { +// opRegisterRegister performs an operation on two registers. +func (f *Function) opRegisterRegister(operation token.Token, destination cpu.Register, source cpu.Register) error { switch operation.Text() { case "+", "+=": f.assembler.RegisterRegister(asm.ADD, destination, source) diff --git a/src/build/core/state.go b/src/build/core/state.go new file mode 100644 index 0000000..70078c3 --- /dev/null +++ b/src/build/core/state.go @@ -0,0 +1,58 @@ +package core + +import ( + "fmt" + + "git.akyoto.dev/cli/q/src/build/asm" + "git.akyoto.dev/cli/q/src/build/cpu" + "git.akyoto.dev/go/color/ansi" +) + +// state is the data structure we embed in each function to preserve compilation state. +type state struct { + err error + definitions map[string]*Definition + variables map[string]*Variable + functions map[string]*Function + finished chan struct{} + assembler asm.Assembler + cpu cpu.CPU + count counter + sideEffects int +} + +// counter stores how often a certain statement appeared so we can generate a unique label from it. +type counter struct { + loop int + tmps int +} + +// PrintInstructions shows the assembly instructions. +func (s *state) PrintInstructions() { + ansi.Dim.Println("╭────────────────────────────────────────────────╮") + + for _, x := range s.assembler.Instructions { + ansi.Dim.Print("│ ") + + switch x.Mnemonic { + case asm.LABEL: + ansi.Yellow.Printf("%-46s", x.Data.String()+":") + + case asm.COMMENT: + ansi.Dim.Printf("%-46s", x.Data.String()) + + default: + ansi.Green.Printf("%-8s", x.Mnemonic.String()) + + if x.Data != nil { + fmt.Printf("%-38s", x.Data.String()) + } else { + fmt.Printf("%-38s", "") + } + } + + ansi.Dim.Print(" │\n") + } + + ansi.Dim.Println("╰────────────────────────────────────────────────╯") +} diff --git a/src/build/cpu/CPU.go b/src/build/cpu/CPU.go index 0092154..8f0f9f3 100644 --- a/src/build/cpu/CPU.go +++ b/src/build/cpu/CPU.go @@ -30,3 +30,14 @@ func (c *CPU) FindFree(registers []Register) (Register, bool) { return 0, false } + +func (c *CPU) MustUseFree(registers []Register) Register { + register, exists := c.FindFree(registers) + + if !exists { + panic("no free registers") + } + + c.Use(register) + return register +} diff --git a/src/errors/Base.go b/src/build/errors/Base.go similarity index 100% rename from src/errors/Base.go rename to src/build/errors/Base.go diff --git a/src/errors/CompileErrors.go b/src/build/errors/CompileErrors.go similarity index 100% rename from src/errors/CompileErrors.go rename to src/build/errors/CompileErrors.go diff --git a/src/errors/Error.go b/src/build/errors/Error.go similarity index 100% rename from src/errors/Error.go rename to src/build/errors/Error.go diff --git a/src/errors/InvalidCharacter.go b/src/build/errors/InvalidCharacter.go similarity index 100% rename from src/errors/InvalidCharacter.go rename to src/build/errors/InvalidCharacter.go diff --git a/src/errors/InvalidInstruction.go b/src/build/errors/InvalidInstruction.go similarity index 100% rename from src/errors/InvalidInstruction.go rename to src/build/errors/InvalidInstruction.go diff --git a/src/errors/InvalidOperator.go b/src/build/errors/InvalidOperator.go similarity index 100% rename from src/errors/InvalidOperator.go rename to src/build/errors/InvalidOperator.go diff --git a/src/errors/KeywordNotImplemented.go b/src/build/errors/KeywordNotImplemented.go similarity index 100% rename from src/errors/KeywordNotImplemented.go rename to src/build/errors/KeywordNotImplemented.go diff --git a/src/errors/ScanErrors.go b/src/build/errors/ScanErrors.go similarity index 100% rename from src/errors/ScanErrors.go rename to src/build/errors/ScanErrors.go diff --git a/src/errors/Stack.go b/src/build/errors/Stack.go similarity index 100% rename from src/errors/Stack.go rename to src/build/errors/Stack.go diff --git a/src/errors/UnknownIdentifier.go b/src/build/errors/UnknownIdentifier.go similarity index 100% rename from src/errors/UnknownIdentifier.go rename to src/build/errors/UnknownIdentifier.go diff --git a/src/errors/UnusedVariable.go b/src/build/errors/UnusedVariable.go similarity index 100% rename from src/errors/UnusedVariable.go rename to src/build/errors/UnusedVariable.go diff --git a/src/errors/VariableAlreadyExists.go b/src/build/errors/VariableAlreadyExists.go similarity index 100% rename from src/errors/VariableAlreadyExists.go rename to src/build/errors/VariableAlreadyExists.go diff --git a/src/build/scan.go b/src/build/scanner/Scan.go similarity index 75% rename from src/build/scan.go rename to src/build/scanner/Scan.go index 758bbec..1a4466b 100644 --- a/src/build/scan.go +++ b/src/build/scanner/Scan.go @@ -1,4 +1,4 @@ -package build +package scanner import ( "os" @@ -7,17 +7,16 @@ import ( "sync" "git.akyoto.dev/cli/q/src/build/arch/x64" - "git.akyoto.dev/cli/q/src/build/asm" - "git.akyoto.dev/cli/q/src/build/cpu" + "git.akyoto.dev/cli/q/src/build/core" + "git.akyoto.dev/cli/q/src/build/errors" "git.akyoto.dev/cli/q/src/build/expression" "git.akyoto.dev/cli/q/src/build/fs" "git.akyoto.dev/cli/q/src/build/token" - "git.akyoto.dev/cli/q/src/errors" ) -// scan scans the directory. -func scan(files []string) (<-chan *Function, <-chan error) { - functions := make(chan *Function) +// Scan scans the directory. +func Scan(files []string) (<-chan *core.Function, <-chan error) { + functions := make(chan *core.Function) errors := make(chan error) go func() { @@ -30,7 +29,7 @@ func scan(files []string) (<-chan *Function, <-chan error) { } // scanFiles scans the list of files without channel allocations. -func scanFiles(files []string, functions chan<- *Function, errors chan<- error) { +func scanFiles(files []string, functions chan<- *core.Function, errors chan<- error) { wg := sync.WaitGroup{} for _, file := range files { @@ -81,7 +80,7 @@ func scanFiles(files []string, functions chan<- *Function, errors chan<- error) } // scanFile scans a single file. -func scanFile(path string, functions chan<- *Function) error { +func scanFile(path string, functions chan<- *core.Function) error { contents, err := os.ReadFile(path) if err != nil { @@ -236,49 +235,34 @@ func scanFile(path string, functions chan<- *Function) error { return errors.New(errors.ExpectedFunctionDefinition, file, tokens[i].Position) } - function := &Function{ - Name: tokens[nameStart].Text(), - File: file, - Body: tokens[bodyStart:i], - compiler: compiler{ - assembler: asm.Assembler{ - Instructions: make([]asm.Instruction, 0, 32), - }, - cpu: cpu.CPU{ - Call: x64.CallRegisters, - General: x64.GeneralRegisters, - Syscall: x64.SyscallRegisters, - Return: x64.ReturnValueRegisters, - }, - definitions: map[string]*Definition{}, - variables: map[string]*Variable{}, - finished: make(chan struct{}), - }, - } - + name := tokens[nameStart].Text() + body := tokens[bodyStart:i] + function := core.NewFunction(name, file, body) parameters := tokens[paramsStart:paramsEnd] + count := 0 err := expression.EachParameter(parameters, func(tokens token.List) error { - if len(tokens) == 1 { - name := tokens[0].Text() - register := x64.CallRegisters[len(function.variables)] - uses := countIdentifier(function.Body, name) - - if uses == 0 { - return errors.New(&errors.UnusedVariable{Name: name}, file, tokens[0].Position) - } - - variable := &Variable{ - Name: name, - Register: register, - Alive: uses, - } - - function.addVariable(variable) - return nil + if len(tokens) != 1 { + return errors.New(errors.NotImplemented, file, tokens[0].Position) } - return errors.New(errors.NotImplemented, file, tokens[0].Position) + name := tokens[0].Text() + register := x64.CallRegisters[count] + uses := core.CountIdentifier(function.Body, name) + + if uses == 0 { + return errors.New(&errors.UnusedVariable{Name: name}, file, tokens[0].Position) + } + + variable := &core.Variable{ + Name: name, + Register: register, + Alive: uses, + } + + function.AddVariable(variable) + count++ + return nil }) if err != nil { diff --git a/src/cli/Build.go b/src/cli/Build.go index 8db214f..ac4066c 100644 --- a/src/cli/Build.go +++ b/src/cli/Build.go @@ -9,22 +9,21 @@ import ( "git.akyoto.dev/cli/q/src/build/config" ) -// Build builds an executable. +// Build parses the arguments and creates a build. func Build(args []string) int { b := build.New() - dry := false for i := 0; i < len(args); i++ { switch args[i] { - case "--dry": - dry = true - - case "--assembler", "-a": + case "-a", "--assembler": config.Assembler = true - - case "--verbose", "-v": - config.Verbose = true - + case "-c", "--comments": + config.Comments = true + case "-d", "--dry": + config.Dry = true + case "-v", "--verbose": + config.Assembler = true + config.Comments = true default: if strings.HasPrefix(args[i], "-") { fmt.Printf("Unknown parameter: %s\n", args[i]) @@ -39,6 +38,11 @@ func Build(args []string) int { b.Files = append(b.Files, ".") } + return run(b) +} + +// run starts the build by running the compiler and then writing the result to disk. +func run(b *build.Build) int { result, err := b.Run() if err != nil { @@ -47,12 +51,10 @@ func Build(args []string) int { } if config.Assembler { - result.EachFunction(result.Main, map[*build.Function]bool{}, func(f *build.Function) { - f.PrintInstructions() - }) + result.PrintInstructions() } - if dry { + if config.Dry { return 0 } diff --git a/src/cli/Help.go b/src/cli/Help.go index 7cf99e4..b6c8f58 100644 --- a/src/cli/Help.go +++ b/src/cli/Help.go @@ -18,6 +18,7 @@ func Help(w io.Writer, code int) int { build options: --assembler, -a Show assembler instructions. - --verbose, -v Show verbose output.`) + --comments, -c Show assembler comments. + --verbose, -v Show everything.`) return code } diff --git a/bench_test.go b/tests/benchmarks_test.go similarity index 72% rename from bench_test.go rename to tests/benchmarks_test.go index 46b62ed..6ff03a7 100644 --- a/bench_test.go +++ b/tests/benchmarks_test.go @@ -1,4 +1,4 @@ -package main_test +package tests_test import ( "testing" @@ -8,7 +8,7 @@ import ( ) func BenchmarkEmpty(b *testing.B) { - compiler := build.New("tests/benchmarks/empty.q") + compiler := build.New("benchmarks/empty.q") for i := 0; i < b.N; i++ { _, err := compiler.Run() @@ -17,7 +17,7 @@ func BenchmarkEmpty(b *testing.B) { } func BenchmarkExpressions(b *testing.B) { - compiler := build.New("tests/benchmarks/expressions.q") + compiler := build.New("benchmarks/expressions.q") for i := 0; i < b.N; i++ { _, err := compiler.Run() diff --git a/errors_test.go b/tests/errors_test.go similarity index 93% rename from errors_test.go rename to tests/errors_test.go index f4cb7ab..9de3370 100644 --- a/errors_test.go +++ b/tests/errors_test.go @@ -1,4 +1,4 @@ -package main_test +package tests_test import ( "path/filepath" @@ -6,7 +6,7 @@ import ( "testing" "git.akyoto.dev/cli/q/src/build" - "git.akyoto.dev/cli/q/src/errors" + "git.akyoto.dev/cli/q/src/build/errors" "git.akyoto.dev/go/assert" ) @@ -41,7 +41,7 @@ func TestErrors(t *testing.T) { name := strings.TrimSuffix(test.File, ".q") t.Run(name, func(t *testing.T) { - b := build.New(filepath.Join("tests", "errors", test.File)) + b := build.New(filepath.Join("errors", test.File)) _, err := b.Run() assert.NotNil(t, err) assert.Contains(t, err.Error(), test.ExpectedError.Error()) diff --git a/tests/examples_test.go b/tests/examples_test.go new file mode 100644 index 0000000..a8d9954 --- /dev/null +++ b/tests/examples_test.go @@ -0,0 +1,23 @@ +package tests_test + +import ( + "path/filepath" + "testing" +) + +func TestExamples(t *testing.T) { + var tests = []struct { + Name string + ExpectedOutput string + ExpectedExitCode int + }{ + {"hello", "", 9}, + {"write", "ELF", 0}, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + run(t, filepath.Join("..", "examples", test.Name), test.ExpectedOutput, test.ExpectedExitCode) + }) + } +} diff --git a/tests/programs/successive-calls.q b/tests/programs/successive-calls.q new file mode 100644 index 0000000..bffc930 --- /dev/null +++ b/tests/programs/successive-calls.q @@ -0,0 +1,7 @@ +main() { + syscall(60, f(1) + f(2) + f(3)) +} + +f(x) { + return x + 1 +} \ No newline at end of file diff --git a/examples_test.go b/tests/programs_test.go similarity index 60% rename from examples_test.go rename to tests/programs_test.go index faf1103..e671fd0 100644 --- a/examples_test.go +++ b/tests/programs_test.go @@ -1,38 +1,35 @@ -package main_test +package tests_test import ( "os" "os/exec" + "path/filepath" "testing" "git.akyoto.dev/cli/q/src/build" "git.akyoto.dev/go/assert" ) -func TestExamples(t *testing.T) { - var examples = []struct { +func TestPrograms(t *testing.T) { + var tests = []struct { Name string ExpectedOutput string ExpectedExitCode int }{ - {"hello", "", 25}, - {"write", "ELF", 0}, + {"successive-calls.q", "", 9}, } - for _, example := range examples { - example := example - - t.Run(example.Name, func(t *testing.T) { - runExample(t, example.Name, example.ExpectedOutput, example.ExpectedExitCode) + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + run(t, filepath.Join("programs", test.Name), test.ExpectedOutput, test.ExpectedExitCode) }) } } -// runExample builds and runs the example to check if the output matches the expected output. -func runExample(t *testing.T, name string, expectedOutput string, expectedExitCode int) { - b := build.New("examples/" + name) +// run builds and runs the file to check if the output matches the expected output. +func run(t *testing.T, name string, expectedOutput string, expectedExitCode int) { + b := build.New(name) assert.True(t, len(b.Executable()) > 0) - defer os.Remove(b.Executable()) t.Run("Compile", func(t *testing.T) { result, err := b.Run()