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

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
}