From 2f72938274ff5e20c5fe1d6fe0360434544c76dd Mon Sep 17 00:00:00 2001 From: Adam Eury Date: Fri, 19 Jan 2024 11:52:05 -0500 Subject: [PATCH] Add support for declaring variables. --- ast/ast.go | 17 +++++ examples/print.g | 12 ++-- examples/print2.g | 6 ++ gmachine.go | 143 +++++++++++++++++++++++++++++++----------- gmachine_test.go | 54 +++++++++++++++- go.mod | 1 - go.sum | 2 - parser/parser.go | 36 ++++++++++- parser/parser_test.go | 118 ++++++++++++++++++++++++++++++++++ testdata/gc.txtar | 2 +- token/token.go | 2 + token/token_test.go | 2 + 12 files changed, 343 insertions(+), 52 deletions(-) create mode 100644 examples/print2.g diff --git a/ast/ast.go b/ast/ast.go index cf0f284..802403f 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -36,6 +36,15 @@ type ConstantDefinitionStatement struct { func (cds *ConstantDefinitionStatement) statementNode() {} func (cds *ConstantDefinitionStatement) TokenLiteral() string { return cds.Token.Literal } +type VariableDefinitionStatement struct { + Token token.Token // the token.VARIABLE_DEFINITION token + Name *Identifier + Value Expression +} + +func (vds *VariableDefinitionStatement) statementNode() {} +func (vds *VariableDefinitionStatement) TokenLiteral() string { return vds.Token.Literal } + type LabelDefinitionStatement struct { Token token.Token // the token.LABEL_DEFINITION token } @@ -81,3 +90,11 @@ type CharacterLiteral struct { func (cl *CharacterLiteral) expressionNode() {} func (cl *CharacterLiteral) TokenLiteral() string { return cl.Token.Literal } + +type StringLiteral struct { + Token token.Token // the token.STRING token + Value string +} + +func (sl *StringLiteral) expressionNode() {} +func (sl *StringLiteral) TokenLiteral() string { return sl.Token.Literal } diff --git a/examples/print.g b/examples/print.g index 5bd9efa..89a2345 100644 --- a/examples/print.g +++ b/examples/print.g @@ -1,16 +1,12 @@ -@data msg "hello world" +VARB msg "hello world" .run SETX msg ; set X register to the address of msg -JUMP print - -.done -HALT .print -MOVX A ; move the value of address in X to A -JAEZ done ; jump to label 'done' if A = 0 +MOVE *X -> A ; move the value of address in X to A OUTA ; print A INCX ; increment address stored in X -JUMP print ; jump back to the start of .print to print the next character +JANZ print ; jump to label 'done' if A = 0 +HALT diff --git a/examples/print2.g b/examples/print2.g new file mode 100644 index 0000000..8c0dfaa --- /dev/null +++ b/examples/print2.g @@ -0,0 +1,6 @@ +JUMP start + +VARB msg "hello world" + +.start + diff --git a/gmachine.go b/gmachine.go index b7768a1..83ade91 100644 --- a/gmachine.go +++ b/gmachine.go @@ -12,8 +12,6 @@ import ( "io" "os" "strings" - - "golang.org/x/exp/slices" ) const MemSize = 1024 @@ -32,6 +30,7 @@ const ( OpADDA OpMULA OpMOVA + OpMOVE // temporary solution for destinguishing between moving between memory vs between registers OpSETA OpSETX OpSETY @@ -169,6 +168,9 @@ func (g *Machine) Run() { case RegY: g.Y = g.A } + case OpMOVE: + offset := g.Next() + g.A = g.Memory[g.MemOffset+offset] case OpSETA: g.A = g.Next() case OpSETX: @@ -211,14 +213,16 @@ type ref struct { } type symbolTable struct { - labels map[string]Word - consts map[string]Word + labels map[string]Word + consts map[string]Word + variables map[string]Word } func newSymbolTable() *symbolTable { return &symbolTable{ - labels: make(map[string]Word), - consts: make(map[string]Word), + labels: make(map[string]Word), + consts: make(map[string]Word), + variables: make(map[string]Word), } } @@ -230,6 +234,10 @@ func (t *symbolTable) defineConst(name string, value Word) { t.consts[name] = value } +func (t *symbolTable) defineVariable(name string, value Word) { + t.variables[name] = value +} + func (t *symbolTable) lookup(name string) (Word, bool) { if value, ok := t.labels[name]; ok { return value, ok @@ -237,6 +245,9 @@ func (t *symbolTable) lookup(name string) (Word, bool) { if value, ok := t.consts[name]; ok { return value, ok } + if value, ok := t.variables[name]; ok { + return value, ok + } return Word(0), false } @@ -252,10 +263,10 @@ func Assemble(reader io.Reader) ([]Word, error) { p := parser.New(l) astProgram := p.ParseProgram() if astProgram == nil { - return program, errors.New("failed to parse program") + return nil, errors.New("failed to parse program") } if len(p.Errors()) > 0 { - return program, p.Errors()[0] + return nil, p.Errors()[0] } // Assemble program @@ -267,6 +278,20 @@ func Assemble(reader io.Reader) ([]Word, error) { case *ast.LabelDefinitionStatement: name := strings.TrimPrefix(stmt.TokenLiteral(), ".") symbols.defineLabel(name, Word(len(program))) + case *ast.VariableDefinitionStatement: + symbols.defineVariable(stmt.Name.Value, Word(len(program))) + switch operand := stmt.Value.(type) { + case *ast.IntegerLiteral: + program = append(program, Word(operand.Value)) + case *ast.StringLiteral: + strSlice := make([]Word, len(operand.Value)) + for i, c := range operand.Value { + strSlice[i] = Word(c) + } + program = append(program, strSlice...) + default: + return nil, errors.New("invalid variable definition") + } case *ast.OpcodeStatement: program, refs, err = assembleOpcodeStatement(stmt, program, refs) if err != nil { @@ -290,49 +315,95 @@ func Assemble(reader io.Reader) ([]Word, error) { } func assembleOpcodeStatement(stmt *ast.OpcodeStatement, program []Word, refs []ref) ([]Word, []ref, error) { - opcode, ok := opcodes[stmt.TokenLiteral()] - if !ok { - return nil, nil, fmt.Errorf("%w: %s at line %d", ErrUndefinedInstruction, stmt.TokenLiteral(), stmt.Token.Line) - } - program = append(program, opcode) - if stmt.Operand == nil { + opcode, ok := opcodes[stmt.TokenLiteral()] + if !ok { + return nil, nil, fmt.Errorf("%w: %s at line %d", ErrUndefinedInstruction, stmt.TokenLiteral(), stmt.Token.Line) + } + program = append(program, opcode) return program, refs, nil } - switch operand := stmt.Operand.(type) { - case *ast.RegisterLiteral: - if !slices.Contains([]Word{OpADDA, OpMULA, OpMOVA}, opcode) { + opcodeStr := stmt.TokenLiteral() + + switch opcodeStr { + case "MOVA": + switch operand := stmt.Operand.(type) { + case *ast.RegisterLiteral: + register, ok := registers[operand.TokenLiteral()] + if !ok { + return nil, nil, fmt.Errorf("%w: %s at line %d", ErrInvalidRegister, operand.TokenLiteral(), operand.Token.Line) + } + program = append(program, OpMOVA, register) + case *ast.Identifier: + program = append(program, OpMOVE) + r := ref{ + Name: operand.TokenLiteral(), + Line: operand.Token.Line, + Address: Word(len(program)), + } + refs = append(refs, r) + program = append(program, Word(0)) + default: return nil, nil, fmt.Errorf("%w: %s at line %d", ErrInvalidOperand, stmt.TokenLiteral(), stmt.Token.Line) } - register, ok := registers[operand.TokenLiteral()] + case "MULA", "ADDA": + opcode, ok := opcodes[opcodeStr] if !ok { - return nil, nil, fmt.Errorf("%w: %s at line %d", ErrInvalidRegister, operand.TokenLiteral(), operand.Token.Line) + return nil, nil, fmt.Errorf("%w: %s at line %d", ErrUndefinedInstruction, stmt.TokenLiteral(), stmt.Token.Line) } - program = append(program, register) - case *ast.Identifier: - if !slices.Contains([]Word{OpSETA, OpJUMP, OpJXNZ}, opcode) { + switch operand := stmt.Operand.(type) { + case *ast.RegisterLiteral: + register, ok := registers[operand.TokenLiteral()] + if !ok { + return nil, nil, fmt.Errorf("%w: %s at line %d", ErrInvalidRegister, operand.TokenLiteral(), operand.Token.Line) + } + program = append(program, opcode, register) + default: return nil, nil, fmt.Errorf("%w: %s at line %d", ErrInvalidOperand, stmt.TokenLiteral(), stmt.Token.Line) } - r := ref{ - Name: operand.TokenLiteral(), - Line: operand.Token.Line, - Address: Word(len(program)), + case "SETA", "SETX", "SETY": + opcode, ok := opcodes[opcodeStr] + if !ok { + return nil, nil, fmt.Errorf("%w: %s at line %d", ErrUndefinedInstruction, stmt.TokenLiteral(), stmt.Token.Line) } - refs = append(refs, r) - program = append(program, Word(0)) - case *ast.IntegerLiteral: - if !slices.Contains([]Word{OpSETA, OpSETX, OpSETY, OpJUMP}, opcode) { + switch operand := stmt.Operand.(type) { + case *ast.IntegerLiteral: + program = append(program, opcode, Word(operand.Value)) + case *ast.CharacterLiteral: + program = append(program, opcode, Word(operand.Value)) + case *ast.Identifier: + program = append(program, opcode) + r := ref{ + Name: operand.TokenLiteral(), + Line: operand.Token.Line, + Address: Word(len(program)), + } + refs = append(refs, r) + program = append(program, Word(0)) + default: return nil, nil, fmt.Errorf("%w: %s at line %d", ErrInvalidOperand, stmt.TokenLiteral(), stmt.Token.Line) } - program = append(program, Word(operand.Value)) - case *ast.CharacterLiteral: - if !slices.Contains([]Word{OpSETA, OpSETX, OpSETY}, opcode) { + case "JUMP", "JXNZ": + opcode, ok := opcodes[opcodeStr] + if !ok { + return nil, nil, fmt.Errorf("%w: %s at line %d", ErrUndefinedInstruction, stmt.TokenLiteral(), stmt.Token.Line) + } + switch operand := stmt.Operand.(type) { + case *ast.IntegerLiteral: + program = append(program, opcode, Word(operand.Value)) + case *ast.Identifier: + program = append(program, opcode) + r := ref{ + Name: operand.TokenLiteral(), + Line: operand.Token.Line, + Address: Word(len(program)), + } + refs = append(refs, r) + program = append(program, Word(0)) + default: return nil, nil, fmt.Errorf("%w: %s at line %d", ErrInvalidOperand, stmt.TokenLiteral(), stmt.Token.Line) } - program = append(program, Word(operand.Value)) - default: - return nil, nil, fmt.Errorf("%w: %s at line %d", ErrInvalidOperand, stmt.TokenLiteral(), stmt.Token.Line) } return program, refs, nil diff --git a/gmachine_test.go b/gmachine_test.go index f912f2c..472eb4b 100644 --- a/gmachine_test.go +++ b/gmachine_test.go @@ -594,11 +594,11 @@ func TestMOVAY(t *testing.T) { } } -func TestMOVA_FailsForInvalidRegister(t *testing.T) { +func TestMOVA_FailsForUnknownIdentifier(t *testing.T) { t.Parallel() g := gmachine.New(nil) err := assembleAndRunFromString(g, "MOVA Z") - wantErr := gmachine.ErrInvalidOperand + wantErr := gmachine.ErrUnknownIdentifier if err == nil { t.Fatal("expected an error to be returned for invalid argument to MOVA") } @@ -836,6 +836,53 @@ OUTA } } +func TestVARB_DeclaresAIntegerVariableInMemory(t *testing.T) { + t.Parallel() + g := gmachine.New(nil) + err := assembleAndRunFromString(g, `VARB num 42`) + if err != nil { + t.Fatal("didn't expect an error:", err) + } + var want gmachine.Word = 42 + got := g.Memory[g.MemOffset] + if want != got { + t.Errorf("want num %d, got %d", want, got) + } +} + +func TestMOVA_MovesAVariableToAccumulatorRegister(t *testing.T) { + t.Parallel() + g := gmachine.New(nil) + err := assembleAndRunFromString(g, ` +JUMP start +VARB num 42 +.start +MOVA num +HALT +`) + if err != nil { + t.Fatal("didn't expect an error:", err) + } + var wantA gmachine.Word = 42 + if wantA != g.A { + t.Errorf("want A %d, got %d", wantA, g.A) + } +} + +func TestVARB_DeclaresAStringVariableInMemory(t *testing.T) { + t.Parallel() + g := gmachine.New(nil) + err := assembleAndRunFromString(g, `VARB msg "hello world"`) + if err != nil { + t.Fatal("didn't expect an error:", err) + } + want := []gmachine.Word{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'} + got := g.Memory[int(g.MemOffset) : int(g.MemOffset)+len(want)] + if !cmp.Equal(want, got) { + t.Error(cmp.Diff(want, got)) + } +} + func TestCompile(t *testing.T) { t.Parallel() @@ -890,7 +937,8 @@ func TestCompile_FailsForWriteError(t *testing.T) { } func assembleFromString(input string) ([]gmachine.Word, error) { - return gmachine.Assemble(strings.NewReader(input)) + program, err := gmachine.Assemble(strings.NewReader(input)) + return program, err } func assembleAndRunFromString(g *gmachine.Machine, input string) error { diff --git a/go.mod b/go.mod index 1d0b1a3..51a4626 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.20 require ( github.com/google/go-cmp v0.5.9 github.com/rogpeppe/go-internal v1.11.0 - golang.org/x/exp v0.0.0-20231006140011-7918f672742d ) require ( diff --git a/go.sum b/go.sum index 85bcca2..e64be72 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= diff --git a/parser/parser.go b/parser/parser.go index 5ab9649..c0de7f8 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -12,8 +12,9 @@ import ( ) var ErrInvalidOperand error = errors.New("invalid operand") -var ErrInvalidConstDefinition error = errors.New("invalid constant definition") var ErrInvalidIntegerLiteral error = errors.New("invalid integer literal") +var ErrInvalidConstDefinition error = errors.New("invalid constant definition") +var ErrInvalidVariableDefinition error = errors.New("invalid variable definition") type expressionParserFn func() ast.Expression @@ -33,6 +34,7 @@ func New(l *lexer.Lexer) *Parser { p.exprParsers[token.IDENT] = p.parseIdentifier p.exprParsers[token.INT] = p.parseIntegerLiteral p.exprParsers[token.CHAR] = p.parseCharacterLiteral + p.exprParsers[token.STRING] = p.parseStringLiteral // Read two tokens, so curToken and peekToken are both set p.nextToken() @@ -73,11 +75,39 @@ func (p *Parser) parseStatement() ast.Statement { return p.parseLabelDefinitionStatement() case token.CONSTANT_DEFINITION: return p.parseConstantDefinitionStatement() + case token.VARIABLE_DEFINITION: + return p.parseVariableDefinitionStatement() default: return nil } } +func (p *Parser) parseVariableDefinitionStatement() ast.Statement { + stmt := &ast.VariableDefinitionStatement{Token: p.curToken} + + if p.peekToken.Type != token.IDENT { + p.errors = append(p.errors, fmt.Errorf("%w: %s at line %d", ErrInvalidVariableDefinition, p.peekToken.Literal, p.peekToken.Line)) + return nil + } + + p.nextToken() + stmt.Name = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal} + + switch p.peekToken.Type { + case token.INT: + p.nextToken() + stmt.Value = p.parseIntegerLiteral() + case token.STRING: + p.nextToken() + stmt.Value = p.parseStringLiteral() + default: + p.errors = append(p.errors, fmt.Errorf("%w: %s at line %d", ErrInvalidVariableDefinition, p.peekToken.Literal, p.peekToken.Line)) + return nil + } + + return stmt +} + func (p *Parser) parseConstantDefinitionStatement() ast.Statement { stmt := &ast.ConstantDefinitionStatement{Token: p.curToken} @@ -137,6 +167,10 @@ func (p *Parser) parseIntegerLiteral() ast.Expression { return intLiteral } +func (p *Parser) parseStringLiteral() ast.Expression { + return &ast.StringLiteral{Token: p.curToken, Value: p.curToken.Literal} +} + func (p *Parser) parseCharacterLiteral() ast.Expression { charLiteral := &ast.CharacterLiteral{Token: p.curToken} diff --git a/parser/parser_test.go b/parser/parser_test.go index 3ed2431..44aa253 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -105,6 +105,124 @@ func TestParseProgram_ParsesConstantDefinition(t *testing.T) { } } +func TestParseProgram_ParsesStringVariableDefinition(t *testing.T) { + t.Parallel() + + input := `VARB msg "hello"` + l := newLexerFromString(input) + p := parser.New(l) + + program := p.ParseProgram() + if program == nil { + t.Fatal("ParseProgram() returned nil") + } + + wantStatements := 1 + gotStatements := len(program.Statements) + if wantStatements != gotStatements { + t.Fatalf("program.Statements does not contain %d statements. got %d", wantStatements, gotStatements) + } + + tests := []struct { + wantVarb string + wantIdentifier string + wantValue string + }{ + {"VARB", "msg", "hello"}, + } + + for i, tt := range tests { + stmt := program.Statements[i] + if stmt.TokenLiteral() != tt.wantVarb { + t.Fatalf("stmt.TokenLiteral not %s. got=%q", tt.wantVarb, stmt.TokenLiteral()) + } + + varbDefn, ok := stmt.(*ast.VariableDefinitionStatement) + if !ok { + t.Fatalf("want stmt *ast.VariableDefinitionStatement. got=%T", stmt) + } + + ident := varbDefn.Name + if ident == nil { + t.Fatal("didn't expect variable definition identifier to be nil") + } + if ident.Value != tt.wantIdentifier { + t.Fatalf("want identifier %s, got %s", tt.wantIdentifier, ident.Value) + } + + value := varbDefn.Value + if value == nil { + t.Fatal("didn't expect value expression to be nil") + } + valueExpr, ok := value.(*ast.StringLiteral) + if !ok { + t.Fatalf("value not *ast.StringLiteral. got=%T", value) + } + if valueExpr.Value != tt.wantValue { + t.Fatalf("wanted value %s, got %s", tt.wantValue, valueExpr.Value) + } + } +} + +func TestParseProgram_ParsesIntegerVariableDefinition(t *testing.T) { + t.Parallel() + + input := `VARB num 100` + l := newLexerFromString(input) + p := parser.New(l) + + program := p.ParseProgram() + if program == nil { + t.Fatal("ParseProgram() returned nil") + } + + wantStatements := 1 + gotStatements := len(program.Statements) + if wantStatements != gotStatements { + t.Fatalf("program.Statements does not contain %d statements. got %d", wantStatements, gotStatements) + } + + tests := []struct { + wantVarb string + wantIdentifier string + wantValue uint64 + }{ + {"VARB", "num", 100}, + } + + for i, tt := range tests { + stmt := program.Statements[i] + if stmt.TokenLiteral() != tt.wantVarb { + t.Fatalf("stmt.TokenLiteral not %s. got=%q", tt.wantVarb, stmt.TokenLiteral()) + } + + varbDefn, ok := stmt.(*ast.VariableDefinitionStatement) + if !ok { + t.Fatalf("want stmt *ast.VariableDefinitionStatement. got=%T", stmt) + } + + ident := varbDefn.Name + if ident == nil { + t.Fatal("didn't expect variable definition identifier to be nil") + } + if ident.Value != tt.wantIdentifier { + t.Fatalf("want identifier %s, got %s", tt.wantIdentifier, ident.Value) + } + + value := varbDefn.Value + if value == nil { + t.Fatal("didn't expect value expression to be nil") + } + valueExpr, ok := value.(*ast.IntegerLiteral) + if !ok { + t.Fatalf("value not *ast.IntegerLiteral. got=%T", value) + } + if valueExpr.Value != tt.wantValue { + t.Fatalf("wanted value %d, got %d", tt.wantValue, valueExpr.Value) + } + } +} + func TestParseProgram_ParsesOpcodesWithoutOperand(t *testing.T) { t.Parallel() diff --git a/testdata/gc.txtar b/testdata/gc.txtar index 992755a..dc79569 100644 --- a/testdata/gc.txtar +++ b/testdata/gc.txtar @@ -9,6 +9,6 @@ SETA 42 OUTA -- want -- -0000000 0000 0000 0000 0d00 0000 0000 0000 2a00 +0000000 0000 0000 0000 0e00 0000 0000 0000 2a00 0000010 0000 0000 0000 0300 0000018 diff --git a/token/token.go b/token/token.go index 8d8dd35..01b588a 100644 --- a/token/token.go +++ b/token/token.go @@ -8,6 +8,7 @@ const ( REGISTER = "REGISTER" LABEL_DEFINITION = "LABEL_DEFINITION" CONSTANT_DEFINITION = "CONSTANT_DEFINITION" + VARIABLE_DEFINITION = "VARIABLE_DEFINITION" IDENT = "IDENT" INT = "INT" CHAR = "CHAR" @@ -43,6 +44,7 @@ var opcodes = map[string]TokenType{ var pragmas = map[string]TokenType{ "CONS": CONSTANT_DEFINITION, + "VARB": VARIABLE_DEFINITION, } type TokenType string diff --git a/token/token_test.go b/token/token_test.go index f4248cb..3a9035c 100644 --- a/token/token_test.go +++ b/token/token_test.go @@ -11,6 +11,8 @@ func TestLookupIdent(t *testing.T) { given string want token.TokenType }{ + {"CONS", token.CONSTANT_DEFINITION}, + {"VARB", token.VARIABLE_DEFINITION}, {"HALT", token.OPCODE}, {"NOOP", token.OPCODE}, {"OUTA", token.OPCODE},