Initial commit
This commit is contained in:
commit
541140687f
25 changed files with 4776 additions and 0 deletions
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
9
.idea/anglais.iml
generated
Normal file
9
.idea/anglais.iml
generated
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="GoDfaErrorMayBeNotNil" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<methods>
|
||||||
|
<method importPath="neemek.com/anglais/src" receiver="*Lexer" name="NextToken" />
|
||||||
|
</methods>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/anglais.iml" filepath="$PROJECT_DIR$/.idea/anglais.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
47
README.md
Normal file
47
README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# L'anglais
|
||||||
|
|
||||||
|
The purpose of this project is to learn how to
|
||||||
|
make a virtual machine for interpreting generated
|
||||||
|
bytecode for improved performance. Hopefully, this
|
||||||
|
will apply to making virtual machines/emulators
|
||||||
|
for real processors
|
||||||
|
|
||||||
|
## Programming language
|
||||||
|
|
||||||
|
The programming language should be compiled, so
|
||||||
|
the performance is not massively impacted.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
- Go
|
||||||
|
- Rust
|
||||||
|
- C++ (already used)
|
||||||
|
- js + bun
|
||||||
|
- zig
|
||||||
|
|
||||||
|
I would like to use a language i know, and
|
||||||
|
preferably strongly typed. Rust has a great
|
||||||
|
compiler with warnings, but at the same time, it
|
||||||
|
would be nice to try out go for once.
|
||||||
|
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
The application would need a lexer, to process the
|
||||||
|
grammar into tokens.
|
||||||
|
|
||||||
|
Afterwards, the tokens need to be parsed. The type
|
||||||
|
of parser doesn't matter, but it may be nice to
|
||||||
|
try out an LR parser. The parser needs to generate
|
||||||
|
bytecode for the vm.
|
||||||
|
|
||||||
|
The vm, as my first, will be stack-based. It
|
||||||
|
should go through the generated bytecode and
|
||||||
|
execute it step by step. There should be support
|
||||||
|
for functions using "jump" instructions.
|
||||||
|
|
||||||
|
## Grammar
|
||||||
|
|
||||||
|
The programming language should have a specified
|
||||||
|
grammar. The grammar will be found as examples in
|
||||||
|
./examples/.
|
||||||
|
|
||||||
336
compiler.go
Normal file
336
compiler.go
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "log"
|
||||||
|
|
||||||
|
type Compiler struct {
|
||||||
|
chunk *Chunk
|
||||||
|
ip Pos
|
||||||
|
scope Pos
|
||||||
|
|
||||||
|
stack *Stack[LocalVariable]
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocalVariable struct {
|
||||||
|
name string
|
||||||
|
scope int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCompiler() *Compiler {
|
||||||
|
c := &Compiler{
|
||||||
|
chunk: NewChunk(make([]Bytecode, 0), make([]Value, 0)),
|
||||||
|
ip: 0,
|
||||||
|
scope: 0,
|
||||||
|
stack: NewStack[LocalVariable](256),
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Compiler) add(instruction Bytecode) {
|
||||||
|
for len(c.chunk.Bytecode) <= int(c.ip) {
|
||||||
|
c.chunk.Bytecode = append(c.chunk.Bytecode, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.chunk.Bytecode[c.ip] = instruction
|
||||||
|
|
||||||
|
c.advance(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Compiler) addConstant(value Value) {
|
||||||
|
chunk := c.chunk
|
||||||
|
for i := 0; i < len(chunk.Constants); i++ {
|
||||||
|
if chunk.Constants[i] == value {
|
||||||
|
c.add(Bytecode(i))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk.Constants = append(chunk.Constants, value)
|
||||||
|
|
||||||
|
c.add(Bytecode(len(chunk.Constants) - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Compiler) Compile(tree Node) {
|
||||||
|
if tree == nil {
|
||||||
|
panic("nil value parse tree node")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tree.Type() {
|
||||||
|
case StringNodeType:
|
||||||
|
c.add(InstructionConstant)
|
||||||
|
c.addConstant(StringValue(tree.(*StringNode).value))
|
||||||
|
|
||||||
|
case NumberNodeType:
|
||||||
|
c.add(InstructionConstant)
|
||||||
|
c.addConstant(tree.(*NumberNode).value)
|
||||||
|
|
||||||
|
case ReferenceNodeType:
|
||||||
|
c.getVar(tree.(*ReferenceNode).name)
|
||||||
|
|
||||||
|
case BinaryNodeType:
|
||||||
|
c.compileBinary(tree.(*BinaryNode))
|
||||||
|
|
||||||
|
case BooleanNodeType:
|
||||||
|
if tree.(*BooleanNode).value {
|
||||||
|
c.add(InstructionTrue)
|
||||||
|
} else {
|
||||||
|
c.add(InstructionFalse)
|
||||||
|
}
|
||||||
|
|
||||||
|
case NilNodeType:
|
||||||
|
c.add(InstructionNil)
|
||||||
|
|
||||||
|
case BlockNodeType:
|
||||||
|
c.descend()
|
||||||
|
for _, n := range tree.(*BlockNode).statements {
|
||||||
|
c.Compile(n)
|
||||||
|
}
|
||||||
|
c.ascend()
|
||||||
|
|
||||||
|
case ConditionalNodeType:
|
||||||
|
n := tree.(*ConditionalNode)
|
||||||
|
|
||||||
|
// the stack should have whether the condition was truthful
|
||||||
|
c.Compile(n.condition)
|
||||||
|
|
||||||
|
// if the condition equated to true, we should jump over the body
|
||||||
|
c.add(InstructionJumpFalse)
|
||||||
|
// we save where uint16 jump by value is stored, and update it when
|
||||||
|
// we know the size of this condition (in bytecode)
|
||||||
|
jumpByPos := c.ip
|
||||||
|
c.advance(2)
|
||||||
|
|
||||||
|
// this part would be executed if the value was true
|
||||||
|
c.Compile(n.do)
|
||||||
|
|
||||||
|
// we store the position of the jump over the else code here
|
||||||
|
var jumpOverElse Pos
|
||||||
|
if n.otherwise != nil {
|
||||||
|
// this would jump over the else/otherwise block in the code
|
||||||
|
c.add(InstructionJump)
|
||||||
|
jumpOverElse = c.ip
|
||||||
|
c.advance(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// put the u16 of where to jump if the condition was false
|
||||||
|
c.putU16(jumpByPos, uint16(c.ip-jumpByPos-2))
|
||||||
|
|
||||||
|
if n.otherwise != nil {
|
||||||
|
c.Compile(n.otherwise)
|
||||||
|
c.putU16(jumpOverElse, uint16(c.ip-jumpOverElse-2))
|
||||||
|
}
|
||||||
|
|
||||||
|
case LoopNodeType:
|
||||||
|
n := tree.(*LoopNode)
|
||||||
|
|
||||||
|
conditionPos := c.ip
|
||||||
|
c.Compile(n.condition)
|
||||||
|
|
||||||
|
c.add(InstructionJumpFalse)
|
||||||
|
jumpValuePos := c.ip
|
||||||
|
c.advance(2)
|
||||||
|
|
||||||
|
c.Compile(n.do)
|
||||||
|
|
||||||
|
c.add(InstructionLoop)
|
||||||
|
// condition pos < ip
|
||||||
|
c.addU16(uint16(c.ip - conditionPos + 2))
|
||||||
|
|
||||||
|
c.putU16(jumpValuePos, uint16(c.ip-jumpValuePos-2))
|
||||||
|
|
||||||
|
case AssignNodeType:
|
||||||
|
n := tree.(*AssignNode)
|
||||||
|
|
||||||
|
if n.name == "_" {
|
||||||
|
// allow non-ish statements
|
||||||
|
c.Compile(n.value)
|
||||||
|
c.add(InstructionPop)
|
||||||
|
} else {
|
||||||
|
c.setVar(n.name, n.value, n.declare)
|
||||||
|
}
|
||||||
|
|
||||||
|
case CallNodeType:
|
||||||
|
n := tree.(*CallNode)
|
||||||
|
|
||||||
|
c.descend()
|
||||||
|
|
||||||
|
for _, arg := range n.args {
|
||||||
|
c.Compile(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.getVar(n.name)
|
||||||
|
|
||||||
|
c.add(InstructionCall)
|
||||||
|
|
||||||
|
if !n.keep {
|
||||||
|
c.add(InstructionPop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only descend and remove variables that are no longer within scope when the stack is clean
|
||||||
|
c.ascend()
|
||||||
|
|
||||||
|
case FunctionNodeType:
|
||||||
|
n := tree.(*FunctionNode)
|
||||||
|
|
||||||
|
fi := len(c.chunk.Constants)
|
||||||
|
c.chunk.Constants = append(c.chunk.Constants, nil)
|
||||||
|
|
||||||
|
c.add(InstructionConstant)
|
||||||
|
c.add(Bytecode(fi))
|
||||||
|
|
||||||
|
// keep track of main chunk
|
||||||
|
mc := c.chunk
|
||||||
|
// and ip
|
||||||
|
mip := c.ip
|
||||||
|
|
||||||
|
// assign a new empty chunk
|
||||||
|
c.chunk = NewChunk(make([]Bytecode, 0), make([]Value, 0))
|
||||||
|
// reset instruction pointer (ip)
|
||||||
|
c.ip = 0
|
||||||
|
|
||||||
|
c.descend()
|
||||||
|
|
||||||
|
for _, p := range n.params {
|
||||||
|
c.registerVar(p)
|
||||||
|
}
|
||||||
|
c.Compile(n.logic)
|
||||||
|
if n.logic.Type() != BlockNodeType {
|
||||||
|
c.stack.Pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.Constants[fi] = FunctionValue{
|
||||||
|
n.name,
|
||||||
|
n.params,
|
||||||
|
c.chunk,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ascend()
|
||||||
|
|
||||||
|
// restore old chunk and ip
|
||||||
|
c.chunk = mc
|
||||||
|
c.ip = mip
|
||||||
|
|
||||||
|
case ReturnNodeType:
|
||||||
|
c.Compile(tree.(*ReturnNode).value)
|
||||||
|
c.add(InstructionReturn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Compiler) compileBinary(binary *BinaryNode) {
|
||||||
|
c.Compile(binary.Left)
|
||||||
|
c.Compile(binary.Right)
|
||||||
|
|
||||||
|
switch binary.BinaryOperation {
|
||||||
|
case BinaryAddition:
|
||||||
|
c.add(InstructionAdd)
|
||||||
|
case BinarySubtraction:
|
||||||
|
c.add(InstructionSub)
|
||||||
|
case BinaryMultiplication:
|
||||||
|
c.add(InstructionMul)
|
||||||
|
case BinaryDivision:
|
||||||
|
c.add(InstructionDiv)
|
||||||
|
case BinaryEquality:
|
||||||
|
c.add(InstructionEquals)
|
||||||
|
case BinaryInequality:
|
||||||
|
c.add(InstructionNotEqual)
|
||||||
|
case BinaryLess:
|
||||||
|
c.add(InstructionLess)
|
||||||
|
case BinaryGreater:
|
||||||
|
c.add(InstructionGreater)
|
||||||
|
case BinaryLessEqual:
|
||||||
|
c.add(InstructionLessOrEqual)
|
||||||
|
case BinaryGreaterEqual:
|
||||||
|
c.add(InstructionGreaterOrEqual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Compiler) getVar(name string) {
|
||||||
|
if c.isLocal(name) {
|
||||||
|
c.add(InstructionGetLocal)
|
||||||
|
c.addConstant(StringValue(name))
|
||||||
|
} else if c.isGlobal(name) {
|
||||||
|
c.add(InstructionGetGlobal)
|
||||||
|
c.addConstant(StringValue(name))
|
||||||
|
} else {
|
||||||
|
log.Fatalf("compiling: undefined variable %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Compiler) setVar(name string, value Node, declare bool) {
|
||||||
|
c.Compile(value)
|
||||||
|
|
||||||
|
if declare {
|
||||||
|
c.add(InstructionDeclareLocal)
|
||||||
|
c.registerVar(name)
|
||||||
|
} else {
|
||||||
|
c.add(InstructionSetLocal)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.addConstant(StringValue(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep track that a variable is declared but doesn't necessarily have a deducible type
|
||||||
|
func (c *Compiler) registerVar(name string) {
|
||||||
|
c.stack.Push(LocalVariable{
|
||||||
|
name,
|
||||||
|
int(c.scope),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLocal whether a variable of with the name provided is declared within the local scope
|
||||||
|
func (c *Compiler) isLocal(name string) bool {
|
||||||
|
for i := c.stack.Current - 1; i >= 0; i-- {
|
||||||
|
if c.stack.items[i].name == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isGlobal whether a variable is defined in the standard global environment
|
||||||
|
func (c *Compiler) isGlobal(name string) bool {
|
||||||
|
return DefaultGlobals[name] != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Compiler) ascend() {
|
||||||
|
c.scope--
|
||||||
|
|
||||||
|
for ; c.stack.Current > 0 && c.stack.Peek().scope > int(c.scope); c.stack.Pop() {
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.scope != 0 {
|
||||||
|
c.add(InstructionAscend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Compiler) descend() {
|
||||||
|
c.scope++
|
||||||
|
if c.scope != 1 {
|
||||||
|
c.add(InstructionDescend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Compiler) advance(amount Pos) {
|
||||||
|
c.ip += amount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Compiler) addU16(v uint16) {
|
||||||
|
c.add(Bytecode(v >> 8)) // first 8 bits
|
||||||
|
c.add(Bytecode(v & 0xff)) // last 8 bits
|
||||||
|
}
|
||||||
|
|
||||||
|
// putU16 put a unsigned 16-bit value at an arbitrary position.
|
||||||
|
// p is the position before the value
|
||||||
|
func (c *Compiler) putU16(p Pos, v uint16) {
|
||||||
|
// save original position
|
||||||
|
start := c.ip
|
||||||
|
|
||||||
|
// move to position
|
||||||
|
c.ip = p
|
||||||
|
// set values of the next 2 bytes to the u16
|
||||||
|
c.addU16(v)
|
||||||
|
|
||||||
|
// restore position
|
||||||
|
c.ip = start
|
||||||
|
}
|
||||||
392
compiler_test.go
Normal file
392
compiler_test.go
Normal file
|
|
@ -0,0 +1,392 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewCompiler(t *testing.T) {
|
||||||
|
c := NewCompiler()
|
||||||
|
|
||||||
|
if c == nil {
|
||||||
|
t.Fatal("NewCompiler returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ip != 0 {
|
||||||
|
t.Error("compiler ip doesn't start at zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.chunk == nil {
|
||||||
|
t.Error("compiler chunk initialized to nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNewCompiler(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = NewCompiler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompileTestData struct {
|
||||||
|
tree Node
|
||||||
|
expectedStack []Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCompileTestData() map[string]CompileTestData {
|
||||||
|
return map[string]CompileTestData{
|
||||||
|
"constant_string": {
|
||||||
|
&StringNode{
|
||||||
|
"Hello world!",
|
||||||
|
"\"Hello world!\"",
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
StringValue("Hello world!"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"conditional_false": {
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"a",
|
||||||
|
&NumberNode{
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
&ConditionalNode{
|
||||||
|
&BooleanNode{
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"a",
|
||||||
|
&NumberNode{
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
&VariableValue{
|
||||||
|
"a",
|
||||||
|
NumberValue(0),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"conditional_true": {
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"a",
|
||||||
|
&NumberNode{
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
&ConditionalNode{
|
||||||
|
&BooleanNode{
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"a",
|
||||||
|
&NumberNode{
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
&VariableValue{
|
||||||
|
"a",
|
||||||
|
NumberValue(1),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"conditional_welse_false": {
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"a",
|
||||||
|
&NumberNode{
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
&ConditionalNode{
|
||||||
|
&BooleanNode{
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"a",
|
||||||
|
&NumberNode{
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"a",
|
||||||
|
&NumberNode{
|
||||||
|
2,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
&VariableValue{
|
||||||
|
"a",
|
||||||
|
NumberValue(2),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"conditional_welse_true": {
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"a",
|
||||||
|
&NumberNode{
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
&ConditionalNode{
|
||||||
|
&BooleanNode{
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"a",
|
||||||
|
&NumberNode{
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"a",
|
||||||
|
&NumberNode{
|
||||||
|
2,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
&VariableValue{
|
||||||
|
"a",
|
||||||
|
NumberValue(1),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"addition": {
|
||||||
|
&BinaryNode{
|
||||||
|
BinaryAddition,
|
||||||
|
&NumberNode{
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
&NumberNode{
|
||||||
|
2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(3),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sum_function": {
|
||||||
|
&AssignNode{
|
||||||
|
"sum",
|
||||||
|
&FunctionNode{
|
||||||
|
"sum",
|
||||||
|
[]string{"a", "b"},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&ReturnNode{
|
||||||
|
&BinaryNode{
|
||||||
|
BinaryAddition,
|
||||||
|
&ReferenceNode{"a"},
|
||||||
|
&ReferenceNode{"b"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
&VariableValue{
|
||||||
|
"sum",
|
||||||
|
FunctionValue{
|
||||||
|
"sum",
|
||||||
|
[]string{"a", "b"},
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionDescend,
|
||||||
|
InstructionGetLocal, 0,
|
||||||
|
InstructionGetLocal, 1,
|
||||||
|
InstructionAdd,
|
||||||
|
InstructionReturn,
|
||||||
|
InstructionAscend,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
StringValue("a"), StringValue("b"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printChunk(t *testing.T, name string, chunk *Chunk) {
|
||||||
|
t.Logf("=v= %s =v=", name)
|
||||||
|
for i, bc := range chunk.Bytecode {
|
||||||
|
t.Logf("i=%d \t%d \t(%s)", i, bc, bc)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("=-= constants =-=")
|
||||||
|
|
||||||
|
for i, ct := range chunk.Constants {
|
||||||
|
t.Logf("c=%d \t%s", i, ct)
|
||||||
|
|
||||||
|
f, ok := ct.(FunctionValue)
|
||||||
|
if ok {
|
||||||
|
printChunk(t, f.Name, f.Chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("=^= %s =^=", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompile(t *testing.T) {
|
||||||
|
data := GetCompileTestData()
|
||||||
|
|
||||||
|
for name, testCase := range data {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Log("Initializing compiler")
|
||||||
|
c := NewCompiler()
|
||||||
|
|
||||||
|
t.Log("Compiling node tree")
|
||||||
|
c.Compile(testCase.tree)
|
||||||
|
|
||||||
|
t.Log("Initializing vm")
|
||||||
|
vm := NewVM(c.chunk, 256, 256)
|
||||||
|
|
||||||
|
t.Log("Printing chunk for debug")
|
||||||
|
printChunk(t, name, c.chunk)
|
||||||
|
|
||||||
|
t.Log("Executing bytecode")
|
||||||
|
for vm.HasNext() && vm.Next() {
|
||||||
|
}
|
||||||
|
t.Log("Executed bytecode")
|
||||||
|
|
||||||
|
CompareStacks(t, testCase.expectedStack, vm.stack)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkCompile(b *testing.B) {
|
||||||
|
data := GetCompileTestData()
|
||||||
|
for name, testCase := range data {
|
||||||
|
b.Run(name, func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
c := NewCompiler()
|
||||||
|
c.Compile(testCase.tree)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompiler_AddU16(t *testing.T) {
|
||||||
|
for i := 0; i <= 0xffff; i++ {
|
||||||
|
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
||||||
|
c := NewCompiler()
|
||||||
|
c.addU16(uint16(i))
|
||||||
|
|
||||||
|
if c.chunk.Bytecode[0] != Bytecode(i>>8) {
|
||||||
|
t.Errorf("first 8 bits don't match (got %s, expected %b)", c.chunk.Bytecode[0], byte(i>>8))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.chunk.Bytecode[1] != Bytecode(i&0xff) {
|
||||||
|
t.Errorf("last 8 bits don't match (got %s, expected %b)", c.chunk.Bytecode[1], byte(i&0xff))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompiler_CleanStack(t *testing.T) {
|
||||||
|
cases := GetCompileTestData()
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
switch tc.tree.Type() {
|
||||||
|
// skip all expected unclean nodes
|
||||||
|
case StringNodeType, NumberNodeType, ReferenceNodeType, BooleanNodeType, NilNodeType, BinaryNodeType, ReturnNodeType:
|
||||||
|
continue
|
||||||
|
|
||||||
|
case CallNodeType:
|
||||||
|
if tc.tree.(*CallNode).keep {
|
||||||
|
// if we know it should be unclean, skip it
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean statements
|
||||||
|
case BlockNodeType:
|
||||||
|
case ConditionalNodeType:
|
||||||
|
case LoopNodeType:
|
||||||
|
case AssignNodeType:
|
||||||
|
case FunctionNodeType:
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
c := NewCompiler()
|
||||||
|
c.Compile(tc.tree)
|
||||||
|
|
||||||
|
vm := NewVM(c.chunk, 256, 256)
|
||||||
|
for vm.HasNext() && vm.Next() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure stack has only assigned values
|
||||||
|
for i := 0; i < int(vm.stack.Current); i++ {
|
||||||
|
v := vm.stack.items[i]
|
||||||
|
|
||||||
|
if v == nil || v.Type() != VariableValueType {
|
||||||
|
t.Errorf("Unclean stack! value %v at %d on the stack is intermediary", v.String(), i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
19
examples/arithmetics.ang
Normal file
19
examples/arithmetics.ang
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
write("Bonjour à tout!");
|
||||||
|
|
||||||
|
if 1 == 2 {
|
||||||
|
# unreachable
|
||||||
|
} else {
|
||||||
|
write("Hooray! One does not equal 2!");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var n = 1; n < 10; n = n + 1) {
|
||||||
|
write("Run number " + str(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
var a = 2;
|
||||||
|
|
||||||
|
write(3 * a*a + 10 / 3);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
6
examples/func.ang
Normal file
6
examples/func.ang
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
func sum(a, b) {
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
|
||||||
|
write(sum(1, 2))
|
||||||
13
examples/hello.ang
Normal file
13
examples/hello.ang
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
write("Hello world!")
|
||||||
|
|
||||||
|
a := 1 + 2
|
||||||
|
|
||||||
|
write(a)
|
||||||
|
|
||||||
|
|
||||||
|
if a > 2 {
|
||||||
|
write("Hooray!! a is greater than 2!!!!")
|
||||||
|
} else {
|
||||||
|
write("oh nooo!!! a is less than or equal to 2!!!!!!!!!!")
|
||||||
|
}
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
module neemek.com/anglais
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
require github.com/alecthomas/kong v1.2.1
|
||||||
8
go.sum
Normal file
8
go.sum
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
|
||||||
|
github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
|
github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q=
|
||||||
|
github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM=
|
||||||
|
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||||
|
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
365
lexer.go
Normal file
365
lexer.go
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
Type TokenType
|
||||||
|
Start Pos
|
||||||
|
Length Pos
|
||||||
|
Line Pos
|
||||||
|
Lexeme string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Token) String() string {
|
||||||
|
return fmt.Sprintf("token %s, '%s' %d -> %d, line %d", t.Type.String(), t.Lexeme, t.Start, t.Length, t.Line)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenType uint64
|
||||||
|
|
||||||
|
const (
|
||||||
|
TokenPlus TokenType = iota
|
||||||
|
TokenMinus
|
||||||
|
TokenStar
|
||||||
|
TokenSlash
|
||||||
|
TokenBang
|
||||||
|
TokenSemicolon
|
||||||
|
|
||||||
|
TokenNumber
|
||||||
|
TokenString
|
||||||
|
TokenName
|
||||||
|
|
||||||
|
TokenOpenParenthesis
|
||||||
|
TokenCloseParenthesis
|
||||||
|
TokenOpenBrace
|
||||||
|
TokenCloseBrace
|
||||||
|
|
||||||
|
TokenTrue
|
||||||
|
TokenFalse
|
||||||
|
TokenNil
|
||||||
|
|
||||||
|
TokenFunc
|
||||||
|
TokenReturn
|
||||||
|
TokenWhile
|
||||||
|
TokenVar
|
||||||
|
TokenIf
|
||||||
|
TokenElse
|
||||||
|
|
||||||
|
TokenComma
|
||||||
|
TokenDot
|
||||||
|
|
||||||
|
TokenAssign
|
||||||
|
TokenDeclare
|
||||||
|
TokenBangEquals
|
||||||
|
TokenEquals
|
||||||
|
TokenGreaterThan
|
||||||
|
TokenLessThan
|
||||||
|
TokenGreaterThanOrEqual
|
||||||
|
TokenLessThanOrEqual
|
||||||
|
|
||||||
|
TokenEOF
|
||||||
|
TokenError
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t TokenType) String() string {
|
||||||
|
switch t {
|
||||||
|
case TokenPlus:
|
||||||
|
return "plus"
|
||||||
|
case TokenMinus:
|
||||||
|
return "minus"
|
||||||
|
case TokenStar:
|
||||||
|
return "star"
|
||||||
|
case TokenSlash:
|
||||||
|
return "slash"
|
||||||
|
case TokenBang:
|
||||||
|
return "bang"
|
||||||
|
case TokenNumber:
|
||||||
|
return "number"
|
||||||
|
case TokenString:
|
||||||
|
return "string"
|
||||||
|
case TokenTrue:
|
||||||
|
return "true"
|
||||||
|
case TokenFalse:
|
||||||
|
return "false"
|
||||||
|
case TokenNil:
|
||||||
|
return "nil"
|
||||||
|
case TokenOpenParenthesis:
|
||||||
|
return "open parenthesis"
|
||||||
|
case TokenCloseParenthesis:
|
||||||
|
return "close parenthesis"
|
||||||
|
case TokenOpenBrace:
|
||||||
|
return "open brace"
|
||||||
|
case TokenCloseBrace:
|
||||||
|
return "close brace"
|
||||||
|
case TokenVar:
|
||||||
|
return "var"
|
||||||
|
case TokenIf:
|
||||||
|
return "if"
|
||||||
|
case TokenElse:
|
||||||
|
return "else"
|
||||||
|
case TokenAssign:
|
||||||
|
return "equals"
|
||||||
|
case TokenBangEquals:
|
||||||
|
return "equals"
|
||||||
|
case TokenEquals:
|
||||||
|
return "double equals"
|
||||||
|
case TokenGreaterThan:
|
||||||
|
return "greater than"
|
||||||
|
case TokenLessThan:
|
||||||
|
return "less than"
|
||||||
|
case TokenGreaterThanOrEqual:
|
||||||
|
return "greater than or equal"
|
||||||
|
case TokenLessThanOrEqual:
|
||||||
|
return "less than or equal"
|
||||||
|
case TokenName:
|
||||||
|
return "name"
|
||||||
|
case TokenEOF:
|
||||||
|
return "EOF"
|
||||||
|
case TokenError:
|
||||||
|
return "error"
|
||||||
|
case TokenSemicolon:
|
||||||
|
return "semicolon"
|
||||||
|
case TokenDeclare:
|
||||||
|
return "declare"
|
||||||
|
case TokenFunc:
|
||||||
|
return "func"
|
||||||
|
case TokenReturn:
|
||||||
|
return "return"
|
||||||
|
case TokenWhile:
|
||||||
|
return "while"
|
||||||
|
case TokenComma:
|
||||||
|
return "comma"
|
||||||
|
case TokenDot:
|
||||||
|
return "dot"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "UNDEFINED TOKENTYPE STRING CONVERSION"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Lexer struct {
|
||||||
|
src string
|
||||||
|
start Pos
|
||||||
|
current Pos
|
||||||
|
line Pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLexer(src string) *Lexer {
|
||||||
|
return &Lexer{
|
||||||
|
src: src,
|
||||||
|
start: 0,
|
||||||
|
current: 0,
|
||||||
|
line: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) NextToken() (Token, error) {
|
||||||
|
l.skipWhitespace()
|
||||||
|
|
||||||
|
// if at end of source
|
||||||
|
if l.isAtEnd() {
|
||||||
|
return l.makeToken(TokenEOF), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip comments
|
||||||
|
if l.match('#') {
|
||||||
|
for !l.match('\n') {
|
||||||
|
l.advance()
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.NextToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
l.start = l.current
|
||||||
|
|
||||||
|
var c = []rune(l.src)[l.current]
|
||||||
|
l.advance()
|
||||||
|
|
||||||
|
switch c {
|
||||||
|
case '+':
|
||||||
|
return l.makeToken(TokenPlus), nil
|
||||||
|
case '-':
|
||||||
|
return l.makeToken(TokenMinus), nil
|
||||||
|
case '*':
|
||||||
|
return l.makeToken(TokenStar), nil
|
||||||
|
case '/':
|
||||||
|
return l.makeToken(TokenSlash), nil
|
||||||
|
case '(':
|
||||||
|
return l.makeToken(TokenOpenParenthesis), nil
|
||||||
|
case ')':
|
||||||
|
return l.makeToken(TokenCloseParenthesis), nil
|
||||||
|
case '{':
|
||||||
|
return l.makeToken(TokenOpenBrace), nil
|
||||||
|
case '}':
|
||||||
|
return l.makeToken(TokenCloseBrace), nil
|
||||||
|
case ';':
|
||||||
|
return l.makeToken(TokenSemicolon), nil
|
||||||
|
case ',':
|
||||||
|
return l.makeToken(TokenComma), nil
|
||||||
|
case '.':
|
||||||
|
return l.makeToken(TokenDot), nil
|
||||||
|
case ':':
|
||||||
|
if !l.accept('=') {
|
||||||
|
return l.makeToken(TokenError), errors.New("malformed token (got ':', expected '=' to follow)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.makeToken(TokenDeclare), nil
|
||||||
|
case '!':
|
||||||
|
if l.accept('=') {
|
||||||
|
return l.makeToken(TokenBangEquals), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.makeToken(TokenBang), nil
|
||||||
|
case '=':
|
||||||
|
if l.accept('=') {
|
||||||
|
return l.makeToken(TokenEquals), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.makeToken(TokenAssign), nil
|
||||||
|
case '>':
|
||||||
|
if l.accept('=') {
|
||||||
|
return l.makeToken(TokenGreaterThanOrEqual), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.makeToken(TokenGreaterThan), nil
|
||||||
|
case '<':
|
||||||
|
if l.accept('=') {
|
||||||
|
return l.makeToken(TokenLessThanOrEqual), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.makeToken(TokenLessThan), nil
|
||||||
|
|
||||||
|
case '"':
|
||||||
|
// include ending quote
|
||||||
|
for !l.accept('"') {
|
||||||
|
if l.match('\n') {
|
||||||
|
return l.makeToken(TokenError), errors.New("string did not end in current line")
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.isAtEnd() {
|
||||||
|
return l.makeToken(TokenError), errors.New("string did not before end of source")
|
||||||
|
}
|
||||||
|
|
||||||
|
l.advance()
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.makeToken(TokenString), nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
if unicode.IsLetter(c) || c == '_' {
|
||||||
|
// assemble variable
|
||||||
|
for l.isAlpha(l.peek()) {
|
||||||
|
l.advance()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch l.src[l.start:l.current] {
|
||||||
|
case "true":
|
||||||
|
return l.makeToken(TokenTrue), nil
|
||||||
|
case "false":
|
||||||
|
return l.makeToken(TokenFalse), nil
|
||||||
|
case "nil":
|
||||||
|
return l.makeToken(TokenNil), nil
|
||||||
|
case "if":
|
||||||
|
return l.makeToken(TokenIf), nil
|
||||||
|
case "else":
|
||||||
|
return l.makeToken(TokenElse), nil
|
||||||
|
case "var":
|
||||||
|
return l.makeToken(TokenVar), nil
|
||||||
|
case "func":
|
||||||
|
return l.makeToken(TokenFunc), nil
|
||||||
|
case "while":
|
||||||
|
return l.makeToken(TokenWhile), nil
|
||||||
|
case "return":
|
||||||
|
return l.makeToken(TokenReturn), nil
|
||||||
|
default:
|
||||||
|
return l.makeToken(TokenName), nil
|
||||||
|
}
|
||||||
|
} else if unicode.IsDigit(c) {
|
||||||
|
for unicode.IsDigit(l.peek()) {
|
||||||
|
l.advance()
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.makeToken(TokenNumber), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.makeToken(TokenError), errors.New(fmt.Sprintf("invalid token %c", c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewToken(t TokenType, start Pos, length Pos, line Pos, lexeme string) Token {
|
||||||
|
return Token{
|
||||||
|
Type: t,
|
||||||
|
Start: start,
|
||||||
|
Length: length,
|
||||||
|
Line: line,
|
||||||
|
Lexeme: lexeme,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) Tokenize() ([]Token, error) {
|
||||||
|
tokens := make([]Token, 0)
|
||||||
|
|
||||||
|
tok, err := l.NextToken()
|
||||||
|
for ; err == nil; tok, err = l.NextToken() {
|
||||||
|
tokens = append(tokens, tok)
|
||||||
|
|
||||||
|
if tok.Type == TokenEOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) makeToken(t TokenType) Token {
|
||||||
|
return NewToken(t, l.start, l.current-l.start, l.line, l.src[l.start:l.current])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) peek() rune {
|
||||||
|
if l.isAtEnd() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return []rune(l.src)[l.current]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) match(c rune) bool {
|
||||||
|
return l.peek() == c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) accept(c rune) bool {
|
||||||
|
if l.match(c) {
|
||||||
|
l.advance()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) isAlpha(c rune) bool {
|
||||||
|
return unicode.IsLetter(c) || unicode.IsDigit(c) || c == '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) advance() {
|
||||||
|
if l.isAtEnd() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if []rune(l.src)[l.current] == '\n' {
|
||||||
|
l.line++
|
||||||
|
}
|
||||||
|
|
||||||
|
l.current++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) isAtEnd() bool {
|
||||||
|
return l.current >= Pos(len([]rune(l.src)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) skipWhitespace() {
|
||||||
|
for !l.isAtEnd() && unicode.IsSpace(l.peek()) {
|
||||||
|
l.advance()
|
||||||
|
}
|
||||||
|
}
|
||||||
224
lexer_test.go
Normal file
224
lexer_test.go
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LexerTestData struct {
|
||||||
|
source string
|
||||||
|
expectedTokens []TokenType
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLexerTestData() map[string]LexerTestData {
|
||||||
|
return map[string]LexerTestData{
|
||||||
|
"hello_world_string(1)": {
|
||||||
|
"\"Hello world\"",
|
||||||
|
[]TokenType{TokenString, TokenEOF},
|
||||||
|
},
|
||||||
|
"empty_string(1)": {
|
||||||
|
"\"\"",
|
||||||
|
[]TokenType{TokenString, TokenEOF},
|
||||||
|
},
|
||||||
|
"simple number(1)": {
|
||||||
|
"1024",
|
||||||
|
[]TokenType{TokenNumber, TokenEOF},
|
||||||
|
},
|
||||||
|
"simple_arithmetics(7)": {
|
||||||
|
"1 + 23 / 4 * 3",
|
||||||
|
[]TokenType{
|
||||||
|
TokenNumber, TokenPlus, TokenNumber, TokenSlash,
|
||||||
|
TokenNumber, TokenStar, TokenNumber, TokenEOF,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"condition(3)": {
|
||||||
|
"a <= 200",
|
||||||
|
[]TokenType{TokenName, TokenLessThanOrEqual, TokenNumber, TokenEOF},
|
||||||
|
},
|
||||||
|
"if_statement(10)": {
|
||||||
|
"if a >= 200 {\n write(\"Hello world!\")\n}",
|
||||||
|
[]TokenType{
|
||||||
|
TokenIf, TokenName, TokenGreaterThanOrEqual, TokenNumber, TokenOpenBrace,
|
||||||
|
TokenName, TokenOpenParenthesis, TokenString, TokenCloseParenthesis, TokenCloseBrace,
|
||||||
|
TokenEOF,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"if_else_statement(20)": {
|
||||||
|
"if 23 * 2/3 > 32 {\n write(\"It is larger!\")\n} else {\n write(\"It is lower!\")\n}",
|
||||||
|
[]TokenType{
|
||||||
|
TokenIf, TokenNumber, TokenStar, TokenNumber, TokenSlash, TokenNumber, TokenGreaterThan, TokenNumber, TokenOpenBrace,
|
||||||
|
TokenName, TokenOpenParenthesis, TokenString, TokenCloseParenthesis, TokenCloseBrace,
|
||||||
|
TokenElse, TokenOpenBrace, TokenName, TokenOpenParenthesis, TokenString, TokenCloseParenthesis, TokenCloseBrace,
|
||||||
|
TokenEOF,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"empty_string": {
|
||||||
|
"",
|
||||||
|
[]TokenType{TokenEOF},
|
||||||
|
},
|
||||||
|
"full_arithmetic_equality": {
|
||||||
|
"a + 2 == 10 * 2 / 3",
|
||||||
|
[]TokenType{
|
||||||
|
TokenName, TokenPlus, TokenNumber, TokenEquals,
|
||||||
|
TokenNumber, TokenStar, TokenNumber, TokenSlash, TokenNumber,
|
||||||
|
TokenEOF,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"print",
|
||||||
|
[]TokenType{TokenName, TokenEOF},
|
||||||
|
},
|
||||||
|
"bunch_of_parentheses": {
|
||||||
|
"(((())))",
|
||||||
|
[]TokenType{
|
||||||
|
TokenOpenParenthesis, TokenOpenParenthesis, TokenOpenParenthesis, TokenOpenParenthesis,
|
||||||
|
TokenCloseParenthesis, TokenCloseParenthesis, TokenCloseParenthesis, TokenCloseParenthesis,
|
||||||
|
TokenEOF,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"space_before_string": {
|
||||||
|
"\n \"\"",
|
||||||
|
[]TokenType{TokenString, TokenEOF},
|
||||||
|
},
|
||||||
|
"write_call": {
|
||||||
|
"write(\"Hello world\")",
|
||||||
|
[]TokenType{TokenName, TokenOpenParenthesis, TokenString, TokenCloseParenthesis, TokenEOF},
|
||||||
|
},
|
||||||
|
"complex_comparison": {
|
||||||
|
"!(h__elo123 >= 1)",
|
||||||
|
[]TokenType{
|
||||||
|
TokenBang, TokenOpenParenthesis, TokenName, TokenGreaterThanOrEqual, TokenNumber, TokenCloseParenthesis,
|
||||||
|
TokenEOF,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"3assignments_1condition": {
|
||||||
|
"a = 8 * 32\nb = a > 256\nc = a <= 256\n!b == c",
|
||||||
|
[]TokenType{
|
||||||
|
TokenName, TokenAssign, TokenNumber, TokenStar, TokenNumber,
|
||||||
|
TokenName, TokenAssign, TokenName, TokenGreaterThan, TokenNumber,
|
||||||
|
TokenName, TokenAssign, TokenName, TokenLessThanOrEqual, TokenNumber,
|
||||||
|
TokenBang, TokenName, TokenEquals, TokenName, TokenEOF,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"func": {
|
||||||
|
"func sum(a, b) {\n return a + b\n}",
|
||||||
|
[]TokenType{
|
||||||
|
TokenFunc, TokenName, TokenOpenParenthesis, TokenName, TokenComma, TokenName, TokenCloseParenthesis,
|
||||||
|
TokenOpenBrace, TokenReturn, TokenName, TokenPlus, TokenName, TokenCloseBrace,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"while_loop": {
|
||||||
|
"while a < 5 {\n a = a + 1\n}",
|
||||||
|
[]TokenType{
|
||||||
|
TokenWhile, TokenName, TokenLessThan, TokenNumber, TokenOpenBrace,
|
||||||
|
TokenName, TokenAssign, TokenName, TokenPlus, TokenNumber, TokenCloseBrace,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"lambda": {
|
||||||
|
"sum := func(a, b) {\n" +
|
||||||
|
" return a + b\n" +
|
||||||
|
"}",
|
||||||
|
[]TokenType{
|
||||||
|
TokenName, TokenDeclare, TokenFunc, TokenOpenParenthesis, TokenName, TokenComma, TokenName, TokenCloseParenthesis,
|
||||||
|
TokenOpenBrace, TokenReturn, TokenName, TokenPlus, TokenName, TokenCloseBrace,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLexer_NextToken(t *testing.T) {
|
||||||
|
data := GetLexerTestData()
|
||||||
|
|
||||||
|
for name, tc := range data {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
lex := NewLexer(tc.source)
|
||||||
|
|
||||||
|
t.Logf("Testing source: '%s'", tc.source)
|
||||||
|
|
||||||
|
for _, expectedType := range tc.expectedTokens {
|
||||||
|
tok, err := lex.NextToken()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error while parsing token '%s'. Error: %s", expectedType, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if tok.Type != expectedType {
|
||||||
|
t.Errorf("Expected token type '%s' but got '%s'", expectedType, tok.Type)
|
||||||
|
} else {
|
||||||
|
t.Logf("Got expected token type '%s'", expectedType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewLexer(t *testing.T) {
|
||||||
|
lex := NewLexer("example source")
|
||||||
|
|
||||||
|
if lex == nil {
|
||||||
|
t.Fatalf("Lexer was not initialized correctly.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if lex.start != 0 {
|
||||||
|
t.Errorf("Lexer start position was not initialized correctly.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if lex.current != 0 {
|
||||||
|
t.Errorf("Lexer current position was not initialized correctly.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if lex.src != "example source" {
|
||||||
|
t.Errorf("Lexer lexer was not initialized correctly.")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Successfully initialized lexer")
|
||||||
|
}
|
||||||
|
|
||||||
|
// lexer NextToken provides an error when it comes across an invalid token
|
||||||
|
func TestLexer_NextTokenErrors(t *testing.T) {
|
||||||
|
invalid_codes := []string{
|
||||||
|
// Invalid tokens
|
||||||
|
"^", "@", "$&", "¨",
|
||||||
|
// Non-ending string (in same line)
|
||||||
|
"\"", "Hini minit \"mini moe", "\"this is some test\ncontent\"", "\n\"Hello world",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, code := range invalid_codes {
|
||||||
|
lex := NewLexer(code)
|
||||||
|
tok, err := lex.NextToken()
|
||||||
|
|
||||||
|
for err == nil && tok.Type != TokenEOF {
|
||||||
|
tok, err = lex.NextToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error for invalid code '%s'", code)
|
||||||
|
} else {
|
||||||
|
t.Logf("Got an expected error (%s) for invalid code '%s'", err.Error(), code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNewLexer(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = NewLexer("example source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLexer_NextToken(b *testing.B) {
|
||||||
|
data := GetLexerTestData()
|
||||||
|
|
||||||
|
for name, tc := range data {
|
||||||
|
b.Run(name, func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
lex := NewLexer(tc.source)
|
||||||
|
tok, err := lex.NextToken()
|
||||||
|
|
||||||
|
for err == nil && tok.Type != TokenEOF {
|
||||||
|
tok, err = lex.NextToken()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
201
main.go
Normal file
201
main.go
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
Debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunCmd struct {
|
||||||
|
Bytecode bool `name:"bytecode" short:"c" help:"Run file as if it's bytecode"`
|
||||||
|
File string `arg:"" name:"file" help:"File to read program from" type:"existingfile"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *RunCmd) Run(ctx *Context) error {
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Reading file")
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.ReadFile(cmd.File)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunk *Chunk
|
||||||
|
if !cmd.Bytecode {
|
||||||
|
src := string(f)
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Initialized lexer")
|
||||||
|
}
|
||||||
|
l := NewLexer(src)
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Lexing all tokens")
|
||||||
|
}
|
||||||
|
tokens, err := l.Tokenize()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Initialized parser")
|
||||||
|
}
|
||||||
|
p := NewParser(tokens)
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Parsed tree")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
for _, e := range p.errors {
|
||||||
|
e.Print(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tree := p.Parse()
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Initialized compiler")
|
||||||
|
}
|
||||||
|
c := NewCompiler()
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Compiling parse tree")
|
||||||
|
}
|
||||||
|
c.Compile(tree)
|
||||||
|
|
||||||
|
chunk = c.chunk
|
||||||
|
} else {
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Registering GOB types")
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterGOBTypes()
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Deserializing file")
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk = DeserializeChunk(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Printing chunk")
|
||||||
|
|
||||||
|
print(chunk.String())
|
||||||
|
|
||||||
|
log.Println("Initialized VM")
|
||||||
|
}
|
||||||
|
vm := NewVM(chunk, 256, 256)
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Executing bytecode")
|
||||||
|
log.Println("=v= output =v=")
|
||||||
|
}
|
||||||
|
// execute order 66
|
||||||
|
for vm.HasNext() && vm.Next() {
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompileCmd struct {
|
||||||
|
File string `arg:"" name:"file" help:"File to compile program from" type:"existingfile"`
|
||||||
|
Output string `arg:"" name:"output" optional:"" help:"File path to output bytecode to" type:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *CompileCmd) Run(ctx *Context) error {
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Reading file")
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.ReadFile(cmd.File)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
src := string(f)
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Initializing lexer")
|
||||||
|
}
|
||||||
|
l := NewLexer(src)
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Lexing all tokens")
|
||||||
|
}
|
||||||
|
tokens, err := l.Tokenize()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Initializing parser")
|
||||||
|
}
|
||||||
|
p := NewParser(tokens)
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Parsing tree")
|
||||||
|
}
|
||||||
|
tree := p.Parse()
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Initialized compiler")
|
||||||
|
}
|
||||||
|
c := NewCompiler()
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Compiling parse tree")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Compile(tree)
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Registering GOB types")
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterGOBTypes()
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Serializing chunk")
|
||||||
|
}
|
||||||
|
|
||||||
|
serialized := c.chunk.Serialize()
|
||||||
|
|
||||||
|
if ctx.Debug {
|
||||||
|
log.Println("Writing file")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(cmd.Output, serialized, 0666)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var cli struct {
|
||||||
|
Debug bool `short:"d" help:"Enable debug mode."`
|
||||||
|
|
||||||
|
Run RunCmd `cmd:"" name:"run" help:"Run program."`
|
||||||
|
CompileCmd CompileCmd `cmd:"" name:"compile" help:"Compile program to bytecode."`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := kong.Parse(&cli)
|
||||||
|
// Call the Run() method of the selected parsed command.
|
||||||
|
err := ctx.Run(&Context{Debug: cli.Debug})
|
||||||
|
ctx.FatalIfErrorf(err)
|
||||||
|
}
|
||||||
96
main_test.go
Normal file
96
main_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AllTestCase struct {
|
||||||
|
src string
|
||||||
|
expectedStack []Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllTestCases() map[string]AllTestCase {
|
||||||
|
return map[string]AllTestCase{
|
||||||
|
"constant_number": {
|
||||||
|
"a := 1",
|
||||||
|
[]Value{
|
||||||
|
&VariableValue{
|
||||||
|
"a",
|
||||||
|
NumberValue(1),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAll(t *testing.T) {
|
||||||
|
cases := GetAllTestCases()
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Logf("Initializing lexer")
|
||||||
|
l := NewLexer(tc.src)
|
||||||
|
|
||||||
|
t.Logf("Lexing tokens")
|
||||||
|
tokens, err := l.Tokenize()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpeced error tokenizing: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Initializing parser")
|
||||||
|
p := NewParser(tokens)
|
||||||
|
|
||||||
|
t.Log("Parsing tokens")
|
||||||
|
tree := p.Parse()
|
||||||
|
|
||||||
|
if p.hadError {
|
||||||
|
for _, e := range p.errors {
|
||||||
|
e.Print(tc.src)
|
||||||
|
}
|
||||||
|
t.Fatalf("parser had error(s)")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Initializing compiler")
|
||||||
|
c := NewCompiler()
|
||||||
|
|
||||||
|
t.Log("Compiling parse tree")
|
||||||
|
c.Compile(tree)
|
||||||
|
|
||||||
|
printChunk(t, name, c.chunk)
|
||||||
|
|
||||||
|
t.Log("Initializing vm")
|
||||||
|
vm := NewVM(c.chunk, 256, 256)
|
||||||
|
|
||||||
|
t.Log("Running bytecode")
|
||||||
|
for vm.HasNext() && vm.Next() {
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Comparing stacks")
|
||||||
|
CompareStacks(t, tc.expectedStack, vm.stack)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAll(b *testing.B) {
|
||||||
|
cases := GetAllTestCases()
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
b.Run(name, func(b *testing.B) {
|
||||||
|
l := NewLexer(tc.src)
|
||||||
|
tokens, _ := l.Tokenize()
|
||||||
|
|
||||||
|
p := NewParser(tokens)
|
||||||
|
tree := p.Parse()
|
||||||
|
|
||||||
|
c := NewCompiler()
|
||||||
|
c.Compile(tree)
|
||||||
|
|
||||||
|
vm := NewVM(c.chunk, 256, 256)
|
||||||
|
|
||||||
|
for vm.HasNext() && vm.Next() {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
291
nodes.go
Normal file
291
nodes.go
Normal file
|
|
@ -0,0 +1,291 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NodeType int
|
||||||
|
|
||||||
|
type Node interface {
|
||||||
|
Type() NodeType
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
StringNodeType NodeType = iota
|
||||||
|
NumberNodeType
|
||||||
|
ReferenceNodeType
|
||||||
|
BooleanNodeType
|
||||||
|
NilNodeType
|
||||||
|
BinaryNodeType
|
||||||
|
BlockNodeType
|
||||||
|
ConditionalNodeType
|
||||||
|
LoopNodeType
|
||||||
|
AssignNodeType
|
||||||
|
CallNodeType
|
||||||
|
FunctionNodeType
|
||||||
|
ReturnNodeType
|
||||||
|
)
|
||||||
|
|
||||||
|
func (n NodeType) String() string {
|
||||||
|
switch n {
|
||||||
|
case StringNodeType:
|
||||||
|
return "String"
|
||||||
|
case NumberNodeType:
|
||||||
|
return "Number"
|
||||||
|
case ReferenceNodeType:
|
||||||
|
return "Reference"
|
||||||
|
case BinaryNodeType:
|
||||||
|
return "Binary"
|
||||||
|
case BooleanNodeType:
|
||||||
|
return "Boolean"
|
||||||
|
case NilNodeType:
|
||||||
|
return "Nil"
|
||||||
|
case BlockNodeType:
|
||||||
|
return "Block"
|
||||||
|
case ConditionalNodeType:
|
||||||
|
return "Conditional"
|
||||||
|
case LoopNodeType:
|
||||||
|
return "Loop"
|
||||||
|
case AssignNodeType:
|
||||||
|
return "Assign"
|
||||||
|
case CallNodeType:
|
||||||
|
return "Call"
|
||||||
|
case FunctionNodeType:
|
||||||
|
return "Function"
|
||||||
|
case ReturnNodeType:
|
||||||
|
return "Return"
|
||||||
|
}
|
||||||
|
return "Invalid Node Type"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReferenceNode a reference to a variable on the stack
|
||||||
|
type ReferenceNode struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n ReferenceNode) Type() NodeType {
|
||||||
|
return ReferenceNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n ReferenceNode) String() string {
|
||||||
|
return n.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringNode string/text values
|
||||||
|
type StringNode struct {
|
||||||
|
value string
|
||||||
|
quoted string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n StringNode) Type() NodeType {
|
||||||
|
return StringNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n StringNode) String() string {
|
||||||
|
return n.quoted
|
||||||
|
}
|
||||||
|
|
||||||
|
type NumberNode struct {
|
||||||
|
value NumberValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NumberNode) Type() NodeType {
|
||||||
|
return NumberNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NumberNode) String() string {
|
||||||
|
return strconv.FormatFloat(float64(n.value), 'g', -1, NumberSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BinaryOperation uint
|
||||||
|
|
||||||
|
func (n BinaryOperation) String() string {
|
||||||
|
switch n {
|
||||||
|
case BinaryAddition:
|
||||||
|
return "add"
|
||||||
|
case BinarySubtraction:
|
||||||
|
return "subtract"
|
||||||
|
case BinaryMultiplication:
|
||||||
|
return "multiply"
|
||||||
|
case BinaryDivision:
|
||||||
|
return "divide"
|
||||||
|
case BinaryEquality:
|
||||||
|
return "equality"
|
||||||
|
case BinaryInequality:
|
||||||
|
return "inequality"
|
||||||
|
case BinaryLess:
|
||||||
|
return "less"
|
||||||
|
case BinaryGreater:
|
||||||
|
return "greater"
|
||||||
|
case BinaryLessEqual:
|
||||||
|
return "less or equal"
|
||||||
|
case BinaryGreaterEqual:
|
||||||
|
return "greater or equal"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "undefined arithmetic operation"
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
BinaryAddition BinaryOperation = iota
|
||||||
|
BinarySubtraction
|
||||||
|
BinaryMultiplication
|
||||||
|
BinaryDivision
|
||||||
|
|
||||||
|
// Comparison
|
||||||
|
BinaryEquality
|
||||||
|
BinaryInequality
|
||||||
|
BinaryLess
|
||||||
|
BinaryGreater
|
||||||
|
BinaryLessEqual
|
||||||
|
BinaryGreaterEqual
|
||||||
|
)
|
||||||
|
|
||||||
|
// BinaryNode All operations which take 2 variables
|
||||||
|
type BinaryNode struct {
|
||||||
|
BinaryOperation
|
||||||
|
Left Node
|
||||||
|
Right Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n BinaryNode) Type() NodeType {
|
||||||
|
return BinaryNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n BinaryNode) String() string {
|
||||||
|
return fmt.Sprintf("%s %s %s", n.Left.String(), n.BinaryOperation.String(), n.Right.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// BooleanNode boolean value
|
||||||
|
type BooleanNode struct {
|
||||||
|
value bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n BooleanNode) Type() NodeType {
|
||||||
|
return BooleanNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n BooleanNode) String() string {
|
||||||
|
return strconv.FormatBool(n.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NilNode nil value
|
||||||
|
type NilNode struct{}
|
||||||
|
|
||||||
|
func (n NilNode) Type() NodeType {
|
||||||
|
return NilNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NilNode) String() string {
|
||||||
|
return "nil"
|
||||||
|
}
|
||||||
|
|
||||||
|
// block node with statements
|
||||||
|
type BlockNode struct {
|
||||||
|
statements []Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n BlockNode) Type() NodeType {
|
||||||
|
return BlockNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n BlockNode) String() string {
|
||||||
|
builder := strings.Builder{}
|
||||||
|
|
||||||
|
for _, stmt := range n.statements {
|
||||||
|
builder.WriteString(stmt.String())
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConditionalNode conditionals (if statements)
|
||||||
|
type ConditionalNode struct {
|
||||||
|
condition Node
|
||||||
|
do Node
|
||||||
|
otherwise Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n ConditionalNode) Type() NodeType {
|
||||||
|
return ConditionalNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n ConditionalNode) String() string {
|
||||||
|
return fmt.Sprintf("if %s then %s otheriwise %s", n.condition.String(), n.do.String(), n.otherwise.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loops (for/while)
|
||||||
|
type LoopNode struct {
|
||||||
|
condition Node
|
||||||
|
do Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n LoopNode) Type() NodeType {
|
||||||
|
return LoopNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n LoopNode) String() string {
|
||||||
|
return fmt.Sprintf("while %s loop %s", n.condition.String(), n.do.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// assignment
|
||||||
|
type AssignNode struct {
|
||||||
|
name string
|
||||||
|
value Node
|
||||||
|
declare bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n AssignNode) Type() NodeType {
|
||||||
|
return AssignNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n AssignNode) String() string {
|
||||||
|
return fmt.Sprintf("set %s to %s", n.name, n.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// function call
|
||||||
|
type CallNode struct {
|
||||||
|
name string
|
||||||
|
args []Node
|
||||||
|
keep bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n CallNode) Type() NodeType {
|
||||||
|
return CallNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n CallNode) String() string {
|
||||||
|
return fmt.Sprintf("call %s with args (%s)", n.name, n.args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// definition of function
|
||||||
|
type FunctionNode struct {
|
||||||
|
name string
|
||||||
|
params []string
|
||||||
|
logic Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n FunctionNode) Type() NodeType {
|
||||||
|
return FunctionNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n FunctionNode) String() string {
|
||||||
|
return fmt.Sprintf("definition of %s, do %s", n.name, n.logic.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReturnNode return a value out of this context
|
||||||
|
type ReturnNode struct {
|
||||||
|
value Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n ReturnNode) Type() NodeType {
|
||||||
|
return ReturnNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n ReturnNode) String() string {
|
||||||
|
return fmt.Sprintf("return %s", n.value)
|
||||||
|
}
|
||||||
420
parser.go
Normal file
420
parser.go
Normal file
|
|
@ -0,0 +1,420 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ParsingError struct {
|
||||||
|
Description string
|
||||||
|
Causer *Token
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print a rich and informative error
|
||||||
|
func (p *ParsingError) Print(src string) {
|
||||||
|
lineNumber := 1
|
||||||
|
lineBeginning := 0
|
||||||
|
for i := 0; i < int(p.Causer.Start); i++ {
|
||||||
|
if src[i] == '\n' {
|
||||||
|
lineBeginning = i + 1
|
||||||
|
lineNumber++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lineEnd := len(src)
|
||||||
|
for i := lineBeginning; i < len(src); i++ {
|
||||||
|
if src[i] == '\n' {
|
||||||
|
lineEnd = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print(" \t v ")
|
||||||
|
println(p.Description)
|
||||||
|
|
||||||
|
println(fmt.Sprintf(" %d:%d\t | %s", lineNumber, int(p.Causer.Start)-lineBeginning+1, src[lineBeginning:lineEnd]))
|
||||||
|
|
||||||
|
print("\t ^")
|
||||||
|
for i := lineBeginning; i <= int(p.Causer.Start); i++ {
|
||||||
|
print(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < int(p.Causer.Length); i++ {
|
||||||
|
print("^")
|
||||||
|
}
|
||||||
|
println()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parser struct {
|
||||||
|
tokens []Token
|
||||||
|
prev *Token
|
||||||
|
curr *Token
|
||||||
|
pos Pos
|
||||||
|
|
||||||
|
hadError bool
|
||||||
|
errors []ParsingError
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewParser(tokens []Token) *Parser {
|
||||||
|
return &Parser{
|
||||||
|
tokens: tokens,
|
||||||
|
pos: 0,
|
||||||
|
hadError: false,
|
||||||
|
errors: make([]ParsingError, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) Parse() Node {
|
||||||
|
// top level statements
|
||||||
|
statements := make([]Node, 0)
|
||||||
|
|
||||||
|
// initialize current
|
||||||
|
p.advance()
|
||||||
|
|
||||||
|
for int(p.pos) < len(p.tokens) && p.curr.Type != TokenEOF {
|
||||||
|
statements = append(statements, p.block(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BlockNode{
|
||||||
|
statements: statements,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) accept(tokenType TokenType) bool {
|
||||||
|
if p.curr == nil {
|
||||||
|
log.Fatal("unexpected current token nil")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*p.curr).Type == tokenType {
|
||||||
|
p.advance()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) expect(tokenType TokenType) {
|
||||||
|
if !p.accept(tokenType) {
|
||||||
|
p.error("Expected token "+tokenType.String()+", got "+p.curr.Type.String(), p.curr)
|
||||||
|
p.advance() // just move on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) peek() (Token, error) {
|
||||||
|
if p.pos >= Pos(len(p.tokens)) {
|
||||||
|
return Token{}, errors.New("cannot peek beyond tokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.tokens[p.pos], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) advance() {
|
||||||
|
p.prev = p.curr
|
||||||
|
|
||||||
|
if p.pos < Pos(len(p.tokens)) {
|
||||||
|
p.curr = &p.tokens[p.pos]
|
||||||
|
p.pos++
|
||||||
|
} else {
|
||||||
|
panic("no more tokens")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) error(error string, causer *Token) {
|
||||||
|
p.hadError = true
|
||||||
|
p.errors = append(p.errors, ParsingError{
|
||||||
|
Description: error,
|
||||||
|
Causer: causer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) factor() Node {
|
||||||
|
switch (*p.curr).Type {
|
||||||
|
case TokenString:
|
||||||
|
p.advance()
|
||||||
|
return &StringNode{
|
||||||
|
(*p.prev).Lexeme[1 : len((*p.prev).Lexeme)-1],
|
||||||
|
(*p.prev).Lexeme,
|
||||||
|
}
|
||||||
|
|
||||||
|
case TokenNumber:
|
||||||
|
p.advance()
|
||||||
|
num, err := strconv.ParseFloat((*p.prev).Lexeme, NumberSize)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
p.error(fmt.Sprintf("Error parsing number: %v", err), p.prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &NumberNode{
|
||||||
|
NumberValue(num),
|
||||||
|
}
|
||||||
|
|
||||||
|
case TokenTrue:
|
||||||
|
p.advance()
|
||||||
|
return &BooleanNode{
|
||||||
|
true,
|
||||||
|
}
|
||||||
|
case TokenFalse:
|
||||||
|
p.advance()
|
||||||
|
return &BooleanNode{
|
||||||
|
false,
|
||||||
|
}
|
||||||
|
|
||||||
|
case TokenNil:
|
||||||
|
p.advance()
|
||||||
|
return &NilNode{}
|
||||||
|
|
||||||
|
// unary minus
|
||||||
|
case TokenMinus:
|
||||||
|
p.advance()
|
||||||
|
return &BinaryNode{
|
||||||
|
BinarySubtraction,
|
||||||
|
&NumberNode{NumberValue(0)},
|
||||||
|
p.factor(),
|
||||||
|
}
|
||||||
|
|
||||||
|
case TokenName:
|
||||||
|
p.advance()
|
||||||
|
name := (*p.prev).Lexeme
|
||||||
|
|
||||||
|
if p.curr.Type == TokenOpenParenthesis {
|
||||||
|
args := p.parseArgs()
|
||||||
|
|
||||||
|
return &CallNode{
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ReferenceNode{
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
|
||||||
|
case TokenFunc:
|
||||||
|
p.advance()
|
||||||
|
params := p.parseParams()
|
||||||
|
|
||||||
|
return &FunctionNode{
|
||||||
|
"*",
|
||||||
|
params,
|
||||||
|
p.block(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
case TokenOpenParenthesis:
|
||||||
|
p.advance()
|
||||||
|
v := p.condition()
|
||||||
|
p.expect(TokenCloseParenthesis)
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
default:
|
||||||
|
p.error("invalid factor", p.curr)
|
||||||
|
p.advance()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) product() Node {
|
||||||
|
left := p.factor()
|
||||||
|
|
||||||
|
for p.accept(TokenStar) || p.accept(TokenSlash) {
|
||||||
|
op := BinaryMultiplication
|
||||||
|
|
||||||
|
if (*p.prev).Type == TokenSlash {
|
||||||
|
op = BinaryDivision
|
||||||
|
}
|
||||||
|
|
||||||
|
left = &BinaryNode{
|
||||||
|
op,
|
||||||
|
left,
|
||||||
|
p.factor(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) term() Node {
|
||||||
|
left := p.product()
|
||||||
|
|
||||||
|
for p.accept(TokenPlus) || p.accept(TokenMinus) {
|
||||||
|
op := BinaryAddition
|
||||||
|
|
||||||
|
if (*p.prev).Type == TokenMinus {
|
||||||
|
op = BinarySubtraction
|
||||||
|
}
|
||||||
|
|
||||||
|
left = &BinaryNode{
|
||||||
|
op,
|
||||||
|
left,
|
||||||
|
p.product(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) condition() Node {
|
||||||
|
left := p.term()
|
||||||
|
op := BinaryEquality
|
||||||
|
|
||||||
|
switch (*p.curr).Type {
|
||||||
|
case TokenEquals:
|
||||||
|
op = BinaryEquality
|
||||||
|
case TokenBangEquals:
|
||||||
|
op = BinaryInequality
|
||||||
|
case TokenGreaterThan:
|
||||||
|
op = BinaryGreater
|
||||||
|
case TokenLessThan:
|
||||||
|
op = BinaryLess
|
||||||
|
case TokenLessThanOrEqual:
|
||||||
|
op = BinaryLessEqual
|
||||||
|
case TokenGreaterThanOrEqual:
|
||||||
|
op = BinaryGreaterEqual
|
||||||
|
default:
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
|
||||||
|
p.advance()
|
||||||
|
|
||||||
|
return &BinaryNode{
|
||||||
|
op,
|
||||||
|
left,
|
||||||
|
p.term(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) statement() Node {
|
||||||
|
switch (*p.curr).Type {
|
||||||
|
case TokenIf:
|
||||||
|
p.advance()
|
||||||
|
|
||||||
|
condition := p.condition()
|
||||||
|
then := p.block(false)
|
||||||
|
var otherwise Node
|
||||||
|
|
||||||
|
if p.accept(TokenElse) {
|
||||||
|
otherwise = p.block(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ConditionalNode{
|
||||||
|
condition,
|
||||||
|
then,
|
||||||
|
otherwise,
|
||||||
|
}
|
||||||
|
|
||||||
|
case TokenName:
|
||||||
|
p.advance()
|
||||||
|
name := (*p.prev).Lexeme
|
||||||
|
|
||||||
|
if p.curr.Type == TokenOpenParenthesis {
|
||||||
|
args := p.parseArgs()
|
||||||
|
|
||||||
|
return &CallNode{
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
false,
|
||||||
|
}
|
||||||
|
} else if p.accept(TokenAssign) || p.accept(TokenDeclare) {
|
||||||
|
isDeclaration := p.prev.Type == TokenDeclare
|
||||||
|
|
||||||
|
return &AssignNode{
|
||||||
|
name,
|
||||||
|
p.condition(),
|
||||||
|
isDeclaration,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return p.condition()
|
||||||
|
}
|
||||||
|
|
||||||
|
case TokenFunc:
|
||||||
|
p.advance()
|
||||||
|
|
||||||
|
p.expect(TokenName)
|
||||||
|
name := p.prev.Lexeme
|
||||||
|
|
||||||
|
params := p.parseParams()
|
||||||
|
|
||||||
|
return &AssignNode{
|
||||||
|
name,
|
||||||
|
&FunctionNode{
|
||||||
|
name,
|
||||||
|
params,
|
||||||
|
p.block(false),
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
}
|
||||||
|
|
||||||
|
case TokenReturn:
|
||||||
|
p.advance()
|
||||||
|
|
||||||
|
return &ReturnNode{
|
||||||
|
p.condition(),
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
p.error("invalid statement", p.curr)
|
||||||
|
p.advance()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) block(canBeStatement bool) Node {
|
||||||
|
if canBeStatement {
|
||||||
|
if !p.accept(TokenOpenBrace) {
|
||||||
|
return p.statement()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.expect(TokenOpenBrace)
|
||||||
|
}
|
||||||
|
|
||||||
|
statements := make([]Node, 0)
|
||||||
|
|
||||||
|
for !p.accept(TokenCloseBrace) {
|
||||||
|
statements = append(statements, p.statement())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BlockNode{
|
||||||
|
statements,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseArgs() []Node {
|
||||||
|
args := make([]Node, 0)
|
||||||
|
|
||||||
|
p.expect(TokenOpenParenthesis)
|
||||||
|
|
||||||
|
if !p.accept(TokenCloseParenthesis) {
|
||||||
|
args = append(args, p.condition())
|
||||||
|
for !p.accept(TokenCloseParenthesis) {
|
||||||
|
p.expect(TokenComma)
|
||||||
|
args = append(args, p.condition())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseParams parse parameters and parentheses
|
||||||
|
func (p *Parser) parseParams() []string {
|
||||||
|
p.expect(TokenOpenParenthesis)
|
||||||
|
params := make([]string, 0)
|
||||||
|
|
||||||
|
if p.accept(TokenName) {
|
||||||
|
name := (*p.prev).Lexeme
|
||||||
|
params = append(params, name)
|
||||||
|
for !p.accept(TokenCloseParenthesis) {
|
||||||
|
p.expect(TokenComma)
|
||||||
|
p.expect(TokenName)
|
||||||
|
name = (*p.prev).Lexeme
|
||||||
|
params = append(params, name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.expect(TokenCloseParenthesis)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
629
parser_test.go
Normal file
629
parser_test.go
Normal file
|
|
@ -0,0 +1,629 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewParser(t *testing.T) {
|
||||||
|
tokens := make([]Token, 0)
|
||||||
|
|
||||||
|
p := NewParser(tokens)
|
||||||
|
|
||||||
|
if p == nil {
|
||||||
|
t.Fatal("parser should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.hadError {
|
||||||
|
t.Error("parser should not when initialized report an error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.pos != 0 {
|
||||||
|
t.Error("parser should initialize position at 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.tokens) != len(tokens) {
|
||||||
|
t.Error("parser should have the passed token list")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range tokens {
|
||||||
|
if p.tokens[i] != v {
|
||||||
|
t.Error("parser should have the passed token list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNewParser(b *testing.B) {
|
||||||
|
tokens := make([]Token, 0)
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = NewParser(tokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenTestData struct {
|
||||||
|
tokens []Token
|
||||||
|
tree Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTokenTestData() map[string]TokenTestData {
|
||||||
|
return map[string]TokenTestData{
|
||||||
|
"empty": {
|
||||||
|
[]Token{
|
||||||
|
NewToken(TokenEOF, 0, 0, 0, ""),
|
||||||
|
},
|
||||||
|
&BlockNode{},
|
||||||
|
},
|
||||||
|
"addition": {
|
||||||
|
[]Token{
|
||||||
|
NewToken(TokenName, 0, 1, 0, "_"),
|
||||||
|
NewToken(TokenAssign, 1, 1, 0, "="),
|
||||||
|
NewToken(TokenNumber, 3, 1, 0, "1"),
|
||||||
|
NewToken(TokenPlus, 4, 1, 0, "+"),
|
||||||
|
NewToken(TokenNumber, 5, 1, 0, "2"),
|
||||||
|
NewToken(TokenEOF, 6, 0, 0, ""),
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"_",
|
||||||
|
&BinaryNode{
|
||||||
|
BinaryAddition,
|
||||||
|
&NumberNode{
|
||||||
|
value: NumberValue(1),
|
||||||
|
},
|
||||||
|
&NumberNode{
|
||||||
|
value: NumberValue(2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"assignment": {
|
||||||
|
[]Token{
|
||||||
|
NewToken(TokenName, 0, 5, 0, "hello"),
|
||||||
|
NewToken(TokenAssign, 5, 1, 0, "="),
|
||||||
|
NewToken(TokenString, 6, 12, 0, "\"Hello world!\""),
|
||||||
|
NewToken(TokenEOF, 18, 0, 0, ""),
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"hello",
|
||||||
|
&StringNode{
|
||||||
|
"Hello world!",
|
||||||
|
"\"Hello world!\"",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
[]Token{
|
||||||
|
NewToken(TokenName, 0, 1, 0, "a"),
|
||||||
|
NewToken(TokenDeclare, 1, 2, 0, ":="),
|
||||||
|
NewToken(TokenNumber, 3, 1, 0, "1"),
|
||||||
|
NewToken(TokenPlus, 4, 1, 0, "+"),
|
||||||
|
NewToken(TokenName, 5, 1, 0, "b"),
|
||||||
|
NewToken(TokenEOF, 6, 0, 0, ""),
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"a",
|
||||||
|
&BinaryNode{
|
||||||
|
BinaryAddition,
|
||||||
|
&NumberNode{
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
&ReferenceNode{
|
||||||
|
"b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// (2 + 1) * 5 + 3 / (6 - 2) - 10 / 2
|
||||||
|
"arithmetic_order": {
|
||||||
|
[]Token{
|
||||||
|
NewToken(TokenName, 0, 1, 0, "_"),
|
||||||
|
NewToken(TokenAssign, 1, 2, 0, "="),
|
||||||
|
|
||||||
|
NewToken(TokenOpenParenthesis, 3, 1, 0, "("),
|
||||||
|
NewToken(TokenNumber, 4, 1, 0, "2"),
|
||||||
|
NewToken(TokenPlus, 5, 1, 0, "+"),
|
||||||
|
NewToken(TokenNumber, 6, 1, 0, "1"),
|
||||||
|
NewToken(TokenCloseParenthesis, 7, 1, 0, ")"),
|
||||||
|
NewToken(TokenStar, 8, 1, 0, "*"),
|
||||||
|
NewToken(TokenNumber, 9, 1, 0, "5"),
|
||||||
|
|
||||||
|
NewToken(TokenPlus, 10, 1, 0, "+"),
|
||||||
|
|
||||||
|
NewToken(TokenNumber, 11, 1, 0, "3"),
|
||||||
|
NewToken(TokenSlash, 12, 1, 0, "/"),
|
||||||
|
NewToken(TokenOpenParenthesis, 13, 1, 0, "("),
|
||||||
|
NewToken(TokenNumber, 14, 1, 0, "6"),
|
||||||
|
NewToken(TokenMinus, 15, 1, 0, "-"),
|
||||||
|
NewToken(TokenNumber, 16, 1, 0, "2"),
|
||||||
|
NewToken(TokenCloseParenthesis, 17, 1, 0, ")"),
|
||||||
|
|
||||||
|
NewToken(TokenMinus, 18, 1, 0, "-"),
|
||||||
|
|
||||||
|
NewToken(TokenNumber, 19, 2, 0, "10"),
|
||||||
|
NewToken(TokenSlash, 20, 1, 0, "/"),
|
||||||
|
NewToken(TokenNumber, 21, 1, 0, "2"),
|
||||||
|
NewToken(TokenEOF, 22, 0, 0, ""),
|
||||||
|
},
|
||||||
|
// (2 + 1) * 5 + 3 / (6 - 2) - 10 / 2
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"_",
|
||||||
|
&BinaryNode{
|
||||||
|
BinarySubtraction,
|
||||||
|
&BinaryNode{
|
||||||
|
BinaryAddition,
|
||||||
|
&BinaryNode{
|
||||||
|
BinaryMultiplication,
|
||||||
|
&BinaryNode{
|
||||||
|
BinaryAddition,
|
||||||
|
&NumberNode{2},
|
||||||
|
&NumberNode{1},
|
||||||
|
},
|
||||||
|
&NumberNode{NumberValue(5)},
|
||||||
|
},
|
||||||
|
&BinaryNode{
|
||||||
|
BinaryDivision,
|
||||||
|
&NumberNode{NumberValue(3)},
|
||||||
|
&BinaryNode{
|
||||||
|
BinarySubtraction,
|
||||||
|
&NumberNode{NumberValue(6)},
|
||||||
|
&NumberNode{NumberValue(2)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&BinaryNode{
|
||||||
|
BinaryDivision,
|
||||||
|
&NumberNode{NumberValue(10)},
|
||||||
|
&NumberNode{NumberValue(2)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"condition_equal": {
|
||||||
|
[]Token{
|
||||||
|
NewToken(TokenName, 0, 1, 0, "_"),
|
||||||
|
NewToken(TokenAssign, 1, 1, 0, "="),
|
||||||
|
NewToken(TokenNumber, 2, 2, 0, "20"),
|
||||||
|
NewToken(TokenEquals, 4, 2, 0, "=="),
|
||||||
|
NewToken(TokenNumber, 6, 2, 0, "15"),
|
||||||
|
NewToken(TokenEOF, 8, 0, 0, ""),
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"_",
|
||||||
|
&BinaryNode{
|
||||||
|
BinaryEquality,
|
||||||
|
&NumberNode{
|
||||||
|
20,
|
||||||
|
},
|
||||||
|
&NumberNode{
|
||||||
|
15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"if_statement": {
|
||||||
|
[]Token{
|
||||||
|
NewToken(TokenIf, 0, 2, 0, "if"),
|
||||||
|
NewToken(TokenName, 2, 1, 0, "a"),
|
||||||
|
NewToken(TokenEquals, 3, 2, 0, "=="),
|
||||||
|
NewToken(TokenNumber, 5, 1, 0, "0"),
|
||||||
|
NewToken(TokenOpenBrace, 6, 1, 0, "{"),
|
||||||
|
NewToken(TokenName, 7, 1, 1, "b"),
|
||||||
|
NewToken(TokenAssign, 8, 1, 1, "="),
|
||||||
|
NewToken(TokenNumber, 9, 1, 1, "1"),
|
||||||
|
NewToken(TokenCloseBrace, 10, 1, 2, "}"),
|
||||||
|
NewToken(TokenEOF, 11, 0, 2, ""),
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&ConditionalNode{
|
||||||
|
condition: &BinaryNode{
|
||||||
|
BinaryEquality,
|
||||||
|
&ReferenceNode{
|
||||||
|
"a",
|
||||||
|
},
|
||||||
|
&NumberNode{
|
||||||
|
NumberValue(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
do: &BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"b",
|
||||||
|
&NumberNode{
|
||||||
|
NumberValue(1),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"if_else_statement": {
|
||||||
|
[]Token{
|
||||||
|
NewToken(TokenIf, 0, 2, 0, "if"),
|
||||||
|
NewToken(TokenName, 2, 1, 0, "a"),
|
||||||
|
NewToken(TokenEquals, 3, 2, 0, "=="),
|
||||||
|
NewToken(TokenNumber, 5, 1, 0, "0"),
|
||||||
|
NewToken(TokenOpenBrace, 6, 1, 0, "{"),
|
||||||
|
NewToken(TokenName, 7, 1, 1, "b"),
|
||||||
|
NewToken(TokenAssign, 8, 1, 1, "="),
|
||||||
|
NewToken(TokenNumber, 9, 1, 1, "1"),
|
||||||
|
NewToken(TokenCloseBrace, 10, 1, 2, "}"),
|
||||||
|
NewToken(TokenElse, 11, 4, 2, "else"),
|
||||||
|
NewToken(TokenOpenBrace, 15, 1, 2, "{"),
|
||||||
|
NewToken(TokenName, 16, 1, 2, "b"),
|
||||||
|
NewToken(TokenAssign, 17, 1, 2, "="),
|
||||||
|
NewToken(TokenNumber, 18, 1, 2, "0"),
|
||||||
|
NewToken(TokenCloseBrace, 19, 1, 2, "}"),
|
||||||
|
NewToken(TokenEOF, 20, 0, 2, ""),
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&ConditionalNode{
|
||||||
|
condition: &BinaryNode{
|
||||||
|
BinaryEquality,
|
||||||
|
&ReferenceNode{
|
||||||
|
"a",
|
||||||
|
},
|
||||||
|
&NumberNode{
|
||||||
|
NumberValue(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
do: &BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"b",
|
||||||
|
&NumberNode{
|
||||||
|
NumberValue(1),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
otherwise: &BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"b",
|
||||||
|
&NumberNode{
|
||||||
|
NumberValue(0),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"empty_block": {
|
||||||
|
[]Token{
|
||||||
|
NewToken(TokenOpenBrace, 0, 1, 0, "{"),
|
||||||
|
NewToken(TokenCloseBrace, 1, 1, 0, "}"),
|
||||||
|
NewToken(TokenEOF, 2, 0, 0, ""),
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"lambda": { // a := func(a, b) { return a + b }
|
||||||
|
[]Token{
|
||||||
|
NewToken(TokenName, 0, 1, 0, "a"),
|
||||||
|
NewToken(TokenDeclare, 1, 2, 0, ":="),
|
||||||
|
NewToken(TokenFunc, 3, 4, 0, "func"),
|
||||||
|
NewToken(TokenOpenParenthesis, 7, 1, 0, "("),
|
||||||
|
NewToken(TokenName, 8, 1, 0, "a"),
|
||||||
|
NewToken(TokenComma, 9, 1, 0, ","),
|
||||||
|
NewToken(TokenName, 10, 1, 0, "b"),
|
||||||
|
NewToken(TokenCloseParenthesis, 11, 1, 0, ")"),
|
||||||
|
|
||||||
|
NewToken(TokenOpenBrace, 12, 1, 1, "{"),
|
||||||
|
NewToken(TokenReturn, 13, 6, 1, "return"),
|
||||||
|
NewToken(TokenName, 19, 1, 1, "a"),
|
||||||
|
NewToken(TokenPlus, 20, 1, 1, "+"),
|
||||||
|
NewToken(TokenName, 21, 1, 1, "b"),
|
||||||
|
NewToken(TokenCloseBrace, 22, 1, 2, "}"),
|
||||||
|
|
||||||
|
NewToken(TokenEOF, 23, 0, 2, ""),
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"a",
|
||||||
|
&FunctionNode{
|
||||||
|
"*",
|
||||||
|
[]string{"a", "b"},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&ReturnNode{
|
||||||
|
&BinaryNode{
|
||||||
|
BinaryAddition,
|
||||||
|
&ReferenceNode{
|
||||||
|
"a",
|
||||||
|
},
|
||||||
|
&ReferenceNode{
|
||||||
|
"b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"function_declaration": {
|
||||||
|
[]Token{
|
||||||
|
NewToken(TokenFunc, 0, 4, 0, "func"),
|
||||||
|
NewToken(TokenName, 4, 3, 0, "a"),
|
||||||
|
NewToken(TokenOpenParenthesis, 7, 1, 0, "("),
|
||||||
|
NewToken(TokenName, 8, 1, 0, "a"),
|
||||||
|
NewToken(TokenComma, 9, 1, 0, ","),
|
||||||
|
NewToken(TokenName, 10, 1, 0, "b"),
|
||||||
|
NewToken(TokenCloseParenthesis, 11, 1, 0, ")"),
|
||||||
|
|
||||||
|
NewToken(TokenOpenBrace, 12, 1, 1, "{"),
|
||||||
|
NewToken(TokenReturn, 13, 6, 1, "return"),
|
||||||
|
NewToken(TokenName, 19, 1, 1, "a"),
|
||||||
|
NewToken(TokenPlus, 20, 1, 1, "+"),
|
||||||
|
NewToken(TokenName, 21, 1, 1, "b"),
|
||||||
|
NewToken(TokenCloseBrace, 22, 1, 2, "}"),
|
||||||
|
|
||||||
|
NewToken(TokenEOF, 23, 0, 2, ""),
|
||||||
|
},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&AssignNode{
|
||||||
|
"a",
|
||||||
|
&FunctionNode{
|
||||||
|
"a",
|
||||||
|
[]string{"a", "b"},
|
||||||
|
&BlockNode{
|
||||||
|
[]Node{
|
||||||
|
&ReturnNode{
|
||||||
|
&BinaryNode{
|
||||||
|
BinaryAddition,
|
||||||
|
&ReferenceNode{
|
||||||
|
"a",
|
||||||
|
},
|
||||||
|
&ReferenceNode{
|
||||||
|
"b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NodeEquality(t *testing.T, n1 Node, n2 Node) {
|
||||||
|
if n1 == n2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if n1 == nil || n2 == nil {
|
||||||
|
t.Fatalf("one of the nodes are nil (1: %s; 2: %s)", n1, n2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n1.Type() != n2.Type() {
|
||||||
|
t.Fatalf("node types (%s and %s) don't match", n1.Type(), n2.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Nodes have same non-nil type (%s)", n1.Type())
|
||||||
|
|
||||||
|
switch n1.Type() {
|
||||||
|
case NilNodeType:
|
||||||
|
case StringNodeType:
|
||||||
|
if n1.(*StringNode).value != n2.(*StringNode).value {
|
||||||
|
t.Errorf("String node values don't match (%s and %s)", n1.(*StringNode).value, n2.(*StringNode).value)
|
||||||
|
} else {
|
||||||
|
t.Logf("String node values match (%s)", n1.(*StringNode).value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n1.(*StringNode).quoted != n2.(*StringNode).quoted {
|
||||||
|
t.Errorf("String node quoted values don't match (%s and %s)", n1.(*StringNode).quoted, n2.(*StringNode).quoted)
|
||||||
|
} else {
|
||||||
|
t.Logf("String node quoted values match (%s)", n1.(*StringNode).value)
|
||||||
|
}
|
||||||
|
|
||||||
|
case NumberNodeType:
|
||||||
|
if n1.(*NumberNode).value != n2.(*NumberNode).value {
|
||||||
|
t.Errorf("Number node values don't match (%f and %f)", n1.(*NumberNode).value, n2.(*NumberNode).value)
|
||||||
|
} else {
|
||||||
|
t.Logf("Number node values match (%f)", n1.(*NumberNode).value)
|
||||||
|
}
|
||||||
|
case ReferenceNodeType:
|
||||||
|
if n1.(*ReferenceNode).name != n2.(*ReferenceNode).name {
|
||||||
|
t.Errorf("Reference node values don't match (%s and %s)", n1.(*ReferenceNode).name, n2.(*ReferenceNode).name)
|
||||||
|
} else {
|
||||||
|
t.Logf("Reference node values match (%s)", n1.(*ReferenceNode).name)
|
||||||
|
}
|
||||||
|
case BinaryNodeType:
|
||||||
|
if n1.(*BinaryNode).BinaryOperation != n2.(*BinaryNode).BinaryOperation {
|
||||||
|
t.Errorf("Binary node operation not same (%s and %s)", n1.(*BinaryNode).BinaryOperation, n2.(*BinaryNode).BinaryOperation)
|
||||||
|
} else {
|
||||||
|
t.Logf("Binary node operation matches (%s)", n1.(*BinaryNode).BinaryOperation)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Checking equality of binary left side")
|
||||||
|
NodeEquality(t, n1.(*BinaryNode).Left, n2.(*BinaryNode).Left)
|
||||||
|
t.Log("Checking equality of binary right side")
|
||||||
|
NodeEquality(t, n1.(*BinaryNode).Right, n2.(*BinaryNode).Right)
|
||||||
|
|
||||||
|
case BooleanNodeType:
|
||||||
|
if n1.(*BooleanNode).value != n2.(*BooleanNode).value {
|
||||||
|
t.Errorf("Boolean node values don't match (%s and %s)", strconv.FormatBool(n1.(*BooleanNode).value), strconv.FormatBool(n2.(*BooleanNode).value))
|
||||||
|
} else {
|
||||||
|
t.Logf("Boolean node values match (%s)", strconv.FormatBool(n1.(*BooleanNode).value))
|
||||||
|
}
|
||||||
|
case BlockNodeType:
|
||||||
|
if len(n1.(*BlockNode).statements) != len(n2.(*BlockNode).statements) {
|
||||||
|
t.Errorf("Block node statement count is not equal (%d and %d)", len(n1.(*BlockNode).statements), len(n2.(*BlockNode).statements))
|
||||||
|
} else {
|
||||||
|
t.Logf("Block node statement count is equal (%d) ", len(n1.(*BlockNode).statements))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, n := range n1.(*BlockNode).statements {
|
||||||
|
t.Logf("Checking equality of statements at %d", i)
|
||||||
|
NodeEquality(t, n, n2.(*BlockNode).statements[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
case ConditionalNodeType:
|
||||||
|
t.Log("Checking equality of conditions")
|
||||||
|
NodeEquality(t, n1.(*ConditionalNode).condition, n2.(*ConditionalNode).condition)
|
||||||
|
t.Log("Checking equality of do statement(s)")
|
||||||
|
NodeEquality(t, n1.(*ConditionalNode).do, n2.(*ConditionalNode).do)
|
||||||
|
t.Log("Checking equality of else statement(s)")
|
||||||
|
NodeEquality(t, n1.(*ConditionalNode).otherwise, n2.(*ConditionalNode).otherwise)
|
||||||
|
|
||||||
|
case LoopNodeType:
|
||||||
|
t.Log("Checking equality of loop conditions")
|
||||||
|
NodeEquality(t, n1.(*LoopNode).condition, n2.(*LoopNode).condition)
|
||||||
|
t.Log("Checking equality of do loop statement(s)")
|
||||||
|
NodeEquality(t, n1.(*LoopNode).do, n2.(*LoopNode).do)
|
||||||
|
|
||||||
|
case AssignNodeType:
|
||||||
|
if n1.(*AssignNode).name != n2.(*AssignNode).name {
|
||||||
|
t.Errorf("Assigned value name is not the same (%s and %s)", n1.(*AssignNode).name, n2.(*AssignNode).name)
|
||||||
|
} else {
|
||||||
|
t.Logf("Assigned value name matches (%s)", n1.(*AssignNode).name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n1.(*AssignNode).declare != n2.(*AssignNode).declare {
|
||||||
|
t.Errorf("Not same type of assigning (1: %v; 2: %v)", n1.(*AssignNode).declare, n2.(*AssignNode).declare)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Checking equality of assignment values")
|
||||||
|
NodeEquality(t, n1.(*AssignNode).value, n2.(*AssignNode).value)
|
||||||
|
|
||||||
|
case CallNodeType:
|
||||||
|
n := n1.(*CallNode)
|
||||||
|
m := n2.(*CallNode)
|
||||||
|
|
||||||
|
if n.name != m.name {
|
||||||
|
t.Errorf("Call node names don't match (%s and %s)", n.name, m.name)
|
||||||
|
} else {
|
||||||
|
t.Logf("Call node names match (%s)", n.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.keep == m.keep {
|
||||||
|
t.Logf("Call node keep modifier doesn't match (%v and %v)", n.keep, m.keep)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(n.args) != len(m.args) {
|
||||||
|
t.Fatalf("Call node arguments count does not match (%d and %d)", len(n.args), m.args)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, arg := range m.args {
|
||||||
|
NodeEquality(t, n1.(*CallNode).args[i], arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
case FunctionNodeType:
|
||||||
|
n := n1.(*FunctionNode)
|
||||||
|
m := n2.(*FunctionNode)
|
||||||
|
|
||||||
|
if n.name != m.name {
|
||||||
|
t.Errorf("Function node names don't match (%s and %s)", n.name, m.name)
|
||||||
|
} else {
|
||||||
|
t.Logf("Function node names match (%s)", n.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(n.params) != len(m.params) {
|
||||||
|
t.Fatalf("Function node parameters count does not match (%d and %d)", len(n.params), len(m.params))
|
||||||
|
} else {
|
||||||
|
t.Logf("Function node parameters count is equal (%d) ", len(n.params))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range m.params {
|
||||||
|
if n.params[i] != p {
|
||||||
|
t.Errorf("Function node parameter %d does not match: %s and %s", i, p, m.params)
|
||||||
|
} else {
|
||||||
|
t.Logf("Function node parameter %d matches (%s)", i, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeEquality(t, n.logic, m.logic)
|
||||||
|
|
||||||
|
case ReturnNodeType:
|
||||||
|
NodeEquality(t, n1.(*ReturnNode).value, n2.(*ReturnNode).value)
|
||||||
|
default:
|
||||||
|
panic("unimplemented node equality")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParser_Parse(t *testing.T) {
|
||||||
|
t.Logf("Getting test data")
|
||||||
|
token_data := GetTokenTestData()
|
||||||
|
|
||||||
|
for name, data := range token_data {
|
||||||
|
if name != "empty_block" && name != "lambda" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Logf("Initializing parser")
|
||||||
|
p := NewParser(data.tokens)
|
||||||
|
|
||||||
|
t.Logf("Parsing main")
|
||||||
|
tree := p.Parse()
|
||||||
|
|
||||||
|
if p.hadError {
|
||||||
|
t.Fatalf("Unexpected error(s): %s", p.errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Checking parsed tree")
|
||||||
|
NodeEquality(t, tree, data.tree)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkParser_Parse(b *testing.B) {
|
||||||
|
token_data := GetTokenTestData()
|
||||||
|
|
||||||
|
for name, data := range token_data {
|
||||||
|
b.Run(name, func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
p := NewParser(data.tokens)
|
||||||
|
|
||||||
|
_ = p.Parse()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
55
stack.go
Normal file
55
stack.go
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
type Stack[T any] struct {
|
||||||
|
Current Pos
|
||||||
|
Size Pos
|
||||||
|
|
||||||
|
items []T
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStack[T any](size Pos) *Stack[T] {
|
||||||
|
return &Stack[T]{
|
||||||
|
items: make([]T, size),
|
||||||
|
Size: size,
|
||||||
|
Current: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stack[T]) Push(items ...T) {
|
||||||
|
for _, item := range items {
|
||||||
|
if s.Current >= s.Size {
|
||||||
|
panic("stack overflow")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.items[s.Current] = item
|
||||||
|
s.Current++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stack[T]) Pop() T {
|
||||||
|
if s.Current <= 0 {
|
||||||
|
panic("stack underflow")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Current--
|
||||||
|
return s.items[s.Current]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stack[T]) Peek() T {
|
||||||
|
if s.Current <= 0 {
|
||||||
|
panic("stack underflow")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.items[s.Current-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// check whether the stack is invalid (stack over-/underflow)
|
||||||
|
func (s *Stack[T]) check() {
|
||||||
|
if s.Current >= s.Size {
|
||||||
|
panic("stack underflow")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Current < 0 {
|
||||||
|
panic("stack underflow")
|
||||||
|
}
|
||||||
|
}
|
||||||
128
stack_test.go
Normal file
128
stack_test.go
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CompareStacks[T Value](t *testing.T, expected []T, actual *Stack[T]) {
|
||||||
|
if actual.Current != Pos(len(expected)) {
|
||||||
|
t.Errorf("Unexpected stack size. Expected %d, got %d", len(expected), actual.Current)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range expected {
|
||||||
|
CompareValues(t, actual.items[i], v)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(expected); i < int(actual.Current); i++ {
|
||||||
|
t.Errorf("Unexpected item %d: %s", i, actual.items[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewStack(t *testing.T) {
|
||||||
|
size := 256
|
||||||
|
|
||||||
|
s := NewStack[any](Pos(size))
|
||||||
|
|
||||||
|
if s.Size != Pos(size) {
|
||||||
|
t.Errorf("Stack size (%d) does not match expected size (%d)", s.Size, size)
|
||||||
|
} else {
|
||||||
|
t.Logf("Stack size is expected size (%d)", s.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.items) != size {
|
||||||
|
t.Errorf("internal items slice size (%d) does not match expected size (%d)", len(s.items), size)
|
||||||
|
} else {
|
||||||
|
t.Logf("internal items slice size is as expected (%d)", len(s.items))
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Current != 0 {
|
||||||
|
t.Errorf("Current pos (%d) not initialized to 0", s.Current)
|
||||||
|
} else {
|
||||||
|
t.Log("Current pos initialized to 0 as expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNewStack(b *testing.B) {
|
||||||
|
for n := 0; n <= 2048; n += 256 {
|
||||||
|
b.Run(fmt.Sprintf("size_%d", n), func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = NewStack[any](Pos(n))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStackUnderflow(t *testing.T) {
|
||||||
|
s := NewStack[any](64)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("popping an empty array did not panic (stack underflow)")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.Pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStackUnderflowByPeek(t *testing.T) {
|
||||||
|
s := NewStack[any](64)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("peeking beyond an empty stack did not panic (stack underflow)")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.Peek()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetExampleStackTestCases() []any {
|
||||||
|
return []any{
|
||||||
|
"Hello world!",
|
||||||
|
16,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
2008,
|
||||||
|
"",
|
||||||
|
"Lorem ipsum dolor sit amet",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStack(t *testing.T) {
|
||||||
|
for _, c := range GetExampleStackTestCases() {
|
||||||
|
s := NewStack[any](1)
|
||||||
|
s.Push(c)
|
||||||
|
out := s.Pop()
|
||||||
|
|
||||||
|
if out != c {
|
||||||
|
t.Errorf("inputted item does not match outputted item")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStackOverflow(t *testing.T) {
|
||||||
|
s := NewStack[any](1)
|
||||||
|
s.Push(1)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("pushing beyond a stack did not panic (stack overflow)")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.Push(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStack(b *testing.B) {
|
||||||
|
for n := 256; n <= 2048; n += 256 {
|
||||||
|
b.Run(fmt.Sprintf("size_%d", n), func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
s := NewStack[any](Pos(n))
|
||||||
|
|
||||||
|
s.Push(2)
|
||||||
|
s.Pop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
162
values.go
Normal file
162
values.go
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ValueType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NilValueType ValueType = iota
|
||||||
|
BoolValueType
|
||||||
|
NumberValueType
|
||||||
|
StringValueType
|
||||||
|
FunctionValueType
|
||||||
|
BuiltinFunctionValueType
|
||||||
|
VariableValueType
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v ValueType) String() string {
|
||||||
|
switch v {
|
||||||
|
case NilValueType:
|
||||||
|
return "nil"
|
||||||
|
case BoolValueType:
|
||||||
|
return "bool"
|
||||||
|
case NumberValueType:
|
||||||
|
return "number"
|
||||||
|
case StringValueType:
|
||||||
|
return "string"
|
||||||
|
case FunctionValueType:
|
||||||
|
return "function"
|
||||||
|
case BuiltinFunctionValueType:
|
||||||
|
return "builtin function"
|
||||||
|
case VariableValueType:
|
||||||
|
return "variable"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "undefined"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetType(v string) ValueType {
|
||||||
|
switch v {
|
||||||
|
case "nil":
|
||||||
|
return NilValueType
|
||||||
|
case "bool":
|
||||||
|
return BoolValueType
|
||||||
|
case "number":
|
||||||
|
return NumberValueType
|
||||||
|
case "string":
|
||||||
|
return StringValueType
|
||||||
|
case "function":
|
||||||
|
return FunctionValueType
|
||||||
|
case "builtin":
|
||||||
|
return BuiltinFunctionValueType
|
||||||
|
case "variable":
|
||||||
|
return VariableValueType
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type Value interface {
|
||||||
|
Type() ValueType
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NilValue struct{}
|
||||||
|
|
||||||
|
func (v NilValue) Type() ValueType {
|
||||||
|
return NilValueType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NilValue) String() string {
|
||||||
|
return "nil"
|
||||||
|
}
|
||||||
|
|
||||||
|
type BoolValue bool
|
||||||
|
|
||||||
|
func (v BoolValue) Type() ValueType {
|
||||||
|
return BoolValueType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BoolValue) String() string {
|
||||||
|
if v {
|
||||||
|
return "true"
|
||||||
|
} else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumberValue Integer or floating-point values
|
||||||
|
type NumberValue float32
|
||||||
|
|
||||||
|
const NumberSize int = 32
|
||||||
|
|
||||||
|
func (v NumberValue) Type() ValueType {
|
||||||
|
return NumberValueType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NumberValue) String() string {
|
||||||
|
return strconv.FormatFloat(float64(v), 'g', -1, 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StringValue string
|
||||||
|
|
||||||
|
func (v StringValue) Type() ValueType {
|
||||||
|
return StringValueType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v StringValue) String() string {
|
||||||
|
return string(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FunctionValue struct {
|
||||||
|
Name string
|
||||||
|
Params []string
|
||||||
|
Chunk *Chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v FunctionValue) Type() ValueType {
|
||||||
|
return FunctionValueType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v FunctionValue) String() string {
|
||||||
|
return fmt.Sprintf("<function name=%s>", v.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuiltinFunctionValue struct {
|
||||||
|
name string
|
||||||
|
parameters []string
|
||||||
|
f func(map[string]Value) Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BuiltinFunctionValue) Type() ValueType {
|
||||||
|
return BuiltinFunctionValueType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BuiltinFunctionValue) String() string {
|
||||||
|
return fmt.Sprintf("<function name=%s builtin>", v.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VariableValue a value wrapper for variables kept on the stack
|
||||||
|
type VariableValue struct {
|
||||||
|
name string
|
||||||
|
value Value
|
||||||
|
scope Pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v VariableValue) Type() ValueType {
|
||||||
|
return VariableValueType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v VariableValue) String() string {
|
||||||
|
return fmt.Sprintf("<variable name=%s value=%s scope=%d>", v.name, v.value, v.scope)
|
||||||
|
|
||||||
|
// variables should not be accessed on the stack; normal values should be pushed and popped predictably
|
||||||
|
//panic("tried getting string value of a unreachable value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v VariableValue) equals(other VariableValue) bool {
|
||||||
|
return v.name == other.name && v.value == other.value
|
||||||
|
}
|
||||||
89
values_test.go
Normal file
89
values_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func CompareValues(t *testing.T, got Value, want Value) {
|
||||||
|
if got.Type() != want.Type() {
|
||||||
|
t.Fatalf("type mismatch: got %v want %v", got.Type(), want.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
switch got.Type() {
|
||||||
|
case NilValueType:
|
||||||
|
t.Logf("Both are nil")
|
||||||
|
return
|
||||||
|
case BoolValueType:
|
||||||
|
if got.(BoolValue) != want.(BoolValue) {
|
||||||
|
t.Errorf("bool value mismatch: got %v, want %v", got.(BoolValue), want.(BoolValue))
|
||||||
|
} else {
|
||||||
|
t.Logf("Both are same boolean (%s)", want.(BoolValue).String())
|
||||||
|
}
|
||||||
|
case NumberValueType:
|
||||||
|
if got.(NumberValue) != want.(NumberValue) {
|
||||||
|
t.Errorf("number value mismatch: got %v, want %v", got.(NumberValue), want.(NumberValue))
|
||||||
|
} else {
|
||||||
|
t.Logf("Both are same number (%s)", got.(NumberValue).String())
|
||||||
|
}
|
||||||
|
case StringValueType:
|
||||||
|
if got.(StringValue) != want.(StringValue) {
|
||||||
|
t.Errorf("string value mismatch: got %v, want %v", got.(StringValue), want.(StringValue))
|
||||||
|
} else {
|
||||||
|
t.Logf("Both are same string (%s)", got.(StringValue).String())
|
||||||
|
}
|
||||||
|
case FunctionValueType:
|
||||||
|
n := got.(FunctionValue)
|
||||||
|
m := want.(FunctionValue)
|
||||||
|
|
||||||
|
if n.Name != m.Name {
|
||||||
|
t.Errorf("function name mismatch: got %v, want %v", n.Name, m.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(n.Params) != len(m.Params) {
|
||||||
|
t.Errorf("function params length mismatch: got %v, want %v", len(m.Params), len(n.Params))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range n.Params {
|
||||||
|
if v != m.Params[i] {
|
||||||
|
t.Errorf("function params mismatch: got %v, want %v", v, m.Params[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompareChunks(t, n.Chunk, m.Chunk)
|
||||||
|
case BuiltinFunctionValueType:
|
||||||
|
n := got.(BuiltinFunctionValue)
|
||||||
|
m := want.(BuiltinFunctionValue)
|
||||||
|
|
||||||
|
if n.name != m.name {
|
||||||
|
t.Errorf("builtin function name mismatch: got %v, want %v", n.name, m.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(n.parameters) != len(m.parameters) {
|
||||||
|
t.Errorf("builtin function parameter count mismatch: got %v, want %v", n.parameters, m.parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range n.parameters {
|
||||||
|
if v != m.parameters[i] {
|
||||||
|
t.Errorf("builtin function parameter %d mismatch: got %v, want %v", i, v, m.parameters[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if &n.f != &m.f {
|
||||||
|
t.Errorf("builtin function f mismatch: got %v, want %v", &n.f, &m.f)
|
||||||
|
}
|
||||||
|
case VariableValueType:
|
||||||
|
n := got.(*VariableValue)
|
||||||
|
m := want.(*VariableValue)
|
||||||
|
|
||||||
|
if n.name != m.name {
|
||||||
|
t.Errorf("variable name mismatch: got %v, want %v", n.name, m.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.scope != m.scope {
|
||||||
|
t.Errorf("variable scope mismatch: got %v, want %v", n.scope, m.scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
CompareValues(t, n.value, m.value)
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("unimplemented comparison")
|
||||||
|
}
|
||||||
|
}
|
||||||
572
vm.go
Normal file
572
vm.go
Normal file
|
|
@ -0,0 +1,572 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Pos int
|
||||||
|
type Bytecode byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
// InstructionReturn return to previous call pointer
|
||||||
|
InstructionReturn Bytecode = iota
|
||||||
|
// InstructionPop pop and delete the first item on the stack
|
||||||
|
InstructionPop
|
||||||
|
|
||||||
|
// InstructionAdd pop two and add them
|
||||||
|
InstructionAdd
|
||||||
|
// InstructionSub pop two and subtract the second from the first
|
||||||
|
InstructionSub
|
||||||
|
// InstructionMul pop two and multiply them
|
||||||
|
InstructionMul
|
||||||
|
// InstructionDiv pop two and divide the second by the first
|
||||||
|
InstructionDiv
|
||||||
|
// InstructionEquals whether the two top values on the stack are equal
|
||||||
|
InstructionEquals
|
||||||
|
// InstructionNotEqual whether the two top values on the stack are not equal
|
||||||
|
InstructionNotEqual
|
||||||
|
// InstructionNot inverts boolean (true => false, false => true)
|
||||||
|
InstructionNot
|
||||||
|
// InstructionLess pops two from stack, pushes whether the lowest is less than the highest
|
||||||
|
InstructionLess
|
||||||
|
// InstructionLessOrEqual pops two from stack, pushes whether the lowest is less or equal than the highest
|
||||||
|
InstructionLessOrEqual
|
||||||
|
// InstructionGreater pops two from stack, pushes whether the lowest is greater than the highest
|
||||||
|
InstructionGreater
|
||||||
|
// InstructionGreaterOrEqual pops two from stack, pushes whether the lowest is greater or equal than the highest
|
||||||
|
InstructionGreaterOrEqual
|
||||||
|
|
||||||
|
// InstructionCall pops a function object from the stack and begins execution of the chunk
|
||||||
|
InstructionCall
|
||||||
|
|
||||||
|
// InstructionDescend increase the scope depth
|
||||||
|
InstructionDescend
|
||||||
|
// InstructionAscend decrease the scope depth, and remove all variables on the stack which belong in a higher scope
|
||||||
|
InstructionAscend
|
||||||
|
|
||||||
|
// InstructionJump jump forwards by the value of the next two bytes as a u16
|
||||||
|
InstructionJump
|
||||||
|
// InstructionJumpFalse jump by the value of the two next bytes as an unsigned integer if the first value (popped) from the stack is false
|
||||||
|
InstructionJumpFalse
|
||||||
|
// InstructionLoop jump by the value of the two next bytes as an unsigned integer backwards if the first value (popped) from the stack is true
|
||||||
|
InstructionLoop
|
||||||
|
|
||||||
|
// InstructionGetLocal Push a constant to the stack (2 bytes, second = constant index)
|
||||||
|
InstructionGetLocal
|
||||||
|
// InstructionSetLocal Set a local variable
|
||||||
|
InstructionSetLocal
|
||||||
|
// InstructionDeclareLocal Declare a new local variable in the uppermost scope
|
||||||
|
InstructionDeclareLocal
|
||||||
|
// InstructionGetGlobal Set a global variable (the next byte is the index of the constant with the name of the variable
|
||||||
|
InstructionGetGlobal
|
||||||
|
// InstructionSetGlobal Push a constant to the stack (2 bytes, second = constant index)
|
||||||
|
InstructionSetGlobal
|
||||||
|
|
||||||
|
// InstructionStringConversion Take the top value on the stack and convert it to a string
|
||||||
|
InstructionStringConversion
|
||||||
|
// InstructionStringConcatenation Add two strings together, with the second value on the stack as left and the top as right
|
||||||
|
InstructionStringConcatenation
|
||||||
|
|
||||||
|
// InstructionSwap swap the two top items on the stack (1, 2 -> 2, 1)
|
||||||
|
InstructionSwap
|
||||||
|
|
||||||
|
// InstructionConstant Push a constant to the stack (2 bytes, second = constant index)
|
||||||
|
InstructionConstant
|
||||||
|
// InstructionTrue Push a true literal to the stack
|
||||||
|
InstructionTrue
|
||||||
|
// InstructionFalse Push a false literal to the stack
|
||||||
|
InstructionFalse
|
||||||
|
// InstructionNil Push a nil literal to the stack
|
||||||
|
InstructionNil
|
||||||
|
|
||||||
|
// InstructionBreakpoint for debugging purposes
|
||||||
|
InstructionBreakpoint
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b Bytecode) String() string {
|
||||||
|
switch b {
|
||||||
|
case InstructionReturn:
|
||||||
|
return "RETURN"
|
||||||
|
case InstructionPop:
|
||||||
|
return "POP"
|
||||||
|
case InstructionAdd:
|
||||||
|
return "ADD"
|
||||||
|
case InstructionSub:
|
||||||
|
return "SUB"
|
||||||
|
case InstructionMul:
|
||||||
|
return "MUL"
|
||||||
|
case InstructionDiv:
|
||||||
|
return "DIV"
|
||||||
|
case InstructionEquals:
|
||||||
|
return "EQUALS"
|
||||||
|
case InstructionNotEqual:
|
||||||
|
return "NOT_EQUALS"
|
||||||
|
case InstructionNot:
|
||||||
|
return "NOT"
|
||||||
|
case InstructionLess:
|
||||||
|
return "LESS"
|
||||||
|
case InstructionLessOrEqual:
|
||||||
|
return "LESS_OR_EQUAL"
|
||||||
|
case InstructionGreater:
|
||||||
|
return "GREATER_OR_EQUAL"
|
||||||
|
case InstructionGreaterOrEqual:
|
||||||
|
return "GREATER_OR_EQUAL"
|
||||||
|
case InstructionJump:
|
||||||
|
return "JUMP"
|
||||||
|
case InstructionJumpFalse:
|
||||||
|
return "JUMP_FALSE"
|
||||||
|
case InstructionLoop:
|
||||||
|
return "LOOP"
|
||||||
|
case InstructionConstant:
|
||||||
|
return "CONSTANT"
|
||||||
|
case InstructionTrue:
|
||||||
|
return "TRUE"
|
||||||
|
case InstructionFalse:
|
||||||
|
return "FALSE"
|
||||||
|
case InstructionNil:
|
||||||
|
return "NIL"
|
||||||
|
case InstructionGetLocal:
|
||||||
|
return "GET_LOCAL"
|
||||||
|
case InstructionDeclareLocal:
|
||||||
|
return "DECLARE_LOCAL"
|
||||||
|
case InstructionSetLocal:
|
||||||
|
return "SET_LOCAL"
|
||||||
|
case InstructionGetGlobal:
|
||||||
|
return "GET_GLOBAL"
|
||||||
|
case InstructionSetGlobal:
|
||||||
|
return "SET_GLOBAL"
|
||||||
|
case InstructionCall:
|
||||||
|
return "CALL"
|
||||||
|
case InstructionDescend:
|
||||||
|
return "DESCEND"
|
||||||
|
case InstructionAscend:
|
||||||
|
return "ASCEND"
|
||||||
|
case InstructionStringConversion:
|
||||||
|
return "STRING_CONVERSION"
|
||||||
|
case InstructionStringConcatenation:
|
||||||
|
return "STRING_CONCATENATION"
|
||||||
|
case InstructionSwap:
|
||||||
|
return "SWAP"
|
||||||
|
case InstructionBreakpoint:
|
||||||
|
return "BREAKPOINT"
|
||||||
|
}
|
||||||
|
return "UNDEFINED"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Chunk struct {
|
||||||
|
Bytecode []Bytecode
|
||||||
|
Constants []Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chunk) String() string {
|
||||||
|
b := strings.Builder{}
|
||||||
|
|
||||||
|
b.WriteString("=v= chunk =v=\n")
|
||||||
|
for i, bc := range c.Bytecode {
|
||||||
|
b.WriteString(fmt.Sprintf("i=%d \t%d \t(%s)\n", i, bc, bc))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("=-= constants =-=\n")
|
||||||
|
|
||||||
|
for i, ct := range c.Constants {
|
||||||
|
b.WriteString(fmt.Sprintf("c=%d \t%s\n", i, ct))
|
||||||
|
|
||||||
|
f, ok := ct.(FunctionValue)
|
||||||
|
if ok {
|
||||||
|
b.WriteString(f.Chunk.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("=^= chunk =^=\n")
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChunk(bytecode []Bytecode, constants []Value) *Chunk {
|
||||||
|
return &Chunk{bytecode, constants}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterGOBTypes() {
|
||||||
|
gob.Register(StringValue(""))
|
||||||
|
gob.Register(NumberValue(0))
|
||||||
|
gob.Register(FunctionValue{
|
||||||
|
Name: "",
|
||||||
|
Params: nil,
|
||||||
|
Chunk: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chunk) Serialize() []byte {
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
|
||||||
|
e := gob.NewEncoder(&b)
|
||||||
|
|
||||||
|
err := e.Encode(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeserializeChunk(b []byte) *Chunk {
|
||||||
|
m := Chunk{}
|
||||||
|
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
buf.Write(b)
|
||||||
|
|
||||||
|
d := gob.NewDecoder(&buf)
|
||||||
|
|
||||||
|
err := d.Decode(&m)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &m
|
||||||
|
}
|
||||||
|
|
||||||
|
type VM struct {
|
||||||
|
// Replace with chunk of bytecode
|
||||||
|
chunk *Chunk
|
||||||
|
|
||||||
|
// instruction pointer
|
||||||
|
ip Pos
|
||||||
|
scope Pos
|
||||||
|
|
||||||
|
// global variable storage
|
||||||
|
globals map[string]Value
|
||||||
|
variableEnd Pos
|
||||||
|
|
||||||
|
stack *Stack[Value]
|
||||||
|
call *Stack[Call]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Call struct {
|
||||||
|
chunk *Chunk
|
||||||
|
ip Pos
|
||||||
|
stackEnd Pos
|
||||||
|
variableEnd Pos
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultGlobals = map[string]Value{
|
||||||
|
"write": BuiltinFunctionValue{
|
||||||
|
"write", // always remember where you come from...
|
||||||
|
[]string{"value"},
|
||||||
|
func(v map[string]Value) Value {
|
||||||
|
println(v["value"].String())
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVM(chunk *Chunk, stackSize Pos, callstackSize Pos) *VM {
|
||||||
|
vm := &VM{
|
||||||
|
chunk: chunk,
|
||||||
|
stack: NewStack[Value](stackSize),
|
||||||
|
call: NewStack[Call](callstackSize),
|
||||||
|
|
||||||
|
globals: DefaultGlobals,
|
||||||
|
}
|
||||||
|
|
||||||
|
return vm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next execute instruction
|
||||||
|
// returns true if more instructions should be executed
|
||||||
|
func (vm *VM) Next() bool {
|
||||||
|
switch vm.NextByte() {
|
||||||
|
case InstructionReturn:
|
||||||
|
if vm.call.Current <= 0 {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
v := vm.stack.Pop()
|
||||||
|
c := vm.call.Pop()
|
||||||
|
|
||||||
|
// reset stack current and variable end
|
||||||
|
vm.variableEnd = c.variableEnd
|
||||||
|
vm.stack.Current = c.stackEnd
|
||||||
|
|
||||||
|
// reset to calling position
|
||||||
|
vm.ip = c.ip
|
||||||
|
vm.chunk = c.chunk
|
||||||
|
|
||||||
|
vm.stack.Push(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
case InstructionPop:
|
||||||
|
vm.stack.Pop()
|
||||||
|
|
||||||
|
case InstructionConstant:
|
||||||
|
vm.stack.Push(vm.ReadConstant())
|
||||||
|
|
||||||
|
case InstructionAdd:
|
||||||
|
r := vm.stack.Pop().(NumberValue)
|
||||||
|
l := vm.stack.Pop().(NumberValue)
|
||||||
|
|
||||||
|
vm.stack.Push(l + r)
|
||||||
|
|
||||||
|
case InstructionSub:
|
||||||
|
r := vm.stack.Pop().(NumberValue)
|
||||||
|
l := vm.stack.Pop().(NumberValue)
|
||||||
|
|
||||||
|
vm.stack.Push(l - r)
|
||||||
|
|
||||||
|
case InstructionMul:
|
||||||
|
r := vm.stack.Pop().(NumberValue)
|
||||||
|
l := vm.stack.Pop().(NumberValue)
|
||||||
|
|
||||||
|
vm.stack.Push(l * r)
|
||||||
|
|
||||||
|
case InstructionDiv:
|
||||||
|
r := vm.stack.Pop().(NumberValue)
|
||||||
|
l := vm.stack.Pop().(NumberValue)
|
||||||
|
|
||||||
|
vm.stack.Push(l / r)
|
||||||
|
|
||||||
|
case InstructionEquals:
|
||||||
|
r := vm.stack.Pop().(NumberValue)
|
||||||
|
l := vm.stack.Pop().(NumberValue)
|
||||||
|
|
||||||
|
vm.stack.Push(BoolValue(l == r))
|
||||||
|
|
||||||
|
case InstructionNotEqual:
|
||||||
|
r := vm.stack.Pop().(NumberValue)
|
||||||
|
l := vm.stack.Pop().(NumberValue)
|
||||||
|
|
||||||
|
vm.stack.Push(BoolValue(l != r))
|
||||||
|
|
||||||
|
case InstructionNot:
|
||||||
|
b := vm.stack.Pop().(BoolValue)
|
||||||
|
vm.stack.Push(!b)
|
||||||
|
|
||||||
|
case InstructionLess:
|
||||||
|
r := vm.stack.Pop().(NumberValue)
|
||||||
|
l := vm.stack.Pop().(NumberValue)
|
||||||
|
|
||||||
|
vm.stack.Push(BoolValue(l < r))
|
||||||
|
|
||||||
|
case InstructionLessOrEqual:
|
||||||
|
r := vm.stack.Pop().(NumberValue)
|
||||||
|
l := vm.stack.Pop().(NumberValue)
|
||||||
|
|
||||||
|
vm.stack.Push(BoolValue(l <= r))
|
||||||
|
|
||||||
|
case InstructionGreater:
|
||||||
|
r := vm.stack.Pop().(NumberValue)
|
||||||
|
l := vm.stack.Pop().(NumberValue)
|
||||||
|
|
||||||
|
vm.stack.Push(BoolValue(l > r))
|
||||||
|
|
||||||
|
case InstructionGreaterOrEqual:
|
||||||
|
r := vm.stack.Pop().(NumberValue)
|
||||||
|
l := vm.stack.Pop().(NumberValue)
|
||||||
|
|
||||||
|
vm.stack.Push(BoolValue(l >= r))
|
||||||
|
|
||||||
|
case InstructionCall:
|
||||||
|
v := vm.stack.Pop()
|
||||||
|
switch f := v.(type) {
|
||||||
|
case FunctionValue:
|
||||||
|
vm.call.Push(Call{
|
||||||
|
chunk: vm.chunk,
|
||||||
|
ip: vm.ip,
|
||||||
|
stackEnd: vm.stack.Current - Pos(len(f.Params)),
|
||||||
|
variableEnd: vm.variableEnd,
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := len(f.Params) - 1; i >= 0; i-- {
|
||||||
|
p := vm.stack.Current - Pos(i) - 1
|
||||||
|
vm.stack.items[p] = &VariableValue{
|
||||||
|
f.Params[i],
|
||||||
|
vm.stack.items[p],
|
||||||
|
vm.scope,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.variableEnd = vm.stack.Current
|
||||||
|
|
||||||
|
vm.chunk = f.Chunk
|
||||||
|
vm.ip = 0
|
||||||
|
case BuiltinFunctionValue:
|
||||||
|
args := map[string]Value{}
|
||||||
|
|
||||||
|
for i := len(f.parameters) - 1; i >= 0; i-- {
|
||||||
|
args[f.parameters[i]] = vm.stack.Pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.stack.Push(f.f(args))
|
||||||
|
default:
|
||||||
|
vm.error(fmt.Sprintf("value called is not a function (%s)", v.String()))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case InstructionJump:
|
||||||
|
vm.ip += Pos(vm.NextU16())
|
||||||
|
|
||||||
|
case InstructionLoop:
|
||||||
|
vm.ip -= Pos(vm.NextU16())
|
||||||
|
|
||||||
|
case InstructionJumpFalse:
|
||||||
|
n := vm.NextU16()
|
||||||
|
if !vm.stack.Pop().(BoolValue) {
|
||||||
|
vm.ip += Pos(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
case InstructionGetLocal:
|
||||||
|
name := vm.GetConstant(vm.NextByte()).(StringValue)
|
||||||
|
v := vm.getVar(string(name))
|
||||||
|
|
||||||
|
if v == nil {
|
||||||
|
vm.error(fmt.Sprintf("cannot get local: undefined variable %s", name))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.stack.Push(v.value)
|
||||||
|
|
||||||
|
case InstructionSetLocal:
|
||||||
|
value := vm.stack.Pop().(Value)
|
||||||
|
name := vm.GetConstant(vm.NextByte()).(StringValue)
|
||||||
|
|
||||||
|
v := vm.getVar(string(name))
|
||||||
|
|
||||||
|
if v == nil {
|
||||||
|
vm.error(fmt.Sprintf("cannot set local: undefined variable %s", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
v.value = value
|
||||||
|
|
||||||
|
case InstructionDeclareLocal:
|
||||||
|
vm.addVar(
|
||||||
|
string(vm.GetConstant(vm.NextByte()).(StringValue)),
|
||||||
|
vm.stack.Pop().(Value),
|
||||||
|
)
|
||||||
|
|
||||||
|
case InstructionGetGlobal:
|
||||||
|
vm.stack.Push(vm.globals[string(vm.GetConstant(vm.NextByte()).(StringValue))])
|
||||||
|
|
||||||
|
case InstructionSetGlobal:
|
||||||
|
vm.globals[string(vm.GetConstant(vm.NextByte()).(StringValue))] = vm.stack.Pop()
|
||||||
|
|
||||||
|
case InstructionTrue:
|
||||||
|
vm.stack.Push(BoolValue(true))
|
||||||
|
|
||||||
|
case InstructionFalse:
|
||||||
|
vm.stack.Push(BoolValue(false))
|
||||||
|
|
||||||
|
case InstructionNil:
|
||||||
|
vm.stack.Push(NilValue{})
|
||||||
|
|
||||||
|
case InstructionDescend:
|
||||||
|
vm.descend()
|
||||||
|
|
||||||
|
case InstructionAscend:
|
||||||
|
vm.ascend()
|
||||||
|
|
||||||
|
case InstructionStringConversion:
|
||||||
|
v := vm.stack.Pop()
|
||||||
|
vm.stack.Push(StringValue(v.String()))
|
||||||
|
|
||||||
|
case InstructionStringConcatenation:
|
||||||
|
r := vm.stack.Pop().(StringValue)
|
||||||
|
l := vm.stack.Pop().(StringValue)
|
||||||
|
|
||||||
|
vm.stack.Push(l + r)
|
||||||
|
|
||||||
|
case InstructionSwap:
|
||||||
|
r := vm.stack.Pop()
|
||||||
|
l := vm.stack.Pop()
|
||||||
|
|
||||||
|
vm.stack.Push(r, l)
|
||||||
|
|
||||||
|
case InstructionBreakpoint:
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("invalid byte code")
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VM) TryNextByte() (Bytecode, error) {
|
||||||
|
if !vm.HasNext() {
|
||||||
|
return 0, errors.New("there are no more instructions")
|
||||||
|
}
|
||||||
|
|
||||||
|
v := vm.chunk.Bytecode[vm.ip]
|
||||||
|
vm.ip++
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VM) NextByte() Bytecode {
|
||||||
|
b, err := vm.TryNextByte()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VM) ascend() {
|
||||||
|
vm.scope--
|
||||||
|
|
||||||
|
if vm.scope < 0 {
|
||||||
|
panic("invalid scope")
|
||||||
|
}
|
||||||
|
|
||||||
|
for ; vm.variableEnd > 0 && vm.stack.items[vm.variableEnd-1].(*VariableValue).scope > vm.scope; vm.variableEnd-- {
|
||||||
|
vm.stack.Pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VM) descend() {
|
||||||
|
vm.scope++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VM) addVar(name string, value Value) {
|
||||||
|
vm.variableEnd++
|
||||||
|
vm.stack.Push(&VariableValue{
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
vm.scope,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VM) getVar(name string) *VariableValue {
|
||||||
|
for i := vm.variableEnd - 1; i >= 0; i-- {
|
||||||
|
v := vm.stack.items[i].(*VariableValue)
|
||||||
|
|
||||||
|
if v.name == name {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VM) HasNext() bool {
|
||||||
|
return vm.ip < Pos(len(vm.chunk.Bytecode))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VM) GetConstant(id Bytecode) Value {
|
||||||
|
return vm.chunk.Constants[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VM) ReadConstant() Value {
|
||||||
|
return vm.GetConstant(vm.NextByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VM) NextU16() uint16 {
|
||||||
|
return (uint16(vm.NextByte()) << 8) | uint16(vm.NextByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VM) error(error string) {
|
||||||
|
log.Fatal(error)
|
||||||
|
}
|
||||||
683
vm_test.go
Normal file
683
vm_test.go
Normal file
|
|
@ -0,0 +1,683 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CompareChunks(t *testing.T, got *Chunk, want *Chunk) {
|
||||||
|
if len(got.Constants) != len(want.Constants) {
|
||||||
|
t.Errorf("constant count does not match; got %v, expected %v", len(got.Constants), len(want.Constants))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range got.Constants {
|
||||||
|
if v != want.Constants[i] {
|
||||||
|
t.Errorf("constant %d does not match (%s and %s)", i, v.String(), want.Constants[i].String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(got.Bytecode) != len(want.Bytecode) {
|
||||||
|
t.Errorf("bytecode size does not match; got %v, expected %v", len(got.Bytecode), len(want.Bytecode))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("instruction \t\tchunk got \t\tchunk want")
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
|
||||||
|
for ; i < len(got.Bytecode); i++ {
|
||||||
|
v := got.Bytecode[i]
|
||||||
|
if i < len(want.Bytecode) {
|
||||||
|
if v != want.Bytecode[i] {
|
||||||
|
t.Errorf("i=%d mismatch \t%d (%s) \t\t%d (%s)", i, v, v.String(), want.Bytecode[i], want.Bytecode[i].String())
|
||||||
|
} else {
|
||||||
|
t.Logf("i=%d match \t%d (%s) \t\t%d (%s)", i, v, v.String(), want.Bytecode[i], want.Bytecode[i].String())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("i=%d mismatch \t%d (%s) \t\t- (None)", i, v, v.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if want bytecode is greater than got bytecode
|
||||||
|
for ; i < len(want.Bytecode); i++ {
|
||||||
|
v := want.Bytecode[i]
|
||||||
|
t.Errorf("i=%d mismatch \t- (None) \t\t%d (%s)", i, v, v.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewVM(t *testing.T) {
|
||||||
|
// constants
|
||||||
|
chunk := NewChunk([]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
}, []Value{
|
||||||
|
NumberValue(0),
|
||||||
|
})
|
||||||
|
stackSize := Pos(256)
|
||||||
|
callstackSize := Pos(256)
|
||||||
|
|
||||||
|
vm := NewVM(chunk, stackSize, callstackSize)
|
||||||
|
|
||||||
|
// should start at first instruction
|
||||||
|
if vm.ip != 0 {
|
||||||
|
t.Errorf("vm.ip = %d, want 0", vm.ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// should have given instructions
|
||||||
|
for i, v := range chunk.Bytecode {
|
||||||
|
if v != vm.chunk.Bytecode[i] {
|
||||||
|
t.Errorf("vm.Bytecode[%d] = %d, want %d", i, vm.chunk.Bytecode[i], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// should have given constants
|
||||||
|
for i, v := range chunk.Constants {
|
||||||
|
if v != vm.chunk.Constants[i] {
|
||||||
|
t.Errorf("vm.Constants[%d] = %d, want %d", i, vm.chunk.Constants[i], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// should have given stack size
|
||||||
|
if vm.stack.Size != stackSize {
|
||||||
|
t.Errorf("vm.stack.Size = %d, want %d", vm.stack.Size, stackSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// should have given call stack size
|
||||||
|
if vm.call.Size != callstackSize {
|
||||||
|
t.Errorf("vm.call.Size = %d, want %d", vm.call.Size, callstackSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNewVM(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = NewVM(nil, 256, 256)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetExecutionTestData() map[string]struct {
|
||||||
|
chunk *Chunk
|
||||||
|
resultingStack []Value
|
||||||
|
} {
|
||||||
|
return map[string]struct {
|
||||||
|
chunk *Chunk
|
||||||
|
resultingStack []Value
|
||||||
|
}{
|
||||||
|
"two_plus_one": {
|
||||||
|
NewChunk([]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionConstant, 1,
|
||||||
|
InstructionAdd,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(1), NumberValue(2),
|
||||||
|
}),
|
||||||
|
[]Value{
|
||||||
|
NumberValue(3),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"push_constant": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(1),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
NumberValue(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"push_true": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionTrue,
|
||||||
|
},
|
||||||
|
[]Value{},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
BoolValue(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"push_false": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionFalse,
|
||||||
|
},
|
||||||
|
[]Value{},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
BoolValue(false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"push_nil": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionNil,
|
||||||
|
},
|
||||||
|
[]Value{},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
NilValue{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{},
|
||||||
|
[]Value{},
|
||||||
|
),
|
||||||
|
[]Value{},
|
||||||
|
},
|
||||||
|
// (2 + 1) * 5 / (6 - 2)
|
||||||
|
"full_arithmetic": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionConstant, 1,
|
||||||
|
InstructionAdd,
|
||||||
|
InstructionConstant, 2,
|
||||||
|
InstructionMul,
|
||||||
|
InstructionConstant, 3,
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionSub,
|
||||||
|
InstructionDiv,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(2), NumberValue(1), NumberValue(5), NumberValue(6),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
NumberValue((2.0 + 1.0) * 5.0 / (6.0 - 2.0)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"equality_true": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionEquals,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(1),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
BoolValue(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"equality_false": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionConstant, 1,
|
||||||
|
InstructionEquals,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(1), NumberValue(2),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
BoolValue(false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"inequality_false": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionNotEqual,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(1),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
BoolValue(false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"inequality_true": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionConstant, 1,
|
||||||
|
InstructionNotEqual,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(1), NumberValue(2),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
BoolValue(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"not_true": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionTrue,
|
||||||
|
InstructionNot,
|
||||||
|
},
|
||||||
|
[]Value{},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
BoolValue(false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"not_false": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionFalse,
|
||||||
|
InstructionNot,
|
||||||
|
},
|
||||||
|
[]Value{},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
BoolValue(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"jump": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionJump, 0, 2,
|
||||||
|
InstructionConstant, 0, // should not execute
|
||||||
|
InstructionConstant, 1, // should execute
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(0), NumberValue(1),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
NumberValue(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"jump_false/false": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionFalse,
|
||||||
|
InstructionJumpFalse, 0, 2,
|
||||||
|
InstructionConstant, 0, // should not execute
|
||||||
|
InstructionConstant, 1, // should execute
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(0), NumberValue(1),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
NumberValue(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"jump_false/true": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionTrue,
|
||||||
|
InstructionJumpFalse, 0, 2,
|
||||||
|
InstructionConstant, 0, // should execute
|
||||||
|
InstructionConstant, 1, // should execute
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(0), NumberValue(1),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
NumberValue(0), NumberValue(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"declare_local": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionDeclareLocal, 1,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(0), StringValue("a"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
&VariableValue{
|
||||||
|
"a",
|
||||||
|
NumberValue(0),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"assign_local": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionDeclareLocal, 1,
|
||||||
|
InstructionConstant, 2,
|
||||||
|
InstructionSetLocal, 1, // reassign
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(0), StringValue("a"), NumberValue(1),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
&VariableValue{
|
||||||
|
"a",
|
||||||
|
NumberValue(1),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"get_local": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionDeclareLocal, 1,
|
||||||
|
InstructionGetLocal, 1, // reassign
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(0), StringValue("a"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
&VariableValue{
|
||||||
|
"a",
|
||||||
|
NumberValue(0),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
NumberValue(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"get_reassigned_local": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionDeclareLocal, 1,
|
||||||
|
InstructionGetLocal, 1,
|
||||||
|
InstructionConstant, 2,
|
||||||
|
InstructionSetLocal, 1, // reassign
|
||||||
|
InstructionGetLocal, 1,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(0), StringValue("a"), NumberValue(1),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
&VariableValue{
|
||||||
|
"a",
|
||||||
|
NumberValue(1),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
NumberValue(0),
|
||||||
|
NumberValue(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"variable_scope": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionDeclareLocal, 1,
|
||||||
|
InstructionDescend,
|
||||||
|
InstructionConstant, 2,
|
||||||
|
InstructionDeclareLocal, 3,
|
||||||
|
InstructionDescend,
|
||||||
|
InstructionConstant, 4,
|
||||||
|
InstructionDeclareLocal, 5,
|
||||||
|
InstructionAscend,
|
||||||
|
InstructionAscend,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(0), StringValue("a"),
|
||||||
|
NumberValue(1), StringValue("b"),
|
||||||
|
NumberValue(2), StringValue("c"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
&VariableValue{
|
||||||
|
"a",
|
||||||
|
NumberValue(0),
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"function_call": {
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionConstant, 1,
|
||||||
|
InstructionConstant, 2,
|
||||||
|
InstructionBreakpoint,
|
||||||
|
InstructionCall,
|
||||||
|
InstructionBreakpoint,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(1),
|
||||||
|
NumberValue(2),
|
||||||
|
FunctionValue{
|
||||||
|
Name: "sum",
|
||||||
|
Params: []string{"a", "b"},
|
||||||
|
Chunk: NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionGetLocal, 0,
|
||||||
|
InstructionGetLocal, 1,
|
||||||
|
InstructionAdd,
|
||||||
|
InstructionBreakpoint,
|
||||||
|
InstructionReturn,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
StringValue("a"), StringValue("b"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]Value{
|
||||||
|
NumberValue(3),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVM_Execution(t *testing.T) {
|
||||||
|
data := GetExecutionTestData()
|
||||||
|
|
||||||
|
for name, test := range data {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
vm := NewVM(test.chunk, 256, 256)
|
||||||
|
|
||||||
|
for vm.HasNext() && vm.Next() {
|
||||||
|
}
|
||||||
|
|
||||||
|
CompareStacks(t, test.resultingStack, vm.stack)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkVM_Execution(b *testing.B) {
|
||||||
|
data := GetExecutionTestData()
|
||||||
|
|
||||||
|
for name, test := range data {
|
||||||
|
b.Run(name, func(b *testing.B) {
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
vm := NewVM(test.chunk, 256, 256)
|
||||||
|
for vm.HasNext() && vm.Next() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVM_NextByte(t *testing.T) {
|
||||||
|
vm := NewVM(
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionConstant, 0,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(0),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
)
|
||||||
|
|
||||||
|
b, err := vm.TryNextByte()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b != InstructionConstant {
|
||||||
|
t.Errorf("got %v; want %v", b, InstructionConstant)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err = vm.TryNextByte()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b != 0 {
|
||||||
|
t.Errorf("got %v; want %v", b, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err = vm.TryNextByte()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("didn't get expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if b != 0 {
|
||||||
|
t.Errorf("got %v; want %v", b, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVM_NextU16_Empty(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Fatalf("didn't panic when not enough bytes")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
vm := NewVM(
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{},
|
||||||
|
[]Value{},
|
||||||
|
),
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
)
|
||||||
|
vm.NextU16()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVM_NextU16_One(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Fatalf("didn't panic when not enough bytes")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
vm := NewVM(
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
[]Value{},
|
||||||
|
),
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
)
|
||||||
|
|
||||||
|
vm.NextU16()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVM_NextU16(t *testing.T) {
|
||||||
|
for i := 0; i <= 0xFFFF; i++ {
|
||||||
|
t.Run(fmt.Sprintf("value-%d", i), func(t *testing.T) {
|
||||||
|
vm := NewVM(
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
Bytecode((i >> 8) & 0xFF),
|
||||||
|
Bytecode(i & 0xFF),
|
||||||
|
},
|
||||||
|
[]Value{},
|
||||||
|
),
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
)
|
||||||
|
|
||||||
|
b := vm.NextU16()
|
||||||
|
|
||||||
|
if uint16(i) != b {
|
||||||
|
t.Errorf("got %v; want %v", b, i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVM_Jump(t *testing.T) {
|
||||||
|
vm := NewVM(
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionJump, 0, 2,
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionConstant, 1,
|
||||||
|
InstructionConstant, 2,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(0), NumberValue(1), NumberValue(2),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
)
|
||||||
|
|
||||||
|
vm.Next()
|
||||||
|
|
||||||
|
if vm.ip != 5 {
|
||||||
|
t.Errorf("jumped got %v; want %v", vm.ip-3, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVM_JumpFalse(t *testing.T) {
|
||||||
|
vm := NewVM(
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionFalse,
|
||||||
|
InstructionJumpFalse, 0, 2,
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionConstant, 1,
|
||||||
|
InstructionConstant, 2,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(0), NumberValue(1), NumberValue(2),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
)
|
||||||
|
|
||||||
|
vm.Next()
|
||||||
|
vm.Next()
|
||||||
|
|
||||||
|
if vm.ip != 6 {
|
||||||
|
t.Errorf("jumped got %v; want %v", vm.ip-4, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVM_DontJumpFalse(t *testing.T) {
|
||||||
|
vm := NewVM(
|
||||||
|
NewChunk(
|
||||||
|
[]Bytecode{
|
||||||
|
InstructionTrue,
|
||||||
|
InstructionJumpFalse, 0, 2,
|
||||||
|
InstructionConstant, 0,
|
||||||
|
InstructionConstant, 1,
|
||||||
|
InstructionConstant, 2,
|
||||||
|
},
|
||||||
|
[]Value{
|
||||||
|
NumberValue(0), NumberValue(1), NumberValue(2),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
)
|
||||||
|
|
||||||
|
vm.Next()
|
||||||
|
vm.Next()
|
||||||
|
|
||||||
|
if vm.ip != 4 {
|
||||||
|
t.Errorf("ip is %v; want %v", vm.ip, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVM_GetGlobal(t *testing.T) {}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue