rewrite, implement types

This commit is contained in:
Sky Johnson 2025-05-07 08:18:38 -05:00
parent 29707b4b02
commit f1ac9b1657
15 changed files with 525 additions and 2930 deletions

View File

@ -1,3 +1,44 @@
# Mako
Scripting language!
Lightweight, efficient Lua-like scripting language with a Go backend!
the go mod is `git.sharkk.net/Sharkk/Mako`
### Lexing & Parsing
Recursive descent + Pratt parsing.
Scanner should move through tokens with virtually no allocations.
Easy-to-consume AST for bytecode compilation.
### VM
Stack-based VM, register-based optimizations where it makes sense for performance, and a very minimal bytecode
format to preserve space.
### Memory Management
Go's GC is robust and can be used for most needs; the internals can pool commonly used structures to reduce pressure.
## Syntax
Lua-like, but with more minimalism and eventually some syntactic sugars.
```
// C style comments
// No local for variable definitions; the local behavior should be implicit
x = "foo"
y = "bar"
fn add(a, b)
return a + b
end
echo add(x, y) // Outputs: foobar
if expression and expression2 then
res = add(1, 2)
elseif otherExpression() then
// do other stuff
else
// final thing
end
```

View File

@ -1,565 +0,0 @@
package compiler
import (
"fmt"
"git.sharkk.net/Sharkk/Mako/parser"
"git.sharkk.net/Sharkk/Mako/types"
)
// Compile converts AST to bytecode
func Compile(program *parser.Program) *types.Bytecode {
c := &compiler{
constants: []any{},
instructions: []types.Instruction{},
scopes: []scope{},
currentFunction: nil,
}
// Start in global scope
c.enterScope()
// Add nil check for program
if program == nil {
c.exitScope()
return &types.Bytecode{
Constants: c.constants,
Instructions: c.instructions,
}
}
// Process each statement safely
for _, stmt := range program.Statements {
// Skip nil statements
if stmt == nil {
continue
}
c.compileStatement(stmt)
}
c.exitScope()
return &types.Bytecode{
Constants: c.constants,
Instructions: c.instructions,
}
}
type scope struct {
variables map[string]bool
upvalues map[string]int
}
type compiler struct {
constants []any
instructions []types.Instruction
scopes []scope
currentFunction *functionCompiler
}
type functionCompiler struct {
constants []any
instructions []types.Instruction
numParams int
upvalues []upvalueInfo
}
type upvalueInfo struct {
index int // Index in the upvalue list
isLocal bool // Whether this is a local variable or an upvalue from an outer scope
capturedFrom int // The scope level where this variable was captured from
}
func (c *compiler) enterScope() {
c.scopes = append(c.scopes, scope{
variables: make(map[string]bool),
upvalues: make(map[string]int),
})
c.emit(types.OpEnterScope, 0)
}
func (c *compiler) exitScope() {
c.scopes = c.scopes[:len(c.scopes)-1]
c.emit(types.OpExitScope, 0)
}
func (c *compiler) declareVariable(name string) {
if len(c.scopes) > 0 {
c.scopes[len(c.scopes)-1].variables[name] = true
}
}
func (c *compiler) isLocalVariable(name string) bool {
for i := len(c.scopes) - 1; i >= 0; i-- {
if _, ok := c.scopes[i].variables[name]; ok {
return true
}
}
return false
}
func (c *compiler) compileStatement(stmt parser.Statement) {
if stmt == nil {
return
}
switch s := stmt.(type) {
case *parser.VariableStatement:
c.compileExpression(s.Value)
nameIndex := c.addConstant(s.Name.Value)
// Use SetGlobal for top-level variables to persist between REPL lines
if len(c.scopes) <= 1 {
c.emit(types.OpSetGlobal, nameIndex)
} else {
c.declareVariable(s.Name.Value)
c.emit(types.OpSetLocal, nameIndex)
}
case *parser.IndexAssignmentStatement:
c.compileExpression(s.Left)
c.compileExpression(s.Index)
c.compileExpression(s.Value)
c.emit(types.OpSetIndex, 0)
case *parser.EchoStatement:
c.compileExpression(s.Value)
c.emit(types.OpEcho, 0)
case *parser.ReturnStatement:
if s.Value != nil {
c.compileExpression(s.Value)
} else {
nullIndex := c.addConstant(nil)
c.emit(types.OpConstant, nullIndex)
}
c.emit(types.OpReturn, 0)
case *parser.FunctionStatement:
// Use the dedicated function for function statements
c.compileFunctionDeclaration(s)
// BlockStatement now should only be used for keyword blocks like if-then-else-end
case *parser.BlockStatement:
for _, blockStmt := range s.Statements {
c.compileStatement(blockStmt)
}
case *parser.ExpressionStatement:
c.compileExpression(s.Expression)
// Pop the value since we're not using it
c.emit(types.OpPop, 0)
}
}
func (c *compiler) compileExpression(expr parser.Expression) {
switch e := expr.(type) {
case *parser.StringLiteral:
constIndex := c.addConstant(e.Value)
c.emit(types.OpConstant, constIndex)
case *parser.NumberLiteral:
constIndex := c.addConstant(e.Value)
c.emit(types.OpConstant, constIndex)
case *parser.BooleanLiteral:
constIndex := c.addConstant(e.Value)
c.emit(types.OpConstant, constIndex)
case *parser.NilLiteral:
constIndex := c.addConstant(nil)
c.emit(types.OpConstant, constIndex)
case *parser.Identifier:
nameIndex := c.addConstant(e.Value)
// Check if it's a local variable first
if c.isLocalVariable(e.Value) {
c.emit(types.OpGetLocal, nameIndex)
} else {
// Otherwise treat as global
c.emit(types.OpGetGlobal, nameIndex)
}
case *parser.TableLiteral:
c.emit(types.OpNewTable, 0)
for key, value := range e.Pairs {
c.emit(types.OpDup, 0)
// Special handling for identifier keys in tables
if ident, ok := key.(*parser.Identifier); ok {
// Treat identifiers as string literals in table keys
strIndex := c.addConstant(ident.Value)
c.emit(types.OpConstant, strIndex)
} else {
// For other expressions, compile normally
c.compileExpression(key)
}
c.compileExpression(value)
c.emit(types.OpSetIndex, 0)
c.emit(types.OpPop, 0)
}
case *parser.IndexExpression:
c.compileExpression(e.Left)
c.compileExpression(e.Index)
c.emit(types.OpGetIndex, 0)
case *parser.FunctionLiteral:
c.compileFunctionLiteral(e)
case *parser.CallExpression:
// Compile the function expression first
c.compileExpression(e.Function)
// Then compile the arguments
for _, arg := range e.Arguments {
c.compileExpression(arg)
}
// Emit the call instruction with the number of arguments
c.emit(types.OpCall, len(e.Arguments))
// Arithmetic expressions
case *parser.InfixExpression:
switch e.Operator {
case "and":
// Compile left operand
c.compileExpression(e.Left)
// Duplicate to check condition
c.emit(types.OpDup, 0)
// Jump if false (short-circuit)
jumpFalsePos := len(c.instructions)
c.emit(types.OpJumpIfFalse, 0) // Will backpatch
// Pop the duplicate since we'll replace it
c.emit(types.OpPop, 0)
// Compile right operand
c.compileExpression(e.Right)
// Jump target for short-circuit
endPos := len(c.instructions)
c.instructions[jumpFalsePos].Operand = endPos
case "or":
// Compile left operand
c.compileExpression(e.Left)
// Duplicate to check condition
c.emit(types.OpDup, 0)
// Need to check if it's truthy to short-circuit
falseJumpPos := len(c.instructions)
c.emit(types.OpJumpIfFalse, 0) // Jump to right eval if false
// If truthy, jump to end
trueJumpPos := len(c.instructions)
c.emit(types.OpJump, 0) // Jump to end if true
// Position for false case
falsePos := len(c.instructions)
c.instructions[falseJumpPos].Operand = falsePos
// Pop the duplicate since we'll replace it
c.emit(types.OpPop, 0)
// Compile right operand
c.compileExpression(e.Right)
// End position
endPos := len(c.instructions)
c.instructions[trueJumpPos].Operand = endPos
default:
// Original infix expression compilation
c.compileExpression(e.Left)
c.compileExpression(e.Right)
// Generate the appropriate operation
switch e.Operator {
case "+":
c.emit(types.OpAdd, 0)
case "-":
c.emit(types.OpSubtract, 0)
case "*":
c.emit(types.OpMultiply, 0)
case "/":
c.emit(types.OpDivide, 0)
case "==":
c.emit(types.OpEqual, 0)
case "!=":
c.emit(types.OpNotEqual, 0)
case "<":
c.emit(types.OpLessThan, 0)
case ">":
c.emit(types.OpGreaterThan, 0)
case "<=":
c.emit(types.OpLessEqual, 0)
case ">=":
c.emit(types.OpGreaterEqual, 0)
default:
panic(fmt.Sprintf("Unknown infix operator: %s", e.Operator))
}
}
case *parser.PrefixExpression:
// Compile the operand
c.compileExpression(e.Right)
// Generate the appropriate operation
switch e.Operator {
case "-":
c.emit(types.OpNegate, 0)
case "not":
c.emit(types.OpNot, 0)
default:
panic(fmt.Sprintf("Unknown prefix operator: %s", e.Operator))
}
case *parser.GroupedExpression:
// Just compile the inner expression
c.compileExpression(e.Expr)
case *parser.IfExpression:
// Compile condition
c.compileExpression(e.Condition)
// Emit jump-if-false with placeholder
jumpNotTruePos := len(c.instructions)
c.emit(types.OpJumpIfFalse, 0) // Will backpatch
// Compile consequence (then block)
if e.Consequence != nil {
lastStmtIndex := len(e.Consequence.Statements) - 1
for i, stmt := range e.Consequence.Statements {
if i == lastStmtIndex {
// For the last statement, we need to ensure it leaves a value
if exprStmt, ok := stmt.(*parser.ExpressionStatement); ok {
c.compileExpression(exprStmt.Expression)
} else {
c.compileStatement(stmt)
// Push null if not an expression statement
nullIndex := c.addConstant(nil)
c.emit(types.OpConstant, nullIndex)
}
} else {
c.compileStatement(stmt)
}
}
// If no statements, push null
if len(e.Consequence.Statements) == 0 {
nullIndex := c.addConstant(nil)
c.emit(types.OpConstant, nullIndex)
}
} else {
// No consequence block, push null
nullIndex := c.addConstant(nil)
c.emit(types.OpConstant, nullIndex)
}
// Emit jump to skip else part
jumpPos := len(c.instructions)
c.emit(types.OpJump, 0) // Will backpatch
// Backpatch jump-if-false to point to else
afterConsequencePos := len(c.instructions)
c.instructions[jumpNotTruePos].Operand = afterConsequencePos
// Compile alternative (else block)
if e.Alternative != nil {
lastStmtIndex := len(e.Alternative.Statements) - 1
for i, stmt := range e.Alternative.Statements {
if i == lastStmtIndex {
// For the last statement, we need to ensure it leaves a value
if exprStmt, ok := stmt.(*parser.ExpressionStatement); ok {
c.compileExpression(exprStmt.Expression)
} else {
c.compileStatement(stmt)
// Push null if not an expression statement
nullIndex := c.addConstant(nil)
c.emit(types.OpConstant, nullIndex)
}
} else {
c.compileStatement(stmt)
}
}
// If no statements, push null
if len(e.Alternative.Statements) == 0 {
nullIndex := c.addConstant(nil)
c.emit(types.OpConstant, nullIndex)
}
} else {
// No else - push null
nullIndex := c.addConstant(nil)
c.emit(types.OpConstant, nullIndex)
}
// Backpatch jump to point after else
afterAlternativePos := len(c.instructions)
c.instructions[jumpPos].Operand = afterAlternativePos
}
}
func (c *compiler) compileFunctionLiteral(fn *parser.FunctionLiteral) {
// Save the current compiler state
parentCompiler := c.currentFunction
// Create a new function compiler
fnCompiler := &functionCompiler{
constants: []any{},
instructions: []types.Instruction{},
numParams: len(fn.Parameters),
upvalues: []upvalueInfo{},
}
c.currentFunction = fnCompiler
// Enter a new scope for the function body
c.enterScope()
// Declare parameters as local variables
for _, param := range fn.Parameters {
c.declareVariable(param.Value)
paramIndex := c.addConstant(param.Value)
c.emit(types.OpSetLocal, paramIndex)
}
// Compile the function body
for _, stmt := range fn.Body.Statements {
c.compileStatement(stmt)
}
// Ensure the function always returns a value
// If the last instruction is not a return, add one
if len(fnCompiler.instructions) == 0 ||
(len(fnCompiler.instructions) > 0 && fnCompiler.instructions[len(fnCompiler.instructions)-1].Opcode != types.OpReturn) {
nullIndex := c.addConstant(nil)
c.emit(types.OpConstant, nullIndex)
c.emit(types.OpReturn, 0)
}
// Exit the function scope
c.exitScope()
// Restore the parent compiler
c.currentFunction = parentCompiler
// Extract upvalue information for closure creation
upvalueIndexes := make([]int, len(fnCompiler.upvalues))
for i, upvalue := range fnCompiler.upvalues {
upvalueIndexes[i] = upvalue.index
}
// Create a Function object and add it to the constants
function := types.NewFunction(
fnCompiler.instructions,
fnCompiler.numParams,
fnCompiler.constants,
upvalueIndexes,
)
functionIndex := c.addConstant(function)
c.emit(types.OpFunction, functionIndex)
}
func (c *compiler) addConstant(value any) int {
if c.currentFunction != nil {
c.currentFunction.constants = append(c.currentFunction.constants, value)
return len(c.currentFunction.constants) - 1
}
c.constants = append(c.constants, value)
return len(c.constants) - 1
}
func (c *compiler) emit(op types.Opcode, operand int) {
instruction := types.Instruction{
Opcode: op,
Operand: operand,
}
if c.currentFunction != nil {
c.currentFunction.instructions = append(c.currentFunction.instructions, instruction)
} else {
c.instructions = append(c.instructions, instruction)
}
}
func (c *compiler) compileFunctionDeclaration(fn *parser.FunctionStatement) {
// Save the current compiler state
parentCompiler := c.currentFunction
// Create a new function compiler
fnCompiler := &functionCompiler{
constants: []any{},
instructions: []types.Instruction{},
numParams: len(fn.Parameters),
upvalues: []upvalueInfo{},
}
c.currentFunction = fnCompiler
// Enter a new scope for the function body
c.enterScope()
// Declare parameters as local variables
for _, param := range fn.Parameters {
c.declareVariable(param.Value)
paramIndex := c.addConstant(param.Value)
c.emit(types.OpSetLocal, paramIndex)
}
// Compile the function body
for _, stmt := range fn.Body.Statements {
c.compileStatement(stmt)
}
// Ensure the function always returns a value
// If the last instruction is not a return, add one
if len(fnCompiler.instructions) == 0 || fnCompiler.instructions[len(fnCompiler.instructions)-1].Opcode != types.OpReturn {
nullIndex := c.addConstant(nil)
c.emit(types.OpConstant, nullIndex)
c.emit(types.OpReturn, 0)
}
// Exit the function scope
c.exitScope()
// Restore the parent compiler
c.currentFunction = parentCompiler
// Extract upvalue information for closure creation
upvalueIndexes := make([]int, len(fnCompiler.upvalues))
for i, upvalue := range fnCompiler.upvalues {
upvalueIndexes[i] = upvalue.index
}
// Create a Function object and add it to the constants
function := types.NewFunction(
fnCompiler.instructions,
fnCompiler.numParams,
fnCompiler.constants,
upvalueIndexes,
)
functionIndex := c.addConstant(function)
c.emit(types.OpFunction, functionIndex)
// Store the function in a global variable
nameIndex := c.addConstant(fn.Name.Value)
// Use SetGlobal for top-level variables to persist between REPL lines
if len(c.scopes) <= 1 {
c.emit(types.OpSetGlobal, nameIndex)
} else {
c.declareVariable(fn.Name.Value)
c.emit(types.OpSetLocal, nameIndex)
}
}

