Fixed peon calling convention and various errors with function calls
This commit is contained in:
parent
369dff7da2
commit
f8ab292c27
|
@ -13,14 +13,14 @@
|
|||
# limitations under the License.
|
||||
|
||||
## The Peon runtime environment
|
||||
import strutils
|
||||
import strformat
|
||||
|
||||
import types
|
||||
import ../config
|
||||
when DEBUG_TRACE_VM:
|
||||
import strformat
|
||||
import ../frontend/meta/bytecode
|
||||
import ../util/multibyte
|
||||
|
||||
|
||||
export types
|
||||
|
||||
type
|
||||
|
@ -30,9 +30,8 @@ type
|
|||
ip: int # Instruction pointer
|
||||
cache: array[6, PeonObject] # Singletons cache
|
||||
chunk: Chunk # Piece of bytecode to execute
|
||||
frames: seq[int] # Stores the initial index of stack frames
|
||||
heapVars: seq[PeonObject] # Stores variables that do not have stack semantics (i.e. "static")
|
||||
lastPop*: PeonObject # Used in the REPL
|
||||
frames: seq[int] # Stores the bottom of stack frames
|
||||
heapVars: seq[PeonObject] # Stores variables that do not have stack semantics
|
||||
|
||||
|
||||
proc initCache*(self: PeonVM) =
|
||||
|
@ -58,19 +57,19 @@ proc newPeonVM*: PeonVM =
|
|||
|
||||
## Getters for singleton types (they are cached!)
|
||||
|
||||
proc getNil*(self: PeonVM): PeonObject = self.cache[0]
|
||||
proc getNil*(self: PeonVM): PeonObject {.inline.} = self.cache[0]
|
||||
|
||||
proc getBool*(self: PeonVM, value: bool): PeonObject =
|
||||
proc getBool*(self: PeonVM, value: bool): PeonObject {.inline.} =
|
||||
if value:
|
||||
return self.cache[1]
|
||||
return self.cache[2]
|
||||
|
||||
proc getInf*(self: PeonVM, positive: bool): PeonObject =
|
||||
proc getInf*(self: PeonVM, positive: bool): PeonObject {.inline.} =
|
||||
if positive:
|
||||
return self.cache[3]
|
||||
return self.cache[4]
|
||||
|
||||
proc getNan*(self: PeonVM): PeonObject = self.cache[5]
|
||||
proc getNan*(self: PeonVM): PeonObject {.inline.} = self.cache[5]
|
||||
|
||||
## Stack primitives. Note: all stack accessing that goes
|
||||
## through the get/set wrappers is frame-relative, meaning
|
||||
|
@ -99,18 +98,21 @@ proc peek(self: PeonVM): PeonObject =
|
|||
|
||||
proc get(self: PeonVM, idx: int): PeonObject =
|
||||
## Accessor method that abstracts
|
||||
## stack accessing through stack
|
||||
## stack indexing through stack
|
||||
## frames
|
||||
return self.stack[idx + self.frames[^1]]
|
||||
|
||||
|
||||
proc set(self: PeonVM, idx: int, val: PeonObject) =
|
||||
## Setter method that abstracts
|
||||
## stack accessing through stack
|
||||
## stack indexing through stack
|
||||
## frames
|
||||
self.stack[idx + self.frames[^1]] = val
|
||||
|
||||
|
||||
## Byte-level primitives to read/decode
|
||||
## bytecode
|
||||
|
||||
proc readByte(self: PeonVM): uint8 =
|
||||
## Reads a single byte from the
|
||||
## bytecode and returns it as an
|
||||
|
@ -197,6 +199,7 @@ proc dispatch*(self: PeonVM) =
|
|||
echo &"Instruction: {instruction}"
|
||||
echo &"Stack: {self.stack}"
|
||||
echo &"Current Frame: {self.stack[self.frames[^1]..^1]}"
|
||||
echo &"Heap Vars: {self.heapVars}"
|
||||
discard readLine stdin
|
||||
case instruction:
|
||||
# Constant loading
|
||||
|
@ -216,44 +219,48 @@ proc dispatch*(self: PeonVM) =
|
|||
self.push(self.readUInt64(int(self.readLong())))
|
||||
of LoadUInt32:
|
||||
self.push(self.readUInt32(int(self.readLong())))
|
||||
of LoadInt32:
|
||||
self.push(self.readInt32(int(self.readLong())))
|
||||
of Call:
|
||||
# Calls a function. The calling convention for peon
|
||||
# functions is pretty simple: the return address sits
|
||||
# at the bottom of the stack frame, then follow the
|
||||
# arguments and all temporaries/local variables
|
||||
let newIp = self.readLong()
|
||||
# We do this because if we immediately changed
|
||||
# We store it because if we immediately changed
|
||||
# the instruction pointer, we'd read the wrong
|
||||
# value for the argument count. Storing it and
|
||||
# changing it later fixes this issue
|
||||
self.frames.add(int(self.readLong()))
|
||||
self.ip = int(newIp)
|
||||
of OpCode.Return:
|
||||
# Returns from a void function or terminates the
|
||||
# program entirely if we're at the topmost frame
|
||||
# Returns from a void function
|
||||
let frame = self.frames.pop()
|
||||
if self.frames.len() > 1:
|
||||
for i in countdown(self.stack.high(), frame):
|
||||
discard self.pop()
|
||||
self.ip = int(self.pop().uInt)
|
||||
else:
|
||||
while self.stack.len() > 0:
|
||||
discard self.pop()
|
||||
return
|
||||
for i in 0..<frame - 1:
|
||||
discard self.pop()
|
||||
self.ip = self.pop().uInt.int
|
||||
discard self.pop() # Nil
|
||||
of ProgExit:
|
||||
# Exits the VM's loop
|
||||
while self.stack.len() > 0:
|
||||
discard self.pop()
|
||||
discard self.frames.pop()
|
||||
return
|
||||
of ReturnValue:
|
||||
# Returns from a function which has a return value,
|
||||
# pushing it on the stack
|
||||
let retVal = self.pop()
|
||||
let frame = self.frames.pop()
|
||||
for i in countdown(frame, 1):
|
||||
for i in 0..<frame:
|
||||
discard self.pop()
|
||||
self.ip = int(self.pop().uInt)
|
||||
discard self.pop() # Nil
|
||||
self.push(retVal)
|
||||
of StoreVar:
|
||||
# Stores the value at the top of the stack
|
||||
# into the given stack index
|
||||
self.set(int(self.readLong()), self.pop())
|
||||
of StoreHeap:
|
||||
of StoreClosure:
|
||||
# Stores/updates the value of a closed-over
|
||||
# variable
|
||||
let idx = self.readLong().int
|
||||
|
@ -261,17 +268,53 @@ proc dispatch*(self: PeonVM) =
|
|||
self.heapVars.add(self.pop())
|
||||
else:
|
||||
self.heapVars[idx] = self.pop()
|
||||
of LoadHeap:
|
||||
of LoadClosure:
|
||||
# Loads a closed-over variable onto the
|
||||
# stack
|
||||
self.push(self.heapVars[self.readLong()])
|
||||
of PopClosure:
|
||||
# Pops a closed-over variable off the closure
|
||||
# array
|
||||
discard self.heapVars.pop()
|
||||
of LoadVar:
|
||||
# Stores/updates the value of a local variable
|
||||
self.push(self.get(int(self.readLong())))
|
||||
of NoOp:
|
||||
continue
|
||||
of Pop:
|
||||
self.lastPop = self.pop()
|
||||
discard self.pop()
|
||||
of PopRepl:
|
||||
let popped = self.pop()
|
||||
case popped.kind:
|
||||
of Int64:
|
||||
echo &"{popped.long}'i64"
|
||||
of UInt64:
|
||||
echo &"{popped.uLong}'u64"
|
||||
of Int32:
|
||||
echo &"{popped.`int`}'i32"
|
||||
of UInt32:
|
||||
echo &"{popped.uInt}'u32"
|
||||
of Int16:
|
||||
echo &"{popped.short}'i16"
|
||||
of UInt16:
|
||||
echo &"{popped.uShort}'u16"
|
||||
of Int8:
|
||||
echo &"{popped.tiny}'i8"
|
||||
of UInt8:
|
||||
echo &"{popped.uTiny}'u8"
|
||||
of ObjectKind.Inf:
|
||||
if popped.positive:
|
||||
echo "inf"
|
||||
else:
|
||||
echo "-inf"
|
||||
of ObjectKind.Nan, Nil:
|
||||
echo ($popped.kind).toLowerAscii()
|
||||
else:
|
||||
discard
|
||||
of PopN:
|
||||
for _ in 0..<int(self.readShort()):
|
||||
discard self.pop()
|
||||
# Jump opcodes
|
||||
of Jump:
|
||||
self.ip = int(self.readShort())
|
||||
of JumpForwards:
|
||||
|
@ -321,5 +364,4 @@ proc run*(self: PeonVM, chunk: Chunk) =
|
|||
self.frames = @[0]
|
||||
self.stack = @[]
|
||||
self.ip = 0
|
||||
self.lastPop = self.getNil()
|
||||
self.dispatch()
|
||||
|
|
|
@ -135,6 +135,11 @@ type
|
|||
# The current loop being compiled (used to
|
||||
# keep track of where to jump)
|
||||
currentLoop: Loop
|
||||
# Are we in REPL mode? If so, Pop instructions
|
||||
# for expression statements emit a special
|
||||
# PopRepl instruction that stores the value
|
||||
# to be printed once the expression is evaluated
|
||||
replMode: bool
|
||||
# The current module being compiled
|
||||
# (used to restrict access to statically
|
||||
# defined variables at compile time)
|
||||
|
@ -155,7 +160,7 @@ type
|
|||
closedOver: seq[Name]
|
||||
|
||||
|
||||
proc newCompiler*(enableOptimizations: bool = true): Compiler =
|
||||
proc newCompiler*(enableOptimizations: bool = true, replMode: bool = false): Compiler =
|
||||
## Initializes a new Compiler object
|
||||
new(result)
|
||||
result.ast = @[]
|
||||
|
@ -165,6 +170,7 @@ proc newCompiler*(enableOptimizations: bool = true): Compiler =
|
|||
result.scopeDepth = 0
|
||||
result.currentFunction = nil
|
||||
result.enableOptimizations = enableOptimizations
|
||||
result.replMode = replMode
|
||||
result.currentModule = ""
|
||||
result.frames = @[]
|
||||
|
||||
|
@ -405,7 +411,7 @@ proc detectClosureVariable(self: Compiler, name: Name,
|
|||
return
|
||||
if name.depth > 0 and name.depth < depth:
|
||||
# Ding! The given name is closed over: we need to
|
||||
# change the NoOp instructions that self.declareName
|
||||
# change the dummy Jump instruction that self.declareName
|
||||
# put in place for us into a StoreHeap. We don't need to change
|
||||
# other pieces of code because self.identifier() already
|
||||
# emits LoadHeap if it detects the variable is closed over,
|
||||
|
@ -414,7 +420,7 @@ proc detectClosureVariable(self: Compiler, name: Name,
|
|||
let idx = self.closedOver.high().toTriple()
|
||||
if self.closedOver.len() >= 16777216:
|
||||
self.error("too many consecutive closed-over variables (max is 16777216)")
|
||||
self.chunk.code[name.codePos] = StoreHeap.uint8
|
||||
self.chunk.code[name.codePos] = StoreClosure.uint8
|
||||
self.chunk.code[name.codePos + 1] = idx[0]
|
||||
self.chunk.code[name.codePos + 2] = idx[1]
|
||||
self.chunk.code[name.codePos + 3] = idx[2]
|
||||
|
@ -428,9 +434,9 @@ proc compareTypes(self: Compiler, a, b: Type): bool =
|
|||
# The nil code here is for void functions (when
|
||||
# we compare their return types)
|
||||
if a == nil:
|
||||
return b == nil
|
||||
return b == nil or b.kind == Any
|
||||
elif b == nil:
|
||||
return a == nil
|
||||
return a == nil or a.kind == Any
|
||||
elif a.kind == Any or b.kind == Any:
|
||||
# This is needed internally: user code
|
||||
# cannot generate code for matching
|
||||
|
@ -598,6 +604,8 @@ proc inferType(self: Compiler, node: Expression): Type =
|
|||
let resolved = self.resolve(IdentExpr(node.callee))
|
||||
if resolved != nil:
|
||||
result = resolved.valueType.returnType
|
||||
if result == nil:
|
||||
result = Type(kind: Any)
|
||||
else:
|
||||
result = nil
|
||||
of lambdaExpr:
|
||||
|
@ -790,33 +798,31 @@ proc matchImpl(self: Compiler, name: string, kind: Type): Name =
|
|||
return impl[0]
|
||||
|
||||
|
||||
proc callUnaryOp(self: Compiler, fn: Name, op: UnaryExpr) =
|
||||
## Emits the code to call a unary operator
|
||||
# Pushes the return address
|
||||
proc generateCall(self: Compiler, fn: Name, args: seq[Expression]) =
|
||||
## Small wrapper that abstracts emitting a call instruction
|
||||
## for a given function
|
||||
self.emitByte(LoadNil) # Stack alignment
|
||||
self.emitByte(LoadUInt32)
|
||||
# We patch it later!
|
||||
let idx = self.chunk.consts.len()
|
||||
self.emitBytes(self.chunk.writeConstant((0xffffffff'u32).toQuad()))
|
||||
self.expression(op.a) # Pushes the arguments onto the stack
|
||||
for argument in args:
|
||||
self.expression(argument) # Pushes the arguments onto the stack
|
||||
self.emitByte(Call) # Creates a stack frame
|
||||
self.emitBytes(fn.codePos.toTriple())
|
||||
self.emitBytes(1.toTriple())
|
||||
self.emitBytes((args.len()).toTriple())
|
||||
self.patchReturnAddress(idx)
|
||||
|
||||
|
||||
proc callUnaryOp(self: Compiler, fn: Name, op: UnaryExpr) =
|
||||
## Emits the code to call a unary operator
|
||||
self.generateCall(fn, @[op.a])
|
||||
|
||||
|
||||
proc callBinaryOp(self: Compiler, fn: Name, op: BinaryExpr) =
|
||||
## Emits the code to call a binary operator
|
||||
# Pushes the return address
|
||||
self.emitByte(LoadUInt32)
|
||||
# We patch it later!
|
||||
let idx = self.chunk.consts.len()
|
||||
self.emitBytes(self.chunk.writeConstant((0xffffffff'u32).toQuad()))
|
||||
self.expression(op.a) # Pushes the arguments onto the stack
|
||||
self.expression(op.b)
|
||||
self.emitByte(Call) # Creates a stack frame
|
||||
self.emitBytes(fn.codePos.toTriple())
|
||||
self.emitBytes(2.toTriple())
|
||||
self.patchReturnAddress(idx)
|
||||
self.generateCall(fn, @[op.a, op.b])
|
||||
|
||||
|
||||
proc unary(self: Compiler, node: UnaryExpr) =
|
||||
|
@ -888,11 +894,21 @@ proc declareName(self: Compiler, node: Declaration) =
|
|||
isLet: node.isLet,
|
||||
isClosedOver: false,
|
||||
line: node.token.line))
|
||||
# We emit 4 No-Ops because they may become a
|
||||
# StoreHeap instruction. If not, they'll be
|
||||
# removed before the compiler is finished
|
||||
# TODO: This may break CFI offsets
|
||||
self.emitBytes([NoOp, NoOp, NoOp, NoOp])
|
||||
# We emit a jump of 0 because this may become a
|
||||
# StoreHeap instruction. If they variable is
|
||||
# not closed over, we'll sadly be wasting a
|
||||
# VM cycle. The previous implementation used 4 no-op
|
||||
# instructions, which wasted 4 times as many clock
|
||||
# cycles.
|
||||
# TODO: Optimize this. It's a bit tricky because
|
||||
# deleting bytecode would render all of our
|
||||
# jump offsets and other absolute indeces in the
|
||||
# bytecode wrong
|
||||
if self.scopeDepth > 0:
|
||||
# Closure variables are only used in local
|
||||
# scopes
|
||||
self.emitByte(LongJumpForwards)
|
||||
self.emitBytes(0.toTriple())
|
||||
of NodeKind.funDecl:
|
||||
var node = FunDecl(node)
|
||||
self.names.add(Name(depth: self.scopeDepth,
|
||||
|
@ -965,15 +981,15 @@ proc identifier(self: Compiler, node: IdentExpr) =
|
|||
# were, self.resolve() would have returned nil
|
||||
if not t.closedOver:
|
||||
# Static name resolution, loads value at index in the stack. Very fast. Much wow.
|
||||
if self.scopeDepth > 0:
|
||||
inc(index, 1)
|
||||
self.emitByte(LoadVar)
|
||||
if self.frames.len() > 1:
|
||||
inc(index, 2)
|
||||
self.emitBytes((index - self.frames[^1]).toTriple())
|
||||
else:
|
||||
# Heap-allocated closure variable. Stored in a separate "closure array" in the VM that does not have stack semantics.
|
||||
# This makes closures work as expected and is not much slower than indexing our stack (since they're both
|
||||
# dynamic arrays at runtime anyway)
|
||||
self.emitByte(LoadHeap)
|
||||
# Heap-allocated closure variable. Stored in a separate "closure array" in the VM that does not have stack semantics
|
||||
# and where the no-effect invariant is not kept. This makes closures work as expected and is not much slower than
|
||||
# indexing our stack (since they're both dynamic arrays at runtime anyway)
|
||||
self.emitByte(LoadClosure)
|
||||
self.emitBytes(self.closedOver.high().toTriple())
|
||||
|
||||
|
||||
|
@ -997,7 +1013,7 @@ proc assignment(self: Compiler, node: ASTNode) =
|
|||
if not t.closedOver:
|
||||
self.emitByte(StoreVar)
|
||||
else:
|
||||
self.emitByte(StoreHeap)
|
||||
self.emitByte(StoreClosure)
|
||||
self.emitBytes(index.toTriple())
|
||||
else:
|
||||
self.error(&"reference to undeclared name '{node.token.lexeme}'")
|
||||
|
@ -1017,7 +1033,7 @@ proc beginScope(self: Compiler) =
|
|||
inc(self.scopeDepth)
|
||||
|
||||
|
||||
proc endScope(self: Compiler) =
|
||||
proc endScope(self: Compiler, fromFunc: bool = false) =
|
||||
## Ends the current local scope
|
||||
if self.scopeDepth < 0:
|
||||
self.error("cannot call endScope with scopeDepth < 0 (This is an internal error and most likely a bug)")
|
||||
|
@ -1025,18 +1041,12 @@ proc endScope(self: Compiler) =
|
|||
var names: seq[Name] = @[]
|
||||
for name in self.names:
|
||||
if name.depth > self.scopeDepth:
|
||||
if name.valueType.kind != Function and OpCode(self.chunk.code[name.codePos]) == NoOp:
|
||||
for _ in countup(0, 3):
|
||||
# Since by deleting it the size of the
|
||||
# sequence decreases, we don't need to
|
||||
# increase the index
|
||||
self.chunk.code.delete(name.codePos)
|
||||
names.add(name)
|
||||
if not self.enableOptimizations:
|
||||
if not self.enableOptimizations and not fromFunc:
|
||||
# All variables with a scope depth larger than the current one
|
||||
# are now out of scope. Begone, you're now homeless!
|
||||
self.emitByte(Pop)
|
||||
if self.enableOptimizations and len(names) > 1:
|
||||
if self.enableOptimizations and len(names) > 1 and not fromFunc:
|
||||
# If we're popping less than 65535 variables, then
|
||||
# we can emit a PopN instruction. This is true for
|
||||
# 99.99999% of the use cases of the language (who the
|
||||
|
@ -1050,7 +1060,7 @@ proc endScope(self: Compiler) =
|
|||
for i in countdown(self.names.high(), len(names) - uint16.high().int()):
|
||||
if self.names[i].depth > self.scopeDepth:
|
||||
self.emitByte(Pop)
|
||||
elif len(names) == 1:
|
||||
elif len(names) == 1 and not fromFunc:
|
||||
# We only emit PopN if we're popping more than one value
|
||||
self.emitByte(Pop)
|
||||
# This seems *really* slow, but
|
||||
|
@ -1068,6 +1078,7 @@ proc endScope(self: Compiler) =
|
|||
for name in names:
|
||||
if name.isClosedOver:
|
||||
self.closedOver.delete(idx)
|
||||
self.emitByte(PopClosure)
|
||||
inc(idx)
|
||||
|
||||
|
||||
|
@ -1136,9 +1147,10 @@ proc whileStmt(self: Compiler, node: WhileStmt) =
|
|||
self.emitLoop(start)
|
||||
|
||||
|
||||
proc callFunction(self: Compiler, node: CallExpr) =
|
||||
proc callExpr(self: Compiler, node: CallExpr) =
|
||||
## Compiles code to call a function
|
||||
var args: seq[tuple[name: string, kind: Type]] = @[]
|
||||
var argExpr: seq[Expression] = @[]
|
||||
var kind: Type
|
||||
# TODO: Keyword arguments
|
||||
for i, argument in node.arguments.positionals:
|
||||
|
@ -1148,6 +1160,7 @@ proc callFunction(self: Compiler, node: CallExpr) =
|
|||
self.error(&"reference to undeclared identifier '{IdentExpr(argument).name.lexeme}'")
|
||||
self.error(&"cannot infer the type of argument {i + 1} in function call")
|
||||
args.add(("", kind))
|
||||
argExpr.add(argument)
|
||||
for argument in node.arguments.keyword:
|
||||
discard
|
||||
if args.len() >= 16777216:
|
||||
|
@ -1158,16 +1171,7 @@ proc callFunction(self: Compiler, node: CallExpr) =
|
|||
funct = self.matchImpl(IdentExpr(node.callee).name.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: args))
|
||||
else:
|
||||
discard # TODO: Lambdas
|
||||
self.emitByte(LoadUInt32)
|
||||
# We patch it later!
|
||||
let idx = self.chunk.consts.len()
|
||||
self.emitBytes(self.chunk.writeConstant((0xffffffff'u32).toQuad()))
|
||||
for argument in node.arguments.positionals:
|
||||
self.expression(argument)
|
||||
self.emitByte(Call) # Creates a stack frame
|
||||
self.emitBytes(funct.codePos.toTriple())
|
||||
self.emitBytes(args.len().toTriple())
|
||||
self.patchReturnAddress(idx)
|
||||
self.generateCall(funct, argExpr)
|
||||
|
||||
|
||||
proc expression(self: Compiler, node: Expression) =
|
||||
|
@ -1178,8 +1182,8 @@ proc expression(self: Compiler, node: Expression) =
|
|||
# error in self.identifier()
|
||||
self.error("expression has no type")
|
||||
case node.kind:
|
||||
of callExpr:
|
||||
self.callFunction(CallExpr(node)) # TODO
|
||||
of NodeKind.callExpr:
|
||||
self.callExpr(CallExpr(node)) # TODO
|
||||
of getItemExpr:
|
||||
discard # TODO
|
||||
# Note that for setItem and assign we don't convert
|
||||
|
@ -1243,9 +1247,11 @@ proc returnStmt(self: Compiler, node: ReturnStmt) =
|
|||
let typ = self.inferType(self.currentFunction)
|
||||
## Having the return type
|
||||
if returnType == nil and typ.returnType != nil:
|
||||
if node.value.kind == identExpr:
|
||||
self.error(&"reference to undeclared identifier '{node.value.token.lexeme}'")
|
||||
self.error(&"expected return value of type '{self.typeToStr(typ.returnType)}', but expression has no type")
|
||||
elif typ.returnType == nil and returnType != nil:
|
||||
self.error("empty return statement is not allowed in non-void functions")
|
||||
self.error("non-empty return statement is not allowed in void functions")
|
||||
elif not self.compareTypes(returnType, typ.returnType):
|
||||
self.error(&"expected return value of type '{self.typeToStr(typ.returnType)}', got '{self.typeToStr(returnType)}' instead")
|
||||
if node.value != nil:
|
||||
|
@ -1316,7 +1322,11 @@ proc statement(self: Compiler, node: Statement) =
|
|||
of exprStmt:
|
||||
var expression = ExprStmt(node).expression
|
||||
self.expression(expression)
|
||||
self.emitByte(Pop) # Expression statements discard their value. Their main use case is side effects in function calls
|
||||
# We only print top-level expressions
|
||||
if self.replMode and self.scopeDepth == 0:
|
||||
self.emitByte(PopRepl)
|
||||
else:
|
||||
self.emitByte(Pop) # Expression statements discard their value. Their main use case is side effects in function calls
|
||||
of NodeKind.ifStmt:
|
||||
self.ifStmt(IfStmt(node))
|
||||
of NodeKind.assertStmt:
|
||||
|
@ -1375,11 +1385,21 @@ proc funDecl(self: Compiler, node: FunDecl) =
|
|||
## Compiles function declarations
|
||||
# A function's code is just compiled linearly
|
||||
# and then jumped over
|
||||
self.emitByte(LoadNil) # Aligns the stack
|
||||
|
||||
# While the compiler stores functions as if they
|
||||
# were on the stack, in reality there is nothing
|
||||
# at runtime that represents them (instead, we
|
||||
# compile everything in a single blob and jump back
|
||||
# and forth). If we didn't align the stack to account
|
||||
# for this behavior, all of our offsets would be off
|
||||
# by how many functions we have declared. We could fix
|
||||
# this by storing functions in a separate list of identifiers,
|
||||
# but that's rather unelegant and requires specialized functions
|
||||
# for looking identifiers vs functions, which is not ideal
|
||||
let jmp = self.emitJump(JumpForwards)
|
||||
var function = self.currentFunction
|
||||
self.declareName(node)
|
||||
self.frames.add(self.names.len())
|
||||
self.frames.add(self.names.len() - 1)
|
||||
# TODO: Forward declarations
|
||||
if node.body != nil:
|
||||
if BlockStmt(node.body).code.len() == 0:
|
||||
|
@ -1409,18 +1429,11 @@ proc funDecl(self: Compiler, node: FunDecl) =
|
|||
# We let our debugger know a function is starting
|
||||
let start = self.chunk.code.high()
|
||||
|
||||
self.blockStmt(BlockStmt(node.body))
|
||||
# Yup, we're done. That was easy, huh?
|
||||
# But, after all, functions are just named
|
||||
# scopes, and we compile them just like that:
|
||||
# we declare their name and arguments (before
|
||||
# their body so recursion works) and then just
|
||||
# handle them as a block statement (which takes
|
||||
# care of incrementing self.scopeDepth so locals
|
||||
# are resolved properly). There's a need for a bit
|
||||
# of boilerplate code to make closures work, but
|
||||
# that's about it
|
||||
|
||||
self.beginScope()
|
||||
# self.emitByte(LoadNil)
|
||||
for decl in BlockStmt(node.body).code:
|
||||
self.declaration(decl)
|
||||
self.endScope(fromFunc=true)
|
||||
case self.currentFunction.kind:
|
||||
of NodeKind.funDecl:
|
||||
if not self.currentFunction.hasExplicitReturn:
|
||||
|
@ -1497,8 +1510,7 @@ proc compile*(self: Compiler, ast: seq[Declaration], file: string): Chunk =
|
|||
self.declaration(Declaration(self.step()))
|
||||
if self.ast.len() > 0:
|
||||
# *Technically* an empty program is a valid program
|
||||
self.endScope()
|
||||
self.emitByte(OpCode.Return) # Exits the VM's main loop when used at the global scope
|
||||
self.emitByte(ProgExit)
|
||||
result = self.chunk
|
||||
if self.ast.len() > 0 and self.scopeDepth != -1:
|
||||
self.error(&"invalid state: invalid scopeDepth value (expected -1, got {self.scopeDepth}), did you forget to call endScope/beginScope?")
|
||||
if self.ast.len() > 0 and self.scopeDepth != 0:
|
||||
self.error(&"invalid state: invalid scopeDepth value (expected 0, got {self.scopeDepth}), did you forget to call endScope/beginScope?")
|
||||
|
|
|
@ -90,14 +90,16 @@ type
|
|||
LoadInf,
|
||||
## Basic stack operations
|
||||
Pop, # Pops an element off the stack and discards it
|
||||
PopRepl, # Same as Pop, but also prints the value of what's popped (used in REPL mode)
|
||||
Push, # Pushes x onto the stack
|
||||
PopN, # Pops x elements off the stack (optimization for exiting local scopes which usually pop many elements)
|
||||
## Name resolution/handling
|
||||
LoadAttribute, # Pushes the attribute b of object a onto the stack
|
||||
LoadVar, # Pushes the object at position x in the stack onto the stack
|
||||
StoreVar, # Stores the value of b at position a in the stack
|
||||
LoadHeap, # Pushes the object position x in the closure array onto the stack
|
||||
StoreHeap, # Stores the value of b at position a in the closure array
|
||||
LoadClosure, # Pushes the object position x in the closure array onto the stack
|
||||
StoreClosure, # Stores the value of b at position a in the closure array
|
||||
PopClosure, # Pops a closed-over variable from the closure array
|
||||
## Looping and jumping
|
||||
Jump, # Absolute, unconditional jump into the bytecode
|
||||
JumpForwards, # Relative, unconditional, positive jump in the bytecode
|
||||
|
@ -129,6 +131,7 @@ type
|
|||
## Misc
|
||||
Assert, # Raises an AssertionFailed exception if x is false
|
||||
NoOp, # Just a no-op
|
||||
ProgExit, # Terminates the whole program
|
||||
|
||||
|
||||
# We group instructions by their operation/operand types for easier handling when debugging
|
||||
|
@ -137,9 +140,10 @@ type
|
|||
const simpleInstructions* = {Return, LoadNil,
|
||||
LoadTrue, LoadFalse,
|
||||
LoadNan, LoadInf,
|
||||
Pop, Raise, BeginTry,
|
||||
FinishTry, Yield,
|
||||
Await, NoOp, ReturnValue}
|
||||
Pop, PopRepl, Raise,
|
||||
BeginTry, FinishTry, Yield,
|
||||
Await, NoOp, ReturnValue,
|
||||
PopClosure, ProgExit}
|
||||
|
||||
# Constant instructions are instructions that operate on the bytecode constant table
|
||||
const constantInstructions* = {LoadInt64, LoadUInt64,
|
||||
|
@ -151,7 +155,7 @@ const constantInstructions* = {LoadInt64, LoadUInt64,
|
|||
|
||||
# Stack triple instructions operate on the stack at arbitrary offsets and pop arguments off of it in the form
|
||||
# of 24 bit integers
|
||||
const stackTripleInstructions* = {StoreVar, LoadVar, LoadHeap, StoreHeap}
|
||||
const stackTripleInstructions* = {StoreVar, LoadVar, LoadCLosure, StoreClosure}
|
||||
|
||||
# Stack double instructions operate on the stack at arbitrary offsets and pop arguments off of it in the form
|
||||
# of 16 bit integers
|
||||
|
|
29
src/main.nim
29
src/main.nim
|
@ -47,7 +47,7 @@ proc repl =
|
|||
serialized: Serialized
|
||||
tokenizer = newLexer()
|
||||
parser = newParser()
|
||||
compiler = newCompiler()
|
||||
compiler = newCompiler(replMode=true)
|
||||
debugger = newDebugger()
|
||||
serializer = newSerializer()
|
||||
vm = newPeonVM()
|
||||
|
@ -136,33 +136,6 @@ proc repl =
|
|||
when debugRuntime:
|
||||
styledEcho fgCyan, "\n\nExecution step: "
|
||||
vm.run(serialized.chunk)
|
||||
var popped = vm.lastPop
|
||||
case popped.kind:
|
||||
of Int64:
|
||||
echo &"{popped.long}'i64"
|
||||
of UInt64:
|
||||
echo &"{popped.uLong}'u64"
|
||||
of Int32:
|
||||
echo &"{popped.`int`}'i32"
|
||||
of UInt32:
|
||||
echo &"{popped.uInt}'u32"
|
||||
of Int16:
|
||||
echo &"{popped.short}'i16"
|
||||
of UInt16:
|
||||
echo &"{popped.uShort}'u16"
|
||||
of Int8:
|
||||
echo &"{popped.tiny}'i8"
|
||||
of UInt8:
|
||||
echo &"{popped.uTiny}'u8"
|
||||
of ObjectKind.Inf:
|
||||
if popped.positive:
|
||||
echo "inf"
|
||||
else:
|
||||
echo "-inf"
|
||||
of ObjectKind.Nan, Nil:
|
||||
echo ($popped.kind).toLowerAscii()
|
||||
else:
|
||||
discard
|
||||
except LexingError:
|
||||
input = ""
|
||||
let exc = LexingError(getCurrentException())
|
||||
|
|
|
@ -131,7 +131,7 @@ proc callInstruction(self: Debugger, instruction: OpCode) =
|
|||
var args = [self.chunk.code[self.current + 4], self.chunk.code[self.current + 5], self.chunk.code[self.current + 6]].fromTriple()
|
||||
printInstruction(instruction)
|
||||
stdout.styledWrite(fgGreen, &", jumps to address ", fgYellow, $slot, fgGreen, " with ", fgYellow, $args, fgGreen, " argument")
|
||||
if args > 1:
|
||||
if args > 1 or args == 0:
|
||||
stdout.styledWrite(fgGreen, "s")
|
||||
nl()
|
||||
self.current += 7
|
||||
|
|
|
@ -3,12 +3,7 @@ fn outer {
|
|||
fn inner {
|
||||
var y = x;
|
||||
}
|
||||
inner();
|
||||
}
|
||||
|
||||
|
||||
fn outerTwo: fn: int {
|
||||
fn inner: int {
|
||||
return 0;
|
||||
}
|
||||
return inner;
|
||||
}
|
||||
outer();
|
||||
|
|
Loading…
Reference in New Issue