Extra Features
This commit is contained in:
23
ast/ast.go
23
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()
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -75,7 +75,10 @@ func (c *Compiler) Compile(node ast.Node) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,4 +8,4 @@ let fib = fn(x) {
|
||||
return fib(x-1) + fib(x-2)
|
||||
}
|
||||
|
||||
puts(fib(35))
|
||||
print(fib(35))
|
||||
@@ -8,4 +8,4 @@ let fib = fn(n, a, b) {
|
||||
return fib(n - 1, b, a + b)
|
||||
}
|
||||
|
||||
puts(fib(35, 0, 1))
|
||||
print(fib(35, 0, 1))
|
||||
3
examples/input.monkey
Normal file
3
examples/input.monkey
Normal file
@@ -0,0 +1,3 @@
|
||||
let name = input("What is your name? ")
|
||||
|
||||
print("Hello " + name)
|
||||
113
main.go
113
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);
|
||||
func init() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] [<filename>]\n", path.Base(os.Args[0]))
|
||||
flag.PrintDefaults()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
};
|
||||
fibonacci(35);
|
||||
`
|
||||
|
||||
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
|
||||
if version {
|
||||
fmt.Printf("%s %s", path.Base(os.Args[0]), FullVersion())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
l := lexer.New(input)
|
||||
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 *engine == "vm" {
|
||||
comp := compiler.New()
|
||||
err := comp.Compile(program)
|
||||
if err != nil {
|
||||
fmt.Printf("compiler error: %s", err)
|
||||
return
|
||||
if len(p.Errors()) != 0 {
|
||||
log.Fatal(p.Errors())
|
||||
}
|
||||
|
||||
machine := vm.New(comp.Bytecode())
|
||||
|
||||
start := time.Now()
|
||||
|
||||
err = machine.Run()
|
||||
c := compiler.New()
|
||||
err = c.Compile(program)
|
||||
if err != nil {
|
||||
fmt.Printf("vm error: %s", err)
|
||||
return
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
duration = time.Since(start)
|
||||
result = machine.LastPoppedStackElem()
|
||||
code := c.Bytecode()
|
||||
fmt.Printf("%s\n", code.Instructions)
|
||||
} else {
|
||||
env := object.NewEnvironment()
|
||||
start := time.Now()
|
||||
result = evaluator.Eval(program, env)
|
||||
duration = time.Since(start)
|
||||
opts := &repl.Options{
|
||||
Debug: debug,
|
||||
Engine: engine,
|
||||
Interactive: interactive,
|
||||
}
|
||||
repl := repl.New(user.Username, args, opts)
|
||||
repl.Run()
|
||||
}
|
||||
|
||||
fmt.Printf(
|
||||
"engine=%s, result=%s, duration=%s\n",
|
||||
*engine,
|
||||
result.Inspect(),
|
||||
duration)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
277
repl/repl.go
277
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
18
version.go
Normal file
18
version.go
Normal file
@@ -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)
|
||||
}
|
||||
35
vim/monkey.vim
Normal file
35
vim/monkey.vim
Normal file
@@ -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"
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user