Simplified calling convention, added for PeonObject, added some comments, fixed bug with StoreVar in stack frames, fixed issues with functions assigned to variables, changed the way closures are emitted so that empty jumps are no longer needed

This commit is contained in:
Mattia Giambirtone 2022-07-09 12:47:53 +02:00
parent 2c6325d33b
commit cc0aab850e
7 changed files with 283 additions and 316 deletions

View File

@ -287,6 +287,41 @@ proc constReadFloat64(self: PeonVM, idx: int): PeonObject =
copyMem(result.`float`.addr, arr.addr, sizeof(arr)) copyMem(result.`float`.addr, arr.addr, sizeof(arr))
proc `$`(self: PeonObject): string =
case self.kind:
of Int64:
result = &"{self.long}'i64"
of UInt64:
result = &"{self.uLong}'u64"
of Int32:
result = &"{self.`int`}'i32"
of UInt32:
result = &"{self.uInt}'u32"
of Int16:
result = &"{self.short}'i16"
of UInt16:
result = &"{self.uShort}'u16"
of Int8:
result = &"{self.tiny}'i8"
of UInt8:
result = &"{self.uTiny}'u8"
of Float32:
result = &"{self.halfFloat}'f32"
of Float64:
result = &"{self.`float`}'f64"
of ObjectKind.Inf:
if self.positive:
result ="inf"
else:
result ="-inf"
of ObjectKind.Nan, Nil:
result =($self.kind).toLowerAscii()
of Function:
result = &"fn(ip: {self.ip})"
else:
discard
proc dispatch*(self: PeonVM) = proc dispatch*(self: PeonVM) =
## Main bytecode dispatch loop ## Main bytecode dispatch loop
var instruction {.register.}: OpCode var instruction {.register.}: OpCode
@ -342,11 +377,13 @@ proc dispatch*(self: PeonVM) =
of LoadFloat64: of LoadFloat64:
self.push(self.constReadFloat64(int(self.readLong()))) self.push(self.constReadFloat64(int(self.readLong())))
of LoadFunction: of LoadFunction:
self.pushc(PeonObject(kind: Function, ip: self.readLong())) # Loads a function onto the operand stack by reading its
of LoadFunctionObj: # instruction pointer
self.push(PeonObject(kind: Function, ip: self.readLong())) self.push(PeonObject(kind: Function, ip: self.readLong()))
of LoadReturnAddress: of LoadReturnAddress:
self.pushc(PeonObject(kind: UInt32, uInt: self.readUInt())) # Loads a 32-bit unsigned integer. Used to load function
# return addresses
self.push(PeonObject(kind: UInt32, uInt: self.readUInt()))
of Call: of Call:
# Calls a function. The calling convention for peon # Calls a function. The calling convention for peon
# functions is pretty simple: the first item in the # functions is pretty simple: the first item in the
@ -354,20 +391,34 @@ proc dispatch*(self: PeonVM) =
# instruction pointer to jump to, followed by a 32-bit # instruction pointer to jump to, followed by a 32-bit
# return address. After that, all arguments and locals # return address. After that, all arguments and locals
# follow # follow
var size {.used.} = self.readLong().int var argc {.used.} = self.readLong().int
self.frames.add(self.calls.len() - 2) let retAddr = self.peek(-argc) # Return address
self.ip = self.peekc(-1).ip let fnObj = self.peek(-argc - 1) # Function object
self.ip = fnObj.ip
self.pushc(fnObj)
self.pushc(retAddr)
# Creates a new result slot for the
# function's return value
self.results.add(self.getNil()) self.results.add(self.getNil())
# TODO: Use the frame size once # Creates a new call frame
self.frames.add(self.calls.len() - 2)
# Loads the arguments onto the stack
for _ in 0..<argc:
self.pushc(self.pop())
# Pops the function object and
# return address off the operand
# stack since they're not needed
# there anymore
discard self.pop()
discard self.pop()
# TODO: Use the frame's initial size once
# we have more control over the # we have more control over the
# memory # memory
#[while size > 0: #[while argc > 0:
dec(size) dec(argc)
self.pushc(self.getNil()) self.pushc(self.getNil())
]# ]#
of LoadArgument: of Return:
self.pushc(self.pop())
of OpCode.Return:
# Returns from a function. # Returns from a function.
# Every peon program is wrapped # Every peon program is wrapped
# in a hidden function, so this # in a hidden function, so this
@ -380,6 +431,7 @@ proc dispatch*(self: PeonVM) =
self.push(self.results.pop()) self.push(self.results.pop())
else: else:
discard self.results.pop() discard self.results.pop()
# Discard a stack frame
discard self.frames.pop() discard self.frames.pop()
if self.frames.len() == 0: if self.frames.len() == 0:
# End of the program! # End of the program!
@ -392,11 +444,11 @@ proc dispatch*(self: PeonVM) =
of StoreVar: of StoreVar:
# Stores the value at the top of the operand stack # Stores the value at the top of the operand stack
# into the given call stack index # into the given call stack index
let idx = int(self.readLong()) + self.frames[^1] let idx = int(self.readLong())
if idx <= self.calls.high(): if idx + self.frames[^1] <= self.calls.high():
self.setc(idx, self.pop()) self.setc(idx, self.pop())
else: else:
self.calls.add(self.pop()) self.pushc(self.pop())
of StoreClosure: of StoreClosure:
# Stores/updates the value of a closed-over # Stores/updates the value of a closed-over
# variable # variable
@ -411,56 +463,30 @@ proc dispatch*(self: PeonVM) =
# Loads a closed-over variable onto the # Loads a closed-over variable onto the
# stack # stack
self.push(self.closedOver[self.readLong()]) self.push(self.closedOver[self.readLong()])
of PopClosure:
# Removes a closed-over variable from the closure
# array
discard self.closedOver.pop()
of LoadVar: of LoadVar:
# Pushes a variable onto the operand
# stack
self.push(self.getc(int(self.readLong()))) self.push(self.getc(int(self.readLong())))
of NoOp: of NoOp:
# Does nothing
continue continue
of PopC: of PopC:
# Pops a value off the call stack
discard self.popc() discard self.popc()
of Pop: of Pop:
# Pops a value off the operand stack
discard self.pop() discard self.pop()
of PushC: of PushC:
# Pushes a value from the operand stack
# onto the call stack
self.pushc(self.pop()) self.pushc(self.pop())
of PopRepl: of PopRepl:
if self.frames.len() > 1: if self.frames.len() > 1:
discard self.pop() discard self.pop()
continue continue
let popped = self.pop() echo 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 Float32:
echo &"{popped.halfFloat}'f32"
of Float64:
echo &"{popped.`float`}'f64"
of ObjectKind.Inf:
if popped.positive:
echo "inf"
else:
echo "-inf"
of ObjectKind.Nan, Nil:
echo ($popped.kind).toLowerAscii()
else:
discard
of PopN: of PopN:
# Pops N elements off the call stack
for _ in 0..<int(self.readShort()): for _ in 0..<int(self.readShort()):
discard self.popc() discard self.popc()
# Jump opcodes # Jump opcodes

View File

@ -27,7 +27,7 @@ when len(PEON_COMMIT_HASH) != 40:
const PEON_BRANCH* = "master" const PEON_BRANCH* = "master"
when len(PEON_BRANCH) > 255: when len(PEON_BRANCH) > 255:
{.fatal: "The git branch name's length must be less than or equal to 255 characters".} {.fatal: "The git branch name's length must be less than or equal to 255 characters".}
const DEBUG_TRACE_VM* = false # Traces VM execution const DEBUG_TRACE_VM* = true # Traces VM execution
const DEBUG_TRACE_GC* = false # Traces the garbage collector (TODO) const DEBUG_TRACE_GC* = false # Traces the garbage collector (TODO)
const DEBUG_TRACE_ALLOCATION* = false # Traces memory allocation/deallocation const DEBUG_TRACE_ALLOCATION* = false # Traces memory allocation/deallocation
const DEBUG_TRACE_COMPILER* = false # Traces the compiler const DEBUG_TRACE_COMPILER* = false # Traces the compiler

View File

