Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

go: Initialise parser and evaluator #10

Merged
merged 1 commit into from
Sep 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
70 changes: 38 additions & 32 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,36 +128,34 @@ not allowed.
map_elems = { ident ":" term [NL] } .

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

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

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

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

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

/* --- Terminals --- */
LETTER = UNICODE_LETTER | "_" .
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