separate core from cli into submodules, add support for boolean and and or operations, make more fields public, fix passing arguments to functions

This commit is contained in:
Neemek 2024-12-13 11:33:47 +01:00
parent 7498c85424
commit 62656c6dff
Signed by: neemek
GPG key ID: 28360A8951CD0E9B
19 changed files with 202 additions and 99 deletions

96
core/all_test.go Normal file
View file

@ -0,0 +1,96 @@
package core
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 {
print(e.Format(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() {
}
})
}
}

330
core/compiler.go Normal file
View file

@ -0,0 +1,330 @@
package core
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)
for _, arg := range n.args {
c.Compile(arg)
}
c.getVar(n.name)
c.add(InstructionCall)
if !n.keep {
c.add(InstructionPop)
}
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
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,
}
// restore old chunk and ip
c.Chunk = mc
c.ip = mip
case ReturnNodeType:
c.Compile(tree.(*ReturnNode).value)
c.add(InstructionReturn)
case BreakpointNodeType:
c.add(InstructionBreakpoint)
}
}
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)
case BinaryAnd:
c.add(InstructionAnd)
case BinaryOr:
c.add(InstructionOr)
}
}
func (c *Compiler) getVar(name string) {
if c.isGlobal(name) {
c.add(InstructionGetGlobal)
c.addConstant(StringValue(name))
} else {
c.add(InstructionGetLocal)
c.addConstant(StringValue(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
}

449
core/compiler_test.go Normal file
View file

@ -0,0 +1,449 @@
package core
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": {&BlockNode{
[]Node{
&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,
},
},
},
"remove_func_vars": {
&BlockNode{
[]Node{
&AssignNode{
"a",
&FunctionNode{
"a",
[]string{},
&BlockNode{
[]Node{
&AssignNode{
"b",
&NumberNode{1},
true,
},
&ReturnNode{
&ReferenceNode{"b"},
},
},
},
},
true,
},
&CallNode{
"a",
[]Node{},
false,
},
},
},
[]Value{
&VariableValue{
"a",
FunctionValue{
"a",
[]string{},
NewChunk(
[]Bytecode{
InstructionDescend,
InstructionConstant, 0,
InstructionDeclareLocal, 1,
InstructionGetLocal, 1,
InstructionReturn,
InstructionAscend,
},
[]Value{
NumberValue(1), 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)
}
}
})
}
}

3
core/go.mod Normal file
View file

@ -0,0 +1,3 @@
module neemek.com/anglais/core
go 1.23.0

8
core/go.sum Normal file
View 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=

408
core/lexer.go Normal file
View file

@ -0,0 +1,408 @@
package core
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
TokenDoubleAmpersand
TokenDoublePipe
TokenBreakpoint
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"
case TokenBreakpoint:
return "breakpoint"
case TokenDoubleAmpersand:
return "double ampersand"
case TokenDoublePipe:
return "double pipe"
}
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 '/':
if l.accept('*') {
for !l.isAtEnd() {
if l.accept('*') && l.accept('/') {
break
}
l.advance()
}
return l.NextToken()
}
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 '&':
if l.accept('&') {
return l.makeToken(TokenDoubleAmpersand), nil
}
return l.makeToken(TokenError), errors.New("malformed token (got '&', expected '&' to follow)")
case '|':
if l.accept('|') {
return l.makeToken(TokenDoublePipe), nil
}
return l.makeToken(TokenError), errors.New("malformed token (got '|', expected '|' to follow)")
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 "breakpoint":
return l.makeToken(TokenBreakpoint), 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()
}
// if the number has a float-part
if l.accept('.') {
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
core/lexer_test.go Normal file
View file

@ -0,0 +1,224 @@
package core
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()
}
}
})
}
}

294
core/nodes.go Normal file
View file

@ -0,0 +1,294 @@
package core
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
BinaryAnd
BinaryOr
// 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)
}

456
core/parser.go Normal file
View file

@ -0,0 +1,456 @@
package core
import (
"errors"
"fmt"
"log"
"strconv"
"strings"
)
type ParsingError struct {
Description string
Causer *Token
}
// Print a rich and informative error
func (p *ParsingError) Format(src string) string {
builder := strings.Builder{}
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
}
}
builder.WriteString(" \t v ")
builder.WriteString(p.Description)
builder.WriteRune('\n')
builder.WriteString(fmt.Sprintf(" %d:%d\t | %s", lineNumber, int(p.Causer.Start)-lineBeginning+1, src[lineBeginning:lineEnd]))
builder.WriteString("\t ^")
for i := lineBeginning; i <= int(p.Causer.Start); i++ {
builder.WriteRune(' ')
}
for i := 0; i < int(p.Causer.Length); i++ {
builder.WriteRune('^')
}
builder.WriteRune('\n')
return builder.String()
}
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()
}
}
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) comparison() 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) condition() Node {
left := p.comparison()
op := BinaryEquality
switch (*p.curr).Type {
case TokenDoubleAmpersand:
op = BinaryAnd
case TokenDoublePipe:
op = BinaryOr
default:
return left
}
p.advance()
return &BinaryNode{
op,
left,
p.comparison(),
}
}
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 TokenWhile:
p.advance()
return &LoopNode{
p.condition(),
p.block(false),
}
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
core/parser_test.go Normal file
View file