View File

@ -1,256 +0,0 @@
package lexer
type TokenType byte
const (
TokenEOF TokenType = iota
TokenIdentifier
TokenString
TokenNumber
TokenEqual
TokenEcho
TokenSemicolon
TokenLeftBrace
TokenRightBrace
TokenLeftBracket
TokenRightBracket
TokenComma
TokenPlus
TokenMinus
TokenStar
TokenSlash
TokenLeftParen
TokenRightParen
TokenIf
TokenThen
TokenElse
TokenTrue
TokenFalse
TokenEqualEqual
TokenNotEqual
TokenLessThan
TokenGreaterThan
TokenLessEqual
TokenGreaterEqual
TokenEnd
TokenAnd
TokenOr
TokenNot
TokenElseIf
TokenNil
TokenFunction
TokenReturn
)
type Token struct {
Type TokenType
Value string
Line int
}
type Lexer struct {
input string
pos int
readPos int
ch byte
line int
}
func New(input string) *Lexer {
l := &Lexer{input: input, line: 1}
l.readChar()
return l
}
func (l *Lexer) readChar() {
if l.readPos >= len(l.input) {
l.ch = 0
} else {
l.ch = l.input[l.readPos]
}
l.pos = l.readPos
l.readPos++
if l.ch == '\n' {
l.line++
}
}
func (l *Lexer) NextToken() Token {
var tok Token
l.skipWhitespace()
l.skipComment()
switch l.ch {
case '=':
if l.peekChar() == '=' {
l.readChar() // consume the current '='
tok = Token{Type: TokenEqualEqual, Value: "==", Line: l.line}
} else {
tok = Token{Type: TokenEqual, Value: "=", Line: l.line}
}
case '!':
if l.peekChar() == '=' {
l.readChar() // consume the current '!'
tok = Token{Type: TokenNotEqual, Value: "!=", Line: l.line}
} else {
tok = Token{Type: TokenEOF, Value: "", Line: l.line} // Not supported yet
}
case '<':
if l.peekChar() == '=' {
l.readChar() // consume the current '<'
tok = Token{Type: TokenLessEqual, Value: "<=", Line: l.line}
} else {
tok = Token{Type: TokenLessThan, Value: "<", Line: l.line}
}
case '>':
if l.peekChar() == '=' {
l.readChar() // consume the current '>'
tok = Token{Type: TokenGreaterEqual, Value: ">=", Line: l.line}
} else {
tok = Token{Type: TokenGreaterThan, Value: ">", Line: l.line}
}
case '"':
tok = Token{Type: TokenString, Value: l.readString(), Line: l.line}
return tok
case '{':
tok = Token{Type: TokenLeftBrace, Value: "{", Line: l.line}
case '}':
tok = Token{Type: TokenRightBrace, Value: "}", Line: l.line}
case '[':
tok = Token{Type: TokenLeftBracket, Value: "[", Line: l.line}
case ']':
tok = Token{Type: TokenRightBracket, Value: "]", Line: l.line}
case ',':
tok = Token{Type: TokenComma, Value: ",", Line: l.line}
case '+':
tok = Token{Type: TokenPlus, Value: "+", Line: l.line}
case '-':
tok = Token{Type: TokenMinus, Value: "-", Line: l.line}
case '*':
tok = Token{Type: TokenStar, Value: "*", Line: l.line}
case '/':
if l.peekChar() == '/' {
// Comment line, skip it
l.skipComment()
return l.NextToken()
}
tok = Token{Type: TokenSlash, Value: "/", Line: l.line}
case '(':
tok = Token{Type: TokenLeftParen, Value: "(", Line: l.line}
case ')':
tok = Token{Type: TokenRightParen, Value: ")", Line: l.line}
case 0:
tok = Token{Type: TokenEOF, Value: "", Line: l.line}
default:
if isLetter(l.ch) {
tok.Value = l.readIdentifier()
switch tok.Value {
case "echo":
tok.Type = TokenEcho
case "if":
tok.Type = TokenIf
case "then":
tok.Type = TokenThen
case "else":
tok.Type = TokenElse
case "elseif":
tok.Type = TokenElseIf
case "true":
tok.Type = TokenTrue
case "false":
tok.Type = TokenFalse
case "end":
tok.Type = TokenEnd
case "and":
tok.Type = TokenAnd
case "or":
tok.Type = TokenOr
case "not":
tok.Type = TokenNot
case "nil":
tok.Type = TokenNil
case "function":
tok.Type = TokenFunction
case "return":
tok.Type = TokenReturn
default:
tok.Type = TokenIdentifier
}
return tok
} else if isDigit(l.ch) {
tok.Type = TokenNumber
tok.Value = l.readNumber()
return tok
} else {
tok = Token{Type: TokenEOF, Value: "", Line: l.line}
}
}
l.readChar()
return tok
}
func (l *Lexer) skipWhitespace() {
for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
l.readChar()
}
}
func (l *Lexer) readIdentifier() string {
pos := l.pos
for isLetter(l.ch) || isDigit(l.ch) {
l.readChar()
}
return l.input[pos:l.pos]
}
func (l *Lexer) readNumber() string {
pos := l.pos
for isDigit(l.ch) {
l.readChar()
}
return l.input[pos:l.pos]
}
func (l *Lexer) readString() string {
pos := l.pos + 1
for {
l.readChar()
if l.ch == '"' || l.ch == 0 {
break
}
}
str := l.input[pos:l.pos]
l.readChar() // Skip closing quote
return str
}
func isLetter(ch byte) bool {
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_'
}
func isDigit(ch byte) bool {
return '0' <= ch && ch <= '9'
}
func (l *Lexer) peekChar() byte {
if l.readPos >= len(l.input) {
return 0
}
return l.input[l.readPos]
}
func (l *Lexer) skipComment() {
if l.ch == '/' && l.peekChar() == '/' {
l.readChar() // Skip first '/'
l.readChar() // Skip second '/'
for l.ch != '\n' && l.ch != 0 {
l.readChar()
}
l.skipWhitespace()
}
}

