Skip to content

Commit

Permalink
go: Initialise parser and evaluator
Browse files Browse the repository at this point in the history
Initialise parser and evaluator packages and wire them through all the
way to the frontend. Variable declaration via inference or type
declaration, as well as calls builtin function `print` with a variadic
number of arguments as literals or variables is possible now.

Tweak the frontend to actually print something with the initial demo
code.

Just like the lexer, many cues are taken from Thorston Ball's
Interpreter book source code.

Link: https://github.com/juliaogris/monkey
Signed-off-by: Julia Ogris <[email protected]>
  • Loading branch information
juliaogris committed Sep 9, 2022
1 parent afece8a commit 88a019d
Show file tree
Hide file tree
Showing 13 changed files with 1,433 additions and 35 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# --- Global -------------------------------------------------------------------
O = out
COVERAGE = 90
COVERAGE = 70
VERSION ?= $(shell git describe --tags --dirty --always)

all: build tiny test test-tiny check-coverage lint frontend ## Build, test, check coverage and lint
Expand Down
60 changes: 33 additions & 27 deletions docs/syntax_grammar.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,27 +63,35 @@ enclosed in double quotes `""`. Comments are fenced by `/* … */`.
The `evy` source code is UTF-8 encoded. The NUL character `U+0000` is
not allowed.

program = { statements | func | event_handler } .
statements = { statement NL } .
statement = assignment | declaration | func_call |
loop | if | return |
BREAK | EMPTY_STATEMENT .
program = { statement | func | event_handler } .
statement = empty_stmt |
assign_stmt | typed_decl_stmt | inferred_decl_stmt |
func_call_stmt |
return_stmt | break_stmt |
for_stmt | while_stmt | if_stmt .

EMPTY_STATEMENT = .
BREAK = "break" .

/* --- Statement ---- */
empty_stmt = NL .

assign_stmt = assignable "=" expr NL .
typed_decl_stmt = typed_decl NL .
inferred_decl_stmt = ident ":=" toplevel_expr NL .

func_call_stmt = func_call NL.

return_stmt = "return" [ toplevel_expr ] NL.
break_stmt = "break" NL .

/* --- Assignment --- */
assignment = assignable "=" expr .
assignable = ident { selector } .
ident = LETTER { LETTER | UNICODE_DIGIT } .
selector = index | dot_selector .
index = "[" expr "]" .
dot_selector = "." ident .

/* --- Declarations --- */
declaration = typed_decl | inferred_decl .
typed_ident = ident ":" type .
inferred_decl = ident ":=" toplevel_expr .
/* --- Type --- */
typed_decl = ident ":" type .

type = BASIC_TYPE | array_type | map_type | "any" .
BASIC_TYPE = "num" | "string" | "bool" .
Expand Down Expand Up @@ -120,35 +128,33 @@ not allowed.
map_elems = { ident ":" term [NL] } .

/* --- Control flow --- */
loop = for | while .
for = "for" range NL
statements
for_stmt = "for" range NL
{ statement }
"end" .
range = ident ( ":=" | "=" ) "range" range_args .
range_args = term [ term [ term ] ] .
while = "while" toplevel_expr NL
statements
while_stmt = "while" toplevel_expr NL
{ statement }
"end" .

if = "if" toplevel_expr NL
statements
{ "else" "if" toplevel_expr NL
statements }
[ "else" NL
statements ]
"end" .
if_stmt = "if" toplevel_expr NL
{ statement }
{ "else" "if" toplevel_expr NL
{ statement } }
[ "else" NL
{ statement } ]
"end" .

/* --- Functions ---- */
func = "func" ident func_signature NL
statements
{ statement }
"end" .
func_signature = [ ":" type ] params .
params = { typed_decl } | variadic_param .
variadic_param = typed_decl "..." .
return = "return" [ toplevel_expr ] .

event_handler = "on" ident NL
statements
{ statement }
"end" .

/* --- Terminals --- */
Expand Down
8 changes: 4 additions & 4 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@
<textarea id="source">
move 10 10
line 20 20

x := 12
print "x:" x
if x > 10
print "🍦 big x" x
print "🍦 big x"
end
</textarea>
</div>
<div class="pane">
<textarea id="output" disabled>
🍦 big x 12
</textarea>
<textarea id="output" disabled></textarea>
</div>
</main>
</body>
Expand Down
44 changes: 44 additions & 0 deletions pkg/evaluator/builtin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package evaluator

import (
"strconv"
"strings"
)

type Builtin func(args []Value) Value

func (b Builtin) Type() ValueType { return BUILTIN }
func (b Builtin) String() string { return "builtin function" }

func newBuiltins(e *Evaluator) map[string]Builtin {
return map[string]Builtin{
"print": Builtin(e.Print),
"len": Builtin(Len),
}
}

func (e *Evaluator) Print(args []Value) Value {
argList := make([]string, len(args))
for i, arg := range args {
argList[i] = arg.String()
}
e.print(strings.Join(argList, " "))
return nil
}