@ -0,0 +1,629 @@
package core
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
core/stack.go Normal file
View file

@ -0,0 +1,55 @@
package core
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")
}
}

129
core/stack_test.go Normal file
View file

@ -0,0 +1,129 @@
package core
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 {
t.Logf("Comparing value at index %d", i)
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
core/values.go Normal file
View file

@ -0,0 +1,162 @@
package core
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 float64
const NumberSize int = 64
func (v NumberValue) Type() ValueType {
return NumberValueType
}
func (v NumberValue) String() string {
return strconv.FormatFloat(float64(v), 'g', -1, NumberSize)
}
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
}

93
core/values_test.go Normal file
View file

@ -0,0 +1,93 @@
package core
import "testing"
func CompareValues(t *testing.T, got Value, want Value) {
if got == nil || want == nil {
t.Fatalf("a value is nil: got %v; want %v", got, want)
}
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")
}
}

603
core/vm.go Normal file
View file

@ -0,0 +1,603 @@
package core
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
// InstructionAnd pop two booleans and push true if both are true
InstructionAnd
// InstructionOr pop two booleans and push true if either are true
InstructionOr
// 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 InstructionAnd:
return "AND"
case InstructionOr:
return "OR"
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
},
},
"print": BuiltinFunctionValue{
"print",
[]string{"value"},
func(v map[string]Value) Value {
print(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 InstructionAnd:
vm.stack.Push(vm.stack.Pop().(BoolValue) && vm.stack.Pop().(BoolValue))
case InstructionOr:
vm.stack.Push(vm.stack.Pop().(BoolValue) || vm.stack.Pop().(BoolValue))
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(len(f.Params)) + Pos(i)
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)
}
func (vm *VM) SetGlobal(name string, value Value) {
vm.globals[name] = value
}
func (vm *VM) GetGlobal(name string) Value {
return vm.globals[name]
}

751
core/vm_test.go Normal file
View file

@ -0,0 +1,751 @@
package core
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,
InstructionCall,
},
[]Value{
NumberValue(1),
NumberValue(2),
FunctionValue{
Name: "sum",
Params: []string{"a", "b"},
Chunk: NewChunk(
[]Bytecode{
InstructionGetLocal, 0,
InstructionGetLocal, 1,
InstructionAdd,
InstructionReturn,
},
[]Value{
StringValue("a"), StringValue("b"),
},
),
},
},
),
[]Value{
NumberValue(3),
},
},
"function_calling_function": {
NewChunk(
[]Bytecode{
InstructionConstant, 3,
InstructionDeclareLocal, 4,
InstructionConstant, 0,
InstructionConstant, 1,
InstructionConstant, 2,
InstructionCall,
},
[]Value{
NumberValue(1),
NumberValue(2),
FunctionValue{
Name: "sum",
Params: []string{"a", "b"},
Chunk: NewChunk(
[]Bytecode{
InstructionGetLocal, 0,
InstructionGetLocal, 2, InstructionCall, // square the number
InstructionGetLocal, 1,
InstructionGetLocal, 2, InstructionCall, // square the number
InstructionAdd,
InstructionReturn,
},
[]Value{
StringValue("a"), StringValue("b"), StringValue("square"),
},
),
},
FunctionValue{
Name: "square",
Params: []string{"n"},
Chunk: NewChunk(
[]Bytecode{
InstructionGetLocal, 0,
InstructionGetLocal, 0,
InstructionMul,
InstructionReturn,
},
[]Value{
StringValue("n"),
},
),
},
StringValue("square"),
},
),
[]Value{
&VariableValue{
"square",
FunctionValue{
Name: "square",
Params: []string{"n"},
Chunk: NewChunk(
[]Bytecode{
InstructionGetLocal, 0,
InstructionGetLocal, 0,
InstructionMul,
InstructionReturn,
},
[]Value{
StringValue("n"),
},
),
},
0,
},
NumberValue(5),
},
},
}
}
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) {}