diff --git a/cmd/monkey/main.go b/cmd/monkey/main.go index 1e3d4be..6aead28 100644 --- a/cmd/monkey/main.go +++ b/cmd/monkey/main.go @@ -1,137 +1,38 @@ +// Package main is the main entrypoint for the monkey interpreter package main import ( "flag" - "fmt" - "io" - "log" + "monkey" "os" - "os/user" - "path" - "strings" - - "github.com/pkg/profile" - - "monkey/internal" - "monkey/internal/compiler" - "monkey/internal/lexer" - "monkey/internal/object" - "monkey/internal/parser" - "monkey/internal/repl" ) -var ( - engine string - interactive bool - compile bool - version bool - debug bool - - profileCPU bool - profileMem bool -) - -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)") - - flag.BoolVar(&profileCPU, "profile-cpu", false, "Enable CPU profiling.") - flag.BoolVar(&profileMem, "profile-mem", false, "Enable Memory profiling.") -} - -// Indent indents a block of text with an indent string -func Indent(text, indent string) string { - if text[len(text)-1:] == "\n" { - result := "" - for _, j := range strings.Split(text[:len(text)-1], "\n") { - result += indent + j + "\n" - } - return result - } - result := "" - for _, j := range strings.Split(strings.TrimRight(text, "\n"), "\n") { - result += indent + j + "\n" - } - return result[:len(result)-1] -} - func main() { + var ( + compile bool + version bool + simple bool + ) + + flag.BoolVar(&compile, "c", false, "Compile a monkey file into a '.monkeyc' bytecode file.") + flag.BoolVar(&simple, "s", false, "Use simple REPL instead of opening a terminal.") + flag.BoolVar(&version, "v", false, "Print Monkey version information.") flag.Parse() - if version { - fmt.Printf("%s %s", path.Base(os.Args[0]), internal.FullVersion()) - os.Exit(0) - } + switch { + case compile: + monkey.CompileFiles(flag.Args()) - user, err := user.Current() - if err != nil { - log.Fatalf("could not determine current user: %s", err) - } + case version: + monkey.PrintVersionInfo(os.Stdout) - args := flag.Args() + case flag.NArg() > 0: + monkey.ExecFileVM(flag.Arg(0)) - if profileCPU { - defer profile.Start(profile.CPUProfile).Stop() - } else if profileMem { - defer profile.Start(profile.MemProfile).Stop() - } + case simple: + monkey.SimpleVmREPL() - if compile { - if len(args) < 1 { - log.Fatal("no source file given to compile") - } - f, err := os.Open(args[0]) - if err != nil { - log.Fatal(err) - } - defer f.Close() - - b, err := io.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("Main:\n%s\n", code.Instructions) - - fmt.Print("Constants:\n") - for i, constant := range code.Constants { - fmt.Printf("%04d %s\n", i, constant.Inspect()) - if fn, ok := constant.(*object.CompiledFunction); ok { - fmt.Printf("%s\n", Indent(fn.Instructions.String(), " ")) - } - } - } else { - opts := &repl.Options{ - Debug: debug, - Engine: engine, - Interactive: interactive, - } - repl := repl.New(user.Username, args, opts) - repl.Run() + default: + monkey.VmREPL() } } diff --git a/go.mod b/go.mod index 1efeb85..a612a10 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 require ( github.com/stretchr/testify v1.8.4 github.com/pkg/profile v1.7.0 + golang.org/x/term v0.15.0 ) require ( diff --git a/go.sum b/go.sum index e082f24..5b3a470 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/builtins/args.go b/internal/builtins/args.go index acdb9fa..c91b28e 100644 --- a/internal/builtins/args.go +++ b/internal/builtins/args.go @@ -6,7 +6,7 @@ import ( ) // Args ... -func Args(args ...object.Object) object.Object { +func args(args ...object.Object) object.Object { if err := typing.Check( "args", args, typing.ExactArgs(0), @@ -14,8 +14,8 @@ func Args(args ...object.Object) object.Object { return newError(err.Error()) } - elements := make([]object.Object, len(object.Arguments)) - for i, arg := range object.Arguments { + elements := make([]object.Object, len(object.Args)) + for i, arg := range object.Args { elements[i] = &object.String{Value: arg} } return &object.Array{Elements: elements} diff --git a/internal/builtins/builtins.go b/internal/builtins/builtins.go index 0382b51..b46a891 100644 --- a/internal/builtins/builtins.go +++ b/internal/builtins/builtins.go @@ -22,7 +22,7 @@ var Builtins = map[string]*object.Builtin{ "int": {Name: "int", Fn: Int}, "str": {Name: "str", Fn: Str}, "type": {Name: "type", Fn: TypeOf}, - "args": {Name: "args", Fn: Args}, + "args": {Name: "args", Fn: args}, "lower": {Name: "lower", Fn: Lower}, "upper": {Name: "upper", Fn: Upper}, "join": {Name: "join", Fn: Join}, diff --git a/internal/compiler/bytecode.go b/internal/compiler/bytecode.go new file mode 100644 index 0000000..e282a9b --- /dev/null +++ b/internal/compiler/bytecode.go @@ -0,0 +1,147 @@ +package compiler + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math" + "monkey/internal/code" + "monkey/internal/object" +) + +type Bytecode struct { + Instructions code.Instructions + Constants []object.Object +} + +type encoder struct { + bytes.Buffer +} + +func (e *encoder) Write(b []byte) (err error) { + _, err = e.Buffer.Write(b) + return +} + +func (e *encoder) WriteString(s string) (err error) { + _, err = e.Buffer.WriteString(s) + return +} + +func (e *encoder) WriteValue(data any) error { + return binary.Write(&e.Buffer, binary.BigEndian, data) +} + +func (e *encoder) WriteObjects(objs ...object.Object) (err error) { + for _, obj := range objs { + err = errors.Join(err, e.WriteValue(len(string(obj.Type())))) + err = errors.Join(err, e.WriteString(string(obj.Type()))) + + switch o := obj.(type) { + case *object.Null: + break + case *object.Boolean: + err = errors.Join(err, e.WriteValue(o.Value)) + case *object.Integer: + err = errors.Join(err, e.WriteValue(o.Value)) + case *object.String: + err = errors.Join(err, e.WriteValue(len(o.Value))) + err = errors.Join(err, e.WriteValue(o.Value)) + case *object.CompiledFunction: + err = errors.Join(err, e.WriteValue(o.NumParameters)) + err = errors.Join(err, e.WriteValue(o.NumLocals)) + err = errors.Join(err, e.WriteValue(len(o.Instructions))) + err = errors.Join(err, e.Write(o.Instructions)) + } + } + return +} + +func (b Bytecode) Encode() (data []byte, err error) { + var e encoder + + err = errors.Join(err, e.WriteValue(len(b.Instructions))) + err = errors.Join(err, e.Write(b.Instructions)) + err = errors.Join(err, e.WriteValue(len(b.Constants))) + err = errors.Join(err, e.WriteObjects(b.Constants...)) + return e.Bytes(), err +} + +type decoder struct { + pos int + b []byte +} + +func (d *decoder) Byte() (b byte) { + b = d.b[d.pos] + d.pos++ + return +} + +func (d *decoder) Int() (i int) { + i = int(binary.BigEndian.Uint32(d.b[d.pos:])) + d.pos += 4 + return +} + +func (d *decoder) Uint64() (i uint64) { + i = binary.BigEndian.Uint64(d.b[d.pos:]) + d.pos += 8 + return +} + +func (d *decoder) Int64() (i int64) { + return int64(d.Uint64()) +} + +func (d *decoder) Float64() (f float64) { + return math.Float64frombits(d.Uint64()) +} + +func (d *decoder) Bytes(len int) (b []byte) { + b = d.b[d.pos : d.pos+len] + d.pos += len + return +} + +func (d *decoder) String(len int) (s string) { + s = string(d.b[d.pos : d.pos+len]) + d.pos += len + return +} + +func (d *decoder) Objects(len int) (o []object.Object) { + for i := 0; i < len; i++ { + switch t := d.String(d.Int()); t { + case object.NULL_OBJ: + o = append(o, &object.Null{}) + case object.BOOLEAN_OBJ: + o = append(o, &object.Boolean{Value: d.Byte() == 1}) + case object.INTEGER_OBJ: + o = append(o, &object.Integer{Value: d.Int64()}) + case object.STRING_OBJ: + o = append(o, &object.String{Value: d.String(d.Int())}) + case object.COMPILED_FUNCTION_OBJ: + // The order of the fields has to reflect the data layout in the encoded bytecode. + o = append(o, &object.CompiledFunction{ + NumParameters: d.Int(), + NumLocals: d.Int(), + Instructions: d.Bytes(d.Int()), + }) + default: + panic(fmt.Sprintf("decoder: unsupported decoding for type %s", t)) + } + } + return +} + +func Decode(b []byte) *Bytecode { + var d = decoder{b: b} + + // The order of the fields has to reflect the data layout in the encoded bytecode. + return &Bytecode{ + Instructions: d.Bytes(d.Int()), + Constants: d.Objects(d.Int()), + } +} diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index 640d51e..9539145 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -25,8 +25,11 @@ type CompilationScope struct { type Compiler struct { Debug bool + fn string + input string + l int - constants []object.Object + constants *[]object.Object symbolTable *SymbolTable @@ -48,7 +51,7 @@ func New() *Compiler { } return &Compiler{ - constants: []object.Object{}, + constants: &[]object.Object{}, symbolTable: symbolTable, scopes: []CompilationScope{mainScope}, scopeIndex: 0, @@ -59,7 +62,7 @@ func (c *Compiler) currentInstructions() code.Instructions { return c.scopes[c.scopeIndex].instructions } -func NewWithState(s *SymbolTable, constants []object.Object) *Compiler { +func NewWithState(s *SymbolTable, constants *[]object.Object) *Compiler { compiler := New() compiler.symbolTable = s compiler.constants = constants @@ -442,7 +445,7 @@ func (c *Compiler) Compile(node ast.Node) error { } freeSymbols := c.symbolTable.FreeSymbols - numLocals := c.symbolTable.numDefinitions + numLocals := c.symbolTable.NumDefinitions instructions := c.leaveScope() for _, s := range freeSymbols { @@ -529,8 +532,8 @@ func (c *Compiler) Compile(node ast.Node) error { } func (c *Compiler) addConstant(obj object.Object) int { - c.constants = append(c.constants, obj) - return len(c.constants) - 1 + *c.constants = append(*c.constants, obj) + return len(*c.constants) - 1 } func (c *Compiler) emit(op code.Opcode, operands ...int) int { @@ -542,6 +545,11 @@ func (c *Compiler) emit(op code.Opcode, operands ...int) int { return pos } +func (c *Compiler) SetFileInfo(fn, input string) { + c.fn = fn + c.input = input +} + func (c *Compiler) setLastInstruction(op code.Opcode, pos int) { previous := c.scopes[c.scopeIndex].lastInstruction last := EmittedInstruction{Opcode: op, Position: pos} @@ -553,7 +561,7 @@ func (c *Compiler) setLastInstruction(op code.Opcode, pos int) { func (c *Compiler) Bytecode() *Bytecode { return &Bytecode{ Instructions: c.currentInstructions(), - Constants: c.constants, + Constants: *c.constants, } } @@ -630,11 +638,6 @@ func (c *Compiler) replaceLastPopWithReturn() { c.scopes[c.scopeIndex].lastInstruction.Opcode = code.OpReturn } -type Bytecode struct { - Instructions code.Instructions - Constants []object.Object -} - func (c *Compiler) loadSymbol(s Symbol) { switch s.Scope { case GlobalScope: diff --git a/internal/compiler/compiler_test.go b/internal/compiler/compiler_test.go index e596058..1e4e001 100644 --- a/internal/compiler/compiler_test.go +++ b/internal/compiler/compiler_test.go @@ -1090,7 +1090,7 @@ func runCompilerTests2(t *testing.T, tests []compilerTestCase2) { func parse(input string) *ast.Program { l := lexer.New(input) - p := parser.New(l) + p := parser.New("", name), l) module := p.ParseProgram() if len(p.Errors()) != 0 { diff --git a/internal/evaluator/evaluator_test.go b/internal/evaluator/evaluator_test.go index dd50d9e..49f6e3a 100644 --- a/internal/evaluator/evaluator_test.go +++ b/internal/evaluator/evaluator_test.go @@ -796,7 +796,7 @@ func TestWhileExpressions(t *testing.T) { func testEval(input string) object.Object { l := lexer.New(input) - p := parser.New(l) + p := parser.New("", l) program := p.ParseProgram() env := object.NewEnvironment() @@ -862,9 +862,9 @@ func TestImportExpressions(t *testing.T) { input string expected interface{} }{ - {`mod := import("../../testdata/mod"); mod.A`, 5}, - {`mod := import("../../testdata/mod"); mod.Sum(2, 3)`, 5}, - {`mod := import("../../testdata/mod"); mod.a`, nil}, + {`mod := import("../testdata/mod"); mod.A`, 5}, + {`mod := import("../testdata/mod"); mod.Sum(2, 3)`, 5}, + {`mod := import("../testdata/mod"); mod.a`, nil}, } for _, tt := range tests { diff --git a/internal/object/state.go b/internal/object/state.go index 857952e..288d1c4 100644 --- a/internal/object/state.go +++ b/internal/object/state.go @@ -1,10 +1,14 @@ package object -import "io" +import ( + "io" + "os" +) var ( - Arguments []string - StandardInput io.Reader - StandardOutput io.Writer - ExitFunction func(int) + Args []string = os.Args + Stdin io.Reader = os.Stdin + Stdout io.Writer = os.Stdout + Stderr io.Writer = os.Stderr + ExitFunction func(int) = os.Exit ) diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 3349808..537638d 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -61,6 +61,7 @@ type ( ) type Parser struct { + fn string l *lexer.Lexer errors []string @@ -71,8 +72,9 @@ type Parser struct { infixParseFns map[token.TokenType]infixParseFn } -func New(l *lexer.Lexer) *Parser { +func New(fn string, l *lexer.Lexer) *Parser { p := &Parser{ + fn: fn, l: l, errors: []string{}, } @@ -619,3 +621,11 @@ func (p *Parser) parseImportExpression() ast.Expression { return expression } + +// Parse parses the input source into a top-level AST for either evaluation +// or compilation to bytecode. The parameter fn denotes the filename the source +// originated from. +func Parse(fn, input string) (prog ast.Node, errors []string) { + p := New(fn, lexer.New(input)) + return p.ParseProgram(), p.Errors() +} diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 004ce9d..9da3e18 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -20,7 +20,7 @@ func TestBindExpressions(t *testing.T) { for _, tt := range tests { l := lexer.New(tt.input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -40,7 +40,7 @@ func TestReturnStatements(t *testing.T) { for _, tt := range tests { l := lexer.New(tt.input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -68,7 +68,7 @@ func TestIdentifierExpression(t *testing.T) { input := "foobar;" l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -99,7 +99,7 @@ func TestIntegerLiteralExpression(t *testing.T) { input := "5;" l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -143,7 +143,7 @@ func TestParsingPrefixExpressions(t *testing.T) { for _, tt := range prefixTests { l := lexer.New(tt.input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -202,7 +202,7 @@ func TestParsingInfixExpressions(t *testing.T) { for _, tt := range infixTests { l := lexer.New(tt.input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -353,7 +353,7 @@ func TestOperatorPrecedenceParsing(t *testing.T) { for _, tt := range tests { l := lexer.New(tt.input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -375,7 +375,7 @@ func TestBooleanExpression(t *testing.T) { for _, tt := range tests { l := lexer.New(tt.input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -405,7 +405,7 @@ func TestIfExpression(t *testing.T) { input := `if (x < y) { x }` l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -452,7 +452,7 @@ func TestIfExpression(t *testing.T) { func TestNullExpression(t *testing.T) { l := lexer.New("null") - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -477,7 +477,7 @@ func TestIfElseExpression(t *testing.T) { input := `if (x < y) { x } else { y }` l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -536,7 +536,7 @@ func TestIfElseIfExpression(t *testing.T) { input := `if (x < y) { x } else if (x == y) { y }` l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -615,7 +615,7 @@ func TestFunctionLiteralParsing(t *testing.T) { input := `fn(x, y) { x + y; }` l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -670,7 +670,7 @@ func TestFunctionParameterParsing(t *testing.T) { for _, tt := range tests { l := lexer.New(tt.input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -692,7 +692,7 @@ func TestCallExpressionParsing(t *testing.T) { input := "add(1, 2 * 3, 4 + 5);" l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -751,7 +751,7 @@ func TestCallExpressionParameterParsing(t *testing.T) { for _, tt := range tests { l := lexer.New(tt.input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -784,7 +784,7 @@ func TestStringLiteralExpression(t *testing.T) { input := `"hello world";` l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -803,7 +803,7 @@ func TestParsingArrayLiterals(t *testing.T) { input := "[1, 2 * 2, 3 + 3]" l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -826,7 +826,7 @@ func TestParsingSelectorExpressions(t *testing.T) { input := "myHash.foo" l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) stmt, ok := program.Statements[0].(*ast.ExpressionStatement) @@ -860,7 +860,7 @@ func TestParsingIndexExpressions(t *testing.T) { input := "myArray[1 + 1]" l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -883,7 +883,7 @@ func TestParsingHashLiteralsStringKeys(t *testing.T) { input := `{"one": 1, "two": 2, "three": 3}` l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -919,7 +919,7 @@ func TestParsingEmptyHashLiteral(t *testing.T) { input := "{}" l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -938,7 +938,7 @@ func TestParsingHashLiteralsWithExpressions(t *testing.T) { input := `{"one": 0 + 1, "two": 10 - 8, "three": 15 / 5}` l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -987,7 +987,7 @@ func TestFunctionDefinitionParsing(t *testing.T) { input := `add := fn(x, y) { x + y; }` l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -998,7 +998,7 @@ func TestWhileExpression(t *testing.T) { input := `while (x < y) { x }` l := lexer.New(input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -1055,7 +1055,7 @@ func TestAssignmentExpressions(t *testing.T) { for _, tt := range tests { l := lexer.New(tt.input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -1197,7 +1197,7 @@ func TestComments(t *testing.T) { for _, tt := range tests { l := lexer.New(tt.input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) @@ -1240,7 +1240,7 @@ func TestParsingImportExpressions(t *testing.T) { for _, tt := range tests { l := lexer.New(tt.input) - p := New(l) + p := New("", l) program := p.ParseProgram() checkParserErrors(t, p) diff --git a/internal/repl/repl.go b/internal/repl/repl.go deleted file mode 100644 index a4e146f..0000000 --- a/internal/repl/repl.go +++ /dev/null @@ -1,257 +0,0 @@ -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" - "log" - "monkey/internal/compiler" - "monkey/internal/evaluator" - "monkey/internal/lexer" - "monkey/internal/object" - "monkey/internal/parser" - "monkey/internal/vm" - "os" -) - -// PROMPT is the REPL prompt displayed for each input -const PROMPT = ">> " - -// MonkeyFace is the REPL's face of shock and horror when you encounter a -// parser error :D -const MonkeyFace = ` __,__ - .--. .-" "-. .--. - / .. \/ .-. .-. \/ .. \ - | | '| / Y \ |' | | - | \ \ \ 0 | 0 / / / | - \ '- ,\.-"""""""-./, -' / - ''-' /_ ^ ^ _\ '-'' - | \._ _./ | - \ \ '~' / / - '._ '-=-' _.' - '-----' -` - -type Options struct { - Debug bool - Engine string - Interactive bool -} - -type REPL struct { - user string - args []string - opts *Options -} - -func New(user string, args []string, opts *Options) *REPL { - object.StandardInput = os.Stdin - object.StandardOutput = os.Stdout - object.ExitFunction = os.Exit - - 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 := io.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 *vm.VMState) { - b, err := io.ReadAll(f) - if err != nil { - fmt.Fprintf(os.Stderr, "error reading source file: %s", err) - return - } - - state = vm.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) - c.Debug = r.opts.Debug - 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.NewWithState(code, state) - machine.Debug = r.opts.Debug - 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 _, ok := obj.(*object.Null); !ok { - 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 *vm.VMState) { - scanner := bufio.NewScanner(in) - - if state == nil { - state = vm.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) - c.Debug = r.opts.Debug - 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.NewWithState(code, state) - machine.Debug = r.opts.Debug - err = machine.Run() - if err != nil { - fmt.Fprintf(os.Stderr, "Woops! Executing bytecode failed:\n %s\n", err) - return - } - - obj := machine.LastPoppedStackElem() - if _, ok := obj.(*object.Null); !ok { - io.WriteString(out, obj.Inspect()) - io.WriteString(out, "\n") - } - } -} - -func (r *REPL) Run() { - object.Arguments = make([]string, len(r.args)) - copy(object.Arguments, r.args) - - if len(r.args) == 0 { - 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) - } - return - } - - if len(r.args) > 0 { - f, err := os.Open(r.args[0]) - if err != nil { - log.Fatalf("could not open source file %s: %s", r.args[0], err) - } - - // Remove program argument (zero) - r.args = r.args[1:] - object.Arguments = object.Arguments[1:] - - 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) - } - } - } -} - -func printParserErrors(out io.Writer, errors []string) { - 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+"\n") - } -} diff --git a/testdata/arrays.monkey b/internal/testdata/arrays.monkey similarity index 100% rename from testdata/arrays.monkey rename to internal/testdata/arrays.monkey diff --git a/testdata/assign.monkey b/internal/testdata/assign.monkey similarity index 100% rename from testdata/assign.monkey rename to internal/testdata/assign.monkey diff --git a/testdata/binding.monkey b/internal/testdata/binding.monkey similarity index 100% rename from testdata/binding.monkey rename to internal/testdata/binding.monkey diff --git a/testdata/builtins.monkey b/internal/testdata/builtins.monkey similarity index 100% rename from testdata/builtins.monkey rename to internal/testdata/builtins.monkey diff --git a/testdata/closures.monkey b/internal/testdata/closures.monkey similarity index 100% rename from testdata/closures.monkey rename to internal/testdata/closures.monkey diff --git a/testdata/expressions.monkey b/internal/testdata/expressions.monkey similarity index 100% rename from testdata/expressions.monkey rename to internal/testdata/expressions.monkey diff --git a/testdata/functions.monkey b/internal/testdata/functions.monkey similarity index 100% rename from testdata/functions.monkey rename to internal/testdata/functions.monkey diff --git a/testdata/hashes.monkey b/internal/testdata/hashes.monkey similarity index 100% rename from testdata/hashes.monkey rename to internal/testdata/hashes.monkey diff --git a/testdata/if.monkey b/internal/testdata/if.monkey similarity index 100% rename from testdata/if.monkey rename to internal/testdata/if.monkey diff --git a/testdata/mod.monkey b/internal/testdata/mod.monkey similarity index 100% rename from testdata/mod.monkey rename to internal/testdata/mod.monkey diff --git a/testdata/selectors.monkey b/internal/testdata/selectors.monkey similarity index 100% rename from testdata/selectors.monkey rename to internal/testdata/selectors.monkey diff --git a/testdata/strings.monkey b/internal/testdata/strings.monkey similarity index 100% rename from testdata/strings.monkey rename to internal/testdata/strings.monkey diff --git a/internal/version.go b/internal/version.go deleted file mode 100644 index e7ee508..0000000 --- a/internal/version.go +++ /dev/null @@ -1,18 +0,0 @@ -package internal - -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/internal/vm/vm.go b/internal/vm/vm.go index a33ff21..5fe42ac 100644 --- a/internal/vm/vm.go +++ b/internal/vm/vm.go @@ -2,7 +2,6 @@ package vm import ( "fmt" - "io/ioutil" "log" "monkey/internal/builtins" "monkey/internal/code" @@ -11,6 +10,8 @@ import ( "monkey/internal/object" "monkey/internal/parser" "monkey/internal/utils" + "os" + "path/filepath" "strings" "unicode" ) @@ -30,20 +31,20 @@ func ExecModule(name string, state *VMState) (object.Object, error) { return nil, fmt.Errorf("ImportError: no module named '%s'", name) } - b, err := ioutil.ReadFile(filename) + b, err := os.ReadFile(filename) if err != nil { return nil, fmt.Errorf("IOError: error reading module '%s': %s", name, err) } l := lexer.New(string(b)) - p := parser.New(l) + p := parser.New(fmt.Sprintf("", name), l) module := p.ParseProgram() if len(p.Errors()) != 0 { return nil, fmt.Errorf("ParseError: %s", p.Errors()) } - c := compiler.NewWithState(state.Symbols, state.Constants) + c := compiler.NewWithState(state.Symbols, &state.Constants) err = c.Compile(module) if err != nil { return nil, fmt.Errorf("CompileError: %s", err) @@ -52,7 +53,7 @@ func ExecModule(name string, state *VMState) (object.Object, error) { code := c.Bytecode() state.Constants = code.Constants - machine := NewWithState(code, state) + machine := NewWithState(fmt.Sprintf("", name), code, state) err = machine.Run() if err != nil { return nil, fmt.Errorf("RuntimeError: error loading module '%s'", err) @@ -102,6 +103,9 @@ type VM struct { state *VMState + dir string + file string + stack []object.Object sp int // Always points to the next value. Top of stack is stack[sp-1] @@ -111,7 +115,7 @@ type VM struct { } // New constructs a new monkey-lang bytecode virtual machine -func New(bytecode *compiler.Bytecode) *VM { +func New(fn string, bytecode *compiler.Bytecode) *VM { mainFn := &object.CompiledFunction{Instructions: bytecode.Instructions} mainClosure := &object.Closure{Fn: mainFn} mainFrame := NewFrame(mainClosure, 0) @@ -122,7 +126,7 @@ func New(bytecode *compiler.Bytecode) *VM { state := NewVMState() state.Constants = bytecode.Constants - return &VM{ + vm := &VM{ state: state, stack: make([]object.Object, StackSize), @@ -132,9 +136,13 @@ func New(bytecode *compiler.Bytecode) *VM { frame: mainFrame, fp: 1, } + + vm.dir, vm.file = filepath.Split(fn) + + return vm } -func NewWithState(bytecode *compiler.Bytecode, state *VMState) *VM { +func NewWithState(fn string, bytecode *compiler.Bytecode, state *VMState) *VM { mainFn := &object.CompiledFunction{Instructions: bytecode.Instructions} mainClosure := &object.Closure{Fn: mainFn} mainFrame := NewFrame(mainClosure, 0) @@ -142,7 +150,7 @@ func NewWithState(bytecode *compiler.Bytecode, state *VMState) *VM { frames := make([]*Frame, MaxFrames) frames[0] = mainFrame - return &VM{ + vm := &VM{ state: state, frames: frames, @@ -152,6 +160,10 @@ func NewWithState(bytecode *compiler.Bytecode, state *VMState) *VM { stack: make([]object.Object, StackSize), sp: 0, } + + vm.dir, vm.file = filepath.Split(fn) + + return vm } func (vm *VM) pushFrame(f *Frame) { @@ -429,7 +441,7 @@ func (vm *VM) Run() error { case code.OpGetBuiltin: builtinIndex := code.ReadUint8(ins[ip+1:]) - vm.frame.ip += 1 + vm.frame.ip++ builtin := builtins.BuiltinsIndex[builtinIndex] diff --git a/internal/vm/vm_test.go b/internal/vm/vm_test.go index 51916da..535fde9 100644 --- a/internal/vm/vm_test.go +++ b/internal/vm/vm_test.go @@ -49,7 +49,7 @@ func runVmTests(t *testing.T, tests []vmTestCase) { // fmt.Printf("\n") //} - vm := New(comp.Bytecode()) + vm := New("", comp.Bytecode()) err = vm.Run() if err != nil { t.Fatalf("vm error: %s", err) @@ -148,7 +148,7 @@ func testExpectedObject(t *testing.T, expected interface{}, actual object.Object func parse(input string) *ast.Program { l := lexer.New(input) - p := parser.New(l) + p := parser.New("", l) return p.ParseProgram() } @@ -694,7 +694,7 @@ func TestCallingFunctionsWithWrongArguments(t *testing.T) { t.Fatalf("compiler error: %s", err) } - vm := New(comp.Bytecode()) + vm := New("", comp.Bytecode()) err = vm.Run() if err == nil { t.Fatalf("expected VM error but resulted in none.") @@ -1040,7 +1040,7 @@ func TestIntegration(t *testing.T) { t.Fatalf("compiler error: %s", err) } - vm := New(c.Bytecode()) + vm := New("", c.Bytecode()) err = vm.Run() if err != nil { @@ -1089,7 +1089,7 @@ func TestExamples(t *testing.T) { t.Fatalf("compiler error: %s", err) } - vm := New(c.Bytecode()) + vm := New("", c.Bytecode()) err = vm.Run() if err != nil { @@ -1166,7 +1166,7 @@ func BenchmarkFibonacci(b *testing.B) { b.Fatalf("compiler error: %s", err) } - vm := New(c.Bytecode()) + vm := New("", c.Bytecode()) err = vm.Run() if err != nil { @@ -1185,15 +1185,15 @@ func BenchmarkFibonacci(b *testing.B) { func TestImportExpressions(t *testing.T) { tests := []vmTestCase{ { - input: `mod := import("../../testdata/mod"); mod.A`, + input: `mod := import("../testdata/mod"); mod.A`, expected: 5, }, { - input: `mod := import("../../testdata/mod"); mod.Sum(2, 3)`, + input: `mod := import("../testdata/mod"); mod.Sum(2, 3)`, expected: 5, }, { - input: `mod := import("../../testdata/mod"); mod.a`, + input: `mod := import("../testdata/mod"); mod.a`, expected: nil, }, } @@ -1202,11 +1202,11 @@ func TestImportExpressions(t *testing.T) { } func TestImportSearchPaths(t *testing.T) { - utils.AddPath("../../testdata") + utils.AddPath("../testdata") tests := []vmTestCase{ { - input: `mod := import("../../testdata/mod"); mod.A`, + input: `mod := import("../testdata/mod"); mod.A`, expected: 5, }, } diff --git a/monkey.go b/monkey.go new file mode 100644 index 0000000..7b3e307 --- /dev/null +++ b/monkey.go @@ -0,0 +1,140 @@ +package monkey + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "strings" + + "monkey/internal/ast" + "monkey/internal/compiler" + "monkey/internal/parser" + "monkey/internal/vm" +) + +const MonkeyVersion = "v0.0.1" + +var ErrParseError = errors.New("error: parse error") + +func mustReadFile(fname string) []byte { + b, err := os.ReadFile(fname) + if err != nil { + panic(err) + } + return b +} + +func writeFile(fname string, cont []byte) { + if err := os.WriteFile(fname, cont, 0644); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func decode(path string) (*compiler.Bytecode, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return compiler.Decode(b), nil +} + +func compile(path string) (bc *compiler.Bytecode, err error) { + input := string(mustReadFile(path)) + res, errs := parser.Parse(path, input) + if len(errs) > 0 { + var buf strings.Builder + + for _, e := range errs { + buf.WriteString(e) + buf.WriteByte('\n') + } + return nil, errors.New(buf.String()) + } + + c := compiler.New() + c.SetFileInfo(path, input) + if err = c.Compile(res); err != nil { + return + } + + return c.Bytecode(), nil +} + +func ExecFileVM(f string) (err error) { + var bytecode *compiler.Bytecode + + if filepath.Ext(f) == ".monkeyc" { + bytecode, err = decode(f) + } else { + bytecode, err = compile(f) + } + + if err != nil { + fmt.Println(err) + return + } + + mvm := vm.New(f, bytecode) + if err = mvm.Run(); err != nil { + fmt.Println(err) + return + } + + return +} + +func CompileFiles(files []string) error { + for _, f := range files { + b := mustReadFile(f) + + res, errs := parser.Parse(f, string(b)) + if len(errs) != 0 { + for _, e := range errs { + fmt.Println(e) + } + return ErrParseError + } + + c := compiler.New() + if err := c.Compile(res); err != nil { + fmt.Println(err) + continue + } + + cnt, err := c.Bytecode().Encode() + if err != nil { + fmt.Println(err) + continue + } + + ext := filepath.Ext(f) + writeFile(f[:len(f)-len(ext)]+".monkeyc", cnt) + } + + return nil +} + +func PrintVersionInfo(w io.Writer) { + fmt.Fprintf(w, "Monkey %s on %s\n", MonkeyVersion, strings.Title(runtime.GOOS)) +} + +func Parse(src string) (ast.Node, error) { + tree, errs := parser.Parse("", src) + if len(errs) > 0 { + var buf strings.Builder + + buf.WriteString("parser error:\n") + for _, e := range errs { + buf.WriteString(e) + buf.WriteByte('\n') + } + + return nil, errors.New(buf.String()) + } + + return tree, nil +} diff --git a/repl.go b/repl.go new file mode 100644 index 0000000..d6d6dbe --- /dev/null +++ b/repl.go @@ -0,0 +1,191 @@ +package monkey + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + + "golang.org/x/term" + + "monkey/internal/compiler" + "monkey/internal/object" + "monkey/internal/parser" + "monkey/internal/vm" +) + +// Package repl implements the Read-Eval-Print-Loop or interactive console +// by lexing, parsing and evaluating the input in the interpreter + +func VmREPL() error { + var state = vm.NewVMState() + + initState, err := term.MakeRaw(0) + if err != nil { + fmt.Println(err) + return fmt.Errorf("error opening terminal: %w", err) + } + defer term.Restore(0, initState) + + t := term.NewTerminal(os.Stdin, ">>> ") + t.AutoCompleteCallback = autoComplete + object.Stdout = t + + PrintVersionInfo(t) + for { + input, err := t.ReadLine() + check(t, initState, err) + + input = strings.TrimRight(input, " ") + if len(input) > 0 && input[len(input)-1] == '{' { + input, err = acceptUntil(t, input, "\n\n") + check(t, initState, err) + } + + res, errs := parser.Parse("", input) + if len(errs) != 0 { + for _, e := range errs { + fmt.Fprintln(t, e) + } + continue + } + + c := compiler.NewWithState(state.Symbols, &state.Constants) + if err := c.Compile(res); err != nil { + fmt.Fprintln(t, err) + continue + } + + mvm := vm.NewWithState("", c.Bytecode(), state) + if err := mvm.Run(); err != nil { + fmt.Fprintf(t, "runtime error: %v\n", err) + continue + } + + if val := mvm.LastPoppedStackElem(); val.Type() != object.NULL_OBJ { + fmt.Fprintln(t, val.Inspect()) + } + } +} + +func autoComplete(line string, pos int, key rune) (newLine string, newPos int, ok bool) { + if key == '\t' { + return line + " ", pos + 4, true + } + return +} + +func check(t *term.Terminal, initState *term.State, err error) { + if err != nil { + // Quit without error on Ctrl^D. + if err != io.EOF { + fmt.Fprintln(t, err) + } + term.Restore(0, initState) + fmt.Println() + os.Exit(0) + } +} + +func acceptUntil(t *term.Terminal, start, end string) (string, error) { + var buf strings.Builder + + buf.WriteString(start) + buf.WriteRune('\n') + t.SetPrompt("... ") + defer t.SetPrompt(">>> ") + + for { + line, err := t.ReadLine() + if err != nil { + return "", err + } + + line = strings.TrimRight(line, " ") + buf.WriteString(line) + buf.WriteRune('\n') + + if s := buf.String(); len(s) > len(end) && s[len(s)-len(end):] == end { + break + } + } + + return buf.String(), nil +} + +func SimpleVmREPL() { + var ( + state = vm.NewVMState() + reader = bufio.NewReader(os.Stdin) + ) + + PrintVersionInfo(os.Stdout) + for { + fmt.Print(">>> ") + input, err := reader.ReadString('\n') + simpleCheck(err) + + input = strings.TrimRight(input, " \n") + if len(input) > 0 && input[len(input)-1] == '{' { + input, err = simpleAcceptUntil(reader, input, "\n\n") + simpleCheck(err) + } + + res, errs := parser.Parse("", input) + if len(errs) != 0 { + for _, e := range errs { + fmt.Println(e) + } + continue + } + + c := compiler.NewWithState(state.Symbols, &state.Constants) + c.SetFileInfo("", input) + if err := c.Compile(res); err != nil { + fmt.Println(err) + continue + } + mvm := vm.NewWithState("", c.Bytecode(), state) + + if err := mvm.Run(); err != nil { + fmt.Printf("runtime error: %v\n", err) + continue + } + + if val := mvm.LastPoppedStackElem(); val.Type() != object.NULL_OBJ { + fmt.Println(val.Inspect()) + } + } +} + +func simpleCheck(err error) { + if err != nil { + fmt.Println(err) + os.Exit(0) + } +} + +func simpleAcceptUntil(r *bufio.Reader, start, end string) (string, error) { + var buf strings.Builder + + buf.WriteString(start) + buf.WriteRune('\n') + for { + fmt.Print("... ") + line, err := r.ReadString('\n') + if err != nil { + return "", err + } + + line = strings.TrimRight(line, " \n") + buf.WriteString(line) + buf.WriteRune('\n') + + if s := buf.String(); len(s) > len(end) && s[len(s)-len(end):] == end { + break + } + } + + return buf.String(), nil +}