From c63550170a30bc3038ea27418dacae6b3adf725c Mon Sep 17 00:00:00 2001 From: Boris Kayi Date: Mon, 14 Aug 2023 22:22:33 +0200 Subject: [PATCH 01/12] test: unit test for `token` package --- glox/token/token_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/glox/token/token_test.go b/glox/token/token_test.go index 3f3083b..69477f1 100644 --- a/glox/token/token_test.go +++ b/glox/token/token_test.go @@ -40,3 +40,31 @@ func TestLookupIdentifier(t *testing.T) { } } } + +func TestIsLoopController(t *testing.T) { + tests := []struct { + tok TokenType + expected bool + }{ + {tok: BREAK, expected: true}, + {tok: CONTINUE, expected: true}, + {tok: RETURN, expected: false}, + {tok: ELSE, expected: false}, + {tok: IF, expected: false}, + {tok: WHILE, expected: false}, + {tok: FOR, expected: false}, + {tok: THIS, expected: false}, + {tok: SUPER, expected: false}, + {tok: LET, expected: false}, + {tok: CLASS, expected: false}, + } + + for _, test := range tests { + got := IsLoopController(test.tok) + expected := test.expected + + if got != expected { + t.Fatalf("failed to get token type for '%s'. got='%v' expected='%v'", test.tok, got, expected) + } + } +} From 0e6881ef053433f2fe672964e85fe8e565b58d88 Mon Sep 17 00:00:00 2001 From: Boris Kayi Date: Mon, 14 Aug 2023 23:01:29 +0200 Subject: [PATCH 02/12] feat: replace function keyword --- glox/lexer/lexer_test.go | 16 ++++++++-------- glox/token/token.go | 2 +- glox/token/token_test.go | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/glox/lexer/lexer_test.go b/glox/lexer/lexer_test.go index d224cf4..d51d64f 100644 --- a/glox/lexer/lexer_test.go +++ b/glox/lexer/lexer_test.go @@ -1,13 +1,13 @@ package lexer import ( -"glox/token" -"strings" -"testing" + "glox/token" + "strings" + "testing" ) func TestTokenize(t *testing.T) { -input := ` + input := ` let age = 12; 5 + 10 1 - 2 @@ -16,7 +16,7 @@ let age = 12; 2 >= 1 1 <= 1 class Example {} - fn call_me() + fun call_me() true != false // this is a comment this.name @@ -67,7 +67,7 @@ let age = 12; {token.IDENTIFIER, "Example"}, {token.L_BRACE, "{"}, {token.R_BRACE, "}"}, - {token.FUNCTION, "fn"}, + {token.FUNCTION, "fun"}, {token.IDENTIFIER, "call_me"}, {token.L_PAREN, "("}, {token.R_PAREN, ")"}, @@ -98,8 +98,8 @@ let age = 12; many lines"`}, {token.AND, "and"}, {token.OR, "or"}, - {token.BREAK, "break"}, - {token.CONTINUE, "continue"}, + {token.BREAK, "break"}, + {token.CONTINUE, "continue"}, {token.EOF, ""}, } diff --git a/glox/token/token.go b/glox/token/token.go index f046c10..d444cef 100644 --- a/glox/token/token.go +++ b/glox/token/token.go @@ -73,7 +73,7 @@ var keywords = map[string]TokenType{ "false": FALSE, "true": TRUE, "for": FOR, - "fn": FUNCTION, + "fun": FUNCTION, "if": IF, "nil": NIL, "or": OR, diff --git a/glox/token/token_test.go b/glox/token/token_test.go index 69477f1..24a64a2 100644 --- a/glox/token/token_test.go +++ b/glox/token/token_test.go @@ -18,7 +18,7 @@ func TestLookupIdentifier(t *testing.T) { {word: "true", expected: TRUE}, {word: "false", expected: FALSE}, {word: "print", expected: PRINT}, - {word: "fn", expected: FUNCTION}, + {word: "fun", expected: FUNCTION}, {word: "while", expected: WHILE}, {word: "for", expected: FOR}, {word: "this", expected: THIS}, From 15b1baf01faf29666863b45e7fb5b002dfcfbc9a Mon Sep 17 00:00:00 2001 From: Boris Kayi Date: Sun, 20 Aug 2023 16:38:38 +0200 Subject: [PATCH 03/12] feat: added parsing functions for `ast.Call` expressions --- glox/ast/ast.go | 33 ++++++++++ glox/ast/print.go | 4 ++ glox/parser/parser.go | 41 +++++++++++- glox/parser/parser_test.go | 127 +++++++++++++++++++++++++++++++++++++ 4 files changed, 204 insertions(+), 1 deletion(-) diff --git a/glox/ast/ast.go b/glox/ast/ast.go index 110ddbc..dcc1d80 100644 --- a/glox/ast/ast.go +++ b/glox/ast/ast.go @@ -18,6 +18,7 @@ const ( ASSIGNMENT_EXP ExpType = "assignment" LOGICAL_OR_EXP ExpType = "logical_or" LOGICAL_AND_EXP ExpType = "logical_and" + CALL_EXP ExpType = "call" ) type Expression interface { @@ -35,6 +36,7 @@ type Visitor interface { VisitVariable(exp *Variable) any VisitAssignment(exp *Assignment) any VisitLogical(exp *Logical) any + VisitCall(exp *Call) any } type Literal struct { @@ -265,3 +267,34 @@ func parenthesize(name ExpType, value string) string { return out.String() } + +type Call struct { + Callee Expression + Paren token.Token // Closing parenthesis, we keep track of this in order to know the location of the code in case an error happens in the arguments list. + Args []Expression +} + +func NewCall(callee Expression, paren token.Token, arguments []Expression) *Call { + return &Call{Callee: callee, Paren: paren, Args: arguments} +} + +func (exp *Call) Type() ExpType { + return CALL_EXP +} + +func (exp *Call) Accept(v Visitor) any { + return v.VisitCall(exp) +} + +func (exp *Call) String() string { + var out bytes.Buffer + out.WriteString(exp.Callee.String() + "(arguments ") + for i, arg := range exp.Args { + out.WriteString(arg.String()) + if i+1 != len(exp.Args) { + out.WriteString(", ") + } + } + out.WriteString(")") + return parenthesize(exp.Type(), out.String()) +} diff --git a/glox/ast/print.go b/glox/ast/print.go index 193baef..a09ec90 100644 --- a/glox/ast/print.go +++ b/glox/ast/print.go @@ -49,3 +49,7 @@ func (p *printer) VisitAssignment(assign *Assignment) any { func (p *printer) VisitLogical(exp *Logical) any { return exp.String() } + +func (p *printer) VisitCall(exp *Call) any { + return exp.String() +} diff --git a/glox/parser/parser.go b/glox/parser/parser.go index a081eeb..c16d68d 100644 --- a/glox/parser/parser.go +++ b/glox/parser/parser.go @@ -444,7 +444,46 @@ func (p *Parser) unary() (ast.Expression, error) { return ast.NewUnaryExpression(operator, right), err } - return p.primary() + return p.call() +} + +func (p *Parser) call() (ast.Expression, error) { + expr, err := p.primary() + if err == nil { + for { + if p.match(token.L_PAREN) { + expr, err = p.finishCall(expr) + if err != nil { + break + } + } else { + break + } + } + } + + return expr, err +} + +func (p *Parser) finishCall(expr ast.Expression) (ast.Expression, error) { + args := []ast.Expression{} + if !p.check(token.R_PAREN) { + for { + if len(args) >= 255 { + return nil, exception.Runtime(p.peek(), "call cannot have more than 255 arguments.") + } + if arg, err := p.expression(); err == nil { + args = append(args, arg) + } else { + return nil, err + } + if !p.match(token.COMMA) { + break + } + } + } + paren, err := p.consume(token.R_PAREN, "expected ')' after arguments.") + return ast.NewCall(expr, paren, args), err } func (p *Parser) primary() (ast.Expression, error) { diff --git a/glox/parser/parser_test.go b/glox/parser/parser_test.go index 190820c..61869ec 100644 --- a/glox/parser/parser_test.go +++ b/glox/parser/parser_test.go @@ -1,9 +1,11 @@ package parser import ( + "fmt" "glox/ast" "glox/lexer" "glox/token" + "strconv" "strings" "testing" ) @@ -777,6 +779,80 @@ func TestParseLogical(t *testing.T) { } +func TestParseCall(t *testing.T) { + tests := []struct { + code string + want *ast.Call + }{ + { + code: "function();", + want: ast.NewCall( + ast.NewVariable(token.Token{Type: token.IDENTIFIER, Lexeme: "function", Line: 1}), + token.Token{Type: token.R_PAREN, Lexeme: ")", Line: 1}, + []ast.Expression{}, + ), + }, + { + code: "isOdd(12);", + want: ast.NewCall( + ast.NewVariable(token.Token{Type: token.IDENTIFIER, Lexeme: "isOdd", Line: 1}), + token.Token{Type: token.R_PAREN, Lexeme: ")", Line: 1}, + []ast.Expression{ + ast.NewLiteralExpression(12), + }, + ), + }, + { + code: "add(5.1, 90);", + want: ast.NewCall( + ast.NewVariable(token.Token{Type: token.IDENTIFIER, Lexeme: "add", Line: 1}), + token.Token{Type: token.R_PAREN, Lexeme: ")", Line: 1}, + []ast.Expression{ + ast.NewLiteralExpression(5.1), + ast.NewLiteralExpression(90), + }, + ), + }, + } + + for _, test := range tests { + t.Logf("TestParseCall code='%s'", test.code) + lxr := lexer.New(test.code) + tokens, err := lxr.Tokenize() + if err != nil { + t.Fatalf("failed to tokenize code `%s`", test.code) + } + prsr := New(tokens) + stmts, err := prsr.Parse() + if err != nil { + t.Fatalf("failed to parse code `%s`", test.code) + } + if len(stmts) != 1 { + t.Fatalf("wrong number of statements. want=1 got=%d", len(stmts)) + } + stmt, isOk := stmts[0].(*ast.ExpressionStmt) + if !isOk { + t.Fatalf("stmts[0] is not a *ast.ExpressionStmt. got=%T", stmts[0]) + } + if isOk := testCall(stmt.Exp, test.want, t); !isOk { + t.Fail() + } + } + + code := callWith256Args("example") + lxr := lexer.New(code) + tokens, err := lxr.Tokenize() + if err != nil { + t.Fatalf("failed to tokenize code `%s`", code) + } + prsr := New(tokens) + _, err = prsr.Parse() + if err == nil { + t.Fatalf("Parsing should have caught an error on code='%s'", code) + } + +} + func TestParseWhile(t *testing.T) { tests := []struct { code string @@ -978,6 +1054,11 @@ func testVariable(exp ast.Expression, want *ast.Variable, t *testing.T) bool { t.Errorf("wrong value for variable.Name. want='%v' got='%v'", want, got) return false } + if variable.Name.Lexeme != want.Name.Lexeme { + want, got := want.Name.Lexeme, variable.Name.Lexeme + t.Errorf("wrong variable name. want='%s' got='%s'", want, got) + return false + } return true } @@ -1013,6 +1094,40 @@ func testLogical(stmt ast.Expression, want *ast.Logical, t *testing.T) bool { return testExpression(logical.Left, want.Left, t) && testExpression(logical.Right, want.Right, t) } +func testCall(got ast.Expression, want *ast.Call, t *testing.T) bool { + call, isOk := got.(*ast.Call) + if !isOk { + t.Errorf("passed expression is not a *ast.Call. got='%T' want='%T'", got, want) + return false + } + if len(call.Args) != len(want.Args) { + t.Errorf("call expression has wrong number of arguments. got='%d' want='%d'", len(call.Args), len(want.Args)) + return false + } + if call.Paren.Type != want.Paren.Type { + t.Errorf("call.Paren has the wrong token. got='%v' want='%v'", call.Paren, want.Paren) + return false + } + if !testExpression(call.Callee, want.Callee, t) { + t.Errorf("call.Calle has the wrong expression. got='%v' want='%v'", call.Callee, want.Callee) + return false + } + + if len(call.Args) == 0 && len(want.Args) == 0 { + return true + } + + for i, arg := range call.Args { + wantArg := want.Args[i] + if !testExpression(arg, wantArg, t) { + t.Errorf("argument '%d' is wrong. got='%v' want='%v'", i, arg, wantArg) + return false + } + } + + return true +} + func testExpression(got ast.Expression, want ast.Expression, t *testing.T) bool { if want == nil { if got != nil { @@ -1039,6 +1154,8 @@ func testExpression(got ast.Expression, want ast.Expression, t *testing.T) bool return testAssignment(got, want, t) case *ast.Logical: return testLogical(got, want, t) + case *ast.Call: + return testCall(got, want, t) default: t.Errorf("expression %T does not have a testing function. consider adding one", want) return false @@ -1165,3 +1282,13 @@ func testBranch(got ast.Statement, want *ast.BranchStmt, t *testing.T) bool { return true } + +// Generates the code for a function call with 256 arguments +// e.g. `foo(1, 2, 3, ..., 256);` +func callWith256Args(name string) string { + args := []string{} + for i := 1; i <= 256; i++ { + args = append(args, strconv.Itoa(i)) + } + return fmt.Sprintf("%s(%s);", name, strings.Join(args, ", ")) +} From ef00429c5ec57a9d3c8ae00d36d327a5b8add637 Mon Sep 17 00:00:00 2001 From: Boris Kayi Date: Sun, 20 Aug 2023 17:36:14 +0200 Subject: [PATCH 04/12] feat: interpret callable values --- glox/ast/ast.go | 2 +- glox/interpreter/interpreter.go | 36 +++++++++++++++++++++++++++++++++ glox/object/object.go | 6 ++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 glox/object/object.go diff --git a/glox/ast/ast.go b/glox/ast/ast.go index dcc1d80..6ce625e 100644 --- a/glox/ast/ast.go +++ b/glox/ast/ast.go @@ -263,7 +263,7 @@ func parenthesize(name ExpType, value string) string { out.WriteString("(" + string(name) + " ") out.WriteString(value) - out.WriteString(" )") + out.WriteString(")") return out.String() } diff --git a/glox/interpreter/interpreter.go b/glox/interpreter/interpreter.go index d6e391a..f944957 100644 --- a/glox/interpreter/interpreter.go +++ b/glox/interpreter/interpreter.go @@ -5,6 +5,7 @@ import ( "glox/ast" "glox/env" "glox/exception" + "glox/object" "glox/token" "io" "math" @@ -319,6 +320,41 @@ func (i *Interpreter) VisitWhile(exp *ast.WhileStmt) any { return nil } +func (i *Interpreter) VisitCall(expr *ast.Call) any { + calle := i.evaluate(expr.Callee) + err, isErr := calle.(error) + if !isErr { + args := []any{} + for _, arg := range expr.Args { + val := i.evaluate(arg) + if err, isErr = val.(error); isErr { + break + } + args = append(args, val) + } + if err != nil { + return err + } + function, isOk := calle.(object.Callable[*Interpreter]) + if !isOk { + return exception.Runtime(expr.Paren, fmt.Sprintf("'%v' cannot be called.", expr.Callee.String())) + } else if function.Arity() != len(args) { + want, got := function.Arity(), len(args) + var msg string + if got > want { + msg = fmt.Sprintf("too many arguments passed. expected %d but got %d.", want, got) + } else { + msg = fmt.Sprintf("not enough arguments passed. expected %d but got %d.", want, got) + } + return exception.Runtime(expr.Paren, msg) + } + return function.Call(i, args) + } + + return err + +} + func (i *Interpreter) execLoop(loop *ast.WhileStmt) (res any, err error) { //FIXME: The `continue` statement doesn't seem to work as expected. defer func() { diff --git a/glox/object/object.go b/glox/object/object.go new file mode 100644 index 0000000..7eb6ce7 --- /dev/null +++ b/glox/object/object.go @@ -0,0 +1,6 @@ +package object + +type Callable[T any] interface { + Call(i T, arguments []any) any + Arity() int +} From b7ed3a489b9b4a39f79eefcd3bc6eda28f4380f9 Mon Sep 17 00:00:00 2001 From: Boris Kayi Date: Sun, 20 Aug 2023 22:33:05 +0200 Subject: [PATCH 05/12] feat: add native `clock` fuction --- glox/interpreter/interpreter.go | 5 ++++- glox/native/native.go | 35 +++++++++++++++++++++++++++++++++ glox/native/native_test.go | 29 +++++++++++++++++++++++++++ glox/object/object.go | 1 + 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 glox/native/native.go create mode 100644 glox/native/native_test.go diff --git a/glox/interpreter/interpreter.go b/glox/interpreter/interpreter.go index f944957..aeb8e89 100644 --- a/glox/interpreter/interpreter.go +++ b/glox/interpreter/interpreter.go @@ -5,6 +5,7 @@ import ( "glox/ast" "glox/env" "glox/exception" + "glox/native" "glox/object" "glox/token" "io" @@ -18,7 +19,9 @@ type Interpreter struct { } func New(stderr io.Writer, stdout io.Writer) *Interpreter { - return &Interpreter{StdOut: stdout, StdErr: stderr, Env: env.Global()} + globals := env.Global() + globals.Define("clock", native.Clock[*Interpreter]()) + return &Interpreter{StdOut: stdout, StdErr: stderr, Env: globals} } func (i *Interpreter) Interpret(stmts []ast.Statement) any { diff --git a/glox/native/native.go b/glox/native/native.go new file mode 100644 index 0000000..41e672d --- /dev/null +++ b/glox/native/native.go @@ -0,0 +1,35 @@ +package native + +import "time" + +const NATIVE_FN_STR = "" + +type native[T any] struct { + call func(i T, argumets []any) any + arity int + toString string +} + +func (n *native[T]) Call(i T, args []any) any { + return n.call(i, args) +} + +func (n *native[T]) Arity() int { + return n.arity +} + +func (n *native[T]) String() string { + if n.toString == "" { + return NATIVE_FN_STR + } + return n.toString +} + +func Clock[T any]() native[T] { + return native[T]{ + arity: 0, + call: func(i T, argumets []any) any { + return float64(time.Now().UnixNano()) / float64(time.Second) + }, + } +} diff --git a/glox/native/native_test.go b/glox/native/native_test.go new file mode 100644 index 0000000..e8328a1 --- /dev/null +++ b/glox/native/native_test.go @@ -0,0 +1,29 @@ +package native + +import ( + "testing" + "time" +) + +func TestClock(t *testing.T) { + clock := Clock[any]() + got := clock.call(nil, []any{}) + want := (float64(time.Now().UnixNano()) / float64(time.Millisecond)) / 1000 + + val, isOk := got.(float64) + if !isOk { + t.Fatalf("clock time is not a float64. got='%T'", got) + } + if (want/val) > 1 || (want/val) < 0.9 { + t.Fatalf("wrong value for time. want='~%v' got='%v'", want, val) + } + + if clock.String() != NATIVE_FN_STR { + t.Errorf("clock.String() as a wrong value. want='%s' got='%s'", NATIVE_FN_STR, clock.String()) + t.Fail() + } + + if clock.Arity() != 0 { + t.Errorf("clock.Arity() has wrong value. want='0' got='%d'", clock.Arity()) + } +} diff --git a/glox/object/object.go b/glox/object/object.go index 7eb6ce7..1437342 100644 --- a/glox/object/object.go +++ b/glox/object/object.go @@ -3,4 +3,5 @@ package object type Callable[T any] interface { Call(i T, arguments []any) any Arity() int + String() string } From fa14ac7e26347f1e415a5aa802983e6414e7b3ab Mon Sep 17 00:00:00 2001 From: Boris Kayi Date: Mon, 21 Aug 2023 02:47:02 +0200 Subject: [PATCH 06/12] feat: start parsing functions --- README.md | 6 ++- glox/ast/stmt.go | 15 +++++++ glox/parser/parser.go | 44 +++++++++++++++++++- glox/parser/parser_test.go | 85 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5a83bda..fdbe7f9 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,12 @@ Production rules: ```bnf program -> declaration* EOF ; - declaration-> letDecl + declaration-> funDecl + | letDecl | statement ; + funDecl -> "fun" function ; + function -> IDENTIFIER "(" parameters ")" block ; + parameters -> IDENTIFIER ("," IDENTIER)* : letDecl -> ("var" | "let") IDENTIFIER ("=" expression) ? ";" ; statement -> exprStmt diff --git a/glox/ast/stmt.go b/glox/ast/stmt.go index 6b11ad0..58a4eaa 100644 --- a/glox/ast/stmt.go +++ b/glox/ast/stmt.go @@ -12,6 +12,7 @@ type StmtVisitor interface { VisitIfStmt(*IfStmt) any VisitWhile(*WhileStmt) any VisitBranch(*BranchStmt) any + VisitFunction(*Function) any } type Statement interface { @@ -106,3 +107,17 @@ func NewBranch(tok token.Token) *BranchStmt { func (b *BranchStmt) Accept(v StmtVisitor) any { return v.VisitBranch(b) } + +type Function struct { + Name token.Token + Params []token.Token + Body []Statement +} + +func NewFunction(name token.Token, params []token.Token, body []Statement) *Function { + return &Function{Name: name, Params: params, Body: body} +} + +func (fn *Function) Accept(v StmtVisitor) any { + return v.VisitFunction(fn) +} diff --git a/glox/parser/parser.go b/glox/parser/parser.go index c16d68d..48f43cc 100644 --- a/glox/parser/parser.go +++ b/glox/parser/parser.go @@ -43,7 +43,9 @@ func (p *Parser) program() ([]ast.Statement, error) { } func (p *Parser) declaration() (ast.Statement, error) { - if p.match(token.IF) { + if p.match(token.FUNCTION) { + return p.function("function") + } else if p.match(token.IF) { return p.ifStatement() } else if p.match(token.LET) { return p.letDeclaration() @@ -51,6 +53,46 @@ func (p *Parser) declaration() (ast.Statement, error) { return p.statement() } +func (p *Parser) function(kind string) (ast.Statement, error) { + name, err := p.consume(token.IDENTIFIER, "expected "+kind+" name.") + if err == nil { + if _, err = p.consume(token.L_PAREN, "expect '(' after "+kind+" name."); err != nil { + return nil, err + } + + params := []token.Token{} + if !p.check(token.R_PAREN) { + for { + if len(params) >= 255 { + return nil, exception.Runtime(p.peek(), kind+" cannot have more than 255 parameters.") + } + param, err := p.consume(token.IDENTIFIER, "expected a parameter name.") + if err != nil { + return nil, err + } + params = append(params, param) + + if !p.match(token.COMMA) { + break + } + } + } + + if _, err = p.consume(token.R_PAREN, "expected ')' after parameters."); err != nil { + return nil, err + } + if body, e := p.block(); e != nil { + return ast.NewFunction(name, params, body), e + } else { + return nil, e + } + + } + + return nil, err + +} + func (p *Parser) ifStatement() (ast.Statement, error) { var err error _, err = p.consume(token.L_PAREN, "expected '(' after 'if'.") diff --git a/glox/parser/parser_test.go b/glox/parser/parser_test.go index 61869ec..7a2231b 100644 --- a/glox/parser/parser_test.go +++ b/glox/parser/parser_test.go @@ -968,6 +968,47 @@ func TestParseWhile(t *testing.T) { } } +func TestParseFunction(t *testing.T) { + tests := []struct { + code string + want *ast.Function + }{ + { + code: `fun out(a){ print a; }`, + want: ast.NewFunction( + token.Token{Type: token.IDENTIFIER, Lexeme: "out", Line: 1}, + []token.Token{{Type: token.IDENTIFIER, Lexeme: "a", Line: 1}}, + []ast.Statement{ + ast.NewPrintStmt(ast.NewVariable(token.Token{Type: token.IDENTIFIER, Lexeme: "a", Line: 1})), + }, + ), + }, + } + + for _, test := range tests { + t.Logf("TestParseFunction code='%s'", test.code) + lxr := lexer.New(test.code) + tokens, err := lxr.Tokenize() + if err != nil { + msg := err.Error() + t.Fatalf("failed to tokenize code `%s`. got error `%s`", test.code, msg) + } + prsr := New(tokens) + stmts, err := prsr.Parse() + if err != nil { + msg := err.Error() + t.Fatalf("failed to parse code `%s`. got error `%s`", test.code, msg) + } + if len(stmts) != 1 { + t.Fatalf("wrong number of statements. want=1 got=%d", len(stmts)) + } + if !testFunction(stmts[0], test.want, t) { + t.Errorf("testFunction failed for '%s'", test.code) + t.Fail() + } + } +} + func testLiteral(exp ast.Expression, wantValue any, t *testing.T) bool { isLiteral, literal := assertLiteral(exp, ast.NewLiteralExpression(wantValue)) if !isLiteral { @@ -1253,6 +1294,8 @@ func testStmt(stmt ast.Statement, want ast.Statement, t *testing.T) bool { return testWhile(stmt, want, t) case *ast.BranchStmt: return testBranch(stmt, want, t) + case *ast.Function: + return testFunction(stmt, want, t) default: t.Errorf("statement %T does not have a testing function. consider adding one", want) return false @@ -1283,6 +1326,48 @@ func testBranch(got ast.Statement, want *ast.BranchStmt, t *testing.T) bool { return true } +func testFunction(got ast.Statement, want *ast.Function, t *testing.T) bool { + fn, isOk := got.(*ast.Function) + if !isOk { + t.Errorf("expected *ast.Function but got='%T'", got) + return false + } + if fn.Name.Lexeme != want.Name.Lexeme { + t.Errorf("wrong function name got='%s' want='%s'", fn.Name.Lexeme, want.Name.Lexeme) + return false + } + if len(fn.Params) != len(want.Params) { + t.Errorf("wrong number of parameters. got='%d' want='%d'", len(fn.Params), len(want.Params)) + return false + } + + if len(fn.Body) != len(want.Body) { + t.Errorf("function body has wrong number of statemnt. got='%d' want='%d'", len(fn.Body), len(want.Body)) + return false + } + + if len(fn.Params) != 0 && len(want.Params) != 0 { + for i, param := range fn.Params { + wantedParam := want.Params[i] + if param.Lexeme != wantedParam.Lexeme { + t.Errorf("error at param %d. got='%s' want='%s'", i+1, param.Lexeme, wantedParam.Lexeme) + return false + } + } + if len(fn.Body) != 0 && len(want.Body) != 0 { + for i, stmt := range fn.Body { + wantedStmt := want.Body[i] + if !testStmt(stmt, wantedStmt, t) { + t.Errorf("function stmt %d is wrong. got='%v' want='%v'", i+1, stmt, wantedStmt) + return false + } + } + } + } + + return true +} + // Generates the code for a function call with 256 arguments // e.g. `foo(1, 2, 3, ..., 256);` func callWith256Args(name string) string { From 5c18778350542f23b0e279e485775605012f88d1 Mon Sep 17 00:00:00 2001 From: Boris Kayi Date: Wed, 23 Aug 2023 02:02:14 +0200 Subject: [PATCH 07/12] fix: error in parsing function for `functions` --- glox/parser/parser.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/glox/parser/parser.go b/glox/parser/parser.go index 48f43cc..fde1db3 100644 --- a/glox/parser/parser.go +++ b/glox/parser/parser.go @@ -55,11 +55,11 @@ func (p *Parser) declaration() (ast.Statement, error) { func (p *Parser) function(kind string) (ast.Statement, error) { name, err := p.consume(token.IDENTIFIER, "expected "+kind+" name.") + if err != nil { + return nil, err + } + _, err = p.consume(token.L_PAREN, "expected '(' after "+kind+" name.") if err == nil { - if _, err = p.consume(token.L_PAREN, "expect '(' after "+kind+" name."); err != nil { - return nil, err - } - params := []token.Token{} if !p.check(token.R_PAREN) { for { @@ -81,12 +81,11 @@ func (p *Parser) function(kind string) (ast.Statement, error) { if _, err = p.consume(token.R_PAREN, "expected ')' after parameters."); err != nil { return nil, err } - if body, e := p.block(); e != nil { - return ast.NewFunction(name, params, body), e - } else { - return nil, e + if _, err = p.consume(token.L_BRACE, "expected '{' before "+kind+" body"); err != nil { + return nil, err } - + body, err := p.block() + return ast.NewFunction(name, params, body), err } return nil, err From b7e077f6c0f6c2e8850f12c4d8389f84dff1d2ca Mon Sep 17 00:00:00 2001 From: Boris Kayi Date: Wed, 23 Aug 2023 02:59:12 +0200 Subject: [PATCH 08/12] feat: interpret functions --- glox/interpreter/function.go | 36 +++++++++++++++++++++++++++++++++ glox/interpreter/interpreter.go | 6 ++++++ 2 files changed, 42 insertions(+) create mode 100644 glox/interpreter/function.go diff --git a/glox/interpreter/function.go b/glox/interpreter/function.go new file mode 100644 index 0000000..a57a1d5 --- /dev/null +++ b/glox/interpreter/function.go @@ -0,0 +1,36 @@ +package interpreter + +import ( + "fmt" + "glox/ast" + "glox/env" + "glox/object" + "glox/token" +) + +type LoxFunction struct { + object.Callable[*Interpreter] + declaration *ast.Function +} + +func NewFunction(declaration *ast.Function) *LoxFunction { + return &LoxFunction{declaration: declaration} +} + +func (fn *LoxFunction) Call(i *Interpreter, args []token.Token) any { + env := env.New(i.Env) + for i, param := range fn.declaration.Params { + arg := args[i] + env.Define(param.Lexeme, arg) + } + i.executeBlock(fn.declaration.Body, env) + return nil +} + +func (fn *LoxFunction) Arity() int { + return len(fn.declaration.Params) +} + +func (fn *LoxFunction) String() string { + return fmt.Sprintf("", fn.declaration.Name.Lexeme) +} diff --git a/glox/interpreter/interpreter.go b/glox/interpreter/interpreter.go index aeb8e89..f657a83 100644 --- a/glox/interpreter/interpreter.go +++ b/glox/interpreter/interpreter.go @@ -358,6 +358,12 @@ func (i *Interpreter) VisitCall(expr *ast.Call) any { } +func (i *Interpreter) VisitFunction(stmt *ast.Function) any { + fn := NewFunction(stmt) + i.Env.Define(stmt.Name.Lexeme, fn) + return nil +} + func (i *Interpreter) execLoop(loop *ast.WhileStmt) (res any, err error) { //FIXME: The `continue` statement doesn't seem to work as expected. defer func() { From 9bd0d3d3bdc6b8e4d74238eff7fbb72da4abfd1f Mon Sep 17 00:00:00 2001 From: Boris Kayi Date: Wed, 23 Aug 2023 03:01:52 +0200 Subject: [PATCH 09/12] chore: increase margin of error on clock test --- glox/native/native_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glox/native/native_test.go b/glox/native/native_test.go index e8328a1..3566bcf 100644 --- a/glox/native/native_test.go +++ b/glox/native/native_test.go @@ -14,7 +14,7 @@ func TestClock(t *testing.T) { if !isOk { t.Fatalf("clock time is not a float64. got='%T'", got) } - if (want/val) > 1 || (want/val) < 0.9 { + if (want/val) > 1 || (want/val) < 0.85 { t.Fatalf("wrong value for time. want='~%v' got='%v'", want, val) } From 0ad1e123719a70cb13456332624aff068bfe54db Mon Sep 17 00:00:00 2001 From: Boris Kayi Date: Wed, 23 Aug 2023 03:17:47 +0200 Subject: [PATCH 10/12] fix: test for clock, roun margin of error --- glox/native/native_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/glox/native/native_test.go b/glox/native/native_test.go index 3566bcf..f40182c 100644 --- a/glox/native/native_test.go +++ b/glox/native/native_test.go @@ -1,6 +1,7 @@ package native import ( + "math" "testing" "time" ) @@ -14,7 +15,9 @@ func TestClock(t *testing.T) { if !isOk { t.Fatalf("clock time is not a float64. got='%T'", got) } - if (want/val) > 1 || (want/val) < 0.85 { + + margin := math.Round((want/val)*100) / 100 + if margin > 1 || margin < 0.9 { t.Fatalf("wrong value for time. want='~%v' got='%v'", want, val) } From 1e50fbc36a06e6f99e2c004e7984d915eff7c4df Mon Sep 17 00:00:00 2001 From: Boris Kayi Date: Wed, 30 Aug 2023 00:26:57 +0200 Subject: [PATCH 11/12] fix: calling function throws exception --- glox/interpreter/callable.go | 7 +++++++ glox/interpreter/function.go | 5 +---- glox/interpreter/interpreter.go | 3 +-- glox/object/object.go | 7 ------- 4 files changed, 9 insertions(+), 13 deletions(-) create mode 100644 glox/interpreter/callable.go delete mode 100644 glox/object/object.go diff --git a/glox/interpreter/callable.go b/glox/interpreter/callable.go new file mode 100644 index 0000000..8de5092 --- /dev/null +++ b/glox/interpreter/callable.go @@ -0,0 +1,7 @@ +package interpreter + +type Callable interface { + Call(i *Interpreter, arguments []any) any + Arity() int + String() string +} diff --git a/glox/interpreter/function.go b/glox/interpreter/function.go index a57a1d5..0e30365 100644 --- a/glox/interpreter/function.go +++ b/glox/interpreter/function.go @@ -4,12 +4,9 @@ import ( "fmt" "glox/ast" "glox/env" - "glox/object" - "glox/token" ) type LoxFunction struct { - object.Callable[*Interpreter] declaration *ast.Function } @@ -17,7 +14,7 @@ func NewFunction(declaration *ast.Function) *LoxFunction { return &LoxFunction{declaration: declaration} } -func (fn *LoxFunction) Call(i *Interpreter, args []token.Token) any { +func (fn *LoxFunction) Call(i *Interpreter, args []any) any { env := env.New(i.Env) for i, param := range fn.declaration.Params { arg := args[i] diff --git a/glox/interpreter/interpreter.go b/glox/interpreter/interpreter.go index f657a83..598d655 100644 --- a/glox/interpreter/interpreter.go +++ b/glox/interpreter/interpreter.go @@ -6,7 +6,6 @@ import ( "glox/env" "glox/exception" "glox/native" - "glox/object" "glox/token" "io" "math" @@ -338,7 +337,7 @@ func (i *Interpreter) VisitCall(expr *ast.Call) any { if err != nil { return err } - function, isOk := calle.(object.Callable[*Interpreter]) + function, isOk := calle.(Callable) if !isOk { return exception.Runtime(expr.Paren, fmt.Sprintf("'%v' cannot be called.", expr.Callee.String())) } else if function.Arity() != len(args) { diff --git a/glox/object/object.go b/glox/object/object.go deleted file mode 100644 index 1437342..0000000 --- a/glox/object/object.go +++ /dev/null @@ -1,7 +0,0 @@ -package object - -type Callable[T any] interface { - Call(i T, arguments []any) any - Arity() int - String() string -} From 692679f9d1289a19f70a96cc01855b8377751823 Mon Sep 17 00:00:00 2001 From: Boris Kayi Date: Wed, 30 Aug 2023 00:29:06 +0200 Subject: [PATCH 12/12] test: add test case for void functions --- glox/interpreter/interpreter_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/glox/interpreter/interpreter_test.go b/glox/interpreter/interpreter_test.go index e3c6cfe..153b319 100644 --- a/glox/interpreter/interpreter_test.go +++ b/glox/interpreter/interpreter_test.go @@ -46,6 +46,7 @@ func TestInterpret(t *testing.T) { `let age = 17; if(age>18 or age > 21){ print "can drink"; } else { print "can't drink"; }`: "can't drink", `let count=0; while(count<1){count=count+1;}`: "1", `let count=0; while(count<5){count=count+1;}`: "1\n2\n3\n4\n5", + `fun greets(name){print "Hello "+name+"!";}greets("John");`: "Hello John!\n", } for code, expected := range fixtures {