Skip to content

Commit

Permalink
while loops and continue statements
Browse files Browse the repository at this point in the history
  • Loading branch information
jairo-litman committed Feb 15, 2024
1 parent ea986c4 commit 244691b
Show file tree
Hide file tree
Showing 15 changed files with 525 additions and 78 deletions.
35 changes: 22 additions & 13 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,42 +155,42 @@ func (blockStmt *BlockStatement) String() string {
return out.String()
}

// A for loop statement, e.g. for (let i = 0; i < 10; i = i + 1) { ... }
type ForLoopStatement struct {
Token token.Token // token.FOR
// A loop statement, e.g. for (let i = 0; i < 10; i = i + 1) { ... } or while (i < 10) { ... }
type LoopStatement struct {
Token token.Token // token.FOR or token.WHILE
Initializer Statement // statement that initializes the loop e.g. let i = 0 // or nil
Condition Expression // expression that evaluates to the condition of the loop e.g. i < 10 // or nil
Update Statement // statement that updates the loop e.g. i = i + 1 // or nil
Body *BlockStatement // block statement that makes up the body of the loop
}

func (forLoop *ForLoopStatement) statementNode() {}
func (forLoop *ForLoopStatement) TokenLiteral() string { return forLoop.Token.Literal }
func (forLoop *ForLoopStatement) String() string {
func (loop *LoopStatement) statementNode() {}
func (loop *LoopStatement) TokenLiteral() string { return loop.Token.Literal }
func (loop *LoopStatement) String() string {
var out bytes.Buffer

out.WriteString("for")
out.WriteString(" (")

if forLoop.Initializer != nil {
out.WriteString(forLoop.Initializer.String())
if loop.Initializer != nil {
out.WriteString(loop.Initializer.String())
out.WriteString(" ")
} else {
out.WriteString("; ")
}

if forLoop.Condition != nil {
out.WriteString(forLoop.Condition.String())
if loop.Condition != nil {
out.WriteString(loop.Condition.String())
}
out.WriteString("; ")

if forLoop.Update != nil {
out.WriteString(forLoop.Update.String())
if loop.Update != nil {
out.WriteString(loop.Update.String())
}
out.WriteString(") ")

out.WriteString("{ ")
out.WriteString(forLoop.Body.String())
out.WriteString(loop.Body.String())
out.WriteString(" }")

return out.String()
Expand All @@ -205,6 +205,15 @@ func (breakStmt *BreakStatement) statementNode() {}
func (breakStmt *BreakStatement) TokenLiteral() string { return breakStmt.Token.Literal }
func (breakStmt *BreakStatement) String() string { return breakStmt.TokenLiteral() }

// A continue statement, e.g. continue;
type ContinueStatement struct {
Token token.Token // token.CONTINUE
}

func (continueStmt *ContinueStatement) statementNode() {}
func (continueStmt *ContinueStatement) TokenLiteral() string { return continueStmt.Token.Literal }
func (continueStmt *ContinueStatement) String() string { return continueStmt.TokenLiteral() }

// ----------------------------------------------------------------------------
// Expressions
// ----------------------------------------------------------------------------
Expand Down
8 changes: 4 additions & 4 deletions code/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ const (

// Loop Opcodes

OpForLoop // Push a for loop onto the stack // pops after the loop
OpBreak // Break out of a loop // pops a loop
OpLoop // Push a loop onto the stack // pops after the loop
OpBreak // Break out of a loop // pops a loop
)

// Opcode definitions
Expand Down Expand Up @@ -199,8 +199,8 @@ var definitions = map[Opcode]*Definition{

// Loop Opcodes

OpForLoop: {"OpForLoop", []int{2}}, // Single operand of 2 bytes, 3 bytes in total
OpBreak: {"OpBreak", []int{}}, // No operands, 1 byte in total
OpLoop: {"OpLoop", []int{2}}, // Single operand of 2 bytes, 3 bytes in total
OpBreak: {"OpBreak", []int{}}, // No operands, 1 byte in total
}

// Returns the Definition of the opcode
Expand Down
26 changes: 21 additions & 5 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import (
"sort"
)

var (
loopContinuePos = []int{}
)

type Bytecode struct {
Instructions code.Instructions
Constants []object.Object
Expand Down Expand Up @@ -150,9 +154,11 @@ func (c *Compiler) Compile(node ast.Node) error {
}
}

case *ast.ForLoopStatement:
case *ast.LoopStatement:
c.enterScope()

currentContinueCount := len(loopContinuePos)

if node.Initializer != nil {
err := c.Compile(node.Initializer)
if err != nil {
Expand Down Expand Up @@ -182,6 +188,11 @@ func (c *Compiler) Compile(node ast.Node) error {
return err
}

// Update any Continue statements with the correct value
for _, pos := range loopContinuePos[currentContinueCount:] {
c.changeOperand(pos, len(c.currentInstructions()))
}

if node.Update != nil {
err = c.Compile(node.Update)
if err != nil {
Expand All @@ -196,8 +207,8 @@ func (c *Compiler) Compile(node ast.Node) error {
jumpPos := c.emit(code.OpJump, 9999)

// Update the `OpJumpNotTruthy` with the correct value
afterBodyPos := len(c.currentInstructions())
c.changeOperand(jumptNotTruthyPos, afterBodyPos)
afterUpdatePos := len(c.currentInstructions())
c.changeOperand(jumptNotTruthyPos, afterUpdatePos)

// Update the `OpJump` with the correct value
c.changeOperand(jumpPos, conditionPos)
Expand All @@ -213,19 +224,24 @@ func (c *Compiler) Compile(node ast.Node) error {
free[i] = s.Index
}

compiled := &object.CompiledFor{
compiled := &object.CompiledLoop{
Instructions: ins,
NumLocals: numLoc,
Free: free,
}

loopContinuePos = loopContinuePos[:currentContinueCount]

idx := c.addConstant(compiled)

c.emit(code.OpForLoop, idx)
c.emit(code.OpLoop, idx)

case *ast.BreakStatement:
c.emit(code.OpBreak)

case *ast.ContinueStatement:
loopContinuePos = append(loopContinuePos, c.emit(code.OpJump, 9999))

// Expressions
case *ast.Identifier:
symbol, ok := c.symbolTable.Resolve(node.Value)
Expand Down
164 changes: 160 additions & 4 deletions compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1220,7 +1220,7 @@ func TestForLoops(t *testing.T) {
},
},
expectedInstructions: []code.Instructions{
code.Make(code.OpForLoop, 3),
code.Make(code.OpLoop, 3),
},
},
{
Expand Down Expand Up @@ -1274,7 +1274,7 @@ func TestForLoops(t *testing.T) {
expectedInstructions: []code.Instructions{
code.Make(code.OpConstant, 0),
code.Make(code.OpSetGlobal, 0),
code.Make(code.OpForLoop, 4),
code.Make(code.OpLoop, 4),
code.Make(code.OpGetGlobal, 0),
code.Make(code.OpPop),
},
Expand Down Expand Up @@ -1353,7 +1353,7 @@ func TestForLoops(t *testing.T) {
// 0003
code.Make(code.OpSetGlobal, 0),
// 0006
code.Make(code.OpForLoop, 5),
code.Make(code.OpLoop, 5),
// 0009
code.Make(code.OpGetGlobal, 0),
// 0012
Expand Down Expand Up @@ -1424,7 +1424,7 @@ func TestForLoops(t *testing.T) {
// 0003
code.Make(code.OpSetGlobal, 0),
// 0006
code.Make(code.OpForLoop, 3),
code.Make(code.OpLoop, 3),
// 0009
code.Make(code.OpGetGlobal, 0),
// 0012
Expand Down Expand Up @@ -1534,3 +1534,159 @@ func TestCompoundAssignment(t *testing.T) {

runCompilerTests(t, tests)
}

func TestContinueStatement(t *testing.T) {
tests := []compilerTestCase{
{
input: `
while (true) {
continue;
}
`,
expectedConstants: []interface{}{
[]code.Instructions{
// 0000 - init
code.Make(code.OpNull),
// 0001
code.Make(code.OpPop),
// 0002 - condition
code.Make(code.OpTrue),
// 0003
code.Make(code.OpJumpNotTruthy, 14),
// 0006 - loop body
code.Make(code.OpJump, 9),
// 0009 - update
code.Make(code.OpNull),
// 0010
code.Make(code.OpPop),
// 0011 - jump back to condition
code.Make(code.OpJump, 2),
// 0014 - exit loop
code.Make(code.OpBreak),
},
},
expectedInstructions: []code.Instructions{
code.Make(code.OpLoop, 0),
},
},
{
input: `
let sum = 0;
let i = 0;
while (i < 10) {
for (let j = 0; j < 10; j += 1) {
if (j == 5) {
continue;
}
sum += j;
}
i += 1;
}
sum;
`,
expectedConstants: []interface{}{
0,
0,
10,
0,
10,
5,
1,
[]code.Instructions{
// 0000 - init (let j = 0)
code.Make(code.OpConstant, 3),
// 0003
code.Make(code.OpSetLocal, 0),
// 0005 - condition (j < 10)
code.Make(code.OpGetLocal, 0),
// 0007
code.Make(code.OpConstant, 4),
// 0010
code.Make(code.OpLessThan),
// 0011 - exit loop if false
code.Make(code.OpJumpNotTruthy, 51),
// 0014 - loop body ({ if ... })
code.Make(code.OpGetLocal, 0),
// 0016
code.Make(code.OpConstant, 5),
// 0019
code.Make(code.OpEqual),
// 0020
code.Make(code.OpJumpNotTruthy, 29),
// 0023
code.Make(code.OpJump, 40),
// 0026
code.Make(code.OpJump, 30),
// 0029
code.Make(code.OpNull),
// 0030
code.Make(code.OpPop),
// 0031
code.Make(code.OpGetGlobal, 0),
// 0034
code.Make(code.OpGetLocal, 0),
// 0036
code.Make(code.OpAdd),
// 0037
code.Make(code.OpSetGlobal, 0),
// 0040 - loop increment (j = j + 1)
code.Make(code.OpGetLocal, 0),
// 0042
code.Make(code.OpConstant, 6),
// 0045
code.Make(code.OpAdd),
// 0046
code.Make(code.OpSetLocal, 0),
// 0048 - jump back to condition
code.Make(code.OpJump, 5),
// 0051 - exit loop
code.Make(code.OpBreak),
},
1,
[]code.Instructions{
// 0000 - init (nil)
code.Make(code.OpNull),
// 0001
code.Make(code.OpPop),
// 0002 - condition (i < 10)
code.Make(code.OpGetGlobal, 1),
// 0005
code.Make(code.OpConstant, 2),
// 0008
code.Make(code.OpLessThan),
// 0009 - exit loop if false
code.Make(code.OpJumpNotTruthy, 30),
// 0012 - loop body
code.Make(code.OpLoop, 7),
// 0015
code.Make(code.OpGetGlobal, 1),
// 0018
code.Make(code.OpConstant, 8),
// 0021
code.Make(code.OpAdd),
// 0022
code.Make(code.OpSetGlobal, 1),
// 0025 - loop increment (nil)
code.Make(code.OpNull),
// 0026
code.Make(code.OpPop),
// 0027 - jump back to condition
code.Make(code.OpJump, 2),
// 0030 - exit loop
code.Make(code.OpBreak),
},
},
expectedInstructions: []code.Instructions{
code.Make(code.OpConstant, 0),
code.Make(code.OpSetGlobal, 0),
code.Make(code.OpConstant, 1),
code.Make(code.OpSetGlobal, 1),
code.Make(code.OpLoop, 9),
code.Make(code.OpGetGlobal, 0),
code.Make(code.OpPop),
},
},
}

runCompilerTests(t, tests)
}
8 changes: 4 additions & 4 deletions compiler/compiler_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,12 @@ func testConstants(t *testing.T, expected []interface{}, actual []object.Object)
return fmt.Errorf("constant %d - not a function: %T", i, actual[i])
}
compiledInstructions = fn.Instructions
case *object.CompiledFor:
fl, ok := actual[i].(*object.CompiledFor)
case *object.CompiledLoop:
l, ok := actual[i].(*object.CompiledLoop)
if !ok {
return fmt.Errorf("constant %d - not a for loop: %T", i, actual[i])
return fmt.Errorf("constant %d - not a loop: %T", i, actual[i])
}
compiledInstructions = fl.Instructions
compiledInstructions = l.Instructions
default:
return fmt.Errorf("constant %d - not a function or loop: %T", i, actual[i])
}
Expand Down
Loading

0 comments on commit 244691b

Please sign in to comment.