func Len(args []Value) Value {
if len(args) != 1 {
return newError("'len' takes 1 argument not " + strconv.Itoa(len(args)))
}
switch arg := args[0].(type) {
case *Map:
return &Num{Val: float64(len(arg.Pairs))}
case *Array:
return &Num{Val: float64(len(arg.Elements))}
case *String:
return &Num{Val: float64(len(arg.Val))}
default:
return newError("'len' takes 1 argument of type 'string', array '[]' or map '{}' not " + args[0].Type().String())
}

}
98 changes: 96 additions & 2 deletions pkg/evaluator/evaluator.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,101 @@
package evaluator

import "strings"
import (
"foxygo.at/evy/pkg/parser"
)

func Run(input string, print func(string)) {
print(strings.ToUpper(input))
p := parser.New(input)
prog := p.Parse()
e := &Evaluator{print: print}
e.builtins = newBuiltins(e)
val := e.Eval(prog, NewScope())
if isError(val) {
print(val.String())
}
}

type Evaluator struct {
print func(string)
builtins map[string]Builtin
}

func (e *Evaluator) Eval(node parser.Node, scope *Scope) Value {
switch node := node.(type) {
case *parser.Program:
return e.evalProgram(node, scope)
case *parser.Declaration:
return e.evalDeclaration(node, scope)
case *parser.Var:
v := e.evalVar(node, scope)
return v
case *parser.Term:
return e.evalTerm(node, scope)
case *parser.NumLiteral:
return &Num{Val: node.Value}
case *parser.StringLiteral:
return &String{Val: node.Value}
case *parser.Bool:
return &Bool{Val: node.Value}
case *parser.FunctionCall:
return e.evalFunctionCall(node, scope)
}
return nil
}

func (e *Evaluator) evalProgram(program *parser.Program, scope *Scope) Value {
var result Value
for _, statement := range program.Statements {
result = e.Eval(statement, scope)
if isError(result) {
return result
}
}
return result
}

func (e *Evaluator) evalDeclaration(decl *parser.Declaration, scope *Scope) Value {
val := e.Eval(decl.Value, scope)
if isError(val) {
return val
}
scope.Set(decl.Var.Name, val)
return nil
}

func (e *Evaluator) evalFunctionCall(funcCall *parser.FunctionCall, scope *Scope) Value {
args := e.evalTerms(funcCall.Arguments, scope)
if len(args) == 1 && isError(args[0]) {
return args[0]
}
builtin, ok := e.builtins[funcCall.Name]
if !ok {
return newError("cannot find builtin function " + funcCall.Name)
}
return builtin(args)
}

func (e *Evaluator) evalVar(v *parser.Var, scope *Scope) Value {
if val, ok := scope.Get(v.Name); ok {
return val
}
return newError("cannot find variable " + v.Name)
}

func (e *Evaluator) evalTerm(term parser.Node, scope *Scope) Value {
return e.Eval(term, scope)
}

func (e *Evaluator) evalTerms(terms []parser.Node, scope *Scope) []Value {
result := make([]Value, len(terms))

for i, t := range terms {
evaluated := e.Eval(t, scope)
if isError(evaluated) {
return []Value{evaluated}
}
result[i] = evaluated
}

return result
}
53 changes: 53 additions & 0 deletions pkg/evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package evaluator

import (
"bytes"
"testing"

"foxygo.at/evy/pkg/assert"
)

func TestBasicEval(t *testing.T) {
in := "a:=1\n print a 2"
want := "1 2"
b := bytes.Buffer{}
fn := func(s string) { b.WriteString(s) }
Run(in, fn)
assert.Equal(t, want, b.String())
}

func TestParseDeclaration(t *testing.T) {
tests := map[string]string{
"a:=1": "1",
`a:="abc"`: "abc",
`a:=true`: "true",
`a:= len "abc"`: "3",
}
for in, want := range tests {
in, want := in, want
t.Run(in, func(t *testing.T) {
in += "\n print a"
b := bytes.Buffer{}
fn := func(s string) { b.WriteString(s) }
Run(in, fn)
assert.Equal(t, want, b.String())
})
}
}

func TestDemo(t *testing.T) {
prog := `
move 10 10
line 20 20
x := 12
print "x:" x
if x > 10
print "🍦 big x"
end`
b := bytes.Buffer{}
fn := func(s string) { b.WriteString(s) }
Run(prog, fn)
want := "x: 12"
assert.Equal(t, want, b.String())
}
29 changes: 29 additions & 0 deletions pkg/evaluator/scope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package evaluator

type Scope struct {
store map[string]Value
outer *Scope
}

func NewScope() *Scope {
return &Scope{store: map[string]Value{}}
}

func NewEnclosedScope(outer *Scope) *Scope {
return &Scope{store: map[string]Value{}, outer: outer}
}

func (s *Scope) Get(name string) (Value, bool) {
if s == nil {
return nil, false
}
if val, ok := s.store[name]; ok {
return val, ok
}
return s.outer.Get(name)
}

func (s *Scope) Set(name string, val Value) Value {
s.store[name] = val
return val
}
Loading

0 comments on commit 88a019d

Please sign in to comment.