65
mako.go
View File

@ -1,68 +1,7 @@
package main
import (
"bufio"
"fmt"
"os"
"git.sharkk.net/Sharkk/Mako/compiler"
"git.sharkk.net/Sharkk/Mako/lexer"
"git.sharkk.net/Sharkk/Mako/parser"
"git.sharkk.net/Sharkk/Mako/vm"
)
func RunRepl() {
scanner := bufio.NewScanner(os.Stdin)
virtualMachine := vm.New()
fmt.Println("Mako REPL (type 'exit' to quit)")
for {
fmt.Print(">> ")
if !scanner.Scan() {
break
}
input := scanner.Text()
if input == "exit" {
break
}
ExecuteCode(input, virtualMachine)
}
}
func ExecuteCode(code string, virtualMachine *vm.VM) {
lex := lexer.New(code)
p := parser.New(lex)
program := p.ParseProgram()
if len(p.Errors()) > 0 {
for _, err := range p.Errors() {
fmt.Printf("Error: %s\n", err)
}
return
}
bytecode := compiler.Compile(program)
virtualMachine.Run(bytecode)
}
import "fmt"
func main() {
args := os.Args[1:]
// If there's a command line argument, try to execute it as a file
if len(args) > 0 {
filename := args[0]
fileContent, err := os.ReadFile(filename)
if err != nil {
fmt.Printf("Error reading file %s: %v\n", filename, err)
os.Exit(1)
}
// Execute the file content
ExecuteCode(string(fileContent), vm.New())
} else {
// No arguments, run the REPL
RunRepl()
}
fmt.Println("Foo")
}

View File

@ -1,203 +0,0 @@
package parser
import "git.sharkk.net/Sharkk/Mako/lexer"
type Node interface {
TokenLiteral() string
}
type Statement interface {
Node
statementNode()
}
type Expression interface {
Node
expressionNode()
}
type Program struct {
Statements []Statement
}
func (p *Program) TokenLiteral() string {
if len(p.Statements) > 0 {
return p.Statements[0].TokenLiteral()
}
return ""
}
type VariableStatement struct {
Token lexer.Token
Name *Identifier
Value Expression
}
func (vs *VariableStatement) statementNode() {}
func (vs *VariableStatement) TokenLiteral() string { return vs.Token.Value }
type EchoStatement struct {
Token lexer.Token
Value Expression
}
func (es *EchoStatement) statementNode() {}
func (es *EchoStatement) TokenLiteral() string { return es.Token.Value }
type Identifier struct {
Token lexer.Token
Value string
}
func (i *Identifier) expressionNode() {}
func (i *Identifier) TokenLiteral() string { return i.Token.Value }
type StringLiteral struct {
Token lexer.Token
Value string
}
func (sl *StringLiteral) expressionNode() {}
func (sl *StringLiteral) TokenLiteral() string { return sl.Token.Value }
type NumberLiteral struct {
Token lexer.Token
Value float64
}
func (nl *NumberLiteral) expressionNode() {}
func (nl *NumberLiteral) TokenLiteral() string { return nl.Token.Value }
// TableLiteral represents a table: {key1 = val1, key2 = val2}
type TableLiteral struct {
Token lexer.Token
Pairs map[Expression]Expression
}
func (tl *TableLiteral) expressionNode() {}
func (tl *TableLiteral) TokenLiteral() string { return tl.Token.Value }
// IndexExpression represents table access: table[key]
type IndexExpression struct {
Token lexer.Token
Left Expression
Index Expression
}
func (ie *IndexExpression) expressionNode() {}
func (ie *IndexExpression) TokenLiteral() string { return ie.Token.Value }
// IndexAssignmentStatement represents: table[key] = value
type IndexAssignmentStatement struct {
Token lexer.Token
Left Expression
Index Expression
Value Expression
}
func (ias *IndexAssignmentStatement) statementNode() {}
func (ias *IndexAssignmentStatement) TokenLiteral() string { return ias.Token.Value }
// BlockStatement represents a block of code enclosed in braces
type BlockStatement struct {
Token lexer.Token
Statements []Statement
}
func (bs *BlockStatement) statementNode() {}
func (bs *BlockStatement) TokenLiteral() string { return bs.Token.Value }
// New AST nodes for arithmetic expressions
// InfixExpression represents binary operations like: a + b
type InfixExpression struct {
Token lexer.Token // The operator token, e.g. +
Left Expression
Operator string
Right Expression
}
func (ie *InfixExpression) expressionNode() {}
func (ie *InfixExpression) TokenLiteral() string { return ie.Token.Value }
// PrefixExpression represents unary operations like: -a
type PrefixExpression struct {
Token lexer.Token // The prefix token, e.g. -
Operator string
Right Expression
}
func (pe *PrefixExpression) expressionNode() {}
func (pe *PrefixExpression) TokenLiteral() string { return pe.Token.Value }
// GroupedExpression represents an expression in parentheses: (a + b)
type GroupedExpression struct {
Token lexer.Token // The '(' token
Expr Expression
}
func (ge *GroupedExpression) expressionNode() {}
func (ge *GroupedExpression) TokenLiteral() string { return ge.Token.Value }
type IfExpression struct {
Token lexer.Token // The 'if' token
Condition Expression
Consequence *BlockStatement
Alternative *BlockStatement // nil if no 'else'
}
func (ie *IfExpression) expressionNode() {}
func (ie *IfExpression) TokenLiteral() string { return ie.Token.Value }
type BooleanLiteral struct {
Token lexer.Token
Value bool
}
func (bl *BooleanLiteral) expressionNode() {}
func (bl *BooleanLiteral) TokenLiteral() string { return bl.Token.Value }
type NilLiteral struct {
Token lexer.Token
}
func (nl *NilLiteral) expressionNode() {}
func (nl *NilLiteral) TokenLiteral() string { return nl.Token.Value }
// Function-related AST nodes
type FunctionLiteral struct {
Token lexer.Token // The 'function' token
Parameters []*Identifier
Body *BlockStatement
}
func (fl *FunctionLiteral) expressionNode() {}
func (fl *FunctionLiteral) TokenLiteral() string { return fl.Token.Value }
type CallExpression struct {
Token lexer.Token // The '(' token
Function Expression // Identifier or FunctionLiteral
Arguments []Expression
}
func (ce *CallExpression) expressionNode() {}
func (ce *CallExpression) TokenLiteral() string { return ce.Token.Value }
type ReturnStatement struct {
Token lexer.Token // The 'return' token
Value Expression
}
func (rs *ReturnStatement) statementNode() {}
func (rs *ReturnStatement) TokenLiteral() string { return rs.Token.Value }
type FunctionStatement struct {
Token lexer.Token // The 'function' token
Name *Identifier
Parameters []*Identifier
Body *BlockStatement
}
func (fs *FunctionStatement) statementNode() {}
func (fs *FunctionStatement) TokenLiteral() string { return fs.Token.Value }

View File

