From 3fe2cd1da2ee049c269abf0dda222e75662c1f61 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 30 Jun 2024 00:17:14 +0200 Subject: [PATCH] Improved variable lifetime tracking --- bench_test.go | 9 --------- errors_test.go | 1 + examples/write/write.q | 2 +- src/build/Execute.go | 9 +++++++++ src/build/Function.go | 11 +++++++++++ src/build/FunctionCall.go | 5 +++++ src/build/SaveRegister.go | 7 ++++++- src/build/Variable.go | 6 +++--- src/build/VariableDefinition.go | 33 +++++++++++++++++++++++++-------- src/build/config/config.go | 3 ++- src/build/scan.go | 13 ++++++++++++- src/cli/Build.go | 5 ++++- src/cli/Help.go | 17 ++++++++++++----- src/errors/UnusedVariable.go | 13 +++++++++++++ tests/benchmarks/expressions.q | 7 ++++++- tests/errors/InvalidOperator.q | 1 + tests/errors/UnusedVariable.q | 3 +++ 17 files changed, 114 insertions(+), 31 deletions(-) create mode 100644 src/errors/UnusedVariable.go create mode 100644 tests/errors/UnusedVariable.q diff --git a/bench_test.go b/bench_test.go index 4bb3f8b..46b62ed 100644 --- a/bench_test.go +++ b/bench_test.go @@ -24,12 +24,3 @@ func BenchmarkExpressions(b *testing.B) { assert.Nil(b, err) } } - -func BenchmarkHello(b *testing.B) { - compiler := build.New("examples/hello") - - for i := 0; i < b.N; i++ { - _, err := compiler.Run() - assert.Nil(b, err) - } -} diff --git a/errors_test.go b/errors_test.go index c18344e..f4cb7ab 100644 --- a/errors_test.go +++ b/errors_test.go @@ -34,6 +34,7 @@ func TestErrors(t *testing.T) { {"VariableAlreadyExists.q", &errors.VariableAlreadyExists{Name: "x"}}, {"UnknownIdentifier.q", &errors.UnknownIdentifier{Name: "x"}}, {"UnknownIdentifier2.q", &errors.UnknownIdentifier{Name: "x"}}, + {"UnusedVariable.q", &errors.UnusedVariable{Name: "x"}}, } for _, test := range tests { diff --git a/examples/write/write.q b/examples/write/write.q index 0f343c1..c06884e 100644 --- a/examples/write/write.q +++ b/examples/write/write.q @@ -1,6 +1,6 @@ main() { address := 4194304 + 1 - length := (0 + 50 - 20) * 10 / 100 + length := 3 print(address, length) } diff --git a/src/build/Execute.go b/src/build/Execute.go index 9939aaa..8947218 100644 --- a/src/build/Execute.go +++ b/src/build/Execute.go @@ -4,6 +4,7 @@ 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/expression" "git.akyoto.dev/cli/q/src/build/token" @@ -12,6 +13,10 @@ import ( // 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) } @@ -122,6 +127,10 @@ func (f *Function) ExecuteRegisterRegister(operation token.Token, destination cp // 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() { diff --git a/src/build/Function.go b/src/build/Function.go index 2370fc8..8c271e0 100644 --- a/src/build/Function.go +++ b/src/build/Function.go @@ -1,6 +1,8 @@ package build import ( + "fmt" + "git.akyoto.dev/cli/q/src/build/config" "git.akyoto.dev/cli/q/src/build/expression" "git.akyoto.dev/cli/q/src/build/fs" @@ -44,6 +46,10 @@ func (f *Function) CompileTokens(body token.List) error { instruction := body[start:i] if config.Verbose { + f.Logf("compiling: %s", instruction) + } + + if config.Assembler { f.debug = append(f.debug, debug{ position: len(f.assembler.Instructions), source: instruction, @@ -108,6 +114,11 @@ func (f *Function) CompileInstruction(line token.List) error { } } +// Logf formats a message for verbose output. +func (f *Function) Logf(format string, data ...any) { + fmt.Printf("[%s @ %d] %s\n", f, len(f.assembler.Instructions), fmt.Sprintf(format, data...)) +} + // String returns the function name. func (f *Function) String() string { return f.Name diff --git a/src/build/FunctionCall.go b/src/build/FunctionCall.go index 99b71ba..8b62e25 100644 --- a/src/build/FunctionCall.go +++ b/src/build/FunctionCall.go @@ -1,6 +1,7 @@ package build 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" ) @@ -28,6 +29,10 @@ func (f *Function) CompileFunctionCall(expr *expression.Expression) error { err := f.ExpressionsToRegisters(parameters, registers) + if config.Verbose { + f.Logf("call: %s", funcName) + } + if isSyscall { f.assembler.Syscall() } else { diff --git a/src/build/SaveRegister.go b/src/build/SaveRegister.go index 08b7d11..d74c95d 100644 --- a/src/build/SaveRegister.go +++ b/src/build/SaveRegister.go @@ -2,6 +2,7 @@ package build import ( "git.akyoto.dev/cli/q/src/build/asm" + "git.akyoto.dev/cli/q/src/build/config" "git.akyoto.dev/cli/q/src/build/cpu" ) @@ -20,7 +21,7 @@ func (f *Function) SaveRegister(register cpu.Register) { } } - if variable == nil || variable.UsesRemaining == 0 { + if variable == nil || variable.Alive == 0 { return } @@ -30,6 +31,10 @@ func (f *Function) SaveRegister(register cpu.Register) { panic("no free registers") } + if config.Verbose { + f.Logf("moving %s from %s to %s (alive: %d)", variable.Name, variable.Register, newRegister, variable.Alive) + } + f.assembler.RegisterRegister(asm.MOVE, newRegister, register) f.cpu.Free(register) f.cpu.Use(newRegister) diff --git a/src/build/Variable.go b/src/build/Variable.go index cb6ff43..9d18c37 100644 --- a/src/build/Variable.go +++ b/src/build/Variable.go @@ -6,7 +6,7 @@ import ( // Variable represents a variable in a function. type Variable struct { - Name string - Register cpu.Register - UsesRemaining int + Name string + Register cpu.Register + Alive int } diff --git a/src/build/VariableDefinition.go b/src/build/VariableDefinition.go index 560aff4..f0624da 100644 --- a/src/build/VariableDefinition.go +++ b/src/build/VariableDefinition.go @@ -1,6 +1,7 @@ package build import ( + "git.akyoto.dev/cli/q/src/build/config" "git.akyoto.dev/cli/q/src/build/expression" "git.akyoto.dev/cli/q/src/build/token" "git.akyoto.dev/cli/q/src/errors" @@ -18,6 +19,12 @@ func (f *Function) CompileVariableDefinition(expr *expression.Expression) error return errors.New(&errors.VariableAlreadyExists{Name: name}, f.File, expr.Children[0].Token.Position) } + uses := countIdentifier(f.Body, name) - 1 + + if uses == 0 { + return errors.New(&errors.UnusedVariable{Name: name}, f.File, expr.Children[0].Token.Position) + } + reg, exists := f.cpu.FindFree(f.cpu.General) if !exists { @@ -30,30 +37,40 @@ func (f *Function) CompileVariableDefinition(expr *expression.Expression) error return err } - variable := &Variable{ + f.addVariable(&Variable{ Name: name, Register: reg, - } + Alive: uses, + }) - f.addVariable(variable) - f.useVariable(variable) return nil } func (f *Function) addVariable(variable *Variable) { - variable.UsesRemaining = countIdentifier(f.Body, variable.Name) + if config.Verbose { + f.Logf("%s occupies %s (alive: %d)", variable.Name, variable.Register, variable.Alive) + } + f.variables[variable.Name] = variable f.cpu.Use(variable.Register) } func (f *Function) useVariable(variable *Variable) { - variable.UsesRemaining-- + variable.Alive-- - if variable.UsesRemaining < 0 { + 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.UsesRemaining == 0 { + if variable.Alive == 0 { + if config.Verbose { + f.Logf("%s is no longer used, free register: %s", variable.Name, variable.Register) + } + f.cpu.Free(variable.Register) } } diff --git a/src/build/config/config.go b/src/build/config/config.go index baaf72b..d87cf95 100644 --- a/src/build/config/config.go +++ b/src/build/config/config.go @@ -8,5 +8,6 @@ const ( ) var ( - Verbose = false + Assembler = false + Verbose = false ) diff --git a/src/build/scan.go b/src/build/scan.go index b9058e8..695a401 100644 --- a/src/build/scan.go +++ b/src/build/scan.go @@ -260,7 +260,18 @@ func scanFile(path string, functions chan<- *Function) error { if len(tokens) == 1 { name := tokens[0].Text() register := x64.CallRegisters[len(function.variables)] - variable := &Variable{Name: name, Register: register} + 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 } diff --git a/src/cli/Build.go b/src/cli/Build.go index 99a5118..8db214f 100644 --- a/src/cli/Build.go +++ b/src/cli/Build.go @@ -19,6 +19,9 @@ func Build(args []string) int { case "--dry": dry = true + case "--assembler", "-a": + config.Assembler = true + case "--verbose", "-v": config.Verbose = true @@ -43,7 +46,7 @@ func Build(args []string) int { return 1 } - if config.Verbose { + if config.Assembler { result.EachFunction(result.Main, map[*build.Function]bool{}, func(f *build.Function) { f.PrintInstructions() }) diff --git a/src/cli/Help.go b/src/cli/Help.go index cf7c00f..7cf99e4 100644 --- a/src/cli/Help.go +++ b/src/cli/Help.go @@ -7,10 +7,17 @@ import ( // Help shows the command line argument usage. func Help(w io.Writer, code int) int { - fmt.Fprintln(w, "Usage: q [command] [options]") - fmt.Fprintln(w, "") - fmt.Fprintln(w, " build [directory] [file]") - fmt.Fprintln(w, " help") - fmt.Fprintln(w, " system") + fmt.Fprintln(w, `Usage: q [command] [options] + + commands: + + build [directory | file] + help + system + + build options: + + --assembler, -a Show assembler instructions. + --verbose, -v Show verbose output.`) return code } diff --git a/src/errors/UnusedVariable.go b/src/errors/UnusedVariable.go new file mode 100644 index 0000000..00a5a6f --- /dev/null +++ b/src/errors/UnusedVariable.go @@ -0,0 +1,13 @@ +package errors + +import "fmt" + +// UnusedVariable error is created when a defined variable is never used. +type UnusedVariable struct { + Name string +} + +// Error generates the string representation. +func (err *UnusedVariable) Error() string { + return fmt.Sprintf("Unused variable '%s'", err.Name) +} diff --git a/tests/benchmarks/expressions.q b/tests/benchmarks/expressions.q index 194f47d..aace245 100644 --- a/tests/benchmarks/expressions.q +++ b/tests/benchmarks/expressions.q @@ -1,3 +1,8 @@ main() { - return 1+2*3 + x := f(2, 3) + syscall(60, x) +} + +f(x, y) { + return (x + y) * (x + y) } \ No newline at end of file diff --git a/tests/errors/InvalidOperator.q b/tests/errors/InvalidOperator.q index 7b5d4d2..a97c489 100644 --- a/tests/errors/InvalidOperator.q +++ b/tests/errors/InvalidOperator.q @@ -1,3 +1,4 @@ main() { x := 123 +++ 456 + syscall(60, x) } \ No newline at end of file diff --git a/tests/errors/UnusedVariable.q b/tests/errors/UnusedVariable.q new file mode 100644 index 0000000..f57a9e8 --- /dev/null +++ b/tests/errors/UnusedVariable.q @@ -0,0 +1,3 @@ +main() { + x := 1 +} \ No newline at end of file