Skip to content

Commit

Permalink
[semantics]:block statement (#37)
Browse files Browse the repository at this point in the history
* block statement
* variable scopes
* some minor improvements on the parser's error handling
  • Loading branch information
silverhairs authored Jul 27, 2023
1 parent 5969220 commit 141ca5e
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 34 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ Production rules:
letDecl -> ("var" | "let") IDENTIFIER ("=" expression) ? ";" ;
statement -> exprStmt
| printStmt ;
| printStmt
| blockStmt ;
exprStmt -> expression ";" ;
printStmt -> "print" expression ";" ;
blockStmt -> "{" declaration* "}" ;
expression -> literal
| unary
Expand Down
13 changes: 13 additions & 0 deletions glox/ast/stmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type StmtVisitor interface {
VisitPrintStmt(*PrintStmt) any
VisitExprStmt(*ExpressionStmt) any
VisitLetStmt(*LetStmt) any
VisitBlockStmt(*BlockStmt) any
}

type Statement interface {
Expand Down Expand Up @@ -48,3 +49,15 @@ func NewExprStmt(exp Expression) *ExpressionStmt {
func (stmt *ExpressionStmt) Accept(v StmtVisitor) any {
return v.VisitExprStmt(stmt)
}

type BlockStmt struct {
Stmts []Statement
}

func NewBlockStmt(stmts []Statement) *BlockStmt {
return &BlockStmt{Stmts: stmts}
}

func (stmt *BlockStmt) Accept(v StmtVisitor) any {
return v.VisitBlockStmt(stmt)
}
17 changes: 14 additions & 3 deletions glox/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import (
)

type Environment struct {
values map[string]any
values map[string]any
enclosing *Environment
}

func New() *Environment {
return &Environment{values: make(map[string]any)}
func Global() *Environment {
return New(nil)
}

func New(enclosing *Environment) *Environment {
return &Environment{values: make(map[string]any), enclosing: enclosing}
}

func (env *Environment) Define(name string, value any) {
Expand All @@ -21,11 +26,17 @@ func (env *Environment) Get(name token.Token) any {
if val, isOk := env.values[name.Lexeme]; isOk {
return val
}
if env.enclosing != nil {
return env.enclosing.Get(name)
}
return exception.Runtime(name, "undefined variable '"+name.Lexeme+"'.")
}

func (env *Environment) Assign(name token.Token, value any) error {
if _, isOk := env.values[name.Lexeme]; !isOk {
if env.enclosing != nil {
return env.enclosing.Assign(name, value)
}
return exception.Runtime(name, "undefined variable '"+name.Lexeme+"'.")
}
env.values[name.Lexeme] = value
Expand Down
85 changes: 71 additions & 14 deletions glox/env/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,57 @@ package env

import (
"glox/token"
"math/rand"
"strings"
"testing"
)

func TestDefine(t *testing.T) {
e := New()
global := Global()
vars := map[string]any{
"age": 25,
"name": `"boris"`,
"isHuman": true,
}

for name, value := range vars {
e.Define(name, value)
if e.values[name] != value {
t.Fatalf("failed to define a variable %q with a value. expected=%v got=%v", name, value, e.values[name])
global.Define(name, value)
if global.values[name] != value {
t.Fatalf("failed to define a variable %q with a value. expected=%v got=%v", name, value, global.values[name])
}
}

local := New(global)
if local.enclosing != global {
t.Fatalf("local env must have a reference to its enclosing env. expected=%v got=%v", global, local.enclosing)
}

expected := "bob"
local.Define("name", expected)
got := local.values["name"]
if got != expected {
t.Fatalf("failed to shadow variable in local scope. got='%v'. expected='%s'", got, expected)
}
}

func TestGet(t *testing.T) {
e := New()
global := Global()
vars := map[string]any{
"age": 25,
"name": `"boris"`,
"isHuman": true,
}
for name, value := range vars {
e.Define(name, value)
global.Define(name, value)
tok := token.Token{Type: token.IDENTIFIER, Lexeme: name, Literal: nil, Line: 1}
got := e.Get(tok)
got := global.Get(tok)

if got != value {
t.Fatalf("failed to get variable %q. expected=%v got=%v", name, value, got)
}
}
undefined := token.Token{Type: token.IDENTIFIER, Lexeme: "undefined", Literal: nil, Line: 1}
got := e.Get(undefined)
got := global.Get(undefined)

if err, isErr := got.(error); isErr {
if !(strings.Contains(err.Error(), "undefined variable") && strings.Contains(err.Error(), "RuntimeException")) {
Expand All @@ -48,24 +61,55 @@ func TestGet(t *testing.T) {
} else {
t.Fatalf("failed to capture 'undefined variable'exception. got=%v", got)
}

local := New(global)

if local.enclosing != global {
t.Fatalf("non-global scope must have a reference to their enclosing scope. got='%v' expected='%v'", local.enclosing, global)
}

for name := range vars {
tok := token.Token{Type: token.IDENTIFIER, Lexeme: name, Literal: nil, Line: 1}
got = local.Get(tok)
expected := global.Get(tok)

if expected != got {
t.Fatalf("failed to get variable from enclosing environment in local scope. expected='%v'. got='%v'", expected, got)
}
}

expected := rand.Int()
local.Define(undefined.Lexeme, expected)
got = local.Get(undefined)

if expected != got {
t.Fatalf("failed to get variable defined in local environment. got='%v' expected='%v'", got, expected)
}

gotGlobal := global.Get(undefined)
if _, isErr := gotGlobal.(error); !isErr {
t.Fatalf("variable defined in local scope should not be accessible in enclosing scope. got='%v'. expected='RuntimeException'", gotGlobal)
}

}

func TestAssign(t *testing.T) {
e := New()
e.Define("name", `"boris"`)
global := Global()
global.Define("name", `"boris"`)
tok := token.Token{Type: token.IDENTIFIER, Lexeme: "name", Literal: nil, Line: 1}

err := e.Assign(token.Token{Type: token.IDENTIFIER, Lexeme: "name", Literal: nil, Line: 1}, "anya")
err := global.Assign(tok, "anya")

if err != nil {
t.Fatalf("failed to assign new value to variable=%v. got=%s", "name", err.Error())
}

if e.values["name"] != "anya" {
t.Fatalf("failed to assign new value to variable. expected=%v got=%v", "anya", e.values["name"])
if global.values["name"] != "anya" {
t.Fatalf("failed to assign new value to variable. expected=%v got=%v", "anya", global.values["name"])
}
undefined := token.Token{Type: token.IDENTIFIER, Lexeme: "nothing", Literal: nil, Line: 1}

err = e.Assign(undefined, 12)
err = global.Assign(undefined, 12)
if err == nil {
t.Fatal("assigning an undefined variable should results on an error.")
}
Expand All @@ -75,4 +119,17 @@ func TestAssign(t *testing.T) {
t.Fatalf("wrong error message for undefined variable. got='%s' expected contains=['%s', '%s']", msg, "undefined variable", undefined.Lexeme)
}

local := New(global)

if local.enclosing != global {
t.Fatalf("non-global scope must have a reference to their enclosing scope. got='%v' expected='%v'", local.enclosing, global)
}

expected := "bob"
local.Assign(tok, expected)
got := local.Get(tok)

if expected != got {
t.Fatalf("failed to assign value to global variable inside local scope. got='%v' expected='%v'", got, expected)
}
}
23 changes: 21 additions & 2 deletions glox/interpreter/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Interpreter struct {
}

func New(stderr io.Writer, stdout io.Writer) *Interpreter {
return &Interpreter{StdOut: stdout, StdErr: stderr, Env: env.New()}
return &Interpreter{StdOut: stdout, StdErr: stderr, Env: env.Global()}
}

func (i *Interpreter) Interpret(stmts []ast.Statement) any {
Expand Down Expand Up @@ -53,6 +53,7 @@ func (i *Interpreter) VisitExprStmt(stmt *ast.ExpressionStmt) any {
if err, isErr := val.(error); isErr {
return err
}
fmt.Fprintf(i.StdOut, "%v\n", val)
return nil
}

Expand All @@ -66,8 +67,26 @@ func (i *Interpreter) VisitPrintStmt(stmt *ast.PrintStmt) any {
return nil
}

func (i *Interpreter) VisitBlockStmt(stmt *ast.BlockStmt) any {
i.executeBlock(stmt.Stmts, env.New(i.Env))
return nil
}

func (i *Interpreter) executeBlock(stmts []ast.Statement, env *env.Environment) {
prev := i.Env
i.Env = env
for _, stmt := range stmts {
i.execute(stmt)
}
i.Env = prev
}

func (i *Interpreter) VisitVariable(exp *ast.Variable) any {
return i.Env.Get(exp.Name)
if res := i.Env.Get(exp.Name); res != nil {
return res
}

return exception.Runtime(exp.Name, fmt.Sprintf("tried to access variable '%s' which holds a nil value.", exp.Name.Lexeme))
}

func (i *Interpreter) VisitLiteral(exp *ast.Literal) any {
Expand Down
18 changes: 9 additions & 9 deletions glox/interpreter/interpreter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,18 @@ func TestInterpret(t *testing.T) {
}

failures := map[string][]string{
"1+false": {
"1+false;": {
"unsupported operands. This operation can only be performed with numbers and strings.",
"+",
},
"1/0": {"division by zero", "/"},
"1-false": {"Operator \"-\" only accepts number operands", "-"},
"-\"yes\"": {"Operator \"-\" only accepts number operands", "-"},
"true*false": {"Operator \"*\" only accepts number operands", "*"},
"false>true": {"Operator \">\" only accepts number operands", ">"},
"false>=12": {"Operator \">=\" only accepts number operands", ">="},
"true<=12": {"Operator \"<=\" only accepts number operands", "<="},
"true<true": {"Operator \"<\" only accepts number operands", "<"},
"1/0;": {"division by zero", "/"},
"1-false;": {"Operator \"-\" only accepts number operands", "-"},
"-\"yes\";": {"Operator \"-\" only accepts number operands", "-"},
"true*false;": {"Operator \"*\" only accepts number operands", "*"},
"false>true;": {"Operator \">\" only accepts number operands", ">"},
"false>=12;": {"Operator \">=\" only accepts number operands", ">="},
"true<=12;": {"Operator \"<=\" only accepts number operands", "<="},
"true<true;": {"Operator \"<\" only accepts number operands", "<"},
}

for code, chunks := range failures {
Expand Down
33 changes: 29 additions & 4 deletions glox/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,28 +54,51 @@ func (p *Parser) letDeclaration() (ast.Statement, error) {
val, err = p.expression()
}

p.consume(token.SEMICOLON, "expected ';' after variable declaration,")
if _, e := p.consume(token.SEMICOLON, "expect ';' after variable declaration."); e != nil {
err = e
}
return ast.NewLetStmt(tok, val), err
}

func (p *Parser) statement() (ast.Statement, error) {

if p.match(token.PRINT) {
return p.printStatement()
} else if p.match(token.L_BRACE) {
block, err := p.block()
return ast.NewBlockStmt(block), err
}
return p.expressionStatement()
}

func (p *Parser) block() ([]ast.Statement, error) {
stmts := []ast.Statement{}
var err error
for !p.check(token.R_BRACE) && !p.isAtEnd() {
stmt, e := p.declaration()
if err != nil {
return stmts, e
}
stmts = append(stmts, stmt)
}
_, err = p.consume(token.R_BRACE, "expect '}' after block.")
return stmts, err
}

func (p *Parser) printStatement() (ast.Statement, error) {
exp, err := p.expression()
p.consume(token.SEMICOLON, "expect ';' after value.")
if _, e := p.consume(token.SEMICOLON, "expect ';' after value."); e != nil {
err = e
}
return ast.NewPrintStmt(exp), err

}

func (p *Parser) expressionStatement() (ast.Statement, error) {
exp, err := p.expression()
p.consume(token.SEMICOLON, "expect ';' after value.")
if _, e := p.consume(token.SEMICOLON, "expect ';' after value."); e != nil {
err = e
}
return ast.NewExprStmt(exp), err
}

Expand Down Expand Up @@ -261,7 +284,9 @@ func (p *Parser) primary() (ast.Expression, error) {
return exp, err
}

_, err = p.consume(token.R_PAREN, "Expect ')' after expression")
if _, e := p.consume(token.R_PAREN, "Expect ')' after expression"); e != nil {
err = e
}
return ast.NewGroupingExp(exp), err

}
Expand Down
2 changes: 1 addition & 1 deletion glox/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestParseTernary(t *testing.T) {
}

func TestParseUnary(t *testing.T) {
code := `!true`
code := `!true;`
lxr := lexer.New(code)
prsr := New(lxr.Tokenize())

Expand Down

0 comments on commit 141ca5e

Please sign in to comment.