@ -1,846 +0,0 @@
package parser
import (
"fmt"
"strconv"
"git.sharkk.net/Sharkk/Mako/lexer"
)
// Precedence levels for expression parsing
const (
_ int = iota
LOWEST
LOGICAL_OR // or
LOGICAL_AND // and
EQUALITY // ==, !=
COMPARISON // <, >, <=, >=
SUM // +, -
PRODUCT // *, /
PREFIX // -X or !X
CALL // myFunction(X)
INDEX // array[index]
)
var precedences = map[lexer.TokenType]int{
lexer.TokenPlus: SUM,
lexer.TokenMinus: SUM,
lexer.TokenStar: PRODUCT,
lexer.TokenSlash: PRODUCT,
lexer.TokenLeftBracket: INDEX,
lexer.TokenLeftParen: CALL, // Add precedence for function calls
lexer.TokenEqualEqual: EQUALITY,
lexer.TokenNotEqual: EQUALITY,
lexer.TokenLessThan: COMPARISON,
lexer.TokenGreaterThan: COMPARISON,
lexer.TokenLessEqual: COMPARISON,
lexer.TokenGreaterEqual: COMPARISON,
lexer.TokenAnd: LOGICAL_AND,
lexer.TokenOr: LOGICAL_OR,
}
type (
prefixParseFn func() Expression
infixParseFn func(Expression) Expression
)
type Parser struct {
l *lexer.Lexer
errors []string
curToken lexer.Token
peekToken lexer.Token
prefixParseFns map[lexer.TokenType]prefixParseFn
infixParseFns map[lexer.TokenType]infixParseFn
}
func New(l *lexer.Lexer) *Parser {
p := &Parser{
l: l,
errors: []string{},
}
// Initialize prefix parse functions
p.prefixParseFns = make(map[lexer.TokenType]prefixParseFn)
p.registerPrefix(lexer.TokenIdentifier, p.parseIdentifier)
p.registerPrefix(lexer.TokenString, p.parseStringLiteral)
p.registerPrefix(lexer.TokenNumber, p.parseNumberLiteral)
p.registerPrefix(lexer.TokenLeftBrace, p.parseTableLiteral)
p.registerPrefix(lexer.TokenMinus, p.parsePrefixExpression)
p.registerPrefix(lexer.TokenLeftParen, p.parseGroupedExpression)
p.registerPrefix(lexer.TokenIf, p.parseIfExpression)
p.registerPrefix(lexer.TokenElse, p.parseUnexpectedToken)
p.registerPrefix(lexer.TokenEnd, p.parseUnexpectedToken)
p.registerPrefix(lexer.TokenThen, p.parseUnexpectedToken)
p.registerPrefix(lexer.TokenTrue, p.parseBooleanLiteral)
p.registerPrefix(lexer.TokenFalse, p.parseBooleanLiteral)
p.registerPrefix(lexer.TokenNot, p.parsePrefixExpression)
p.registerPrefix(lexer.TokenNil, p.parseNilLiteral)
p.registerPrefix(lexer.TokenFunction, p.parseFunctionLiteral)
p.registerPrefix(lexer.TokenRightParen, p.parseUnexpectedToken)
// Initialize infix parse functions
p.infixParseFns = make(map[lexer.TokenType]infixParseFn)
p.registerInfix(lexer.TokenPlus, p.parseInfixExpression)
p.registerInfix(lexer.TokenMinus, p.parseInfixExpression)
p.registerInfix(lexer.TokenStar, p.parseInfixExpression)
p.registerInfix(lexer.TokenSlash, p.parseInfixExpression)
p.registerInfix(lexer.TokenLeftBracket, p.parseIndexExpression)
p.registerInfix(lexer.TokenLeftParen, p.parseCallExpression)
p.registerInfix(lexer.TokenAnd, p.parseInfixExpression)
p.registerInfix(lexer.TokenOr, p.parseInfixExpression)
// Register comparison operators
p.registerInfix(lexer.TokenEqualEqual, p.parseInfixExpression)
p.registerInfix(lexer.TokenNotEqual, p.parseInfixExpression)
p.registerInfix(lexer.TokenLessThan, p.parseInfixExpression)
p.registerInfix(lexer.TokenGreaterThan, p.parseInfixExpression)
p.registerInfix(lexer.TokenLessEqual, p.parseInfixExpression)
p.registerInfix(lexer.TokenGreaterEqual, p.parseInfixExpression)
// Read two tokens, so curToken and peekToken are both set
p.nextToken()
p.nextToken()
return p
}
func (p *Parser) registerPrefix(tokenType lexer.TokenType, fn prefixParseFn) {
p.prefixParseFns[tokenType] = fn
}
func (p *Parser) registerInfix(tokenType lexer.TokenType, fn infixParseFn) {
p.infixParseFns[tokenType] = fn
}
func (p *Parser) nextToken() {
p.curToken = p.peekToken
p.peekToken = p.l.NextToken()
}
func (p *Parser) curTokenIs(t lexer.TokenType) bool {
return p.curToken.Type == t
}
func (p *Parser) peekTokenIs(t lexer.TokenType) bool {
return p.peekToken.Type == t
}
func (p *Parser) expectPeek(t lexer.TokenType) bool {
if p.peekTokenIs(t) {
p.nextToken()
return true
}
p.peekError(t)
return false
}
func (p *Parser) peekError(t lexer.TokenType) {
msg := fmt.Sprintf("line %d: expected next token to be %d, got %d instead",
p.peekToken.Line, t, p.peekToken.Type)
p.errors = append(p.errors, msg)
}
func (p *Parser) Errors() []string {
return p.errors
}
func (p *Parser) peekPrecedence() int {
if p, ok := precedences[p.peekToken.Type]; ok {
return p
}
return LOWEST
}
func (p *Parser) curPrecedence() int {
if p, ok := precedences[p.curToken.Type]; ok {
return p
}
return LOWEST
}
func (p *Parser) ParseProgram() *Program {
program := &Program{Statements: []Statement{}}
for !p.curTokenIs(lexer.TokenEOF) {
stmt := p.parseStatement()
program.Statements = append(program.Statements, stmt)
p.nextToken()
}
return program
}
func (p *Parser) parseStatement() Statement {
switch p.curToken.Type {
case lexer.TokenIdentifier:
if p.peekTokenIs(lexer.TokenEqual) {
return p.parseVariableStatement()
} else if p.peekTokenIs(lexer.TokenLeftBracket) {
return p.parseIndexAssignmentStatement()
}
return p.parseExpressionStatement()
case lexer.TokenEcho:
return p.parseEchoStatement()
case lexer.TokenReturn:
return p.parseReturnStatement()
case lexer.TokenFunction:
// If the next token is an identifier, it's a function declaration
if p.peekTokenIs(lexer.TokenIdentifier) {
return p.parseFunctionStatement()
}
// Otherwise, it's a function expression
return p.parseExpressionStatement()
default:
return p.parseExpressionStatement()
}
}
// Parse return statements
func (p *Parser) parseReturnStatement() *ReturnStatement {
stmt := &ReturnStatement{Token: p.curToken}
p.nextToken() // Skip 'return'
// If there's no expression after 'return', set value to nil
if p.curTokenIs(lexer.TokenEnd) {
stmt.Value = &NilLiteral{Token: p.curToken}
return stmt
}
stmt.Value = p.parseExpression(LOWEST)
return stmt
}
// New method for expression statements
func (p *Parser) parseExpressionStatement() *ExpressionStatement {
stmt := &ExpressionStatement{Token: p.curToken}
stmt.Expression = p.parseExpression(LOWEST)
return stmt
}
// Add ExpressionStatement to ast.go
type ExpressionStatement struct {
Token lexer.Token
Expression Expression
}
func (es *ExpressionStatement) statementNode() {}
func (es *ExpressionStatement) TokenLiteral() string { return es.Token.Value }
func (p *Parser) parseVariableStatement() *VariableStatement {
stmt := &VariableStatement{Token: p.curToken}
stmt.Name = &Identifier{Token: p.curToken, Value: p.curToken.Value}
if !p.expectPeek(lexer.TokenEqual) {
return nil
}
p.nextToken() // Skip the equals sign
stmt.Value = p.parseExpression(LOWEST)
return stmt
}
func (p *Parser) parseEchoStatement() *EchoStatement {
stmt := &EchoStatement{Token: p.curToken}
p.nextToken()
stmt.Value = p.parseExpression(LOWEST)
return stmt
}
func (p *Parser) parseIndexAssignmentStatement() *IndexAssignmentStatement {
stmt := &IndexAssignmentStatement{
Token: p.curToken,
Left: &Identifier{Token: p.curToken, Value: p.curToken.Value},
}
p.nextToken() // Skip identifier
if !p.expectPeek(lexer.TokenLeftBracket) {
return nil
}
p.nextToken() // Skip '['
stmt.Index = p.parseExpression(LOWEST)
if !p.expectPeek(lexer.TokenRightBracket) {
return nil
}
if !p.expectPeek(lexer.TokenEqual) {
return nil
}
p.nextToken() // Skip '='
stmt.Value = p.parseExpression(LOWEST)
return stmt
}
// Core expression parser with precedence climbing
func (p *Parser) parseExpression(precedence int) Expression {
prefix := p.prefixParseFns[p.curToken.Type]
if prefix == nil {
p.noPrefixParseFnError(p.curToken.Type)
return nil
}
leftExp := prefix()
// Continue while we have valid infix operators
// and stop at special tokens that end expressions
for !p.peekTokenIs(lexer.TokenEnd) &&
!p.peekTokenIs(lexer.TokenThen) &&
!p.peekTokenIs(lexer.TokenElse) &&
precedence < p.peekPrecedence() {
infix := p.infixParseFns[p.peekToken.Type]
if infix == nil {
return leftExp
}
p.nextToken()
leftExp = infix(leftExp)
}
return leftExp
}
func (p *Parser) noPrefixParseFnError(t lexer.TokenType) {
msg := fmt.Sprintf("line %d: no prefix parse function for %d found",
p.curToken.Line, t)
p.errors = append(p.errors, msg)
}
// Expression parsing methods
func (p *Parser) parseIdentifier() Expression {
return &Identifier{Token: p.curToken, Value: p.curToken.Value}
}
func (p *Parser) parseStringLiteral() Expression {
return &StringLiteral{Token: p.curToken, Value: p.curToken.Value}
}
func (p *Parser) parseNumberLiteral() Expression {
lit := &NumberLiteral{Token: p.curToken}
value, err := strconv.ParseFloat(p.curToken.Value, 64)
if err != nil {
msg := fmt.Sprintf("could not parse %q as float", p.curToken.Value)
p.errors = append(p.errors, msg)
return nil
}
lit.Value = value
return lit
}
func (p *Parser) parseTableLiteral() Expression {
table := &TableLiteral{
Token: p.curToken, // This should be '{'
Pairs: make(map[Expression]Expression),
}
p.nextToken() // Skip '{'
if p.curTokenIs(lexer.TokenRightBrace) {
return table // Empty table
}
// Parse the first key-value pair
key := p.parseExpression(LOWEST)
if !p.expectPeek(lexer.TokenEqual) {
return nil
}
p.nextToken() // Skip '='
value := p.parseExpression(LOWEST)
table.Pairs[key] = value
// Parse remaining key-value pairs
for p.peekTokenIs(lexer.TokenComma) {
p.nextToken() // Skip current value
p.nextToken() // Skip comma
if p.curTokenIs(lexer.TokenRightBrace) {
break // Allow trailing comma
}
key = p.parseExpression(LOWEST)
if !p.expectPeek(lexer.TokenEqual) {
return nil
}
p.nextToken() // Skip '='
value = p.parseExpression(LOWEST)
table.Pairs[key] = value
}
if !p.expectPeek(lexer.TokenRightBrace) {
return nil
}
return table
}
func (p *Parser) parseIndexExpression(left Expression) Expression {
exp := &IndexExpression{
Token: p.curToken,
Left: left,
}
p.nextToken() // Skip '['
exp.Index = p.parseExpression(LOWEST)
if !p.expectPeek(lexer.TokenRightBracket) {
return nil
}
return exp
}
// New methods for arithmetic expressions
func (p *Parser) parsePrefixExpression() Expression {
expression := &PrefixExpression{
Token: p.curToken,
Operator: p.curToken.Value,
}
p.nextToken() // Skip the prefix token
expression.Right = p.parseExpression(PREFIX)
return expression
}
func (p *Parser) parseInfixExpression(left Expression) Expression {
expression := &InfixExpression{
Token: p.curToken,
Operator: p.curToken.Value,
Left: left,
}
precedence := p.curPrecedence()
p.nextToken() // Skip the operator
expression.Right = p.parseExpression(precedence)
return expression
}
func (p *Parser) parseGroupedExpression() Expression {
p.nextToken() // Skip '('
exp := p.parseExpression(LOWEST)
if !p.expectPeek(lexer.TokenRightParen) {
return nil
}
// Wrap in GroupedExpression to maintain the AST structure
return &GroupedExpression{
Token: p.curToken,
Expr: exp,
}
}
func (p *Parser) parseBooleanLiteral() Expression {
return &BooleanLiteral{
Token: p.curToken,
Value: p.curTokenIs(lexer.TokenTrue),
}
}
func (p *Parser) parseIfExpression() Expression {
expression := &IfExpression{Token: p.curToken}
p.nextToken() // Skip 'if'
// Parse condition
expression.Condition = p.parseExpression(LOWEST)
// Expect 'then' after condition
if !p.expectPeek(lexer.TokenThen) {
return nil
}
p.nextToken() // Skip 'then'
// Create a block statement for the consequence
consequence := &BlockStatement{Token: p.curToken}
consequence.Statements = []Statement{}
// Parse statements until we hit 'else', 'elseif', or 'end'
for !p.curTokenIs(lexer.TokenElse) && !p.curTokenIs(lexer.TokenElseIf) &&
!p.curTokenIs(lexer.TokenEnd) && !p.curTokenIs(lexer.TokenEOF) {
stmt := p.parseStatement()
consequence.Statements = append(consequence.Statements, stmt)
p.nextToken()
}
expression.Consequence = consequence
// Check for 'elseif'
if p.curTokenIs(lexer.TokenElseIf) {
// Create a block statement for the alternative
alternative := &BlockStatement{Token: p.curToken}
alternative.Statements = []Statement{}
// Parse the nested elseif as a new if expression
nestedIf := p.parseElseIfExpression()
// Add it as an expression statement in the alternative block
alternative.Statements = append(alternative.Statements,
&ExpressionStatement{Token: p.curToken, Expression: nestedIf})
expression.Alternative = alternative
return expression
}
// Check for 'else'
if p.curTokenIs(lexer.TokenElse) {
p.nextToken() // Skip 'else'
// Create a block statement for the alternative
alternative := &BlockStatement{Token: p.curToken}
alternative.Statements = []Statement{}
// Parse statements until we hit 'end'
for !p.curTokenIs(lexer.TokenEnd) && !p.curTokenIs(lexer.TokenEOF) {
stmt := p.parseStatement()
alternative.Statements = append(alternative.Statements, stmt)
p.nextToken()
}
expression.Alternative = alternative
}
// We should now be at the 'end' token
if !p.curTokenIs(lexer.TokenEnd) {
p.errors = append(p.errors, fmt.Sprintf("line %d: expected 'end' to close if expression",
p.curToken.Line))
return nil
}
return expression
}
func (p *Parser) parseElseIfExpression() Expression {
expression := &IfExpression{Token: p.curToken}
p.nextToken() // Skip 'elseif'
// Parse condition
expression.Condition = p.parseExpression(LOWEST)
// Expect 'then' after condition
if !p.expectPeek(lexer.TokenThen) {
return nil
}
p.nextToken() // Skip 'then'
// Create a block statement for the consequence
consequence := &BlockStatement{Token: p.curToken}
consequence.Statements = []Statement{}
// Parse statements until we hit 'else', 'elseif', or 'end'
for !p.curTokenIs(lexer.TokenElse) && !p.curTokenIs(lexer.TokenElseIf) &&
!p.curTokenIs(lexer.TokenEnd) && !p.curTokenIs(lexer.TokenEOF) {
stmt := p.parseStatement()
consequence.Statements = append(consequence.Statements, stmt)
p.nextToken()
}
expression.Consequence = consequence
// Handle nested elseif
if p.curTokenIs(lexer.TokenElseIf) {
// Create a block statement for the alternative
alternative := &BlockStatement{Token: p.curToken}
alternative.Statements = []Statement{}
// Parse the nested elseif recursively
nestedIf := p.parseElseIfExpression()
// Add it as an expression statement in the alternative block
alternative.Statements = append(alternative.Statements,
&ExpressionStatement{Token: p.curToken, Expression: nestedIf})
expression.Alternative = alternative
return expression
}
// Handle else
if p.curTokenIs(lexer.TokenElse) {
p.nextToken() // Skip 'else'
// Create a block statement for the alternative
alternative := &BlockStatement{Token: p.curToken}
alternative.Statements = []Statement{}
// Parse statements until we hit 'end'
for !p.curTokenIs(lexer.TokenEnd) && !p.curTokenIs(lexer.TokenEOF) {
stmt := p.parseStatement()
alternative.Statements = append(alternative.Statements, stmt)
p.nextToken()
}
expression.Alternative = alternative
}
return expression
}
func (p *Parser) parseUnexpectedToken() Expression {
p.errors = append(p.errors, fmt.Sprintf("line %d: unexpected token: %s",
p.curToken.Line, p.curToken.Value))
return nil
}
func (p *Parser) parseNilLiteral() Expression {
return &NilLiteral{Token: p.curToken}
}
func (p *Parser) parseFunctionLiteral() Expression {
lit := &FunctionLiteral{Token: p.curToken}
// Check if next token is a left paren
if !p.expectPeek(lexer.TokenLeftParen) {
return nil
}
// Parse the parameters
lit.Parameters = []*Identifier{}
// Check for empty parameter list
if p.peekTokenIs(lexer.TokenRightParen) {
p.nextToken() // Skip to the right paren
} else {
p.nextToken() // Skip the left paren
// Parse first parameter
if !p.curTokenIs(lexer.TokenIdentifier) {
p.errors = append(p.errors, fmt.Sprintf("line %d: expected parameter name, got %s",
p.curToken.Line, p.curToken.Value))
return nil
}
ident := &Identifier{Token: p.curToken, Value: p.curToken.Value}
lit.Parameters = append(lit.Parameters, ident)
// Parse additional parameters
for p.peekTokenIs(lexer.TokenComma) {
p.nextToken() // Skip current parameter
p.nextToken() // Skip comma
if !p.curTokenIs(lexer.TokenIdentifier) {
p.errors = append(p.errors, fmt.Sprintf("line %d: expected parameter name after comma",
p.curToken.Line))
return nil
}
ident := &Identifier{Token: p.curToken, Value: p.curToken.Value}
lit.Parameters = append(lit.Parameters, ident)
}
// After parsing parameters, expect closing parenthesis
if !p.expectPeek(lexer.TokenRightParen) {
return nil
}
}
// Parse function body
bodyStmts := []Statement{}
for p.nextToken(); !p.curTokenIs(lexer.TokenEnd) && !p.curTokenIs(lexer.TokenEOF); p.nextToken() {
stmt := p.parseStatement()
if stmt != nil {
bodyStmts = append(bodyStmts, stmt)
}
}
// Expect 'end' token
if !p.curTokenIs(lexer.TokenEnd) {
p.errors = append(p.errors, fmt.Sprintf("line %d: expected 'end' to close function",
p.curToken.Line))
return nil
}
lit.Body = &BlockStatement{
Token: p.curToken,
Statements: bodyStmts,
}
return lit
}
func (p *Parser) parseFunctionParameters() []*Identifier {
identifiers := []*Identifier{}
// Empty parameter list
if p.peekTokenIs(lexer.TokenRightParen) {
p.nextToken() // Skip to right paren
p.nextToken() // Skip right paren
return identifiers
}
p.nextToken() // Skip left paren
// First parameter
if !p.curTokenIs(lexer.TokenIdentifier) {
// Expected identifier for parameter but didn't get one
p.errors = append(p.errors, fmt.Sprintf("line %d: expected parameter name, got %d",
p.curToken.Line, p.curToken.Type))
if p.expectPeek(lexer.TokenRightParen) {
return identifiers
}
return nil
}
ident := &Identifier{Token: p.curToken, Value: p.curToken.Value}
identifiers = append(identifiers, ident)
// Additional parameters
for p.peekTokenIs(lexer.TokenComma) {
p.nextToken() // Skip current identifier
p.nextToken() // Skip comma
if !p.curTokenIs(lexer.TokenIdentifier) {
p.errors = append(p.errors, fmt.Sprintf("line %d: expected parameter name after comma",
p.curToken.Line))
break
}
ident := &Identifier{Token: p.curToken, Value: p.curToken.Value}
identifiers = append(identifiers, ident)
}
if !p.expectPeek(lexer.TokenRightParen) {
p.errors = append(p.errors, fmt.Sprintf("line %d: expected ')' to close parameter list",
p.curToken.Line))
return nil
}
return identifiers
}
func (p *Parser) parseCallExpression(function Expression) Expression {
exp := &CallExpression{
Token: p.curToken,
Function: function,
Arguments: []Expression{},
}
// Empty argument list
if p.peekTokenIs(lexer.TokenRightParen) {
p.nextToken()
return exp
}
p.nextToken() // Skip '('
// First argument
exp.Arguments = append(exp.Arguments, p.parseExpression(LOWEST))
// Additional arguments
for p.peekTokenIs(lexer.TokenComma) {
p.nextToken() // Skip current argument
p.nextToken() // Skip comma
exp.Arguments = append(exp.Arguments, p.parseExpression(LOWEST))
}
if !p.expectPeek(lexer.TokenRightParen) {
return nil
}
return exp
}
func (p *Parser) parseFunctionStatement() *FunctionStatement {
stmt := &FunctionStatement{Token: p.curToken}
p.nextToken() // Skip 'function'
// Parse function name
if !p.curTokenIs(lexer.TokenIdentifier) {
p.errors = append(p.errors, fmt.Sprintf("line %d: expected function name", p.curToken.Line))
return nil
}
stmt.Name = &Identifier{Token: p.curToken, Value: p.curToken.Value}
// Check if next token is a left paren
if !p.expectPeek(lexer.TokenLeftParen) {
return nil
}
// Parse parameters
stmt.Parameters = []*Identifier{}
// Check for empty parameter list
if p.peekTokenIs(lexer.TokenRightParen) {
p.nextToken() // Skip to the right paren
} else {
p.nextToken() // Skip the left paren
// Parse first parameter
if !p.curTokenIs(lexer.TokenIdentifier) {
p.errors = append(p.errors, fmt.Sprintf("line %d: expected parameter name, got %s",
p.curToken.Line, p.curToken.Value))
return nil
}
ident := &Identifier{Token: p.curToken, Value: p.curToken.Value}
stmt.Parameters = append(stmt.Parameters, ident)
// Parse additional parameters
for p.peekTokenIs(lexer.TokenComma) {
p.nextToken() // Skip current parameter
p.nextToken() // Skip comma
if !p.curTokenIs(lexer.TokenIdentifier) {
p.errors = append(p.errors, fmt.Sprintf("line %d: expected parameter name after comma",
p.curToken.Line))
return nil
}
ident := &Identifier{Token: p.curToken, Value: p.curToken.Value}
stmt.Parameters = append(stmt.Parameters, ident)
}
// After parsing parameters, expect closing parenthesis
if !p.expectPeek(lexer.TokenRightParen) {
return nil
}
}
// Parse function body
bodyStmts := []Statement{}
for p.nextToken(); !p.curTokenIs(lexer.TokenEnd) && !p.curTokenIs(lexer.TokenEOF); p.nextToken() {
stmt := p.parseStatement()
if stmt != nil {
bodyStmts = append(bodyStmts, stmt)
}
}
// Expect 'end' token
if !p.curTokenIs(lexer.TokenEnd) {
p.errors = append(p.errors, fmt.Sprintf("line %d: expected 'end' to close function",
p.curToken.Line))
return nil
}
stmt.Body = &BlockStatement{
Token: p.curToken,
Statements: bodyStmts,
}
return stmt
}

