Extra Features
Some checks failed
Build / build (push) Failing after 1m40s
Test / build (push) Failing after 11m47s

This commit is contained in:
Chuck Smith
2024-03-14 21:25:47 -04:00
parent 36f04713bd
commit 997f0865f4
20 changed files with 757 additions and 128 deletions

View File

@@ -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()
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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()

View File

@@ -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"),
}

View File

@@ -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

View File

@@ -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))
}
}

View File

@@ -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);

View File

@@ -8,4 +8,4 @@ let fib = fn(x) {
return fib(x-1) + fib(x-2)
}
puts(fib(35))
print(fib(35))

View File

@@ -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
View File

@@ -0,0 +1,3 @@
let name = input("What is your name? ")
print("Hello " + name)

113
main.go
View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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())

View File

@@ -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")
}
}

View File

@@ -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
View 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
View 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"

View File

@@ -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)
}
}
}