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