View File

@ -1,12 +0,0 @@
// Assignment style
add = function(a, b)
return a + b
end
// Direct declaration style
function multiply(a, b)
return a * b
end
echo add(1, 2)
echo multiply(2, 2)

View File

@ -1,88 +0,0 @@
x = 10
result = if x > 5 then "greater" else "smaller" end
echo result
if x == 10 and x < 20 then
echo "x is ten and less than 20"
end
if x < 20 or x == 5 then
echo "x is either less than 20 or equals 5"
end
if not (x < 5) then
echo "x is not less than 5"
end
if x < 5 then
echo "x is small"
elseif x < 15 then
echo "x is medium"
elseif x < 25 then
echo "x is large"
else
echo "x is very large"
end
if x < 20 and x > 5 then
echo "x is between 5 and 20"
x = x + 1
else
echo "x is not between 5 and 20"
x = x - 1
end
a = 5
b = 10
c = 15
if a < b and b < c then
echo "a < b < c - values are in ascending order"
end
if a > 10 or b > 5 then
echo "either a > 10 or b > 5 (or both)"
end
if not (a == b) then
echo "a is not equal to b"
end
if (a < b and b < c) or (a == 5 and c == 15) then
echo "Complex condition is true"
end
if a > b and b > c then
echo "Descending order"
elseif a < b and b < c then
echo "Ascending order"
elseif a == b and b == c then
echo "All equal"
else
echo "No specific order"
end
scores = {
math = if x > 10 then "excellent" else "good" end,
science = "passing"
}
echo "math score: " + scores["math"]
echo "science score: " + scores["science"]
if a < b and b < c then
if a > 0 and c < 20 then
echo "a is positive, less than b, and c is less than 20"
else
echo "a is less than b, but either a is not positive or c >= 20"
end
end
if not (a < 0) and not (b > 20) then
echo "Both a is not negative and b is not greater than 20"
end
test1 = if a > 0 and b < 5 then "yes" else "no" end // Should be "no" (short-circuits on second condition)
test2 = if a < 0 or b > 5 then "yes" else "no" end // Should be "yes" (short-circuits on second condition)
echo "Test1: " + test1
echo "Test2: " + test2