@ -102,6 +102,10 @@ type
isFunctionArgument: bool isFunctionArgument: bool
# Where is this node declared in the file? # Where is this node declared in the file?
line: int line: int
# Is this a function declaration or a variable
# with a function as value? (The distinction *is*
# important! Check emitFunction())
isFunDecl: bool
Loop = object Loop = object
## A "loop object" used ## A "loop object" used
## by the compiler to emit ## by the compiler to emit
@ -181,11 +185,11 @@ proc declaration(self: Compiler, node: Declaration)
proc peek(self: Compiler, distance: int = 0): ASTNode proc peek(self: Compiler, distance: int = 0): ASTNode
proc identifier(self: Compiler, node: IdentExpr) proc identifier(self: Compiler, node: IdentExpr)
proc varDecl(self: Compiler, node: VarDecl) proc varDecl(self: Compiler, node: VarDecl)
proc inferType(self: Compiler, node: LiteralExpr, strictMutable: bool = true): Type proc inferType(self: Compiler, node: LiteralExpr): Type
proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Type proc inferType(self: Compiler, node: Expression): Type
proc findByName(self: Compiler, name: string): seq[Name] proc findByName(self: Compiler, name: string): seq[Name]
proc findByType(self: Compiler, name: string, kind: Type, strictMutable: bool = true): seq[Name] proc findByType(self: Compiler, name: string, kind: Type): seq[Name]
proc compareTypes(self: Compiler, a, b: Type, strictMutable: bool = true): bool proc compareTypes(self: Compiler, a, b: Type): bool
proc patchReturnAddress(self: Compiler, pos: int) proc patchReturnAddress(self: Compiler, pos: int)
proc handleMagicPragma(self: Compiler, pragma: Pragma, node: ASTnode) proc handleMagicPragma(self: Compiler, pragma: Pragma, node: ASTnode)
proc handlePurePragma(self: Compiler, pragma: Pragma, node: ASTnode) proc handlePurePragma(self: Compiler, pragma: Pragma, node: ASTnode)
@ -372,22 +376,25 @@ proc resolve(self: Compiler, name: IdentExpr,
proc getStackPos(self: Compiler, name: IdentExpr, depth: int = self.scopeDepth): int = proc getStackPos(self: Compiler, name: IdentExpr, depth: int = self.scopeDepth): int =
## Returns the predicted call stack position of a given name, relative ## Returns the predicted call stack position of a given name, relative
## to the current frame ## to the current frame
result = 2
var found = false var found = false
for variable in reversed(self.names): result = 2
for variable in self.names:
if variable.valueType.kind == Function:
continue
inc(result)
if name.name.lexeme == variable.name.name.lexeme: if name.name.lexeme == variable.name.name.lexeme:
if variable.isPrivate and variable.owner != self.currentModule: if variable.isPrivate and variable.owner != self.currentModule:
continue continue
if variable.depth == depth or variable.depth == 0: elif variable.depth == depth or variable.depth == 0:
# variable.depth == 0 for globals! # variable.depth == 0 for globals!
found = true found = true
dec(result)
break break
inc(result)
if not found: if not found:
return -1 return -1
proc getClosurePos(self: Compiler, name: IdentExpr, depth: int = self.scopeDepth): int = proc getClosurePos(self: Compiler, name: IdentExpr): int =
## Iterates the internal list of declared closure names backwards and ## Iterates the internal list of declared closure names backwards and
## returns the predicted closure array position of a given name. ## returns the predicted closure array position of a given name.
## Returns -1 if the name can't be found (this includes names that ## Returns -1 if the name can't be found (this includes names that
@ -398,7 +405,7 @@ proc getClosurePos(self: Compiler, name: IdentExpr, depth: int = self.scopeDepth
if name.name.lexeme == variable.name.name.lexeme: if name.name.lexeme == variable.name.name.lexeme:
if variable.isPrivate and variable.owner != self.currentModule: if variable.isPrivate and variable.owner != self.currentModule:
continue continue
elif variable.depth == depth: else:
found = true found = true
break break
dec(result) dec(result)
@ -423,7 +430,7 @@ proc resolve(self: Compiler, name: string,
return nil return nil
proc detectClosureVariable(self: Compiler, name: Name, depth: int = self.scopeDepth) = proc detectClosureVariable(self: Compiler, name: var Name, depth: int = self.scopeDepth, decl: bool = false) =
## Detects if the given name is used in a local scope deeper ## Detects if the given name is used in a local scope deeper
## than the given one and modifies the code emitted for it ## than the given one and modifies the code emitted for it
## to store it as a closure variable if it is. Does nothing if the name ## to store it as a closure variable if it is. Does nothing if the name
@ -434,27 +441,25 @@ proc detectClosureVariable(self: Compiler, name: Name, depth: int = self.scopeDe
## unpredictably or crash ## unpredictably or crash
if name.isNil() or name.depth == 0: if name.isNil() or name.depth == 0:
return return
elif name.depth < depth and not name.isClosedOver: elif name.depth < depth:
# Ding! The given name is closed over: we need to # Ding! The given name is closed over: we need to
# change the dummy Jump instruction that self.declareName # change the dummy Jump instruction that self.declareName
# put in place for us into a StoreClosure. We also update # put in place for us into a StoreClosure. We also update
# the name's isClosedOver field so that self.identifier() # the name's isClosedOver field so that self.identifier()
# can emit a LoadClosure instruction instead of a LoadVar # can emit a LoadClosure instruction instead of a LoadVar
self.closedOver.add(name) if not name.isClosedOver:
let idx = self.closedOver.high().toTriple() self.closedOver.add(name)
if self.closedOver.len() >= 16777216: if self.closedOver.len() >= 16777216:
self.error("too many consecutive closed-over variables (max is 16777215)") self.error("too many consecutive closed-over variables (max is 16777215)")
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]
name.isClosedOver = true name.isClosedOver = true
if decl:
self.emitByte(StoreClosure)
self.emitBytes(self.closedOver.high().toTriple())
proc compareTypes(self: Compiler, a, b: Type, strictMutable: bool = true): bool = proc compareTypes(self: Compiler, a, b: Type): bool =
## Compares two type objects ## Compares two type objects
## for equality (works with nil!) ## for equality (works with nil!)
# The nil code here is for void functions (when # The nil code here is for void functions (when
# we compare their return types) # we compare their return types)
if a.isNil(): if a.isNil():
@ -477,10 +482,6 @@ proc compareTypes(self: Compiler, a, b: Type, strictMutable: bool = true): bool
# If they're different, then they can't # If they're different, then they can't
# be the same type! # be the same type!
return false return false
elif a.mutable != b.mutable and strictMutable:
# Are they both (im)mutable? If not,
# they're different
return false
case a.kind: case a.kind:
# If all previous checks pass, it's time # If all previous checks pass, it's time
# to go through each possible type peon # to go through each possible type peon
@ -504,10 +505,11 @@ proc compareTypes(self: Compiler, a, b: Type, strictMutable: bool = true): bool
elif not self.compareTypes(a.returnType, b.returnType): elif not self.compareTypes(a.returnType, b.returnType):
return false return false
for (argA, argB) in zip(a.args, b.args): for (argA, argB) in zip(a.args, b.args):
if not self.compareTypes(argA.kind, argB.kind, strictMutable): if not self.compareTypes(argA.kind, argB.kind):
return false return false
return true return true
else: else:
# TODO: Custom types
discard discard
@ -553,7 +555,7 @@ proc toIntrinsic(name: string): Type =
return nil return nil
proc inferType(self: Compiler, node: LiteralExpr, strictMutable: bool = true): Type = proc inferType(self: Compiler, node: LiteralExpr): Type =
## Infers the type of a given literal expression ## Infers the type of a given literal expression
if node.isNil(): if node.isNil():
return nil return nil
@ -565,7 +567,7 @@ proc inferType(self: Compiler, node: LiteralExpr, strictMutable: bool = true): T
if size.len() == 1: if size.len() == 1:
return Type(kind: Int64) return Type(kind: Int64)
let typ = size[1].toIntrinsic() let typ = size[1].toIntrinsic()
if not self.compareTypes(typ, nil, strictMutable): if not self.compareTypes(typ, nil):
return typ return typ
else: else:
self.error(&"invalid type specifier '{size[1]}' for int") self.error(&"invalid type specifier '{size[1]}' for int")
@ -576,7 +578,7 @@ proc inferType(self: Compiler, node: LiteralExpr, strictMutable: bool = true): T
if size.len() == 1 or size[1] == "f64": if size.len() == 1 or size[1] == "f64":
return Type(kind: Float64) return Type(kind: Float64)
let typ = size[1].toIntrinsic() let typ = size[1].toIntrinsic()
if not self.compareTypes(typ, nil, strictMutable): if not self.compareTypes(typ, nil):
return typ return typ
else: else:
self.error(&"invalid type specifier '{size[1]}' for float") self.error(&"invalid type specifier '{size[1]}' for float")
@ -594,7 +596,7 @@ proc inferType(self: Compiler, node: LiteralExpr, strictMutable: bool = true): T
discard # TODO discard # TODO
proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Type = proc inferType(self: Compiler, node: Expression): Type =
## Infers the type of a given expression and ## Infers the type of a given expression and
## returns it ## returns it
if node.isNil(): if node.isNil():
@ -611,9 +613,9 @@ proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Ty
return self.inferType(UnaryExpr(node).a) return self.inferType(UnaryExpr(node).a)
of binaryExpr: of binaryExpr:
let node = BinaryExpr(node) let node = BinaryExpr(node)
var a = self.inferType(node.a, strictMutable) var a = self.inferType(node.a)
var b = self.inferType(node.b, strictMutable) var b = self.inferType(node.b)
if not self.compareTypes(a, b, strictMutable): if not self.compareTypes(a, b):
return nil return nil
return a return a
of {intExpr, hexExpr, binExpr, octExpr, of {intExpr, hexExpr, binExpr, octExpr,
@ -627,7 +629,7 @@ proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Ty
if not node.returnType.isNil(): if not node.returnType.isNil():
result.returnType = self.inferType(node.returnType) result.returnType = self.inferType(node.returnType)
for argument in node.arguments: for argument in node.arguments:
result.args.add((argument.name.token.lexeme, self.inferType(argument.valueType, strictMutable))) result.args.add((argument.name.token.lexeme, self.inferType(argument.valueType)))
of callExpr: of callExpr:
var node = CallExpr(node) var node = CallExpr(node)
case node.callee.kind: case node.callee.kind:
@ -640,16 +642,16 @@ proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Ty
else: else:
result = nil result = nil
of lambdaExpr: of lambdaExpr:
result = self.inferType(LambdaExpr(node.callee).returnType, strictMutable) result = self.inferType(LambdaExpr(node.callee).returnType)
else: else:
discard # Unreachable discard # Unreachable
of varExpr: of varExpr:
result = self.inferType(Var(node).value) result = self.inferType(Var(node).value)
result.mutable = true result.mutable = true
of refExpr: of refExpr:
result = Type(kind: Reference, value: self.inferType(Ref(node).value, strictMutable)) result = Type(kind: Reference, value: self.inferType(Ref(node).value))
of ptrExpr: of ptrExpr:
result = Type(kind: Pointer, value: self.inferType(Ptr(node).value, strictMutable)) result = Type(kind: Pointer, value: self.inferType(Ptr(node).value))
else: else:
discard # Unreachable discard # Unreachable
@ -715,19 +717,29 @@ proc findByName(self: Compiler, name: string): seq[Name] =
result.add(obj) result.add(obj)
proc findByType(self: Compiler, name: string, kind: Type, strictMutable: bool = true): seq[Name] = proc findByType(self: Compiler, name: string, kind: Type): seq[Name] =
## Looks for objects that have already been declared ## Looks for objects that have already been declared
## with the given name and type ## with the given name and type
for obj in self.findByName(name): for obj in self.findByName(name):
if self.compareTypes(obj.valueType, kind, strictMutable): if self.compareTypes(obj.valueType, kind):
result.add(obj) result.add(obj)
#[
proc findAtDepth(self: Compiler, name: string, depth: int): seq[Name] =
## Looks for objects that have been already declared
## with the given name at the given scope depth.
## Returns all objects that apply
for obj in self.findByName(name):
if obj.depth == depth:
result.add(obj)
]#
proc matchImpl(self: Compiler, name: string, kind: Type, strictMutable: bool = true): Name =
proc matchImpl(self: Compiler, name: string, kind: Type): Name =
## Tries to find a matching function implementation ## Tries to find a matching function implementation
## compatible with the given type and returns its ## compatible with the given type and returns its
## name object ## name object
let impl = self.findByType(name, kind, strictMutable) let impl = self.findByType(name, kind)
if impl.len() == 0: if impl.len() == 0:
var msg = &"cannot find a suitable implementation for '{name}'" var msg = &"cannot find a suitable implementation for '{name}'"
let names = self.findByName(name) let names = self.findByName(name)
@ -761,9 +773,20 @@ proc matchImpl(self: Compiler, name: string, kind: Type, strictMutable: bool = t
proc emitFunction(self: Compiler, name: Name) = proc emitFunction(self: Compiler, name: Name) =
## Wrapper to emit LoadFunction instructions ## Wrapper to emit LoadFunction instructions
self.emitByte(LoadFunction) if name.isFunDecl:
self.emitBytes(name.codePos.toTriple()) self.emitByte(LoadFunction)
self.emitBytes(name.codePos.toTriple())
# If we're not loading a statically declared
# function, then it must be a function object
# created by previous LoadFunction instructions
# that is now bound to some variable, so we just
# load it
elif not name.isClosedOver:
self.emitByte(LoadVar)
self.emitBytes(self.getStackPos(name.name).toTriple())
else:
self.emitByte(LoadClosure)
self.emitBytes(self.getClosurePos(name.name).toTriple())
## End of utility functions ## End of utility functions
@ -850,11 +873,11 @@ proc literal(self: Compiler, node: ASTNode) =
proc handleBuiltinFunction(self: Compiler, fn: Name, args: seq[Expression]) = proc handleBuiltinFunction(self: Compiler, fn: Name, args: seq[Expression]) =
## Emits single instructions for builtin functions ## Emits instructions for builtin functions
## such as addition or subtraction ## such as addition or subtraction
if fn.valueType.builtinOp notin ["GenericLogicalOr", "GenericLogicalAnd"]: if fn.valueType.builtinOp notin ["GenericLogicalOr", "GenericLogicalAnd"]:
for argument in args: self.expression(args[1])
self.expression(argument) self.expression(args[0])
case fn.valueType.builtinOp: case fn.valueType.builtinOp:
of "AddInt64": of "AddInt64":
self.emitByte(AddInt64) self.emitByte(AddInt64)
@ -936,29 +959,27 @@ proc handleBuiltinFunction(self: Compiler, fn: Name, args: seq[Expression]) =
self.emitByte(DivFloat32) self.emitByte(DivFloat32)
of "MulFloat32": of "MulFloat32":
self.emitByte(MulFloat32) self.emitByte(MulFloat32)
of "GenericLogicalOr": of "LogicalOr":
self.expression(args[0]) self.expression(args[0])
let jump = self.emitJump(JumpIfTrue) let jump = self.emitJump(JumpIfTrue)
self.expression(args[1]) self.expression(args[1])
self.patchJump(jump) self.patchJump(jump)
of "GenericLogicalAnd": of "LogicalAnd":
self.expression(args[0]) self.expression(args[0])
var jump: int var jump = self.emitJump(JumpIfFalseOrPop)
if self.enableOptimizations:
jump = self.emitJump(JumpIfFalseOrPop)
else:
jump = self.emitJump(JumpIfFalse)
self.emitByte(Pop)
self.expression(args[1]) self.expression(args[1])
self.patchJump(jump) self.patchJump(jump)
else: else:
discard # Unreachable self.error(&"unknown built-in: '{fn.valueType.builtinOp}'")
proc generateCall(self: Compiler, fn: Name, args: seq[Expression]) = proc generateCall(self: Compiler, fn: Name, args: seq[Expression]) =
## Small wrapper that abstracts emitting a call instruction ## Small wrapper that abstracts emitting a call instruction
## for a given function ## for a given function
if fn.valueType.isBuiltinFunction: if fn.valueType.isBuiltinFunction:
# Builtins map to individual instructions
# (usually 1, but some use more) so we handle
# them differently
self.handleBuiltinFunction(fn, args) self.handleBuiltinFunction(fn, args)
return return
if any(fn.valueType.args, proc (arg: tuple[name: string, kind: Type]): bool = arg[1].kind == Generic): if any(fn.valueType.args, proc (arg: tuple[name: string, kind: Type]): bool = arg[1].kind == Generic):
@ -968,41 +989,19 @@ proc generateCall(self: Compiler, fn: Name, args: seq[Expression]) =
self.emitFunction(fn) self.emitFunction(fn)
self.emitByte(LoadReturnAddress) self.emitByte(LoadReturnAddress)
let pos = self.chunk.code.len() let pos = self.chunk.code.len()
# We initially emit a dummy return
# address. It is patched later
self.emitBytes(0.toQuad()) self.emitBytes(0.toQuad())
for argument in args: for argument in reversed(args):
# We pass the arguments in reverse
# because of how stack semantics
# work. They'll be fixed at runtime
self.expression(argument) self.expression(argument)
self.emitByte(Call) # Creates a new call frame # Creates a new call frame and jumps
var size = 2 # We start at 2 because each call frame # to the function's first instruction
# contains at least 2 elements (function # in the code
# object and return address) self.emitByte(Call)
for name in reversed(self.names): self.emitBytes(fn.valueType.args.len().toTriple())
# Then, for each local variable
# we increase the frame size by 1
if name.depth == self.scopeDepth:
inc(size)
self.emitBytes(size.toTriple())
self.patchReturnAddress(pos)
proc generateObjCall(self: Compiler, args: seq[Expression]) =
## Small wrapper that abstracts emitting a call instruction
## for a given function already loaded on the operand stack
self.emitByte(PushC) # Pops the function off the operand stack onto the call stack
self.emitByte(LoadReturnAddress)
let pos = self.chunk.code.len()
self.emitBytes(0.toQuad())
for argument in args:
self.expression(argument)
self.emitByte(Call) # Creates a new call frame
var size = 2 # We start at 2 because each call frame
# contains at least 2 elements (function
# object and return address)
for name in reversed(self.names):
# Then, for each local variable
# we increase the frame size by 1
if name.depth == self.scopeDepth:
inc(size)
self.emitBytes(size.toTriple())
self.patchReturnAddress(pos) self.patchReturnAddress(pos)
@ -1013,23 +1012,22 @@ proc callUnaryOp(self: Compiler, fn: Name, op: UnaryExpr) =
proc callBinaryOp(self: Compiler, fn: Name, op: BinaryExpr) = proc callBinaryOp(self: Compiler, fn: Name, op: BinaryExpr) =
## Emits the code to call a binary operator ## Emits the code to call a binary operator
# Pushes the return address
self.generateCall(fn, @[op.a, op.b]) self.generateCall(fn, @[op.a, op.b])
proc unary(self: Compiler, node: UnaryExpr) = proc unary(self: Compiler, node: UnaryExpr) =
## Compiles unary expressions such as decimal ## Compiles unary expressions such as decimal
## and bitwise negation ## and bitwise negation
let valueType = self.inferType(node.a, strictMutable=false) let valueType = self.inferType(node.a)
let funct = self.matchImpl(node.token.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", valueType)]), strictMutable=false) let funct = self.matchImpl(node.token.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", valueType)]))
self.callUnaryOp(funct, node) self.callUnaryOp(funct, node)
proc binary(self: Compiler, node: BinaryExpr) = proc binary(self: Compiler, node: BinaryExpr) =
## Compiles all binary expressions ## Compiles all binary expressions
let typeOfA = self.inferType(node.a, strictMutable=false) let typeOfA = self.inferType(node.a)
let typeOfB = self.inferType(node.b, strictMutable=false) let typeOfB = self.inferType(node.b)
let funct = self.matchImpl(node.token.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", typeOfA), ("", typeOfB)]), strictMutable=false) let funct = self.matchImpl(node.token.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", typeOfA), ("", typeOfB)]))
self.callBinaryOp(funct, node) self.callBinaryOp(funct, node)
@ -1049,8 +1047,8 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
# slap myself 100 times with a sign saying "I'm dumb". Mark my words # slap myself 100 times with a sign saying "I'm dumb". Mark my words
self.error("cannot declare more than 16777215 variables at a time") self.error("cannot declare more than 16777215 variables at a time")
for name in self.findByName(node.name.token.lexeme): for name in self.findByName(node.name.token.lexeme):
if name.depth == self.scopeDepth and name.valueType.kind notin {Function, CustomType} and not name.isFunctionArgument: if name.depth == self.scopeDepth and not name.isFunctionArgument:
# Trying to redeclare a variable in the same module is an error, but it's okay # Trying to redeclare a variable in the same scope/context is an error, but it's okay
# if it's a function argument (for example, if you want to copy a number to # if it's a function argument (for example, if you want to copy a number to
# mutate it) # mutate it)
self.error(&"attempt to redeclare '{node.name.token.lexeme}', which was previously defined in '{name.owner}' at line {name.line}") self.error(&"attempt to redeclare '{node.name.token.lexeme}', which was previously defined in '{name.owner}' at line {name.line}")
@ -1066,21 +1064,6 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
line: node.token.line)) line: node.token.line))
if mutable: if mutable:
self.names[^1].valueType.mutable = true self.names[^1].valueType.mutable = true
# 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(JumpForwards)
self.emitBytes(0.toTriple())
of NodeKind.funDecl: of NodeKind.funDecl:
var node = FunDecl(node) var node = FunDecl(node)
# We declare the generics before the function so we # We declare the generics before the function so we
@ -1106,14 +1089,15 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
name: node.name, name: node.name,
isLet: false, isLet: false,
isClosedOver: false, isClosedOver: false,
line: node.token.line)) line: node.token.line,
isFunDecl: true))
let fn = self.names[^1] let fn = self.names[^1]
var name: Name var name: Name
for argument in node.arguments: for argument in node.arguments:
if self.names.high() > 16777215: if self.names.high() > 16777215:
self.error("cannot declare more than 16777215 variables at a time") self.error("cannot declare more than 16777215 variables at a time")
# wait, no LoadVar? Yes! That's because when calling functions, # wait, no LoadVar? Yes! That's because when calling functions,
# arguments will already be on the stack so there's no need to # arguments will already be on the stack, so there's no need to
# load them here # load them here
name = Name(depth: self.scopeDepth + 1, name = Name(depth: self.scopeDepth + 1,
isPrivate: true, isPrivate: true,
@ -1138,7 +1122,7 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
proc identifier(self: Compiler, node: IdentExpr) = proc identifier(self: Compiler, node: IdentExpr) =
## Compiles access to identifiers ## Compiles access to identifiers
let s = self.resolve(node) var s = self.resolve(node)
if s.isNil(): if s.isNil():
self.error(&"reference to undeclared name '{node.token.lexeme}'") self.error(&"reference to undeclared name '{node.token.lexeme}'")
elif s.isConst: elif s.isConst:
@ -1148,11 +1132,11 @@ proc identifier(self: Compiler, node: IdentExpr) =
else: else:
self.detectClosureVariable(s) self.detectClosureVariable(s)
if s.valueType.kind == Function: if s.valueType.kind == Function:
if not s.valueType.isBuiltinFunction: # Functions have no runtime
self.emitByte(LoadFunctionObj) # representation, so we need
self.emitBytes(s.codePos.toTriple()) # to create one on the fly
else: self.emitByte(LoadFunction)
self.emitByte(LoadNil) self.emitBytes(s.codePos.toTriple())
elif not s.isClosedOver: elif not s.isClosedOver:
# Static name resolution, loads value at index in the stack. Very fast. Much wow. # Static name resolution, loads value at index in the stack. Very fast. Much wow.
self.emitByte(LoadVar) self.emitByte(LoadVar)
@ -1163,7 +1147,7 @@ proc identifier(self: Compiler, node: IdentExpr) =
# align its semantics with the call stack. This makes closures work as expected and is # align its semantics with the call stack. This makes closures work as expected and is
# not much slower than indexing our stack (since they're both dynamic arrays at runtime anyway) # not much slower than indexing our stack (since they're both dynamic arrays at runtime anyway)
self.emitByte(LoadClosure) self.emitByte(LoadClosure)
self.emitBytes(self.closedOver.high().toTriple()) self.emitBytes(self.getClosurePos(s.name).toTriple())
@ -1173,7 +1157,7 @@ proc assignment(self: Compiler, node: ASTNode) =
of assignExpr: of assignExpr:
let node = AssignExpr(node) let node = AssignExpr(node)
let name = IdentExpr(node.name) let name = IdentExpr(node.name)
let r = self.resolve(name) var r = self.resolve(name)
if r.isNil(): if r.isNil():
self.error(&"assignment to undeclared name '{name.token.lexeme}'") self.error(&"assignment to undeclared name '{name.token.lexeme}'")
elif r.isConst: elif r.isConst:
@ -1207,7 +1191,7 @@ proc beginScope(self: Compiler) =
inc(self.scopeDepth) inc(self.scopeDepth)
proc endScope(self: Compiler, deleteNames: bool = true, fromFunc: bool = false) = proc endScope(self: Compiler) =
## Ends the current local scope ## Ends the current local scope
if self.scopeDepth < 0: if self.scopeDepth < 0:
self.error("cannot call endScope with scopeDepth < 0 (This is an internal error and most likely a bug)") self.error("cannot call endScope with scopeDepth < 0 (This is an internal error and most likely a bug)")
@ -1216,11 +1200,7 @@ proc endScope(self: Compiler, deleteNames: bool = true, fromFunc: bool = false)
for name in self.names: for name in self.names:
if name.depth > self.scopeDepth: if name.depth > self.scopeDepth:
names.add(name) names.add(name)
if not self.enableOptimizations and not fromFunc: if len(names) > 1:
# All variables with a scope depth larger than the current one
# are now out of scope. Begone, you're now homeless!
self.emitByte(PopC)
if self.enableOptimizations and len(names) > 1 and not fromFunc:
# If we're popping less than 65535 variables, then # If we're popping less than 65535 variables, then
# we can emit a PopN instruction. This is true for # we can emit a PopN instruction. This is true for
# 99.99999% of the use cases of the language (who the # 99.99999% of the use cases of the language (who the
@ -1234,32 +1214,25 @@ proc endScope(self: Compiler, deleteNames: bool = true, fromFunc: bool = false)
for i in countdown(self.names.high(), len(names) - uint16.high().int()): for i in countdown(self.names.high(), len(names) - uint16.high().int()):
if self.names[i].depth > self.scopeDepth: if self.names[i].depth > self.scopeDepth:
self.emitByte(PopC) self.emitByte(PopC)
elif len(names) == 1 and not fromFunc: elif len(names) == 1:
# We only emit PopN if we're popping more than one value # We only emit PopN if we're popping more than one value
self.emitByte(PopC) self.emitByte(PopC)
# This seems *really* slow, but # This seems *really* slow, but
# what else should I do? Nim doesn't # what else should I do? Nim doesn't
# allow the removal of items during # allow the removal of items during
# seq iteration so ¯\_(ツ)_/¯ # seq iteration so ¯\_(ツ)_/¯
if deleteNames: var idx = 0
var idx = 0 while idx < self.names.len():
while idx < self.names.len(): for name in names:
for name in names: if self.names[idx] == name:
if self.names[idx] == name: self.names.delete(idx)
self.names.delete(idx) inc(idx)
inc(idx)
idx = 0
while idx < self.closedOver.len():
for name in names:
if name.isClosedOver:
self.closedOver.delete(idx)
self.emitByte(PopClosure)
inc(idx)
proc blockStmt(self: Compiler, node: BlockStmt) = proc blockStmt(self: Compiler, node: BlockStmt) =
## Compiles block statements, which create a new ## Compiles block statements, which create a new
## local scope. ## local scope
self.beginScope() self.beginScope()
for decl in node.code: for decl in node.code:
self.declaration(decl) self.declaration(decl)
@ -1270,25 +1243,13 @@ proc ifStmt(self: Compiler, node: IfStmt) =
## Compiles if/else statements for conditional ## Compiles if/else statements for conditional
## execution of code ## execution of code
var cond = self.inferType(node.condition) var cond = self.inferType(node.condition)
self.expression(node.condition)
if not self.compareTypes(cond, Type(kind: Bool)): if not self.compareTypes(cond, Type(kind: Bool)):
if cond.isNil(): if cond.isNil():
if node.condition.kind == identExpr: self.error(&"expecting value of type 'bool', but expression has no type")
self.error(&"reference to undeclared identifier '{IdentExpr(node.condition).name.lexeme}'")
elif node.condition.kind == callExpr and CallExpr(node.condition).callee.kind == identExpr:
self.error(&"reference to undeclared identifier '{IdentExpr(CallExpr(node.condition).callee).name.lexeme}'")
else:
self.error(&"expecting value of type 'bool', but expression has no type")
else: else:
self.error(&"expecting value of type 'bool', got '{self.typeToStr(cond)}' instead") self.error(&"expecting value of type 'bool', got '{self.typeToStr(cond)}' instead")
self.expression(node.condition) let jump = self.emitJump(JumpIfFalsePop)
var jumpCode: OpCode
if self.enableOptimizations:
jumpCode = JumpIfFalsePop
else:
jumpCode = JumpIfFalse
let jump = self.emitJump(jumpCode)
if not self.enableOptimizations:
self.emitByte(Pop)
self.statement(node.thenBranch) self.statement(node.thenBranch)
let jump2 = self.emitJump(JumpForwards) let jump2 = self.emitJump(JumpForwards)
self.patchJump(jump) self.patchJump(jump)
@ -1310,51 +1271,23 @@ proc emitLoop(self: Compiler, begin: int) =
proc whileStmt(self: Compiler, node: WhileStmt) = proc whileStmt(self: Compiler, node: WhileStmt) =
## Compiles C-style while loops and ## Compiles C-style while loops and
## desugared C-style for loops ## desugared C-style for loops
let start = self.chunk.code.len() var cond = self.inferType(node.condition)
self.expression(node.condition) self.expression(node.condition)
var jump: int if not self.compareTypes(cond, Type(kind: Bool)):
if self.enableOptimizations: if cond.isNil():
jump = self.emitJump(JumpIfFalsePop) self.error(&"expecting value of type 'bool', but expression has no type")
else: else:
jump = self.emitJump(JumpIfFalse) self.error(&"expecting value of type 'bool', got '{self.typeToStr(cond)}' instead")
self.emitByte(Pop) let start = self.chunk.code.len()
var jump = self.emitJump(JumpIfFalsePop)
self.statement(node.body) self.statement(node.body)
self.patchJump(jump) self.patchJump(jump)
self.emitLoop(start) self.emitLoop(start)
proc isPure(self: Compiler, node: ASTNode): bool =
## Checks if a function has any side effects
var pragmas: seq[Pragma]
case node.kind:
of lambdaExpr:
pragmas = LambdaExpr(node).pragmas
else:
pragmas = Declaration(node).pragmas
if pragmas.len() == 0:
return false
for pragma in pragmas:
if pragma.name.name.lexeme == "pure":
return true
return false
proc checkCallIsPure(self: Compiler, node: ASTnode): bool = proc checkCallIsPure(self: Compiler, node: ASTnode): bool =
## Checks if a call has any side effects ## Checks if a call has any side effects
if not self.isPure(node): return true # TODO
return true
var pragmas: seq[Pragma]
case node.kind:
of lambdaExpr:
pragmas = LambdaExpr(node).pragmas
else:
pragmas = Declaration(node).pragmas
if pragmas.len() == 0:
return false
for pragma in pragmas:
if pragma.name.name.lexeme == "pure":
return true
return false
proc callExpr(self: Compiler, node: CallExpr) = proc callExpr(self: Compiler, node: CallExpr) =
@ -1362,7 +1295,6 @@ proc callExpr(self: Compiler, node: CallExpr) =
var args: seq[tuple[name: string, kind: Type]] = @[] var args: seq[tuple[name: string, kind: Type]] = @[]
var argExpr: seq[Expression] = @[] var argExpr: seq[Expression] = @[]
var kind: Type var kind: Type
var strictMutable = true
# TODO: Keyword arguments # TODO: Keyword arguments
for i, argument in node.arguments.positionals: for i, argument in node.arguments.positionals:
kind = self.inferType(argument) kind = self.inferType(argument)
@ -1370,8 +1302,6 @@ proc callExpr(self: Compiler, node: CallExpr) =
if argument.kind == identExpr: if argument.kind == identExpr:
self.error(&"reference to undeclared identifier '{IdentExpr(argument).name.lexeme}'") self.error(&"reference to undeclared identifier '{IdentExpr(argument).name.lexeme}'")
self.error(&"cannot infer the type of argument {i + 1} in function call") self.error(&"cannot infer the type of argument {i + 1} in function call")
if kind.mutable:
strictMutable = false
args.add(("", kind)) args.add(("", kind))
argExpr.add(argument) argExpr.add(argument)
for argument in node.arguments.keyword: for argument in node.arguments.keyword:
@ -1381,7 +1311,7 @@ proc callExpr(self: Compiler, node: CallExpr) =
var funct: Name var funct: Name
case node.callee.kind: case node.callee.kind:
of identExpr: of identExpr:
funct = self.matchImpl(IdentExpr(node.callee).name.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: args), strictMutable) funct = self.matchImpl(IdentExpr(node.callee).name.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: args))
of NodeKind.callExpr: of NodeKind.callExpr:
var node = node.callee var node = node.callee
while node.kind == callExpr: while node.kind == callExpr:
@ -1394,13 +1324,15 @@ proc callExpr(self: Compiler, node: CallExpr) =
self.error(&"expression has no type") self.error(&"expression has no type")
else: else:
self.error(&"object of type '{self.typeToStr(typ)}' is not callable") self.error(&"object of type '{self.typeToStr(typ)}' is not callable")
if not funct.isNil(): if any(funct.valueType.args, proc (arg: tuple[name: string, kind: Type]): bool = arg[1].kind == Generic):
if funct.valueType.isBuiltinFunction: # The function has generic arguments! We need to compile a version
self.handleBuiltinFunction(funct, argExpr) # of it with the right type data
else: self.funDecl(nil, funct, argExpr)
self.generateCall(funct, argExpr) # TODO: What next?
elif funct.valueType.isBuiltinFunction:
self.handleBuiltinFunction(funct, argExpr)
else: else:
self.generateObjCall(argExpr) self.generateCall(funct, argExpr)
if self.scopeDepth > 0 and not self.checkCallIsPure(node.callee): if self.scopeDepth > 0 and not self.checkCallIsPure(node.callee):
if self.currentFunction.name != "": if self.currentFunction.name != "":
self.error(&"cannot make sure that calls to '{self.currentFunction.name}' are side-effect free") self.error(&"cannot make sure that calls to '{self.currentFunction.name}' are side-effect free")
@ -1477,9 +1409,9 @@ proc endFunctionBeforeReturn(self: Compiler) =
## its return instruction ## its return instruction
var popped = 0 var popped = 0
for name in self.names: for name in self.names:
if name.depth == self.scopeDepth and name.valueType.kind notin {Function, Generic}: if name.depth == self.scopeDepth and name.valueType.kind notin {Function, Generic, CustomType} and not name.isClosedOver:
inc(popped) inc(popped)
if self.enableOptimizations and popped > 1: if popped > 1:
self.emitByte(PopN) self.emitByte(PopN)
self.emitBytes(popped.toDouble()) self.emitBytes(popped.toDouble())
dec(popped, uint16.high().int) dec(popped, uint16.high().int)
@ -1491,17 +1423,19 @@ proc endFunctionBeforeReturn(self: Compiler) =
proc returnStmt(self: Compiler, node: ReturnStmt) = proc returnStmt(self: Compiler, node: ReturnStmt) =
## Compiles return statements ## Compiles return statements
let actual = self.inferType(node.value) let actual = self.inferType(node.value)
let expected = self.currentFunction var expected = self.currentFunction.returnType
if actual.isNil() and not expected.returnType.isNil(): if not expected.isNil() and expected.kind == Generic:
expected = actual
if actual.isNil() and not expected.isNil():
if not node.value.isNil(): if not node.value.isNil():
if node.value.kind == identExpr: if node.value.kind == identExpr:
self.error(&"reference to undeclared identifier '{node.value.token.lexeme}'") self.error(&"reference to undeclared identifier '{node.value.token.lexeme}'")
elif node.value.kind == callExpr and CallExpr(node.value).callee.kind == identExpr: elif node.value.kind == callExpr and CallExpr(node.value).callee.kind == identExpr:
self.error(&"call to undeclared function '{CallExpr(node.value).callee.token.lexeme}'") self.error(&"call to undeclared function '{CallExpr(node.value).callee.token.lexeme}'")
self.error(&"expected return value of type '{self.typeToStr(expected.returnType)}', but expression has no type") self.error(&"expected return value of type '{self.typeToStr(expected)}', but expression has no type")
elif expected.returnType.isNil() and not actual.isNil(): elif expected.isNil() and not actual.isNil():
self.error("non-empty return statement is not allowed in void functions") self.error("empty return statement is only allowed in void functions")
elif not self.compareTypes(actual, expected.returnType): elif not self.compareTypes(actual, expected):
self.error(&"expected return value of type '{self.typeToStr(expected)}', got '{self.typeToStr(actual)}' instead") self.error(&"expected return value of type '{self.typeToStr(expected)}', got '{self.typeToStr(actual)}' instead")
if not node.value.isNil(): if not node.value.isNil():
self.expression(node.value) self.expression(node.value)
@ -1653,8 +1587,10 @@ proc varDecl(self: Compiler, node: VarDecl) =
self.error(&"expected value of type '{self.typeToStr(expected)}', but '{node.name.token.lexeme}' is of type '{self.typeToStr(actual)}'") self.error(&"expected value of type '{self.typeToStr(expected)}', but '{node.name.token.lexeme}' is of type '{self.typeToStr(actual)}'")
self.expression(node.value) self.expression(node.value)
self.declareName(node, mutable=node.token.kind == TokenType.Var) self.declareName(node, mutable=node.token.kind == TokenType.Var)
var r = self.names[^1]
self.detectClosureVariable(r, decl=true)
self.emitByte(StoreVar) self.emitByte(StoreVar)
self.emitBytes(self.names.len().toTriple()) self.emitBytes((self.getStackPos(r.name) + 1).toTriple())
proc typeDecl(self: Compiler, node: TypeDecl) = proc typeDecl(self: Compiler, node: TypeDecl) =
@ -1708,12 +1644,16 @@ proc dispatchPragmas(self: Compiler, node: ASTnode) =
proc fixGenericFunc(self: Compiler, name: Name, args: seq[Expression]): Type = proc fixGenericFunc(self: Compiler, name: Name, args: seq[Expression]): Type =
## Specializes generic arguments in functions ## Specializes generic arguments in functions
var fn = name.valueType var fn = name.valueType.deepCopy()
result = fn.deepCopy() result = fn
var node = fn.fun var typ: Type
for i in 0..args.high(): for i in 0..args.high():
if fn.args[i].kind.kind == Generic: if fn.args[i].kind.kind == Generic:
self.resolve(fn.args[i].name).valueType = self.inferType(args[i]) typ = self.inferType(args[i])
fn.args[i].kind = typ
self.resolve(fn.args[i].name).valueType = typ
if fn.args[i].kind.isNil():
self.error(&"cannot specialize generic function: argument {i + 1} has no type")
proc funDecl(self: Compiler, node: FunDecl, fn: Name = nil, args: seq[Expression] = @[]) = proc funDecl(self: Compiler, node: FunDecl, fn: Name = nil, args: seq[Expression] = @[]) =
@ -1728,9 +1668,12 @@ proc funDecl(self: Compiler, node: FunDecl, fn: Name = nil, args: seq[Expression
self.dispatchPragmas(node) self.dispatchPragmas(node)
var node = node var node = node
var fn = if fn.isNil(): self.names[^(node.arguments.len() + 1)] else: fn var fn = if fn.isNil(): self.names[^(node.arguments.len() + 1)] else: fn
if fn.valueType.returnType.isNil(): if fn.valueType.isBuiltinFunction:
self.error(&"cannot infer the type of '{node.returnType.token.lexeme}'") # We take the arguments off of our name list
if not fn.valueType.isBuiltinFunction: # because they become temporaries on the stack
for i in self.names.high() - node.arguments.high()..self.names.high():
self.names.delete(i)
else:
var function = self.currentFunction var function = self.currentFunction
var jmp: int var jmp: int
# Builtin functions map to a single # Builtin functions map to a single
@ -1747,11 +1690,6 @@ proc funDecl(self: Compiler, node: FunDecl, fn: Name = nil, args: seq[Expression
fn.codePos = self.chunk.code.len() fn.codePos = self.chunk.code.len()
# We store the current function # We store the current function
self.currentFunction = fn.valueType self.currentFunction = fn.valueType
let argLen = if node.isNil(): fn.valueType.args.len() else: node.arguments.len()
for _ in 0..<argLen:
# Pops off the operand stack onto the
# call stack
self.emitByte(LoadArgument)
if node.isNil(): if node.isNil():
# We got called back with more specific type # We got called back with more specific type
# arguments: time to fix them! # arguments: time to fix them!
@ -1795,13 +1733,14 @@ proc funDecl(self: Compiler, node: FunDecl, fn: Name = nil, args: seq[Expression
hasVal = LambdaExpr(Declaration(self.currentFunction.fun)).hasExplicitReturn hasVal = LambdaExpr(Declaration(self.currentFunction.fun)).hasExplicitReturn
else: else:
discard # Unreachable discard # Unreachable
if hasVal and self.currentFunction.returnType.isNil() and not typ.returnType.isNil(): if not hasVal and not typ.isNil():
self.error("non-empty return statement is not allowed in void functions") # There is no explicit return statement anywhere in the function's
elif not hasVal and not self.currentFunction.returnType.isNil(): # body: while this is not a tremendously useful piece of information (since
# the presence of at least one doesn't mean all control flow cases are
# covered), it definitely is an error worth reporting
self.error("function has an explicit return type, but no return statement was found") self.error("function has an explicit return type, but no return statement was found")
self.endFunctionBeforeReturn()
hasVal = hasVal and not typ.isNil() hasVal = hasVal and not typ.isNil()
self.endScope(deleteNames=true, fromFunc=true) self.endScope()
# Terminates the function's context # Terminates the function's context
self.emitByte(OpCode.Return) self.emitByte(OpCode.Return)
if hasVal: if hasVal:
@ -1895,15 +1834,16 @@ proc compile*(self: Compiler, ast: seq[Declaration], file: string): Chunk =
self.names.add(main) self.names.add(main)
self.emitByte(LoadFunction) self.emitByte(LoadFunction)
self.emitBytes(main.codePos.toTriple()) self.emitBytes(main.codePos.toTriple())
self.emitByte(LoadReturnAddress) self.emitByte(LoadReturnAddress)
let pos = self.chunk.code.len() let pos = self.chunk.code.len()
self.emitBytes(0.toQuad()) self.emitBytes(0.toQuad())
self.emitByte(Call) self.emitByte(Call)
self.emitBytes(2.toTriple()) self.emitBytes(0.toTriple())
while not self.done(): while not self.done():
self.declaration(Declaration(self.step())) self.declaration(Declaration(self.step()))
self.endScope(fromFunc=true) self.endFunctionBeforeReturn()
self.patchReturnAddress(pos) self.patchReturnAddress(pos)
self.emitByte(OpCode.Return) self.emitByte(OpCode.Return)
self.emitByte(0) self.emitByte(0) # Entry point has no return value
result = self.chunk result = self.chunk

View File

@ -138,15 +138,13 @@ type
## Basic stack operations ## Basic stack operations
Pop, # Pops an element off the stack and discards it 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) 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 call stack (optimization for exiting local scopes which usually pop many elements)
PopN, # Pops x elements off the stack (optimization for exiting local scopes which usually pop many elements)
## Name resolution/handling ## Name resolution/handling
LoadAttribute, # Pushes the attribute b of object a onto the stack LoadAttribute, # Pushes the attribute b of object a onto the stack
LoadVar, # Pushes the object at position x in the stack 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 StoreVar, # Stores the value of b at position a in the stack
LoadClosure, # Pushes the object position x in the closure array onto the stack 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 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 ## Looping and jumping
Jump, # Absolute, unconditional jump into the bytecode Jump, # Absolute, unconditional jump into the bytecode
JumpForwards, # Relative, unconditional, positive jump in the bytecode JumpForwards, # Relative, unconditional, positive jump in the bytecode
@ -168,10 +166,8 @@ type
## Coroutines ## Coroutines
Await, # Calls an asynchronous function Await, # Calls an asynchronous function
## Misc ## Misc
Assert, # Raises an AssertionFailed exception if x is false Assert, # Raises an AssertionFailed exception if x is false
NoOp, # Just a no-op NoOp, # Just a no-op
LoadArgument, # The caller uses this to pop off the argument from the operand stack onto the call stack
LoadFunctionObj, # Creates and pushes a function object onto the stack with ip X
PopC, # Pop off the call stack onto the operand stack PopC, # Pop off the call stack onto the operand stack
PushC # Pop off the operand stack onto the call stack PushC # Pop off the operand stack onto the call stack
@ -185,7 +181,7 @@ const simpleInstructions* = {Return, LoadNil,
Pop, PopRepl, Raise, Pop, PopRepl, Raise,
BeginTry, FinishTry, Yield, BeginTry, FinishTry, Yield,
Await, NoOp, SetResult, Await, NoOp, SetResult,
LoadArgument, PopC, PushC, PopC, PushC,
AddInt64, AddUInt64, AddInt32, AddInt64, AddUInt64, AddInt32,
AddUInt32, AddInt16, AddUInt16, AddUInt32, AddInt16, AddUInt16,
AddInt8, AddUInt8, SubInt64, AddInt8, AddUInt8, SubInt64,
@ -222,7 +218,7 @@ const stackDoubleInstructions* = {}
const argumentDoubleInstructions* = {PopN, } const argumentDoubleInstructions* = {PopN, }
# Argument double argument instructions take hardcoded arguments as 24 bit integers # Argument double argument instructions take hardcoded arguments as 24 bit integers
const argumentTripleInstructions* = {LoadFunctionObj, StoreClosure, PopClosure} const argumentTripleInstructions* = {StoreClosure}
# Instructions that call functions # Instructions that call functions
const callInstructions* = {Call, } const callInstructions* = {Call, }

View File

@ -314,7 +314,7 @@ proc primary(self: Parser): Expression =
self.expect(RightParen, "unterminated parenthesized expression") self.expect(RightParen, "unterminated parenthesized expression")
of Yield: of Yield:
let tok = self.step() let tok = self.step()
if self.currentFunction == nil: if self.currentFunction.isNil():
self.error("'yield' cannot be used outside functions") self.error("'yield' cannot be used outside functions")
elif self.currentFunction.token.kind != Generator: elif self.currentFunction.token.kind != Generator:
# It's easier than doing conversions for lambda/funDecl # It's easier than doing conversions for lambda/funDecl
@ -327,7 +327,7 @@ proc primary(self: Parser): Expression =
result = newYieldExpr(newNilExpr(Token()), tok) result = newYieldExpr(newNilExpr(Token()), tok)
of Await: of Await:
let tok = self.step() let tok = self.step()
if self.currentFunction == nil: if self.currentFunction.isNil():
self.error("'await' cannot be used outside functions") self.error("'await' cannot be used outside functions")
if self.currentFunction.token.kind != Coroutine: if self.currentFunction.token.kind != Coroutine:
self.error("'await' can only be used inside coroutines") self.error("'await' can only be used inside coroutines")
@ -549,7 +549,7 @@ proc blockStmt(self: Parser): Statement =
var code: seq[Declaration] = @[] var code: seq[Declaration] = @[]
while not self.check(RightBrace) and not self.done(): while not self.check(RightBrace) and not self.done():
code.add(self.declaration()) code.add(self.declaration())
if code[^1] == nil: if code[^1].isNil():
code.delete(code.high()) code.delete(code.high())
self.expect(RightBrace, "expecting '}'") self.expect(RightBrace, "expecting '}'")
result = newBlockStmt(code, tok) result = newBlockStmt(code, tok)
@ -568,7 +568,7 @@ proc breakStmt(self: Parser): Statement =
proc deferStmt(self: Parser): Statement = proc deferStmt(self: Parser): Statement =
## Parses defer statements ## Parses defer statements
let tok = self.peek(-1) let tok = self.peek(-1)
if self.currentFunction == nil: if self.currentFunction.isNil():
self.error("'defer' cannot be used outside functions") self.error("'defer' cannot be used outside functions")
result = newDeferStmt(self.expression(), tok) result = newDeferStmt(self.expression(), tok)
endOfLine("missing semicolon after defer statement") endOfLine("missing semicolon after defer statement")
@ -586,7 +586,7 @@ proc continueStmt(self: Parser): Statement =
proc returnStmt(self: Parser): Statement = proc returnStmt(self: Parser): Statement =
## Parses return statements ## Parses return statements
let tok = self.peek(-1) let tok = self.peek(-1)
if self.currentFunction == nil: if self.currentFunction.isNil():
self.error("'return' cannot be used outside functions") self.error("'return' cannot be used outside functions")
var value: Expression var value: Expression
if not self.check(Semicolon): if not self.check(Semicolon):
@ -606,7 +606,7 @@ proc returnStmt(self: Parser): Statement =
proc yieldStmt(self: Parser): Statement = proc yieldStmt(self: Parser): Statement =
## Parses yield statements ## Parses yield statements
let tok = self.peek(-1) let tok = self.peek(-1)
if self.currentFunction == nil: if self.currentFunction.isNil():
self.error("'yield' cannot be outside functions") self.error("'yield' cannot be outside functions")
elif self.currentFunction.token.kind != Generator: elif self.currentFunction.token.kind != Generator:
self.error("'yield' can only be used inside generators") self.error("'yield' can only be used inside generators")
@ -620,7 +620,7 @@ proc yieldStmt(self: Parser): Statement =
proc awaitStmt(self: Parser): Statement = proc awaitStmt(self: Parser): Statement =
## Parses await statements ## Parses await statements
let tok = self.peek(-1) let tok = self.peek(-1)
if self.currentFunction == nil: if self.currentFunction.isNil():
self.error("'await' cannot be used outside functions") self.error("'await' cannot be used outside functions")
if self.currentFunction.token.kind != Coroutine: if self.currentFunction.token.kind != Coroutine:
self.error("'await' can only be used inside coroutines") self.error("'await' can only be used inside coroutines")
@ -690,10 +690,10 @@ proc tryStmt(self: Parser): Statement =
elseClause = self.statement() elseClause = self.statement()
if self.match(Finally): if self.match(Finally):
finallyClause = self.statement() finallyClause = self.statement()
if handlers.len() == 0 and elseClause == nil and finallyClause == nil: if handlers.len() == 0 and elseClause.isNil() and finallyClause.isNil():
self.error("expecting 'except', 'finally' or 'else' statement after 'try' block") self.error("expecting 'except', 'finally' or 'else' statement after 'try' block")
for i, handler in handlers: for i, handler in handlers:
if handler.exc == nil and i != handlers.high(): if handler.exc.isNil() and i != handlers.high():
self.error("catch-all exception handler with bare 'except' must come last in try statement") self.error("catch-all exception handler with bare 'except' must come last in try statement")
result = newTryStmt(body, handlers, finallyClause, elseClause, tok) result = newTryStmt(body, handlers, finallyClause, elseClause, tok)
@ -737,18 +737,18 @@ proc forStmt(self: Parser): Statement =
increment = self.expression() increment = self.expression()
self.expect(RightParen, "unterminated for loop increment") self.expect(RightParen, "unterminated for loop increment")
var body = self.statement() var body = self.statement()
if increment != nil: if not increment.isNil():
# The increment runs after each iteration, so we # The increment runs after each iteration, so we
# inject it into the block as the last statement # inject it into the block as the last statement
body = newBlockStmt(@[Declaration(body), newExprStmt(increment, body = newBlockStmt(@[Declaration(body), newExprStmt(increment,
increment.token)], tok) increment.token)], tok)
if condition == nil: if condition.isNil():
## An empty condition is functionally ## An empty condition is functionally
## equivalent to "true" ## equivalent to "true"
condition = newTrueExpr(Token(lexeme: "true")) condition = newTrueExpr(Token(lexeme: "true"))
# We can use a while loop, which in this case works just as well # We can use a while loop, which in this case works just as well
body = newWhileStmt(condition, body, tok) body = newWhileStmt(condition, body, tok)
if initializer != nil: if not initializer.isNil():
# Nested blocks, so the initializer is # Nested blocks, so the initializer is
# only executed once # only executed once
body = newBlockStmt(@[Declaration(initializer), Declaration(body)], tok) body = newBlockStmt(@[Declaration(initializer), Declaration(body)], tok)
@ -777,7 +777,7 @@ proc ifStmt(self: Parser): Statement =
var condition = self.expression() var condition = self.expression()
self.expect(RightParen, "expecting ')' after if condition") self.expect(RightParen, "expecting ')' after if condition")
var thenBranch = self.statement() var thenBranch = self.statement()
var elseBranch: Statement = nil var elseBranch: Statement
if self.match(Else): if self.match(Else):
elseBranch = self.statement() elseBranch = self.statement()
result = newIfStmt(condition, thenBranch, elseBranch, tok) result = newIfStmt(condition, thenBranch, elseBranch, tok)
@ -790,7 +790,6 @@ template checkDecl(self: Parser, isPrivate: bool) =
self.error("cannot bind public names inside local scopes") self.error("cannot bind public names inside local scopes")
proc parsePragmas(self: Parser): seq[Pragma] = proc parsePragmas(self: Parser): seq[Pragma] =
## Parses pragmas ## Parses pragmas
var var
@ -871,7 +870,7 @@ proc varDecl(self: Parser, isLet: bool = false,
isLet = isLet, valueType = valueType, pragmas = (@[])) isLet = isLet, valueType = valueType, pragmas = (@[]))
else: else:
discard # Unreachable discard # Unreachable
if not hasInit and VarDecl(result).valueType == nil: if not hasInit and VarDecl(result).valueType.isNil():
self.error("expecting initializer or explicit type declaration, but neither was found") self.error("expecting initializer or explicit type declaration, but neither was found")
result.pragmas = pragmas result.pragmas = pragmas
@ -904,7 +903,7 @@ proc parseDeclArguments(self: Parser, arguments: var seq[tuple[name: IdentExpr,
break break
self.expect(RightParen) self.expect(RightParen)
for argument in arguments: for argument in arguments:
if argument.valueType == nil: if argument.valueType.isNil():
self.error(&"missing type declaration for '{argument.name.token.lexeme}' in function declaration") self.error(&"missing type declaration for '{argument.name.token.lexeme}' in function declaration")

View File

@ -227,9 +227,12 @@ proc runFile(f: string, interactive: bool = false, fromString: bool = false) =
styledEcho fgCyan, "Compilation step:\n" styledEcho fgCyan, "Compilation step:\n"
debugger.disassembleChunk(compiled, f) debugger.disassembleChunk(compiled, f)
echo "" echo ""
var path = splitFile(f).dir
serializer.dumpFile(compiled, f, splitFile(f).dir & "/" & splitFile(f).name & ".pbc") if path.len() > 0:
serialized = serializer.loadFile(splitFile(f).dir & "/" & splitFile(f).name & ".pbc") path &= "/"
path &= splitFile(f).name & ".pbc"
serializer.dumpFile(compiled, f, path)
serialized = serializer.loadFile(path)
when debugSerializer: when debugSerializer:
var hashMatches = computeSHA256(input).toHex().toLowerAscii() == serialized.fileHash var hashMatches = computeSHA256(input).toHex().toLowerAscii() == serialized.fileHash
styledEcho fgCyan, "Serialization step: " styledEcho fgCyan, "Serialization step: "

View File

@ -63,8 +63,8 @@ proc printInstruction(instruction: OpCode, newline: bool = false) =
proc checkFrameStart(self: Debugger, n: int) = proc checkFrameStart(self: Debugger, n: int) =
## Todo: Checking frame end offsets needs ## Checks if a call frame begins at the given
## fixes ## bytecode offset
for i, e in self.cfiData: for i, e in self.cfiData:
if n == e.start and not (e.started or e.stopped): if n == e.start and not (e.started or e.stopped):
e.started = true e.started = true
@ -76,6 +76,8 @@ proc checkFrameStart(self: Debugger, n: int) =
proc checkFrameEnd(self: Debugger, n: int) = proc checkFrameEnd(self: Debugger, n: int) =
## Checks if a call frame ends at the given
## bytecode offset
for i, e in self.cfiData: for i, e in self.cfiData:
if n == e.stop and e.started and not e.stopped: if n == e.stop and e.started and not e.stopped:
e.stopped = true e.stopped = true
@ -83,6 +85,7 @@ proc checkFrameEnd(self: Debugger, n: int) =
proc simpleInstruction(self: Debugger, instruction: OpCode) = proc simpleInstruction(self: Debugger, instruction: OpCode) =
## Debugs simple instructions
printInstruction(instruction, true) printInstruction(instruction, true)
self.current += 1 self.current += 1
if instruction == Return: if instruction == Return:
@ -152,7 +155,7 @@ proc loadAddressInstruction(self: Debugger, instruction: OpCode) =
var address = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3], var address = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3],
self.chunk.code[self.current + 4]].fromQuad() self.chunk.code[self.current + 4]].fromQuad()
printInstruction(instruction) printInstruction(instruction)
styledEcho fgGreen, &" loads address ", fgYellow, $address styledEcho fgGreen, &", loads address ", fgYellow, $address
self.current += 5 self.current += 5