diff --git a/.gitea/workflows/ build.yml b/.gitea/workflows/ build.yml new file mode 100644 index 0000000..7597334 --- /dev/null +++ b/.gitea/workflows/ build.yml @@ -0,0 +1,22 @@ + +--- + +name: Build + +on: + push: + branches: [master] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '>=1.21' + - name: Build Binary + run: make build \ No newline at end of file diff --git a/.gitea/workflows/ test.yml b/.gitea/workflows/ test.yml new file mode 100644 index 0000000..f9d8646 --- /dev/null +++ b/.gitea/workflows/ test.yml @@ -0,0 +1,21 @@ +--- + +name: Test + +on: + push: + branches: [master] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '>=1.21' + - name: Run Tests + run: make test \ No newline at end of file diff --git a/code/code.go b/code/code.go index dbd596c..940124c 100644 --- a/code/code.go +++ b/code/code.go @@ -27,6 +27,8 @@ const ( OpJumpNotTruthy OpJump OpNull + OpGetGlobal + OpSetGlobal ) type Definition struct { @@ -51,6 +53,8 @@ var definitions = map[Opcode]*Definition{ OpJumpNotTruthy: {"OpJumpNotTruthy", []int{2}}, OpJump: {"OpJump", []int{2}}, OpNull: {"OpNull", []int{}}, + OpGetGlobal: {"OpGetGlobal", []int{2}}, + OpSetGlobal: {"OpSetGlobal", []int{2}}, } func Lookup(op byte) (*Definition, error) { diff --git a/compiler/compiler.go b/compiler/compiler.go index cbe2dd3..c3de4af 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -18,6 +18,8 @@ type Compiler struct { lastInstruction EmittedInstruction previousInstruction EmittedInstruction + + symbolTable *SymbolTable } func New() *Compiler { @@ -26,6 +28,7 @@ func New() *Compiler { constants: []object.Object{}, lastInstruction: EmittedInstruction{}, previousInstruction: EmittedInstruction{}, + symbolTable: NewSymbolTable(), } } @@ -164,6 +167,22 @@ func (c *Compiler) Compile(node ast.Node) error { } } + case *ast.LetStatement: + err := c.Compile(node.Value) + if err != nil { + return err + } + symbol := c.symbolTable.Define(node.Name.Value) + c.emit(code.OpSetGlobal, symbol.Index) + + case *ast.Identifier: + symbol, ok := c.symbolTable.Resolve(node.Value) + if !ok { + return fmt.Errorf("undefined varible %s", node.Value) + } + + c.emit(code.OpGetGlobal, symbol.Index) + } return nil diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 9d2d9fb..38b6ec2 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -228,6 +228,55 @@ func TestConditionals(t *testing.T) { runCompilerTests(t, tests) } +func TestGlobalLetStatements(t *testing.T) { + tests := []compilerTestCase{ + { + input: ` + let one = 1; + let two = 2; + `, + expectedConstants: []interface{}{1, 2}, + expectedInstructions: []code.Instructions{ + code.Make(code.OpConstant, 0), + code.Make(code.OpSetGlobal, 0), + code.Make(code.OpConstant, 1), + code.Make(code.OpSetGlobal, 1), + }, + }, + { + input: ` + let one = 1; + one; + `, + expectedConstants: []interface{}{1}, + expectedInstructions: []code.Instructions{ + code.Make(code.OpConstant, 0), + code.Make(code.OpSetGlobal, 0), + code.Make(code.OpGetGlobal, 0), + code.Make(code.OpPop), + }, + }, + { + input: ` + let one = 1; + let two = one; + two; + `, + expectedConstants: []interface{}{1}, + expectedInstructions: []code.Instructions{ + code.Make(code.OpConstant, 0), + code.Make(code.OpSetGlobal, 0), + code.Make(code.OpGetGlobal, 0), + code.Make(code.OpSetGlobal, 1), + code.Make(code.OpGetGlobal, 1), + code.Make(code.OpPop), + }, + }, + } + + runCompilerTests(t, tests) +} + func runCompilerTests(t *testing.T, tests []compilerTestCase) { t.Helper() diff --git a/compiler/symbol_table.go b/compiler/symbol_table.go new file mode 100644 index 0000000..2bd84d8 --- /dev/null +++ b/compiler/symbol_table.go @@ -0,0 +1,39 @@ +package compiler + +type SymbolScope string + +const ( + GlobalScope SymbolScope = "Global" +) + +type Symbol struct { + Name string + Scope SymbolScope + Index int +} + +type SymbolTable struct { + store map[string]Symbol + numDefinitions int +} + +func NewSymbolTable() *SymbolTable { + s := make(map[string]Symbol) + return &SymbolTable{store: s} +} + +func (s *SymbolTable) Define(name string) Symbol { + symbol := Symbol{ + Name: name, + Scope: GlobalScope, + Index: s.numDefinitions, + } + s.store[name] = symbol + s.numDefinitions++ + return symbol +} + +func (s *SymbolTable) Resolve(name string) (Symbol, bool) { + obj, ok := s.store[name] + return obj, ok +} diff --git a/compiler/symbol_table_test.go b/compiler/symbol_table_test.go new file mode 100644 index 0000000..b1656dc --- /dev/null +++ b/compiler/symbol_table_test.go @@ -0,0 +1,59 @@ +package compiler + +import "testing" + +func TestDefine(t *testing.T) { + expected := map[string]Symbol{ + "a": { + Name: "a", + Scope: GlobalScope, + Index: 0, + }, + "b": { + Name: "b", + Scope: GlobalScope, + Index: 1, + }, + } + + global := NewSymbolTable() + + a := global.Define("a") + if a != expected["a"] { + t.Errorf("expected a=%+v, got=%+v", expected["a"], a) + } + + b := global.Define("b") + if a != expected["b"] { + t.Errorf("expected b=%+v, got=%+v", expected["b"], b) + } +} + +func TestResolveGlobal(t *testing.T) { + global := NewSymbolTable() + global.Define("a") + global.Define("b") + + expected := []Symbol{ + Symbol{ + Name: "a", + Scope: GlobalScope, + Index: 0, + }, + Symbol{ + Name: "b", + Scope: GlobalScope, + Index: 1, + }, + } + + for _, sym := range expected { + result, ok := global.Resolve(sym.Name) + if !ok { + t.Errorf("name %s not resolvable", sym.Name) + } + if result != sym { + t.Errorf("expected %s to resolve to %+v, got=%+v", sym.Name, sym, result) + } + } +}