167
types/ast.go Normal file
View File

@ -0,0 +1,167 @@
package types
import "fmt"
// Node represents a node in the abstract syntax tree
type Node interface {
String() string
}
// Statement represents any statement
type Statement interface {
Node
statementNode()
}
// Expression represents any expression
type Expression interface {
Node
expressionNode()
}
// --- Expressions ---
// LiteralExpr represents a literal value
type LiteralExpr struct {
Value any
}
func (l LiteralExpr) expressionNode() {}
func (l LiteralExpr) String() string { return fmt.Sprintf("%v", l.Value) }
// BinaryExpr represents a binary operation
type BinaryExpr struct {
Left Expression
Operator Token
Right Expression
}
func (b BinaryExpr) expressionNode() {}
func (b BinaryExpr) String() string {
return fmt.Sprintf("(%s %s %s)", b.Left.String(), b.Operator.Lexeme, b.Right.String())
}
// VariableExpr represents a variable reference
type VariableExpr struct {
Name Token
}
func (v VariableExpr) expressionNode() {}
func (v VariableExpr) String() string { return v.Name.Lexeme }
// CallExpr represents a function call
type CallExpr struct {
Callee Expression
Paren Token
Arguments []Expression
}
func (c CallExpr) expressionNode() {}
func (c CallExpr) String() string {
args := ""
for i, arg := range c.Arguments {
if i > 0 {
args += ", "
}
args += arg.String()
}
return fmt.Sprintf("%s(%s)", c.Callee.String(), args)
}
// --- Statements ---
// ExpressionStmt represents an expression statement
type ExpressionStmt struct {
Expression Expression
}
func (e ExpressionStmt) statementNode() {}
func (e ExpressionStmt) String() string { return e.Expression.String() }
// AssignStmt represents a variable assignment
type AssignStmt struct {
Name Token
Value Expression
}
func (a AssignStmt) statementNode() {}
func (a AssignStmt) String() string {
return fmt.Sprintf("%s = %s", a.Name.Lexeme, a.Value.String())
}
// FunctionStmt represents a function declaration
type FunctionStmt struct {
Name Token
Params []Token
Body []Statement
}
func (f FunctionStmt) statementNode() {}
func (f FunctionStmt) String() string {
params := ""
for i, param := range f.Params {
if i > 0 {
params += ", "
}
params += param.Lexeme
}
return fmt.Sprintf("fn %s(%s) ... end", f.Name.Lexeme, params)
}
// ReturnStmt represents a return statement
type ReturnStmt struct {
Keyword Token
Value Expression
}
func (r ReturnStmt) statementNode() {}
func (r ReturnStmt) String() string {
if r.Value == nil {
return "return"
}
return fmt.Sprintf("return %s", r.Value.String())
}
// IfStmt represents an if statement
type IfStmt struct {
Condition Expression
ThenBranch []Statement
ElseIfs []struct {
Condition Expression
Body []Statement
}
ElseBranch []Statement
}
func (i IfStmt) statementNode() {}
func (i IfStmt) String() string { return "if ... then ... end" }
// EchoStmt represents an echo statement
type EchoStmt struct {
Keyword Token
Value Expression
}
func (e EchoStmt) statementNode() {}
func (e EchoStmt) String() string { return fmt.Sprintf("echo %s", e.Value.String()) }
// UnaryExpr represents a unary operation
type UnaryExpr struct {
Operator Token
Right Expression
}
func (u UnaryExpr) expressionNode() {}
func (u UnaryExpr) String() string {
return fmt.Sprintf("(%s%s)", u.Operator.Lexeme, u.Right.String())
}
// BlockStmt represents a block of statements
type BlockStmt struct {
Statements []Statement
}
func (b BlockStmt) statementNode() {}
func (b BlockStmt) String() string {
return "{ ... }"
}

86
types/bytecode.go Normal file
View File

@ -0,0 +1,86 @@
// bytecode.go
package types
// SourcePos represents a position in the source code
type SourcePos struct {
Line int
Column int
}
// OpCode represents a bytecode instruction
type OpCode byte
const (
OP_CONSTANT OpCode = iota
OP_NIL
OP_TRUE
OP_FALSE
OP_POP
OP_GET_LOCAL
OP_SET_LOCAL
OP_GET_GLOBAL
OP_SET_GLOBAL
OP_EQUAL
OP_GREATER
OP_LESS
OP_ADD
OP_SUBTRACT
OP_MULTIPLY
OP_DIVIDE
OP_NOT
OP_NEGATE
OP_PRINT
OP_JUMP
OP_JUMP_IF_FALSE
OP_CALL
OP_RETURN
)
// String returns the string representation of an OpCode
func (op OpCode) String() string {
names := [...]string{
"OP_CONSTANT", "OP_NIL", "OP_TRUE", "OP_FALSE",
"OP_POP", "OP_GET_LOCAL", "OP_SET_LOCAL", "OP_GET_GLOBAL",
"OP_SET_GLOBAL", "OP_EQUAL", "OP_GREATER", "OP_LESS",
"OP_ADD", "OP_SUBTRACT", "OP_MULTIPLY", "OP_DIVIDE",
"OP_NOT", "OP_NEGATE", "OP_PRINT", "OP_JUMP",
"OP_JUMP_IF_FALSE", "OP_CALL", "OP_RETURN",
}
return names[op]
}
// Instruction represents a single bytecode instruction
type Instruction struct {
Op OpCode
Operands []byte
Pos SourcePos // Source position for debugging
}
// Chunk represents a chunk of bytecode
type Chunk struct {
Code []Instruction
Constants []Value
}
// Function represents a function in bytecode
type Function struct {
Name string
Arity int
Chunk *Chunk
LocalCount int
UpvalueCount int
}
// Environment represents a variable scope
type Environment struct {
Values map[string]Value
Enclosing *Environment
}
// NewEnvironment creates a new environment
func NewEnvironment(enclosing *Environment) *Environment {
return &Environment{
Values: make(map[string]Value),
Enclosing: enclosing,
}
}

23
types/errors.go Normal file
View File

@ -0,0 +1,23 @@
package types
import "fmt"
// MakoError represents an error in Mako code
type MakoError struct {
Message string
Line int
Column int
}
func (e MakoError) Error() string {
return fmt.Sprintf("[%d:%d] %s", e.Line, e.Column, e.Message)
}
// NewError creates a new MakoError
func NewError(message string, line, column int) *MakoError {
return &MakoError{
Message: message,
Line: line,
Column: column,
}
}

99
types/token.go Normal file
View File

@ -0,0 +1,99 @@
package types
// TokenType represents the type of a token
type TokenType int
// Token represents a lexical token
type Token struct {
Type TokenType
Lexeme string
Literal any
Line int
Column int
}
// All token types in the language
const (
// Special tokens
EOF TokenType = iota
ERROR
// Literals
IDENTIFIER
STRING
NUMBER
// Operators
PLUS // +
MINUS // -
STAR // *
SLASH // /
EQUAL // =
EQUAL_EQUAL // ==
BANG_EQUAL // !=
LESS //
LESS_EQUAL // <=
GREATER // >
GREATER_EQUAL // >=
// Keywords
AND
OR
IF
ELSEIF
ELSE
THEN
END
FN
RETURN
ECHO
TRUE
FALSE
NIL
// Punctuation
LEFT_PAREN // (
RIGHT_PAREN // )
COMMA // ,
)
// String returns the string representation of the token type
func (t TokenType) String() string {
return tokenNames[t]
}
// Token type string representations
var tokenNames = [...]string{
EOF: "EOF",
ERROR: "ERROR",
IDENTIFIER: "IDENTIFIER",
STRING: "STRING",
NUMBER: "NUMBER",
PLUS: "PLUS",
MINUS: "MINUS",
STAR: "STAR",
SLASH: "SLASH",
EQUAL: "EQUAL",
EQUAL_EQUAL: "EQUAL_EQUAL",
BANG_EQUAL: "BANG_EQUAL",
LESS: "LESS",
LESS_EQUAL: "LESS_EQUAL",
GREATER: "GREATER",
GREATER_EQUAL: "GREATER_EQUAL",
AND: "AND",
OR: "OR",
IF: "IF",
ELSEIF: "ELSEIF",
ELSE: "ELSE",
THEN: "THEN",
END: "END",
FN: "FN",
RETURN: "RETURN",
ECHO: "ECHO",
TRUE: "TRUE",
FALSE: "FALSE",
NIL: "NIL",
LEFT_PAREN: "LEFT_PAREN",
RIGHT_PAREN: "RIGHT_PAREN",
COMMA: "COMMA",
}

View File

