From 997f0865f462a29eb867357c3f731d45a4c0cc1c Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Thu, 14 Mar 2024 21:25:47 -0400 Subject: [PATCH] Extra Features --- ast/ast.go | 23 +++ code/code.go | 2 + compiler/compiler.go | 32 ++++- compiler/compiler_test.go | 47 +++++- evaluator/builtins.go | 5 +- evaluator/evaluator.go | 26 ++++ evaluator/evaluator_test.go | 76 +++++++++- examples/demo.monkey | 2 +- examples/fib.monkey | 2 +- examples/fibt.monkey | 2 +- examples/input.monkey | 3 + main.go | 131 +++++++++-------- object/builtins.go | 77 +++++++++- parser/parser.go | 24 ++++ parser/parser_test.go | 45 ++++++ repl/repl.go | 277 +++++++++++++++++++++++++++++------- token/token.go | 2 + version.go | 18 +++ vim/monkey.vim | 35 +++++ vm/vm_test.go | 56 +++++++- 20 files changed, 757 insertions(+), 128 deletions(-) create mode 100644 examples/input.monkey create mode 100644 version.go create mode 100644 vim/monkey.vim diff --git a/ast/ast.go b/ast/ast.go index 2ae2b6c..f8298b3 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -356,3 +356,26 @@ func (hl HashLiteral) String() string { } func (hl HashLiteral) expressionNode() {} + +type WhileExpression struct { + Token token.Token // The 'while' token + Condition Expression + Consequence *BlockStatement +} + +func (we *WhileExpression) expressionNode() {} + +// TokenLiteral prints the literal value of the token associated with this node +func (we *WhileExpression) TokenLiteral() string { return we.Token.Literal } + +// String returns a stringified version of the AST for debugging +func (we *WhileExpression) String() string { + var out bytes.Buffer + + out.WriteString("while") + out.WriteString(we.Condition.String()) + out.WriteString(" ") + out.WriteString(we.Consequence.String()) + + return out.String() +} diff --git a/code/code.go b/code/code.go index 2c730fd..69316b7 100644 --- a/code/code.go +++ b/code/code.go @@ -41,6 +41,7 @@ const ( OpClosure OpGetFree OpCurrentClosure + OpNoop ) type Definition struct { @@ -79,6 +80,7 @@ var definitions = map[Opcode]*Definition{ OpClosure: {"OpClosure", []int{2, 1}}, OpGetFree: {"OpGetFree", []int{1}}, OpCurrentClosure: {"OpCurrentClosure", []int{}}, + OpNoop: {"OpNoop", []int{}}, } func Lookup(op byte) (*Definition, error) { diff --git a/compiler/compiler.go b/compiler/compiler.go index 9657355..53aa540 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -75,7 +75,10 @@ func (c *Compiler) Compile(node ast.Node) error { if err != nil { return err } - c.emit(code.OpPop) + + if !c.lastInstructionIs(code.OpNoop) { + c.emit(code.OpPop) + } case *ast.InfixExpression: if node.Operator == "<" { @@ -196,7 +199,11 @@ func (c *Compiler) Compile(node ast.Node) error { } case *ast.LetStatement: - symbol := c.symbolTable.Define(node.Name.Value) + symbol, ok := c.symbolTable.Resolve(node.Name.Value) + if !ok { + symbol = c.symbolTable.Define(node.Name.Value) + } + err := c.Compile(node.Value) if err != nil { return err @@ -328,6 +335,27 @@ func (c *Compiler) Compile(node ast.Node) error { c.emit(code.OpCall, len(node.Arguments)) + case *ast.WhileExpression: + jumpConditionPos := len(c.currentInstructions()) + + err := c.Compile(node.Condition) + if err != nil { + return err + } + + // Emit an `OpJump`with a bogus value + jumpIfFalsePos := c.emit(code.OpJumpNotTruthy, 0xFFFF) + + err = c.Compile(node.Consequence) + if err != nil { + return err + } + + c.emit(code.OpJump, jumpConditionPos) + + afterConsequencePos := c.emit(code.OpNoop) + c.changeOperand(jumpIfFalsePos, afterConsequencePos) + } return nil diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 9845ce9..143ffb9 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -836,6 +836,24 @@ func TestLetStatementScopes(t *testing.T) { code.Make(code.OpPop), }, }, + { + input: ` + let a = 0; + let a = a + 1; + `, + expectedConstants: []interface{}{ + 0, + 1, + }, + expectedInstructions: []code.Instructions{ + code.Make(code.OpConstant, 0), + code.Make(code.OpSetGlobal, 0), + code.Make(code.OpGetGlobal, 0), + code.Make(code.OpConstant, 1), + code.Make(code.OpAdd), + code.Make(code.OpSetGlobal, 0), + }, + }, } runCompilerTests(t, tests) @@ -854,7 +872,7 @@ func TestBuiltins(t *testing.T) { code.Make(code.OpArray, 0), code.Make(code.OpCall, 1), code.Make(code.OpPop), - code.Make(code.OpGetBuiltin, 5), + code.Make(code.OpGetBuiltin, 6), code.Make(code.OpArray, 0), code.Make(code.OpConstant, 0), code.Make(code.OpCall, 2), @@ -950,6 +968,33 @@ func TestRecursiveFunctions(t *testing.T) { runCompilerTests(t, tests) } +func TestIteration(t *testing.T) { + tests := []compilerTestCase{ + { + input: ` + while (true) { 10 }; + `, + expectedConstants: []interface{}{10}, + expectedInstructions: []code.Instructions{ + // 0000 + code.Make(code.OpTrue), + // 0001 + code.Make(code.OpJumpNotTruthy, 11), + // 0004 + code.Make(code.OpConstant, 0), + // 0007 + code.Make(code.OpPop), + // 0008 + code.Make(code.OpJump, 0), + // 0011 + code.Make(code.OpNoop), + }, + }, + } + + runCompilerTests(t, tests) +} + func runCompilerTests(t *testing.T, tests []compilerTestCase) { t.Helper() diff --git a/evaluator/builtins.go b/evaluator/builtins.go index 656cf76..f6632cf 100644 --- a/evaluator/builtins.go +++ b/evaluator/builtins.go @@ -6,9 +6,12 @@ import ( var builtins = map[string]*object.Builtin{ "len": object.GetBuiltinByName("len"), + "input": object.GetBuiltinByName("input"), + "print": object.GetBuiltinByName("print"), "first": object.GetBuiltinByName("first"), "last": object.GetBuiltinByName("last"), "rest": object.GetBuiltinByName("rest"), "push": object.GetBuiltinByName("push"), - "puts": object.GetBuiltinByName("puts"), + "pop": object.GetBuiltinByName("pop"), + "exit": object.GetBuiltinByName("exit"), } diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 9079cad..721c9c4 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -35,6 +35,9 @@ func Eval(node ast.Node, env *object.Environment) object.Object { case *ast.IfExpression: return evalIfExpression(node, env) + case *ast.WhileExpression: + return evalWhileExpression(node, env) + case *ast.ReturnStatement: val := Eval(node.ReturnValue, env) if isError(val) { @@ -123,6 +126,29 @@ func Eval(node ast.Node, env *object.Environment) object.Object { return nil } +func evalWhileExpression(we *ast.WhileExpression, env *object.Environment) object.Object { + var result object.Object + + for { + condition := Eval(we.Condition, env) + if isError(condition) { + return condition + } + + if isTruthy(condition) { + result = Eval(we.Consequence, env) + } else { + break + } + } + + if result != nil { + return result + } else { + return NULL + } +} + func evalProgram(program *ast.Program, env *object.Environment) object.Object { var result object.Object diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index 2e35f8b..484701e 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -1,9 +1,12 @@ package evaluator import ( + "errors" "monkey/lexer" "monkey/object" "monkey/parser" + "os" + "path/filepath" "testing" ) @@ -338,22 +341,25 @@ func TestBuiltinFunctions(t *testing.T) { {`len("")`, 0}, {`len("four")`, 4}, {`len("hello world")`, 11}, - {`len(1)`, "argument to `len` not supported, got INTEGER"}, - {`len("one", "two")`, "wrong number of arguments. got=2, want=1"}, + {`len(1)`, errors.New("argument to `len` not supported, got INTEGER")}, + {`len("one", "two")`, errors.New("wrong number of arguments. got=2, want=1")}, {`len("∑")`, 1}, {`len([1, 2, 3])`, 3}, {`len([])`, 0}, {`first([1, 2, 3])`, 1}, {`first([])`, nil}, - {`first(1)`, "argument to `first` must be ARRAY, got INTEGER"}, + {`first(1)`, errors.New("argument to `first` must be ARRAY, got INTEGER")}, {`last([1, 2, 3])`, 3}, {`last([])`, nil}, - {`last(1)`, "argument to `last` must be ARRAY, got INTEGER"}, + {`last(1)`, errors.New("argument to `last` must be ARRAY, got INTEGER")}, {`rest([1, 2, 3])`, []int{2, 3}}, {`rest([])`, nil}, {`push([], 1)`, []int{1}}, - {`push(1, 1)`, "argument to `push` must be ARRAY, got INTEGER"}, - {`puts("Hello World")`, nil}, + {`push(1, 1)`, errors.New("argument to `push` must be ARRAY, got INTEGER")}, + {`print("Hello World")`, nil}, + {`input()`, ""}, + {`pop([])`, errors.New("cannot pop from an empty array")}, + {`pop([1])`, 1}, } for _, tt := range tests { @@ -363,13 +369,15 @@ func TestBuiltinFunctions(t *testing.T) { case int: testIntegerObject(t, evaluated, int64(expected)) case string: + testStringObject(t, evaluated, expected) + case error: errObj, ok := evaluated.(*object.Error) if !ok { t.Errorf("object is not Error. got=%T (%+v)", evaluated, evaluated) continue } - if errObj.Message != expected { + if errObj.Message != expected.Error() { t.Errorf("wrong error message. expected=%q, got=%q", expected, errObj.Message) } @@ -541,6 +549,31 @@ func TestHashIndexExpressions(t *testing.T) { } } +func TestWhileExpressions(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + {"while (false) { }", nil}, + {"let n = 0; while (n < 10) { let n = n + 1 }; n", 10}, + {"let n = 10; while (n > 0) { let n = n - 1 }; n", 0}, + // FIXME: let is an expression statement and bind new values + // there is currently no assignment expressions :/ + {"let n = 0; while (n < 10) { let n = n + 1 }", nil}, + {"let n = 10; while (n > 0) { let n = n - 1 }", nil}, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + integer, ok := tt.expected.(int) + if ok { + testIntegerObject(t, evaluated, int64(integer)) + } else { + testNullObject(t, evaluated) + } + } +} + func testEval(input string) object.Object { l := lexer.New(input) p := parser.New(l) @@ -585,3 +618,32 @@ func testNullObject(t *testing.T, obj object.Object) bool { } return true } + +func testStringObject(t *testing.T, obj object.Object, expected string) bool { + result, ok := obj.(*object.String) + if !ok { + t.Errorf("object is not String. got=%T (%+v)", obj, obj) + return false + } + if result.Value != expected { + t.Errorf("object has wrong value. got=%s, want=%s", + result.Value, expected) + return false + } + return true +} + +func TestExamples(t *testing.T) { + matches, err := filepath.Glob("../examples/*.monkey") + if err != nil { + t.Error(err) + } + + for _, match := range matches { + b, err := os.ReadFile(match) + if err != nil { + t.Error(err) + } + testEval(string(b)) + } +} diff --git a/examples/demo.monkey b/examples/demo.monkey index bab5ec5..4a06e8d 100644 --- a/examples/demo.monkey +++ b/examples/demo.monkey @@ -10,7 +10,7 @@ let book = { let printBookName = fn(book) { let title = book["title"]; let author = book["author"]; - puts(author + " - " + title); + print(author + " - " + title); }; printBookName(book); diff --git a/examples/fib.monkey b/examples/fib.monkey index f1f654f..51b2b60 100644 --- a/examples/fib.monkey +++ b/examples/fib.monkey @@ -8,4 +8,4 @@ let fib = fn(x) { return fib(x-1) + fib(x-2) } -puts(fib(35)) \ No newline at end of file +print(fib(35)) \ No newline at end of file diff --git a/examples/fibt.monkey b/examples/fibt.monkey index fc5b8fb..fdbdca4 100644 --- a/examples/fibt.monkey +++ b/examples/fibt.monkey @@ -8,4 +8,4 @@ let fib = fn(n, a, b) { return fib(n - 1, b, a + b) } -puts(fib(35, 0, 1)) \ No newline at end of file +print(fib(35, 0, 1)) \ No newline at end of file diff --git a/examples/input.monkey b/examples/input.monkey new file mode 100644 index 0000000..8085a1e --- /dev/null +++ b/examples/input.monkey @@ -0,0 +1,3 @@ +let name = input("What is your name? ") + +print("Hello " + name) \ No newline at end of file diff --git a/main.go b/main.go index 86255a6..8469d5b 100644 --- a/main.go +++ b/main.go @@ -3,73 +3,90 @@ package main import ( "flag" "fmt" - "time" - + "io/ioutil" + "log" "monkey/compiler" - "monkey/evaluator" "monkey/lexer" - "monkey/object" "monkey/parser" - "monkey/vm" + "monkey/repl" + "os" + "os/user" + "path" ) -var engine = flag.String("engine", "vm", "use 'vm' or 'eval'") +var ( + engine string + interactive bool + compile bool + version bool + debug bool +) -var input = ` -let fibonacci = fn(x) { - if (x == 0) { - 0 - } else { - if (x == 1) { - return 1; - } else { - fibonacci(x - 1) + fibonacci(x - 2); - } - } -}; -fibonacci(35); -` +func init() { + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] []\n", path.Base(os.Args[0])) + flag.PrintDefaults() + os.Exit(0) + } + + flag.BoolVar(&version, "v", false, "display version information") + flag.BoolVar(&debug, "d", false, "enable debug mode") + flag.BoolVar(&compile, "c", false, "compile input to bytecode") + + flag.BoolVar(&interactive, "i", false, "enable interactive mode") + flag.StringVar(&engine, "e", "vm", "engine to use (eval or vm)") +} func main() { flag.Parse() - var duration time.Duration - var result object.Object - - l := lexer.New(input) - p := parser.New(l) - program := p.ParseProgram() - - if *engine == "vm" { - comp := compiler.New() - err := comp.Compile(program) - if err != nil { - fmt.Printf("compiler error: %s", err) - return - } - - machine := vm.New(comp.Bytecode()) - - start := time.Now() - - err = machine.Run() - if err != nil { - fmt.Printf("vm error: %s", err) - return - } - - duration = time.Since(start) - result = machine.LastPoppedStackElem() - } else { - env := object.NewEnvironment() - start := time.Now() - result = evaluator.Eval(program, env) - duration = time.Since(start) + if version { + fmt.Printf("%s %s", path.Base(os.Args[0]), FullVersion()) + os.Exit(0) } - fmt.Printf( - "engine=%s, result=%s, duration=%s\n", - *engine, - result.Inspect(), - duration) + user, err := user.Current() + if err != nil { + log.Fatalf("could not determine current user: %s", err) + } + + args := flag.Args() + + if compile { + if len(args) < 1 { + log.Fatal("no source file given to compile") + } + f, err := os.Open(args[0]) + defer f.Close() + + b, err := ioutil.ReadAll(f) + if err != nil { + log.Fatal(err) + } + + l := lexer.New(string(b)) + p := parser.New(l) + + program := p.ParseProgram() + if len(p.Errors()) != 0 { + log.Fatal(p.Errors()) + } + + c := compiler.New() + err = c.Compile(program) + if err != nil { + log.Fatal(err) + } + + code := c.Bytecode() + fmt.Printf("%s\n", code.Instructions) + } else { + opts := &repl.Options{ + Debug: debug, + Engine: engine, + Interactive: interactive, + } + repl := repl.New(user.Username, args, opts) + repl.Run() + } } diff --git a/object/builtins.go b/object/builtins.go index 6b4d8d4..5555bc0 100644 --- a/object/builtins.go +++ b/object/builtins.go @@ -1,6 +1,12 @@ package object -import "fmt" +import ( + "bufio" + "fmt" + "io" + "os" + "unicode/utf8" +) var Builtins = []struct { Name string @@ -18,7 +24,7 @@ var Builtins = []struct { case *Array: return &Integer{Value: int64(len(arg.Elements))} case *String: - return &Integer{Value: int64(len(arg.Value))} + return &Integer{Value: int64(utf8.RuneCountInString(arg.Value))} default: return newError("argument to `len` not supported, got %s", args[0].Type()) @@ -27,7 +33,30 @@ var Builtins = []struct { }, }, { - "puts", + "input", + &Builtin{Fn: func(args ...Object) Object { + if len(args) > 0 { + obj, ok := args[0].(*String) + if !ok { + return newError( + "argument to `input` not supported, got %s", + args[0].Type(), + ) + } + fmt.Fprintf(os.Stdout, obj.Value) + } + + buffer := bufio.NewReader(os.Stdin) + + line, _, err := buffer.ReadLine() + if err != nil && err != io.EOF { + return newError(fmt.Sprintf("error reading input from stdin: %s", err)) + } + return &String{Value: string(line)} + }}, + }, + { + "print", &Builtin{Fn: func(args ...Object) Object { for _, arg := range args { fmt.Println(arg.Inspect()) @@ -123,6 +152,48 @@ var Builtins = []struct { }, }, }, + { + "pop", + &Builtin{Fn: func(args ...Object) Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", + len(args)) + } + if args[0].Type() != ARRAY_OBJ { + return newError("argument to `pop` must be ARRAY, got %s", + args[0].Type()) + } + + arr := args[0].(*Array) + length := len(arr.Elements) + + if length == 0 { + return newError("cannot pop from an empty array") + } + + element := arr.Elements[length-1] + arr.Elements = arr.Elements[:length-1] + + return element + }, + }, + }, + { + "exit", + &Builtin{Fn: func(args ...Object) Object { + if len(args) == 1 { + if args[0].Type() != INTEGER_OBJ { + return newError("argument to `exit` must be INTEGER, got %s", + args[0].Type()) + } + os.Exit(int(args[0].(*Integer).Value)) + } else { + os.Exit(0) + } + return nil + }, + }, + }, } func newError(format string, a ...interface{}) *Error { diff --git a/parser/parser.go b/parser/parser.go index bdf9fae..5ce4316 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -65,6 +65,7 @@ func New(l *lexer.Lexer) *Parser { p.registerPrefix(token.LPAREN, p.parseGroupedExpression) p.registerPrefix(token.IF, p.parseIfExpression) p.registerPrefix(token.FUNCTION, p.parseFunctionLiteral) + p.registerPrefix(token.WHILE, p.parseWhileExpression) p.registerPrefix(token.STRING, p.parseStringLiteral) p.registerPrefix(token.LBRACKET, p.parseArrayLiteral) p.registerPrefix(token.LBRACE, p.parseHashLiteral) @@ -492,3 +493,26 @@ func (p *Parser) parseHashLiteral() ast.Expression { return hash } + +func (p *Parser) parseWhileExpression() ast.Expression { + expression := &ast.WhileExpression{Token: p.curToken} + + if !p.expectPeek(token.LPAREN) { + return nil + } + + p.nextToken() + expression.Condition = p.parseExpression(LOWEST) + + if !p.expectPeek(token.RPAREN) { + return nil + } + + if !p.expectPeek(token.LBRACE) { + return nil + } + + expression.Consequence = p.parseBlockStatement() + + return expression +} diff --git a/parser/parser_test.go b/parser/parser_test.go index 5b2f800..92cb250 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -876,6 +876,51 @@ func TestFunctionLiteralWithName(t *testing.T) { } } +func TestWhileExpression(t *testing.T) { + input := `while (x < y) { x }` + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program.Statements does not contain %d statements. got=%d\n", + 1, len(program.Statements)) + } + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", + program.Statements[0]) + } + + exp, ok := stmt.Expression.(*ast.WhileExpression) + if !ok { + t.Fatalf("stmt.Expression is not ast.WhileExpression. got=%T", + stmt.Expression) + } + + if !testInfixExpression(t, exp.Condition, "x", "<", "y") { + return + } + + if len(exp.Consequence.Statements) != 1 { + t.Errorf("consequence is not 1 statements. got=%d\n", + len(exp.Consequence.Statements)) + } + + consequence, ok := exp.Consequence.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T", + exp.Consequence.Statements[0]) + } + + if !testIdentifier(t, consequence.Expression, "x") { + return + } +} + func testLetStatement(t *testing.T, s ast.Statement, name string) bool { if s.TokenLiteral() != "let" { t.Errorf("s.TokenLiteral not 'let'. got=%q", s.TokenLiteral()) diff --git a/repl/repl.go b/repl/repl.go index ae1f4b9..02ee005 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -1,69 +1,29 @@ package repl +// Package repl implements the Read-Eval-Print-Loop or interactive console +// by lexing, parsing and evaluating the input in the interpreter + import ( "bufio" "fmt" "io" + "io/ioutil" + "log" "monkey/compiler" + "monkey/evaluator" "monkey/lexer" "monkey/object" "monkey/parser" "monkey/vm" + "os" ) +// PROMPT is the REPL prompt displayed for each input const PROMPT = ">> " -func Start(in io.Reader, out io.Writer) { - scanner := bufio.NewScanner(in) - - constants := []object.Object{} - globals := make([]object.Object, vm.GlobalsSize) - symbolTable := compiler.NewSymbolTable() - for i, v := range object.Builtins { - symbolTable.DefineBuiltin(i, v.Name) - } - - for { - fmt.Fprintf(out, PROMPT) - scanned := scanner.Scan() - if !scanned { - return - } - - line := scanner.Text() - l := lexer.New(line) - p := parser.New(l) - - program := p.ParseProgram() - if len(p.Errors()) != 0 { - printParserErrors(out, p.Errors()) - continue - } - - comp := compiler.NewWithState(symbolTable, constants) - err := comp.Compile(program) - if err != nil { - fmt.Fprintf(out, "Woops! Compilation failed:\n %s\n", err) - continue - } - - code := comp.Bytecode() - constants = code.Constants - - machine := vm.NewWithGlobalState(comp.Bytecode(), globals) - err = machine.Run() - if err != nil { - fmt.Fprintf(out, "Woops! Executing bytecode failed:\n %s\n", err) - continue - } - - stackTop := machine.LastPoppedStackElem() - io.WriteString(out, stackTop.Inspect()) - io.WriteString(out, "\n") - } -} - -const MONKEY_FACE = ` __,__ +// MonkeyFace is the REPL's face of shock and horror when you encounter a +// parser error :D +const MonkeyFace = ` __,__ .--. .-" "-. .--. / .. \/ .-. .-. \/ .. \ | | '| / Y \ |' | | @@ -76,11 +36,222 @@ const MONKEY_FACE = ` __,__ '-----' ` +type Options struct { + Debug bool + Engine string + Interactive bool +} + +type VMState struct { + constants []object.Object + globals []object.Object + symbols *compiler.SymbolTable +} + +func NewVMState() *VMState { + symbolTable := compiler.NewSymbolTable() + for i, v := range object.Builtins { + symbolTable.DefineBuiltin(i, v.Name) + } + + return &VMState{ + constants: []object.Object{}, + globals: make([]object.Object, vm.GlobalsSize), + symbols: symbolTable, + } +} + +type REPL struct { + user string + args []string + opts *Options +} + +func New(user string, args []string, opts *Options) *REPL { + return &REPL{user, args, opts} +} + +// Eval parses and evalulates the program given by f and returns the resulting +// environment, any errors are printed to stderr +func (r *REPL) Eval(f io.Reader) (env *object.Environment) { + env = object.NewEnvironment() + + b, err := ioutil.ReadAll(f) + if err != nil { + fmt.Fprintf(os.Stderr, "error reading source file: %s", err) + return + } + + l := lexer.New(string(b)) + p := parser.New(l) + + program := p.ParseProgram() + if len(p.Errors()) != 0 { + printParserErrors(os.Stderr, p.Errors()) + return + } + + evaluator.Eval(program, env) + return +} + +// Exec parses, compiles and executes the program given by f and returns +// the resulting virtual machine, any errors are printed to stderr +func (r *REPL) Exec(f io.Reader) (state *VMState) { + b, err := ioutil.ReadAll(f) + if err != nil { + fmt.Fprintf(os.Stderr, "error reading source file: %s", err) + return + } + + state = NewVMState() + + l := lexer.New(string(b)) + p := parser.New(l) + + program := p.ParseProgram() + if len(p.Errors()) != 0 { + printParserErrors(os.Stderr, p.Errors()) + return + } + + c := compiler.NewWithState(state.symbols, state.constants) + err = c.Compile(program) + if err != nil { + fmt.Fprintf(os.Stderr, "Woops! Compilation failed:\n %s\n", err) + return + } + + code := c.Bytecode() + state.constants = code.Constants + + machine := vm.NewWithGlobalState(code, state.globals) + err = machine.Run() + if err != nil { + fmt.Fprintf(os.Stderr, "Woops! Executing bytecode failed:\n %s\n", err) + return + } + + return +} + +// StartEvalLoop starts the REPL in a continious eval loop +func (r *REPL) StartEvalLoop(in io.Reader, out io.Writer, env *object.Environment) { + scanner := bufio.NewScanner(in) + + if env == nil { + env = object.NewEnvironment() + } + + for { + fmt.Printf(PROMPT) + scanned := scanner.Scan() + if !scanned { + return + } + + line := scanner.Text() + + l := lexer.New(line) + p := parser.New(l) + + program := p.ParseProgram() + if len(p.Errors()) != 0 { + printParserErrors(out, p.Errors()) + continue + } + + obj := evaluator.Eval(program, env) + if obj != nil { + io.WriteString(out, obj.Inspect()) + io.WriteString(out, "\n") + } + } +} + +// StartExecLoop starts the REPL in a continious exec loop +func (r *REPL) StartExecLoop(in io.Reader, out io.Writer, state *VMState) { + scanner := bufio.NewScanner(in) + + if state == nil { + state = NewVMState() + } + + for { + fmt.Printf(PROMPT) + scanned := scanner.Scan() + if !scanned { + return + } + + line := scanner.Text() + + l := lexer.New(line) + p := parser.New(l) + + program := p.ParseProgram() + if len(p.Errors()) != 0 { + printParserErrors(out, p.Errors()) + continue + } + + c := compiler.NewWithState(state.symbols, state.constants) + err := c.Compile(program) + if err != nil { + fmt.Fprintf(os.Stderr, "Woops! Compilation failed:\n %s\n", err) + return + } + + code := c.Bytecode() + state.constants = code.Constants + + machine := vm.NewWithGlobalState(code, state.globals) + err = machine.Run() + if err != nil { + fmt.Fprintf(os.Stderr, "Woops! Executing bytecode failed:\n %s\n", err) + return + } + + stackTop := machine.LastPoppedStackElem() + io.WriteString(out, stackTop.Inspect()) + io.WriteString(out, "\n") + } +} + +func (r *REPL) Run() { + if len(r.args) == 1 { + f, err := os.Open(r.args[0]) + if err != nil { + log.Fatalf("could not open source file %s: %s", r.args[0], err) + } + + if r.opts.Engine == "eval" { + env := r.Eval(f) + if r.opts.Interactive { + r.StartEvalLoop(os.Stdin, os.Stdout, env) + } + } else { + state := r.Exec(f) + if r.opts.Interactive { + r.StartExecLoop(os.Stdin, os.Stdout, state) + } + } + } else { + fmt.Printf("Hello %s! This is the Monkey programming language!\n", r.user) + fmt.Printf("Feel free to type in commands\n") + if r.opts.Engine == "eval" { + r.StartEvalLoop(os.Stdin, os.Stdout, nil) + } else { + r.StartExecLoop(os.Stdin, os.Stdout, nil) + } + } +} + func printParserErrors(out io.Writer, errors []string) { - io.WriteString(out, MONKEY_FACE) + io.WriteString(out, MonkeyFace) io.WriteString(out, "Woops! We ran into some monkey business here!\n") io.WriteString(out, " parser errors:\n") for _, msg := range errors { - io.WriteString(out, "\t"+msg+"\t") + io.WriteString(out, "\t"+msg+"\n") } } diff --git a/token/token.go b/token/token.go index 596dafa..aa33aab 100644 --- a/token/token.go +++ b/token/token.go @@ -50,6 +50,7 @@ const ( IF = "IF" ELSE = "ELSE" RETURN = "RETURN" + WHILE = "WHILE" ) var keywords = map[string]TokenType{ @@ -60,6 +61,7 @@ var keywords = map[string]TokenType{ "if": IF, "else": ELSE, "return": RETURN, + "while": WHILE, } func LookupIdent(ident string) TokenType { diff --git a/version.go b/version.go new file mode 100644 index 0000000..50a8887 --- /dev/null +++ b/version.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" +) + +var ( + // Version release version + Version = "0.0.1" + + // GitCommit will be overwritten automatically by the build system + GitCommit = "HEAD" +) + +// FullVersion returns the full version and commit hash +func FullVersion() string { + return fmt.Sprintf("%s@%s", Version, GitCommit) +} diff --git a/vim/monkey.vim b/vim/monkey.vim new file mode 100644 index 0000000..945e6b7 --- /dev/null +++ b/vim/monkey.vim @@ -0,0 +1,35 @@ +" Vim Syntax File +" Language: monkey +" Creator: James Mills, prologic at shortcircuit dot net dot au +" Last Change: 31st January 2019 + +if version < 600 + syntax clear +elseif exists("b:current_syntax") + finish +endif + +syntax case match + +syntax keyword xType true false + +syntax keyword xKeyword let fn if else return while + +syntax keyword xFunction len input print first last rest push pop exit + +syntax keyword xOperator == != < > ! +syntax keyword xOperator + - * / + +syntax region xString start=/"/ skip=/\\./ end=/"/ + +" syntax region xComment start='#' end='$' keepend + +highlight link xType Type +highlight link xKeyword Keyword +highlight link xFunction Function +highlight link xString String +" highlight link xComment Comment +highlight link xOperator Operator +highlight Operator ctermfg=5 + +let b:current_syntax = "monkey" \ No newline at end of file diff --git a/vm/vm_test.go b/vm/vm_test.go index ce05909..30266c6 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -7,6 +7,8 @@ import ( "monkey/lexer" "monkey/object" "monkey/parser" + "os" + "path/filepath" "testing" ) @@ -597,7 +599,7 @@ func TestBuiltinFunctions(t *testing.T) { }, {`len([1, 2, 3])`, 3}, {`len([])`, 0}, - {`puts("hello", "world!")`, Null}, + {`print("hello", "world!")`, Null}, {`first([1, 2, 3])`, 1}, {`first([])`, Null}, {`first(1)`, @@ -620,6 +622,12 @@ func TestBuiltinFunctions(t *testing.T) { Message: "argument to `push` must be ARRAY, got INTEGER", }, }, + {`input()`, ""}, + {`pop([])`, &object.Error{ + Message: "cannot pop from an empty array", + }, + }, + {`pop([1])`, 1}, } runVmTests(t, tests) @@ -779,3 +787,49 @@ func TestRecursiveFibonacci(t *testing.T) { runVmTests(t, tests) } + +func TestIterations(t *testing.T) { + tests := []vmTestCase{ + {"while (false) { }", nil}, + {"let n = 0; while (n < 10) { let n = n + 1 }; n", 10}, + {"let n = 10; while (n > 0) { let n = n - 1 }; n", 0}, + // FIXME: let is an expression statement and bind new values + // there is currently no assignment expressions :/ + {"let n = 0; while (n < 10) { let n = n + 1 }", nil}, + {"let n = 10; while (n > 0) { let n = n - 1 }", nil}, + } + + runVmTests(t, tests) +} + +func TestExamples(t *testing.T) { + matches, err := filepath.Glob("../examples/*.monkey") + if err != nil { + t.Error(err) + } + + for _, match := range matches { + b, err := os.ReadFile(match) + if err != nil { + t.Error(err) + } + + input := string(b) + program := parse(input) + + c := compiler.New() + err = c.Compile(program) + if err != nil { + t.Log(input) + t.Fatalf("compiler error: %s", err) + } + + vm := New(c.Bytecode()) + + err = vm.Run() + if err != nil { + t.Log(input) + t.Fatalf("vm error: %s", err) + } + } +}