diff --git a/.vscode/launch.json b/.vscode/launch.json index 5c7247b..55240e5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,5 +3,13 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", - "configurations": [] + "configurations": [ + { + "name": "Attach to Process", + "type": "go", + "request": "attach", + "mode": "local", + "processId": 0 + } + ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 1b49d42..6a8c7c1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { "cSpell.words": [ - "glox" + "glox", + "PAREN", + "stmts" ] } \ No newline at end of file diff --git a/ast.go b/ast.go deleted file mode 100644 index 937f159..0000000 --- a/ast.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -type Visitor interface { - visitUnary(u *Unary) any - visitBinary(b *Binary) any - visitLiteral(l *Literal) any - visitGrouping(g *Grouping) any -} - -type Expr interface { - expr() - accept(v Visitor) any -} - -type Unary struct { - op Token - right Expr -} - -type Binary struct { - left Expr - op Token - right Expr -} - -type Literal struct { - value any -} - -type Grouping struct { - expression Expr -} - -func (u *Unary) expr() {} -func (b *Binary) expr() {} -func (l *Literal) expr() {} -func (g *Grouping) expr() {} - -func (u *Unary) accept(v Visitor) any { - return v.visitUnary(u) -} - -func (b *Binary) accept(v Visitor) any { - return v.visitBinary(b) -} - -func (l *Literal) accept(v Visitor) any { - return v.visitLiteral(l) -} - -func (g *Grouping) accept(v Visitor) any { - return v.visitGrouping(g) -} diff --git a/ast_rpn.go b/ast_rpn.go deleted file mode 100644 index 96b2e2f..0000000 --- a/ast_rpn.go +++ /dev/null @@ -1,46 +0,0 @@ -package main - -import ( - "fmt" - "strings" -) - -type AstToRPN struct { - str strings.Builder -} - -func (as AstToRPN) String(expr Expr) string { - - if expr == nil { - return "" - } - - expr.accept(&as) - return as.str.String() -} - -func (as *AstToRPN) visitBinary(b *Binary) any { - b.left.accept(as) - as.str.WriteString(" ") - b.right.accept(as) - as.str.WriteString(" ") - as.str.WriteString(b.op.lexeme) - return nil -} - -func (as *AstToRPN) visitLiteral(l *Literal) any { - as.str.WriteString(fmt.Sprintf("%v", l.value)) - return nil -} - -func (as *AstToRPN) visitGrouping(g *Grouping) any { - g.expression.accept(as) - as.str.WriteString(" group") - return nil -} - -func (as *AstToRPN) visitUnary(u *Unary) any { - u.right.accept(as) - as.str.WriteString(fmt.Sprintf(" %s", u.op.lexeme)) - return nil -} diff --git a/ast_string.go b/ast_string.go index f54f2f9..bb115e8 100644 --- a/ast_string.go +++ b/ast_string.go @@ -9,13 +9,12 @@ type AstStringer struct { str strings.Builder } -func (as AstStringer) String(expr Expr) string { +func (as AstStringer) String(stmts []Stmt) string { - if expr == nil { - return "" + for _, stmt := range stmts { + stmt.accept(&as) } - expr.accept(&as) return as.str.String() } @@ -49,3 +48,35 @@ func (as *AstStringer) visitUnary(u *Unary) any { as.str.WriteString(")") return nil } + +func (as *AstStringer) visitVariable(va *Variable) any { + as.str.WriteString(va.name.lexeme) + return nil +} + +func (as *AstStringer) visitAssignment(a *Assign) any { + as.str.WriteString(fmt.Sprintf("(= %s ", a.variable.lexeme)) + a.value.accept(as) + as.str.WriteString(")") + return nil +} + +func (as *AstStringer) visitPrintStmt(p *PrintStmt) { + as.str.WriteString("(print ") + p.val.accept(as) + as.str.WriteString(")") +} + +func (as *AstStringer) visitExprStmt(se *ExprStmt) { + se.expr.accept(as) +} + +func (as *AstStringer) visitVarStmt(vs *VarStmt) { + if vs.initializer != nil { + as.str.WriteString(fmt.Sprintf("(var %v ", vs.name.literal)) + vs.initializer.accept(as) + as.str.WriteString(")") + } else { + as.str.WriteString(fmt.Sprintf("(var %v)", vs.name.literal)) + } +} diff --git a/error.go b/error.go deleted file mode 100644 index 8f1544f..0000000 --- a/error.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "fmt" - "log" -) - -var hadError = false -var hadRuntimeError = false - -func printError(token Token, message string) { - if token.typ == EOF { - report(token.line, " at and", message) - } else { - report(token.line, fmt.Sprintf(" at '%s'", token.lexeme), message) - } -} - -func report(line int, where string, message string) { - log.Printf("[%d] Error %s: %s", line, where, message) - hadError = true -} - -func reportRuntimeError(token Token, message string) { - log.Printf("[%d] Error: %s", token.line, message) - hadRuntimeError = true -} diff --git a/expr.go b/expr.go new file mode 100644 index 0000000..730b558 --- /dev/null +++ b/expr.go @@ -0,0 +1,74 @@ +package main + +type ExprVisitor interface { + visitUnary(u *Unary) any + visitBinary(b *Binary) any + visitLiteral(l *Literal) any + visitGrouping(g *Grouping) any + visitVariable(v *Variable) any + visitAssignment(a *Assign) any +} + +type Expr interface { + expr() + accept(v ExprVisitor) any +} + +type Unary struct { + op Token + right Expr +} + +type Binary struct { + left Expr + op Token + right Expr +} + +type Literal struct { + value any +} + +type Grouping struct { + expression Expr +} + +type Variable struct { + name Token +} + +type Assign struct { + variable Token + value Expr +} + +func (u *Unary) expr() {} +func (b *Binary) expr() {} +func (l *Literal) expr() {} +func (g *Grouping) expr() {} +func (v *Variable) expr() {} +func (a *Assign) expr() {} + +func (u *Unary) accept(v ExprVisitor) any { + return v.visitUnary(u) +} + +func (b *Binary) accept(v ExprVisitor) any { + return v.visitBinary(b) +} + +func (l *Literal) accept(v ExprVisitor) any { + return v.visitLiteral(l) +} + +func (g *Grouping) accept(v ExprVisitor) any { + return v.visitGrouping(g) +} + +func (va *Variable) accept(v ExprVisitor) any { + return v.visitVariable(va) +} + +func (a *Assign) accept(v ExprVisitor) any { + return v.visitAssignment(a) +} diff --git a/expr_rpn.go b/expr_rpn.go new file mode 100644 index 0000000..53b67bc --- /dev/null +++ b/expr_rpn.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "strings" +) + +type ExprToRPN struct { + str strings.Builder +} + +func (as ExprToRPN) String(expr Expr) string { + + if expr == nil { + return "" + } + + expr.accept(&as) + return as.str.String() +} + +func (as *ExprToRPN) visitBinary(b *Binary) any { + b.left.accept(as) + as.str.WriteString(" ") + b.right.accept(as) + as.str.WriteString(" ") + as.str.WriteString(b.op.lexeme) + return nil +} + +func (as *ExprToRPN) visitLiteral(l *Literal) any { + as.str.WriteString(fmt.Sprintf("%v", l.value)) + return nil +} + +func (as *ExprToRPN) visitGrouping(g *Grouping) any { + g.expression.accept(as) + as.str.WriteString(" group") + return nil +} + +func (as *ExprToRPN) visitUnary(u *Unary) any { + u.right.accept(as) + as.str.WriteString(fmt.Sprintf(" %s", u.op.lexeme)) + return nil +} + +func (as *ExprToRPN) visitVariable(va *Variable) any { + as.str.WriteString(va.name.lexeme) + return nil +} + +func (as *ExprToRPN) visitAssignment(a *Assign) any { + as.str.WriteString(fmt.Sprintf("%v %s =", a.value, a.variable.lexeme)) + return nil +} diff --git a/glox b/glox new file mode 100755 index 0000000..8b9205c Binary files /dev/null and b/glox differ diff --git a/glox.go b/glox.go index 4feeb79..a24620d 100644 --- a/glox.go +++ b/glox.go @@ -2,83 +2,73 @@ package main import ( "bufio" - "fmt" "log" "os" ) +type Glox struct { + Interpreter *Interpreter +} + func main() { + glox := &Glox{newInterpreter()} switch len(os.Args) { case 1: - runPrompt() + glox.runPrompt() case 2: - runFile(os.Args[1]) + glox.runFile(os.Args[1]) default: println("Usage: glox [file]") os.Exit(1) } } -func runPrompt() { +func (gl *Glox) runPrompt() { scanner := bufio.NewScanner(os.Stdin) scanner.Split(bufio.ScanLines) for { print("> ") - scanner.Scan() - line := scanner.Text() - if len(line) == 0 { + if !scanner.Scan() { break } - run([]byte(scanner.Text())) - hadError = false - hadRuntimeError = false + gl.run(scanner.Bytes(), true) } } -func runFile(path string) { +func (gl *Glox) runFile(path string) { file, err := os.ReadFile(path) - try(err) - - run(file) - - switch { - case hadError: - os.Exit(65) - case hadRuntimeError: - os.Exit(70) - default: - os.Exit(0) - } -} - -func run(source []byte) { - tokens := newScanner(source).scan() - - if hadError { - return - } - - ast := newParser(tokens).parse() - - if hadError { - return - } - - println(AstStringer{}.String(ast)) - - res := newInterpreter().evaluate(ast) - - if hadRuntimeError { - return - } - - fmt.Printf("%v\n", res) -} - -func try(err error) { if err != nil { log.Fatal(err) } + + runErrors := gl.run(file, false) + + if len(runErrors) != 0 { + for _, e := range runErrors { + log.Print(e) + } + + os.Exit(1) + } + +} + +func (gl *Glox) run(source []byte, interactive bool) []error { + tokens, err := newScanner(source).scan() + + if err != nil { + return []error{err} + } + + stmts, parseErrs := newParser(tokens).parse() + + if len(parseErrs) != 0 && !interactive { + return parseErrs + } + + println(AstStringer{}.String(stmts)) + + return gl.Interpreter.interpret(stmts) } diff --git a/interpreter.go b/interpreter.go index d621fc4..53807f7 100644 --- a/interpreter.go +++ b/interpreter.go @@ -2,67 +2,79 @@ package main import ( "fmt" + "log" "reflect" ) -type Interpreter struct{} +type Interpreter struct { + env map[string]any + errors []error +} type RuntimeError struct { token Token msg string } -func (re RuntimeError) Error() string { - return re.msg +func (re *RuntimeError) Error() string { + return fmt.Sprintf("RuntimeError [%d][%s] Error: %s", re.token.line, re.token.typ, re.msg) } func newInterpreter() *Interpreter { - return &Interpreter{} + return &Interpreter{env: make(map[string]any)} } -func (i *Interpreter) evaluate(e Expr) any { +func (i *Interpreter) interpret(stmts []Stmt) []error { defer i.recover() - return e.accept(i) + + i.errors = []error{} + + for _, stmt := range stmts { + stmt.accept(i) + } + + return i.errors } func (i *Interpreter) recover() { if err := recover(); err != nil { - pe, ok := err.(RuntimeError) + _, ok := err.(*RuntimeError) if !ok { panic(err) } - - reportRuntimeError(pe.token, pe.msg) - hadRuntimeError = true } } +func (i *Interpreter) evaluate(e Expr) any { + return e.accept(i) +} + func (i *Interpreter) visitBinary(b *Binary) any { left := i.evaluate(b.left) right := i.evaluate(b.right) switch b.op.typ { case MINUS: - checkIfFloats(b.op, left, right) + i.checkIfFloats(b.op, left, right) return left.(float64) - right.(float64) case SLASH: - checkIfFloats(b.op, left, right) + i.checkIfFloats(b.op, left, right) return left.(float64) / right.(float64) case STAR: - checkIfFloats(b.op, left, right) + i.checkIfFloats(b.op, left, right) return left.(float64) * right.(float64) case GREATER: - checkIfFloats(b.op, left, right) + i.checkIfFloats(b.op, left, right) return left.(float64) > right.(float64) case LESS: - checkIfFloats(b.op, left, right) + i.checkIfFloats(b.op, left, right) return left.(float64) < right.(float64) case GREATER_EQUAL: - checkIfFloats(b.op, left, right) + i.checkIfFloats(b.op, left, right) return left.(float64) >= right.(float64) case LESS_EQUAL: - checkIfFloats(b.op, left, right) + i.checkIfFloats(b.op, left, right) return left.(float64) <= right.(float64) case BANG_EQUAL: return !reflect.DeepEqual(left, right) @@ -78,7 +90,7 @@ func (i *Interpreter) visitBinary(b *Binary) any { } } - panic(RuntimeError{b.op, fmt.Sprintf("Operands must be numbers or strings: %v %s %v", left, b.op.lexeme, right)}) + i.panic(&RuntimeError{b.op, fmt.Sprintf("Operands must be numbers or strings: %v %s %v", left, b.op.lexeme, right)}) return nil } @@ -96,7 +108,7 @@ func (i *Interpreter) visitUnary(u *Unary) any { switch u.op.typ { case MINUS: - checkIfFloat(u.op, val) + i.checkIfFloat(u.op, val) return -val.(float64) case BANG: return !isTruthy(val) @@ -105,20 +117,67 @@ func (i *Interpreter) visitUnary(u *Unary) any { return nil } -func checkIfFloat(op Token, val any) { +func (i *Interpreter) visitVariable(v *Variable) any { + if found, ok := i.env[v.name.lexeme]; ok { + return found + } + + i.panic(&RuntimeError{v.name, fmt.Sprintf("Undefined variable '%s'.", v.name.lexeme)}) + + return nil +} + +func (i *Interpreter) visitAssignment(a *Assign) any { + if _, ok := i.env[a.variable.lexeme]; ok { + val := i.evaluate(a.value) + i.env[a.variable.lexeme] = val + return val + } + + i.panic(&RuntimeError{a.variable, fmt.Sprintf("Undefined variable '%s'.", a.variable.lexeme)}) + + return nil +} + +func (i *Interpreter) visitPrintStmt(p *PrintStmt) { + fmt.Printf("%v\n", i.evaluate(p.val)) +} + +func (i *Interpreter) visitExprStmt(se *ExprStmt) { + i.evaluate(se.expr) +} + +func (i *Interpreter) visitVarStmt(v *VarStmt) { + + var val any = nil + + if v.initializer != nil { + val = i.evaluate(v.initializer) + } + + i.env[v.name.lexeme] = val +} + +func (i *Interpreter) panic(re *RuntimeError) { + i.errors = append(i.errors, re) + log.Println(re) + panic(re) +} + +func (i *Interpreter) checkIfFloat(op Token, val any) { if _, ok := val.(float64); ok { return } - panic(RuntimeError{op, "value must ne a number."}) + i.panic(&RuntimeError{op, "value must be a number."}) } -func checkIfFloats(op Token, a any, b any) { +func (i *Interpreter) checkIfFloats(op Token, a any, b any) { if isFloats(a, b) { return } - panic(RuntimeError{op, fmt.Sprintf("Operands must be numbers: %v %s %v", a, op.lexeme, b)}) + i.panic(&RuntimeError{op, fmt.Sprintf("Operands must be numbers: %v %s %v", a, op.lexeme, b)}) } func isFloats(a any, b any) bool { diff --git a/parser.go b/parser.go index 11cf9bb..aa7e956 100644 --- a/parser.go +++ b/parser.go @@ -2,11 +2,13 @@ package main import ( "fmt" + "log" ) type Parser struct { tokens []Token current int + errors []error } type ParseError struct { @@ -14,8 +16,8 @@ type ParseError struct { message string } -func (pe ParseError) Error() string { - return fmt.Sprintf("%s: %s", pe.token.lexeme, pe.message) +func (pe *ParseError) Error() string { + return fmt.Sprintf("ParseError [%d][%s]: %s", pe.token.line, pe.token.typ, pe.message) } func newParser(tokens []Token) *Parser { @@ -25,21 +27,88 @@ func newParser(tokens []Token) *Parser { } } -func (p *Parser) parse() Expr { +// program -> declaration* EOF +func (p *Parser) parse() ([]Stmt, []error) { defer p.recover() - return p.expression() -} -func (p *Parser) recover() { - if err := recover(); err != nil { - pe := err.(ParseError) - printError(pe.token, pe.message) + stmts := []Stmt{} + + for !p.isAtEnd() { + + if stmt := p.declaration(); stmt != nil { + stmts = append(stmts, stmt) + } } + + return stmts, p.errors } -// expression -> equality +// declaration -> varDecl | statement +func (p *Parser) declaration() Stmt { + defer p.synchronize() + if p.match(VAR) { + return p.varDecl() + } + return p.statement() +} + +// varDecl -> "var" IDENTIFIER ("=" expression)? ";" +func (p *Parser) varDecl() Stmt { + name := p.consume(IDENTIFIER, "expect identifier for variable") + + var initializer Expr = nil + if p.match(EQUAL) { + initializer = p.expression() + } + + p.consume(SEMICOLON, "Expect ';' after expression.") + + return &VarStmt{name, initializer} +} + +// statement -> exprStmt | printStmt +func (p *Parser) statement() Stmt { + if p.match(PRINT) { + return p.printStmt() + } + return p.exprStmt() +} + +// exprStmt -> expression ";" +func (p *Parser) exprStmt() Stmt { + expr := p.expression() + p.consume(SEMICOLON, "Expect ';' after expression.") + return &ExprStmt{expr} +} + +// printStmt -> "print" expression ";" +func (p *Parser) printStmt() Stmt { + expr := p.expression() + p.consume(SEMICOLON, "Expect ';' after expression.") + return &PrintStmt{expr} +} + +// expression -> assignment func (p *Parser) expression() Expr { - return p.equality() + return p.assignment() +} + +// assignment -> IDENTIFIER "=" assignment | equality +func (p *Parser) assignment() Expr { + expr := p.equality() + + if p.match(EQUAL) { + eq := p.previous() + val := p.assignment() + + if variable, ok := expr.(*Variable); ok { + return &Assign{variable.name, val} + } + + p.panic(&ParseError{eq, "Invalid assignment target."}) + } + + return expr } // equality -> comparison ( ( "==" | "!=" ) comparison )* @@ -105,7 +174,7 @@ func (p *Parser) unary() Expr { return p.primary() } -// primary -> NUMBER | STRING | "true" | "false" | "nil" | "(" expression ")" +// primary -> NUMBER | STRING | "true" | "false" | "nil" | "(" expression ")" | IDENTIFIER func (p *Parser) primary() Expr { switch { case p.match(FALSE): @@ -120,13 +189,17 @@ func (p *Parser) primary() Expr { return &Literal{p.previous().literal} } + if p.match(IDENTIFIER) { + return &Variable{p.previous()} + } + if p.match(LEFT_PAREN) { expr := p.expression() p.consume(RIGHT_PAREN, "Expect ')' after expression") return &Grouping{expr} } - panic(ParseError{p.peek(), "Expect expression"}) + p.panic(&ParseError{p.peek(), "Expect expression"}) return nil } @@ -176,12 +249,20 @@ func (p *Parser) consume(typ TokenType, mes string) Token { return p.advance() } - panic(ParseError{p.peek(), mes}) + p.panic(&ParseError{p.peek(), mes}) return Token{} } func (p *Parser) synchronize() { + err := recover() + + pe := p.isParseError(err) + + if pe == nil { + return + } + p.advance() for !p.isAtEnd() { @@ -196,4 +277,29 @@ func (p *Parser) synchronize() { p.advance() } + +} + +func (p *Parser) recover() { + p.isParseError(recover()) +} + +func (p *Parser) panic(pe *ParseError) { + p.errors = append(p.errors, pe) + log.Println(pe) + panic(pe) +} + +func (p *Parser) isParseError(err any) *ParseError { + if err == nil { + return nil + } + + pe, ok := err.(*ParseError) + + if !ok { + panic(err) + } + + return pe } diff --git a/scanner.go b/scanner.go index 0d4a12e..a933d4b 100644 --- a/scanner.go +++ b/scanner.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "log" "regexp" "strconv" "unicode" @@ -91,19 +92,29 @@ func (t *Token) String() string { return fmt.Sprintf("%s - %s - %v", t.typ, t.lexeme, t.literal) } +type ScanError struct { + line int + message string +} + +func (se *ScanError) Error() string { + return fmt.Sprintf("ScanError [%d] Error: %s", se.line, se.message) +} + type Scanner struct { source []byte tokens []Token start int current int line int + err error } func newScanner(source []byte) *Scanner { - return &Scanner{source: source, start: 0, current: 0, line: 1} + return &Scanner{source: source, start: 0, current: 0, line: 1, err: nil} } -func (s *Scanner) scan() []Token { +func (s *Scanner) scan() ([]Token, error) { for !s.isAtEnd() { s.start = s.current @@ -112,7 +123,7 @@ func (s *Scanner) scan() []Token { s.tokens = append(s.tokens, Token{EOF, "EOF", struct{}{}, s.line}) - return s.tokens + return s.tokens, s.err } func (s *Scanner) scanToken() { @@ -191,7 +202,7 @@ func (s *Scanner) scanToken() { break } - report(s.line, "", fmt.Sprintf("Unexpected character %s", string(c))) + s.error(&ScanError{s.line, fmt.Sprintf("Unexpected character %s", string(c))}) } } @@ -205,12 +216,12 @@ func (s *Scanner) identifier() { s.advance() } - str := s.source[s.start:s.current] + str := string(s.source[s.start:s.current]) if id, found := keywords[string(str)]; found { - s.addToken(id, struct{}{}) + s.addToken(id, str) } else { - s.addToken(IDENTIFIER, struct{}{}) + s.addToken(IDENTIFIER, str) } } @@ -224,7 +235,7 @@ func (s *Scanner) string() { } if s.isAtEnd() { - report(s.line, "", "Unterminated string") + s.error(&ScanError{s.line, "Unterminated string"}) return } @@ -249,7 +260,7 @@ func (s *Scanner) number() { num, err := strconv.ParseFloat(string(s.source[s.start:s.current]), 64) if err != nil { - report(s.line, "", err.Error()) + s.error(&ScanError{s.line, err.Error()}) } s.addToken(NUMBER, num) @@ -298,3 +309,8 @@ func (s *Scanner) match(ch rune) bool { func (s *Scanner) isAtEnd() bool { return s.current >= len(s.source) } + +func (s *Scanner) error(err *ScanError) { + log.Print(err) + s.err = err +} diff --git a/stmt.go b/stmt.go new file mode 100644 index 0000000..63de63e --- /dev/null +++ b/stmt.go @@ -0,0 +1,41 @@ +package main + +type StmtVisitor interface { + visitPrintStmt(p *PrintStmt) + visitExprStmt(es *ExprStmt) + visitVarStmt(v *VarStmt) +} + +type Stmt interface { + stmt() + accept(v StmtVisitor) +} + +type PrintStmt struct { + val Expr +} + +type ExprStmt struct { + expr Expr +} + +type VarStmt struct { + name Token + initializer Expr +} + +func (p *PrintStmt) stmt() {} +func (es *ExprStmt) stmt() {} +func (vs *VarStmt) stmt() {} + +func (p *PrintStmt) accept(v StmtVisitor) { + v.visitPrintStmt(p) +} + +func (se *ExprStmt) accept(v StmtVisitor) { + v.visitExprStmt(se) +} + +func (vs *VarStmt) accept(v StmtVisitor) { + v.visitVarStmt(vs) +}