@ -1,292 +0,0 @@
package types
import "fmt"
// ValueType represents the type of a value
type ValueType byte
const (
TypeNull ValueType = iota
TypeNumber
TypeString
TypeBoolean
TypeTable
TypeFunction
)
type Opcode byte
const (
OpConstant Opcode = iota
OpSetLocal
OpGetLocal
OpSetGlobal
OpGetGlobal
OpEcho
OpNewTable
OpSetIndex
OpGetIndex
OpDup
OpPop
OpEnterScope
OpExitScope
OpAdd
OpSubtract
OpMultiply
OpDivide
OpNegate
OpJumpIfFalse
OpJump
OpEqual
OpNotEqual
OpLessThan
OpGreaterThan
OpLessEqual
OpGreaterEqual
OpNot
OpFunction
OpCall
OpReturn
OpClosure
)
type Instruction struct {
Opcode Opcode
Operand int
}
type Bytecode struct {
Constants []any
Instructions []Instruction
}
type Value struct {
Type ValueType
Data any
}
// Equal checks if two values are equal
func (v Value) Equal(other Value) bool {
if v.Type != other.Type {
return false
}
switch v.Type {
case TypeNull:
return true // null == null
case TypeNumber:
return v.Data.(float64) == other.Data.(float64)
case TypeString:
return v.Data.(string) == other.Data.(string)
case TypeBoolean:
return v.Data.(bool) == other.Data.(bool)
case TypeTable:
return v.Data.(*Table).Equal(other.Data.(*Table))
case TypeFunction:
// Two functions are equal if they point to the same function object
return v.Data.(*Function) == other.Data.(*Function)
default:
return false
}
}
func NewNull() Value {
return Value{Type: TypeNull, Data: nil}
}
func NewString(s string) Value {
return Value{Type: TypeString, Data: s}
}
func NewNumber(n float64) Value {
return Value{Type: TypeNumber, Data: n}
}
func NewBoolean(b bool) Value {
return Value{Type: TypeBoolean, Data: b}
}
// Function represents a callable function
type Function struct {
Instructions []Instruction
NumParams int
NumLocals int
NumConstants int
Constants []any // Function's own constant pool
UpvalueIndexes []int // Indexes of upvalues (for closures)
}
func NewFunction(instructions []Instruction, numParams int, constants []any, upvalues []int) *Function {
return &Function{
Instructions: instructions,
NumParams: numParams,
NumConstants: len(constants),
Constants: constants,
UpvalueIndexes: upvalues,
}
}
func NewFunctionValue(function *Function) Value {
return Value{Type: TypeFunction, Data: function}
}
// Upvalue represents a reference to a value that has been closed over
type Upvalue struct {
Value *Value // Pointer to the closed-over value
}
// TableEntry maintains insertion order
type TableEntry struct {
Key Value
Value Value
}
// Table with ordered entries
type Table struct {
Entries []TableEntry // Preserves insertion order
HashMap map[string]int // Fast lookups for string keys
NumMap map[float64]int // Fast lookups for number keys
BoolMap map[bool]int // Fast lookups for boolean keys
// We can't have a map for table keys since they're not comparable in Go
// We'll handle table keys by linear search in Entries
}
// Equal compares two tables for equality
func (t *Table) Equal(other *Table) bool {
if len(t.Entries) != len(other.Entries) {
return false
}
// Check if every key-value pair in t exists in other
for _, entry := range t.Entries {
found := false
for _, otherEntry := range other.Entries {
if entry.Key.Equal(otherEntry.Key) && entry.Value.Equal(otherEntry.Value) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
func NewTable() *Table {
return &Table{
Entries: []TableEntry{},
HashMap: make(map[string]int),
NumMap: make(map[float64]int),
BoolMap: make(map[bool]int),
}
}
func NewTableValue() Value {
return Value{Type: TypeTable, Data: NewTable()}
}
// SetKey is a helper to generate a string representation of a key for maps
func (t *Table) SetKey(key Value) string {
switch key.Type {
case TypeString:
return "s:" + key.Data.(string)
case TypeNumber:
return "n:" + fmt.Sprintf("%v", key.Data.(float64))
case TypeBoolean:
if key.Data.(bool) {
return "b:true"
}
return "b:false"
case TypeNull:
return "null"
case TypeTable:
// For tables, we can use a simple hash or just indicate it's a table
return "t:table" // This won't distinguish between different tables
default:
return "unknown"
}
}
// TableSet preserves insertion order
func (t *Table) Set(key, value Value) {
idx := -1
// Check if the key is a table
if key.Type == TypeTable {
// For table keys, we need to do a linear search
for i, entry := range t.Entries {
if entry.Key.Type == TypeTable && entry.Key.Data.(*Table).Equal(key.Data.(*Table)) {
idx = i
break
}
}
} else {
// Use the existing maps for other types
switch key.Type {
case TypeString:
if i, ok := t.HashMap[key.Data.(string)]; ok {
idx = i
}
case TypeNumber:
if i, ok := t.NumMap[key.Data.(float64)]; ok {
idx = i
}
case TypeBoolean:
if i, ok := t.BoolMap[key.Data.(bool)]; ok {
idx = i
}
}
}
if idx >= 0 {
// Update existing entry
t.Entries[idx].Value = value
} else {
// Add new entry
t.Entries = append(t.Entries, TableEntry{Key: key, Value: value})
idx = len(t.Entries) - 1
// Update lookup maps
switch key.Type {
case TypeString:
t.HashMap[key.Data.(string)] = idx
case TypeNumber:
t.NumMap[key.Data.(float64)] = idx
case TypeBoolean:
t.BoolMap[key.Data.(bool)] = idx
}
}
}
func (t *Table) Get(key Value) Value {
// Check if the key is a table
if key.Type == TypeTable {
// For table keys, we need to do a linear search
for _, entry := range t.Entries {
if entry.Key.Type == TypeTable && entry.Key.Data.(*Table).Equal(key.Data.(*Table)) {
return entry.Value
}
}
return NewNull()
}
// Use the existing maps for other types
switch key.Type {
case TypeString:
if i, ok := t.HashMap[key.Data.(string)]; ok {
return t.Entries[i].Value
}
case TypeNumber:
if i, ok := t.NumMap[key.Data.(float64)]; ok {
return t.Entries[i].Value
}
case TypeBoolean:
if i, ok := t.BoolMap[key.Data.(bool)]; ok {
return t.Entries[i].Value
}
}
return NewNull()
}

106
types/value.go Normal file
View File

@ -0,0 +1,106 @@
package types
import "fmt"
// ValueType represents the type of a runtime value
type ValueType int
const (
NIL_TYPE ValueType = iota
BOOL_TYPE
NUMBER_TYPE
STRING_TYPE
FUNCTION_TYPE
TABLE_TYPE
)
// Value interface represents any value in the language at runtime
type Value interface {
Type() ValueType
String() string
}
// NilValue represents a nil value
type NilValue struct{}
func (n NilValue) Type() ValueType {
return NIL_TYPE
}
func (n NilValue) String() string {
return "nil"
}
// BoolValue represents a boolean value
type BoolValue struct {
Value bool
}
func (b BoolValue) Type() ValueType {
return BOOL_TYPE
}
func (b BoolValue) String() string {
if b.Value {
return "true"
}
return "false"
}
// NumberValue represents a numeric value
type NumberValue struct {
Value float64
}
func (n NumberValue) Type() ValueType {
return NUMBER_TYPE
}
func (n NumberValue) String() string {
return fmt.Sprintf("%g", n.Value)
}
// StringValue represents a string value
type StringValue struct {
Value string
}
func (s StringValue) Type() ValueType {
return STRING_TYPE
}
func (s StringValue) String() string {
return s.Value
}
// FunctionValue represents a function value
type FunctionValue struct {
Name string
Arity int
// We'll add bytecode and other function data later
}
func (f FunctionValue) Type() ValueType {
return FUNCTION_TYPE
}
func (f FunctionValue) String() string {
if f.Name == "" {
return "<anonymous function>"
}
return fmt.Sprintf("<function %s>", f.Name)
}
// TableValue represents a table (map/dictionary/array)
type TableValue struct {
Data map[Value]Value
MetaTable *TableValue
}
func (t TableValue) Type() ValueType {
return TABLE_TYPE
}
func (t TableValue) String() string {
return fmt.Sprintf("<table: %p>", &t)
}

604
vm/vm.go
View File

@ -1,604 +0,0 @@
package vm
import (
"fmt"
"git.sharkk.net/Sharkk/Mako/types"
)
// Scope represents a lexical scope
type Scope struct {
Variables map[string]types.Value
}
// Frame represents a call frame on the call stack
type Frame struct {
Function *types.Function // The function being executed
IP int // Instruction pointer
BasePointer int // Base stack pointer for this frame
ReturnAddr int // Return address in the caller
Upvalues []*types.Upvalue // Closed-over variables
}
type VM struct {
constants []any
globals map[string]types.Value
scopes []Scope // Stack of local scopes
stack []types.Value
sp int // Stack pointer
frames []Frame // Call frames
fp int // Frame pointer (index of current frame)
upvalues []*types.Upvalue // Upvalues for closures
}
func New() *VM {
return &VM{
globals: make(map[string]types.Value),
scopes: []Scope{}, // Initially no scopes
stack: make([]types.Value, 1024),
sp: 0,
frames: make([]Frame, 64), // Support up to 64 nested calls
fp: -1, // No active frame yet
upvalues: []*types.Upvalue{},
}
}
// Reset resets the VM to its initial state
// Can be called as vm.Reset() to reuse an existing VM instance
func (vm *VM) Reset() *VM {
vm.constants = nil
vm.globals = make(map[string]types.Value)
vm.scopes = []Scope{}
vm.stack = make([]types.Value, 1024)
vm.sp = 0
vm.frames = make([]Frame, 64)
vm.fp = -1
vm.upvalues = []*types.Upvalue{}
return vm
}
// GetGlobal retrieves a global variable by name
func (vm *VM) GetGlobal(name string) (types.Value, bool) {
val, ok := vm.globals[name]
return val, ok
}
// Global returns all global variables for testing purposes
func (vm *VM) Globals() map[string]types.Value {
return vm.globals
}
// CurrentStack returns the current stack values for testing
func (vm *VM) CurrentStack() []types.Value {
return vm.stack[:vm.sp]
}
func (vm *VM) Run(bytecode *types.Bytecode) {
vm.constants = bytecode.Constants
vm.runCode(bytecode.Instructions, 0)
}
func (vm *VM) runCode(instructions []types.Instruction, basePointer int) types.Value {
for ip := 0; ip < len(instructions); ip++ {
instruction := instructions[ip]
switch instruction.Opcode {
case types.OpConstant:
constIndex := instruction.Operand
constant := vm.constants[constIndex]
switch v := constant.(type) {
case string:
vm.push(types.NewString(v))
case float64:
vm.push(types.NewNumber(v))
case bool:
vm.push(types.NewBoolean(v))
case nil:
vm.push(types.NewNull())
case *types.Function:
vm.push(types.NewFunctionValue(v))
}
case types.OpSetLocal:
constIndex := instruction.Operand
name := vm.constants[constIndex].(string)
value := vm.pop()
// Set in current scope if it exists
if len(vm.scopes) > 0 {
vm.scopes[len(vm.scopes)-1].Variables[name] = value
} else {
// No scope, set as global
vm.globals[name] = value
}
case types.OpGetLocal:
constIndex := instruction.Operand
name := vm.constants[constIndex].(string)
// Check local scopes from innermost to outermost
found := false
for i := len(vm.scopes) - 1; i >= 0; i-- {
if val, ok := vm.scopes[i].Variables[name]; ok {
vm.push(val)
found = true
break
}
}
// If not found in locals, check globals
if !found {
if val, ok := vm.globals[name]; ok {
vm.push(val)
} else {
vm.push(types.NewNull())
}
}
case types.OpSetGlobal:
constIndex := instruction.Operand
name := vm.constants[constIndex].(string)
value := vm.pop()
vm.globals[name] = value
case types.OpGetGlobal:
constIndex := instruction.Operand
name := vm.constants[constIndex].(string)
if val, ok := vm.globals[name]; ok {
vm.push(val)
} else {
vm.push(types.NewNull())
}
case types.OpEnterScope:
// Push a new scope
vm.scopes = append(vm.scopes, Scope{
Variables: make(map[string]types.Value),
})
case types.OpExitScope:
// Pop the current scope
if len(vm.scopes) > 0 {
vm.scopes = vm.scopes[:len(vm.scopes)-1]
}
case types.OpNewTable:
vm.push(types.NewTableValue())
case types.OpSetIndex:
value := vm.pop()
key := vm.pop()
tableVal := vm.pop()
if tableVal.Type != types.TypeTable {
fmt.Println("Error: attempt to index non-table value")
vm.push(types.NewNull())
continue
}
table := tableVal.Data.(*types.Table)
table.Set(key, value)
vm.push(tableVal)
case types.OpGetIndex:
key := vm.pop()
tableVal := vm.pop()
if tableVal.Type != types.TypeTable {
fmt.Println("Error: attempt to index non-table value")
vm.push(types.NewNull())
continue
}
table := tableVal.Data.(*types.Table)
value := table.Get(key)
vm.push(value)
case types.OpDup:
if vm.sp > 0 {
vm.push(vm.stack[vm.sp-1])
}
case types.OpPop:
vm.pop()
case types.OpEcho:
value := vm.pop()
switch value.Type {
case types.TypeString:
fmt.Println(value.Data.(string))
case types.TypeNumber:
fmt.Println(value.Data.(float64))
case types.TypeBoolean:
fmt.Println(value.Data.(bool))
case types.TypeNull:
fmt.Println("nil")
case types.TypeTable:
fmt.Println(vm.formatTable(value.Data.(*types.Table)))
case types.TypeFunction:
fmt.Println("<function>")
}
// Jump instructions
case types.OpJumpIfFalse:
condition := vm.pop()
// Consider falsy: false, null, 0
shouldJump := false
if condition.Type == types.TypeBoolean && !condition.Data.(bool) {
shouldJump = true
} else if condition.Type == types.TypeNull {
shouldJump = true
} else if condition.Type == types.TypeNumber && condition.Data.(float64) == 0 {
shouldJump = true
}
if shouldJump {
ip = instruction.Operand - 1 // -1 because loop will increment
}
case types.OpJump:
ip = instruction.Operand - 1 // -1 because loop will increment
// Function instructions
case types.OpFunction:
constIndex := instruction.Operand
function := vm.constants[constIndex].(*types.Function)
// Use the helper function to create a proper function value
vm.push(types.NewFunctionValue(function))
case types.OpCall:
numArgs := instruction.Operand
// The function is at position sp-numArgs-1
if vm.sp <= numArgs {
fmt.Println("Error: stack underflow during function call")
vm.push(types.NewNull())
continue
}
fnVal := vm.stack[vm.sp-numArgs-1]
if fnVal.Type != types.TypeFunction {
fmt.Printf("Error: attempt to call non-function value\n")
vm.push(types.NewNull())
continue
}
function, ok := fnVal.Data.(*types.Function)
if !ok {
fmt.Printf("Error: function data is invalid\n")
vm.push(types.NewNull())
continue
}
// Check if we have the correct number of arguments
if numArgs != function.NumParams {
fmt.Printf("Error: function expects %d arguments, got %d\n",
function.NumParams, numArgs)
vm.push(types.NewNull())
continue
}
// Create a new call frame
frame := Frame{
Function: function,
IP: 0,
BasePointer: vm.sp - numArgs - 1, // Below the function and args
ReturnAddr: ip,
Upvalues: make([]*types.Upvalue, len(function.UpvalueIndexes)),
}
// Save the current frame
vm.fp++
if vm.fp >= len(vm.frames) {
// Grow the frame stack if needed
newFrames := make([]Frame, len(vm.frames)*2)
copy(newFrames, vm.frames)
vm.frames = newFrames
}
vm.frames[vm.fp] = frame
// Save the current constants
oldConstants := vm.constants
// Switch to function's constants
vm.constants = function.Constants
// Run the function code
returnValue := vm.runCode(function.Instructions, frame.BasePointer)
// Restore the old constants
vm.constants = oldConstants
// Restore state
vm.fp--
// Replace the function with the return value
vm.stack[frame.BasePointer] = returnValue
// Adjust the stack pointer to remove the arguments
vm.sp = frame.BasePointer + 1
case types.OpReturn:
returnValue := vm.pop()
// If we're in a function call, return to the caller
if vm.fp >= 0 {
frame := vm.frames[vm.fp]
// Restore the stack to just below the function
vm.sp = frame.BasePointer
// Push the return value
vm.push(returnValue)
// Return to the caller
return returnValue
}
// Top-level return
vm.push(returnValue)
return returnValue
// Arithmetic operations
case types.OpAdd:
right := vm.pop()
left := vm.pop()
if left.Type == types.TypeNumber && right.Type == types.TypeNumber {
result := left.Data.(float64) + right.Data.(float64)
vm.push(types.NewNumber(result))
} else if left.Type == types.TypeString && right.Type == types.TypeString {
// String concatenation
result := left.Data.(string) + right.Data.(string)
vm.push(types.NewString(result))
} else {
fmt.Println("Error: cannot add values of different types")
vm.push(types.NewNull())
}
case types.OpSubtract:
right := vm.pop()
left := vm.pop()
if left.Type == types.TypeNumber && right.Type == types.TypeNumber {
result := left.Data.(float64) - right.Data.(float64)
vm.push(types.NewNumber(result))
} else {
fmt.Println("Error: cannot subtract non-number values")
vm.push(types.NewNull())
}
case types.OpMultiply:
right := vm.pop()
left := vm.pop()
if left.Type == types.TypeNumber && right.Type == types.TypeNumber {
result := left.Data.(float64) * right.Data.(float64)
vm.push(types.NewNumber(result))
} else {
fmt.Println("Error: cannot multiply non-number values")
vm.push(types.NewNull())
}
case types.OpDivide:
right := vm.pop()
left := vm.pop()
if left.Type == types.TypeNumber && right.Type == types.TypeNumber {
// Check for division by zero
if right.Data.(float64) == 0 {
fmt.Println("Error: division by zero")
vm.push(types.NewNull())
} else {
result := left.Data.(float64) / right.Data.(float64)
vm.push(types.NewNumber(result))
}
} else {
fmt.Println("Error: cannot divide non-number values")
vm.push(types.NewNull())
}
case types.OpNegate:
operand := vm.pop()
if operand.Type == types.TypeNumber {
result := -operand.Data.(float64)
vm.push(types.NewNumber(result))
} else {
fmt.Println("Error: cannot negate non-number value")
vm.push(types.NewNull())
}
case types.OpEqual:
if vm.sp < 2 {
fmt.Println("Error: not enough operands for equality comparison")
vm.push(types.NewBoolean(false))
continue
}
right := vm.pop()
left := vm.pop()
vm.push(types.NewBoolean(left.Equal(right)))
case types.OpNotEqual:
if vm.sp < 2 {
fmt.Println("Error: not enough operands for inequality comparison")
vm.push(types.NewBoolean(true))
continue
}
right := vm.pop()
left := vm.pop()
vm.push(types.NewBoolean(!left.Equal(right)))
case types.OpLessThan:
if vm.sp < 2 {
fmt.Println("Error: not enough operands for less-than comparison")
vm.push(types.NewBoolean(false))
continue
}
// Peek at values first before popping
right := vm.stack[vm.sp-1]
left := vm.stack[vm.sp-2]
if left.Type == types.TypeNumber && right.Type == types.TypeNumber {
// Now pop them
vm.pop()
vm.pop()
vm.push(types.NewBoolean(left.Data.(float64) < right.Data.(float64)))
} else {
// Pop the values to maintain stack balance
vm.pop()
vm.pop()
fmt.Println("Error: cannot compare non-number values with <")
vm.push(types.NewBoolean(false))
}
case types.OpGreaterThan:
if vm.sp < 2 {
fmt.Println("Error: not enough operands for greater-than comparison")
vm.push(types.NewBoolean(false))
continue
}
// Peek at values first before popping
right := vm.stack[vm.sp-1]
left := vm.stack[vm.sp-2]
if left.Type == types.TypeNumber && right.Type == types.TypeNumber {
// Now pop them
vm.pop()
vm.pop()
vm.push(types.NewBoolean(left.Data.(float64) > right.Data.(float64)))
} else {
// Pop the values to maintain stack balance
vm.pop()
vm.pop()
fmt.Println("Error: cannot compare non-number values with >")
vm.push(types.NewBoolean(false))
}
case types.OpLessEqual:
if vm.sp < 2 {
fmt.Println("Error: not enough operands for less-equal comparison")
vm.push(types.NewBoolean(false))
continue
}
// Peek at values first before popping
right := vm.stack[vm.sp-1]
left := vm.stack[vm.sp-2]
if left.Type == types.TypeNumber && right.Type == types.TypeNumber {
// Now pop them
vm.pop()
vm.pop()
vm.push(types.NewBoolean(left.Data.(float64) <= right.Data.(float64)))
} else {
// Pop the values to maintain stack balance
vm.pop()
vm.pop()
fmt.Println("Error: cannot compare non-number values with <=")
vm.push(types.NewBoolean(false))
}
case types.OpGreaterEqual:
if vm.sp < 2 {
fmt.Println("Error: not enough operands for greater-equal comparison")
vm.push(types.NewBoolean(false))
continue
}
// Peek at values first before popping
right := vm.stack[vm.sp-1]
left := vm.stack[vm.sp-2]
if left.Type == types.TypeNumber && right.Type == types.TypeNumber {
// Now pop them
vm.pop()
vm.pop()
vm.push(types.NewBoolean(left.Data.(float64) >= right.Data.(float64)))
} else {
// Pop the values to maintain stack balance
vm.pop()
vm.pop()
fmt.Println("Error: cannot compare non-number values with >=")
vm.push(types.NewBoolean(false))
}
case types.OpNot:
operand := vm.pop()
// Consider falsy: false, null, 0
isFalsy := false
if operand.Type == types.TypeBoolean && !operand.Data.(bool) {
isFalsy = true
} else if operand.Type == types.TypeNull {
isFalsy = true
} else if operand.Type == types.TypeNumber && operand.Data.(float64) == 0 {
isFalsy = true
}
vm.push(types.NewBoolean(isFalsy))
}
}
// Return null for the top-level code when no explicit return is found
return types.NewNull()
}
func (vm *VM) push(value types.Value) {
if vm.sp >= len(vm.stack) {
// Grow stack if needed
newStack := make([]types.Value, len(vm.stack)*2)
copy(newStack, vm.stack)
vm.stack = newStack
}
vm.stack[vm.sp] = value
vm.sp++
}
func (vm *VM) pop() types.Value {
if vm.sp <= 0 {
// Return null instead of causing a panic when trying to pop from an empty stack
fmt.Println("Stack underflow error")
return types.NewNull()
}
vm.sp--
return vm.stack[vm.sp]
}
func (vm *VM) formatTable(table *types.Table) string {
result := "{"
for i, entry := range table.Entries {
result += vm.formatValue(entry.Key) + " = " + vm.formatValue(entry.Value)
if i < len(table.Entries)-1 {
result += ", "
}
}
result += "}"
return result
}
func (vm *VM) formatValue(value types.Value) string {
switch value.Type {
case types.TypeString:
return "\"" + value.Data.(string) + "\""
case types.TypeNumber:
return fmt.Sprintf("%v", value.Data.(float64))
case types.TypeBoolean:
return fmt.Sprintf("%v", value.Data.(bool))
case types.TypeNull:
return "nil"
case types.TypeTable:
return vm.formatTable(value.Data.(*types.Table))
case types.TypeFunction:
return "<function>"
default:
return "unknown"
}
}