Skip to content

Commit

Permalink
[frontend]: assigning values to variable (#35)
Browse files Browse the repository at this point in the history
* feat: variable assignment

* fix: assigning undeclared variable is not returning exception

* chore: minor change

* test: add new test cases to interpreter for variable assignment

* chore: minor cleaning

* refactor: rename `smt` into `stmt`

* test: more test cases

* fix: weak condition in test case

* test: more check in interpreter test cases
  • Loading branch information
silverhairs authored Jul 25, 2023
1 parent 164eaae commit 5969220
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 82 deletions.
40 changes: 34 additions & 6 deletions glox/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import (
type ExpType string

const (
BINARY_EXP ExpType = "binary"
UNARY_EXP ExpType = "unary"
GROUP_EXP ExpType = "group"
LITERAL_EXP ExpType = "literal"
TERNARY_EXP ExpType = "ternary"
VARIABLE_EXP ExpType = "variable"
BINARY_EXP ExpType = "binary"
UNARY_EXP ExpType = "unary"
GROUP_EXP ExpType = "group"
LITERAL_EXP ExpType = "literal"
TERNARY_EXP ExpType = "ternary"
VARIABLE_EXP ExpType = "variable"
ASSIGNMENT_EXP ExpType = "assignment"
)

type Expression interface {
Expand All @@ -30,6 +31,7 @@ type Visitor interface {
VisitLiteral(exp *Literal) any
VisitTernary(exp *Ternary) any
VisitVariable(exp *Variable) any
VisitAssignment(exp *Assignment) any
}

type Literal struct {
Expand Down Expand Up @@ -190,6 +192,32 @@ func (v *Variable) Accept(visitor Visitor) any {
return visitor.VisitVariable(v)
}

type Assignment struct {
Name token.Token
Value Expression
}

func NewAssignment(name token.Token, value Expression) *Assignment {
return &Assignment{Name: name, Value: value}
}

func (exp *Assignment) Type() ExpType {
return ASSIGNMENT_EXP
}

func (exp *Assignment) String() string {
var out bytes.Buffer
out.WriteString("(")
out.WriteString(exp.Name.Lexeme)
out.WriteString(exp.Value.String())
out.WriteString(")")
return parenthesize(exp.Type(), out.String())
}

func (exp *Assignment) Accept(v Visitor) any {
return v.VisitAssignment(exp)
}

func parenthesize(name ExpType, value string) string {
var out bytes.Buffer

Expand Down
4 changes: 4 additions & 0 deletions glox/ast/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ func (p *printer) VisitTernary(ternary *Ternary) any {
func (p *printer) VisitVariable(variable *Variable) any {
return variable.Accept(p)
}

func (p *printer) VisitAssignment(assign *Assignment) any {
return assign.Accept(p)
}
50 changes: 0 additions & 50 deletions glox/ast/smt.go

This file was deleted.

50 changes: 50 additions & 0 deletions glox/ast/stmt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ast

import "glox/token"

type StmtVisitor interface {
VisitPrintStmt(*PrintStmt) any
VisitExprStmt(*ExpressionStmt) any
VisitLetStmt(*LetStmt) any
}

type Statement interface {
Accept(StmtVisitor) any
}

type PrintStmt struct {
Exp Expression
}

type LetStmt struct {
Name token.Token
Value Expression
}

func NewLetStmt(name token.Token, value Expression) *LetStmt {
return &LetStmt{Name: name, Value: value}
}

func (stmt *LetStmt) Accept(v StmtVisitor) any {
return v.VisitLetStmt(stmt)
}

func NewPrintStmt(exp Expression) *PrintStmt {
return &PrintStmt{Exp: exp}
}

func (stmt *PrintStmt) Accept(v StmtVisitor) any {
return v.VisitPrintStmt(stmt)
}

type ExpressionStmt struct {
Exp Expression
}

func NewExprStmt(exp Expression) *ExpressionStmt {
return &ExpressionStmt{Exp: exp}
}

func (stmt *ExpressionStmt) Accept(v StmtVisitor) any {
return v.VisitExprStmt(stmt)
}
8 changes: 8 additions & 0 deletions glox/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,11 @@ func (env *Environment) Get(name token.Token) any {
}
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 {
return exception.Runtime(name, "undefined variable '"+name.Lexeme+"'.")
}
env.values[name.Lexeme] = value
return nil
}
29 changes: 28 additions & 1 deletion glox/env/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,37 @@ func TestGet(t *testing.T) {
got := e.Get(undefined)

if err, isErr := got.(error); isErr {
if !strings.Contains(err.Error(), "undefined variable") || !strings.Contains(err.Error(), "RuntimeException") {
if !(strings.Contains(err.Error(), "undefined variable") && strings.Contains(err.Error(), "RuntimeException")) {
t.Fatalf("wrong error message. expected a RuntimeException with message 'undefined variable'. got=%q", err.Error())
}
} else {
t.Fatalf("failed to capture 'undefined variable'exception. got=%v", got)
}
}

func TestAssign(t *testing.T) {
e := New()
e.Define("name", `"boris"`)

err := e.Assign(token.Token{Type: token.IDENTIFIER, Lexeme: "name", Literal: nil, Line: 1}, "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"])
}
undefined := token.Token{Type: token.IDENTIFIER, Lexeme: "nothing", Literal: nil, Line: 1}

err = e.Assign(undefined, 12)
if err == nil {
t.Fatal("assigning an undefined variable should results on an error.")
}

msg := err.Error()
if !(strings.Contains(msg, "undefined variable") && strings.Contains(msg, undefined.Lexeme)) {
t.Fatalf("wrong error message for undefined variable. got='%s' expected contains=['%s', '%s']", msg, "undefined variable", undefined.Lexeme)
}

}
35 changes: 23 additions & 12 deletions glox/interpreter/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,44 +20,44 @@ func New(stderr io.Writer, stdout io.Writer) *Interpreter {
return &Interpreter{StdOut: stdout, StdErr: stderr, Env: env.New()}
}

func (i *Interpreter) Interpret(smts []ast.Statement) any {
func (i *Interpreter) Interpret(stmts []ast.Statement) any {
var err error
for _, smt := range smts {
i.execute(smt)
for _, stmt := range stmts {
i.execute(stmt)
}
return err
}

func (i *Interpreter) execute(stmt ast.Statement) {
val := stmt.Accept(i)
if err, isErr := val.(error); isErr {
fmt.Fprintf(i.StdErr, "%s", err.Error())
fmt.Fprintf(i.StdErr, "%s\n", err.Error())
}
}

func (i *Interpreter) VisitLetSmt(smt *ast.LetSmt) any {
func (i *Interpreter) VisitLetStmt(stmt *ast.LetStmt) any {
var val any
if smt.Value != nil {
val = i.evaluate(smt.Value)
if stmt.Value != nil {
val = i.evaluate(stmt.Value)
}
if err, isErr := val.(error); isErr {
return err
}

i.Env.Define(smt.Name.Lexeme, val)
i.Env.Define(stmt.Name.Lexeme, val)
return nil
}

func (i *Interpreter) VisitExprStmt(smt *ast.ExpressionStmt) any {
val := i.evaluate(smt.Exp)
func (i *Interpreter) VisitExprStmt(stmt *ast.ExpressionStmt) any {
val := i.evaluate(stmt.Exp)
if err, isErr := val.(error); isErr {
return err
}
return nil
}

func (i *Interpreter) VisitPrintStmt(smt *ast.PrintSmt) any {
val := i.evaluate(smt.Exp)
func (i *Interpreter) VisitPrintStmt(stmt *ast.PrintStmt) any {
val := i.evaluate(stmt.Exp)
if err, isErr := val.(error); isErr {
fmt.Fprintf(i.StdErr, "%s\n", err.Error())
} else {
Expand Down Expand Up @@ -215,6 +215,17 @@ func (i *Interpreter) VisitTernary(exp *ast.Ternary) any {

}

func (i *Interpreter) VisitAssignment(exp *ast.Assignment) any {
val := i.evaluate(exp.Value)
if err, isErr := val.(error); isErr {
return err
}
if err := i.Env.Assign(exp.Name, val); err != nil {
return err
}
return val
}

func (i *Interpreter) evaluate(exp ast.Expression) any {
return exp.Accept(i)
}
Expand Down
64 changes: 60 additions & 4 deletions glox/interpreter/interpreter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"glox/lexer"
"glox/parser"
"glox/token"
"math/rand"
"strings"
"testing"
Expand Down Expand Up @@ -36,18 +37,23 @@ func TestInterpret(t *testing.T) {
intrprtr := New(stderr, stdout)

if expr, err := prsr.Parse(); err != nil {
t.Fatalf("failed to parse code %q", code)
t.Fatalf("failed to parse code %q. \ngot=%v \nexpected=%v", code, err.Error(), expected)
} else {
intrprtr.Interpret(expr)
if stderr.String() != "" {
t.Fatalf("failed to interpret %q. expected=%v got=%v", code, expected, stderr.String())
t.Fatalf("failed to evaluate %q. expected=%v got=%v", code, expected, stderr.String())
}
actual := strings.TrimRight(stdout.String(), "\n")
if actual != expected {
t.Fatalf("failed to interpret %q. expected=%q got=%q", code, expected, actual)
t.Fatalf("failed to evaluate %q. expected=%q got=%q", code, expected, actual)
}
}

if stderr.String() != "" && !strings.HasSuffix(stderr.String(), "\n") {
t.Fatalf("stderr message must end with a new line")
}
if stdout.String() != "" && !strings.HasSuffix(stdout.String(), "\n") {
t.Fatalf("stdout message must end with a new line")
}
stderr.Reset()
stdout.Reset()
}
Expand Down Expand Up @@ -96,8 +102,58 @@ func TestInterpret(t *testing.T) {
}
}

if stderr.String() != "" && !strings.HasSuffix(stderr.String(), "\n") {
t.Fatalf("stderr message must end with a new line")
}
if stdout.String() != "" && !strings.HasSuffix(stdout.String(), "\n") {
t.Fatalf("stdout message must end with a new line")
}

stdout.Reset()
stderr.Reset()
}

vars := []struct {
code string
name string
value any
}{
{code: `let number = 12;`, name: "number", value: 12},
{code: `var seven = 7;`, name: "seven", value: 7},
{code: `let is_boolean=true;`, name: "is_boolean", value: true},
{code: `var name = "anya forger";`, name: "name", value: "anya forger"},
}

for _, variable := range vars {
lxr := lexer.New(variable.code)
prsr := parser.New(lxr.Tokenize())

stmts, err := prsr.Parse()
if err != nil {
t.Fatalf("failed to parse code %q. \ngot=%v", variable.code, err.Error())
}

i := New(stderr, stdout)
i.Interpret(stmts)

if stderr.String() != "" {
t.Fatalf("caught exception when evaluating code=%q. got=%v", variable.code, stderr.String())
}

tok := token.Token{Type: token.IDENTIFIER, Lexeme: variable.name, Literal: nil, Line: 1}
got := i.Env.Get(tok)

expected := variable.value
if num, isOk := expected.(int); isOk {
expected = float64(num)
}

if got != expected {
t.Fatalf("failed to keep state of defined variable in code=%q. got='%v'\nexpected='%v'.", variable.code, got, expected)
}

stderr.Reset()
stdout.Reset()
}

}
Loading

0 comments on commit 5969220

Please sign in to comment.