Various style fixes with nil checks, added PushC opcode, added support for calling function objects

This commit is contained in:
Mattia Giambirtone 2022-06-13 15:04:53 +02:00
parent 11b15abc01
commit 02f1f8a54d
5 changed files with 82 additions and 53 deletions

View File

@ -357,6 +357,8 @@ proc dispatch*(self: PeonVM) =
self.push(self.constReadFloat64(int(self.readLong())))
of LoadFunction:
self.pushc(PeonObject(kind: Function, ip: self.readLong()))
of LoadFunctionObj:
self.push(PeonObject(kind: Function, ip: self.readLong()))
of LoadReturnAddress:
self.pushc(PeonObject(kind: UInt32, uInt: self.readUInt()))
of Call:
@ -433,7 +435,12 @@ proc dispatch*(self: PeonVM) =
discard self.popc()
of Pop:
discard self.pop()
of PushC:
self.pushc(self.pop())
of PopRepl:
if self.frames.len() > 1:
discard self.pop()
continue
let popped = self.pop()
case popped.kind:
of Int64:

View File

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

View File

@ -401,7 +401,7 @@ proc detectClosureVariable(self: Compiler, name: Name, depth: int = self.scopeDe
## each time a name is referenced in order for closed-over variables
## to be emitted properly, otherwise the runtime may behave
## unpredictably or crash
if name == nil or name.depth == 0:
if name.isNil() or name.depth == 0:
return
elif name.depth < depth and not name.isClosedOver:
# Ding! The given name is closed over: we need to
@ -426,10 +426,10 @@ 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 or b.kind == Any
elif b == nil:
return a == nil or a.kind == Any
if a.isNil():
return b.isNil() or b.kind == Any
elif b.isNil():
return a.isNil() or a.kind == Any
elif a.kind == Any or b.kind == Any:
# This is needed internally: user code
# cannot generate code for matching
@ -517,7 +517,7 @@ proc toIntrinsic(name: string): Type =
proc inferType(self: Compiler, node: LiteralExpr): Type =
## Infers the type of a given literal expression
if node == nil:
if node.isNil():
return nil
case node.kind:
of intExpr, binExpr, octExpr, hexExpr:
@ -559,13 +559,13 @@ proc inferType(self: Compiler, node: LiteralExpr): Type =
proc inferType(self: Compiler, node: Expression): Type =
## Infers the type of a given expression and
## returns it
if node == nil:
if node.isNil():
return nil
case node.kind:
of identExpr:
let node = IdentExpr(node)
let name = self.resolve(node)
if name != nil:
if not name.isNil():
return name.valueType
else:
result = node.name.lexeme.toIntrinsic()
@ -586,7 +586,7 @@ proc inferType(self: Compiler, node: Expression): Type =
of lambdaExpr:
var node = LambdaExpr(node)
result = Type(kind: Function, returnType: nil, args: @[], isLambda: true)
if node.returnType != nil:
if not node.returnType.isNil():
result.returnType = self.inferType(node.returnType)
for argument in node.arguments:
result.args.add((argument.name.token.lexeme, self.inferType(argument.valueType)))
@ -595,9 +595,9 @@ proc inferType(self: Compiler, node: Expression): Type =
case node.callee.kind:
of identExpr:
let resolved = self.resolve(IdentExpr(node.callee))
if resolved != nil:
if not resolved.isNil():
result = resolved.valueType.returnType
if result == nil:
if result.isNil():
result = Type(kind: Any)
else:
result = nil
@ -612,18 +612,18 @@ proc inferType(self: Compiler, node: Expression): Type =
proc inferType(self: Compiler, node: Declaration): Type =
## Infers the type of a given declaration
## and returns it
if node == nil:
if node.isNil():
return nil
case node.kind:
of funDecl:
var node = FunDecl(node)
let resolved = self.resolve(node.name)
if resolved != nil:
if not resolved.isNil():
return resolved.valueType
of NodeKind.varDecl:
var node = VarDecl(node)
let resolved = self.resolve(node.name)
if resolved != nil:
if not resolved.isNil():
return resolved.valueType
else:
return self.inferType(node.value)
@ -653,7 +653,7 @@ proc typeToStr(self: Compiler, typ: Type): string =
if i < typ.args.len() - 1:
result &= ", "
result &= ")"
if typ.returnType != nil:
if not typ.returnType.isNil():
result &= &": {self.typeToStr(typ.returnType)}"
else:
discard
@ -824,6 +824,28 @@ proc generateCall(self: Compiler, fn: Name, args: seq[Expression]) =
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)
proc callUnaryOp(self: Compiler, fn: Name, op: UnaryExpr) =
## Emits the code to call a unary operator
self.generateCall(fn, @[op.a])
@ -971,13 +993,13 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
elif argument.isPtr:
name.valueType = Type(kind: Pointer, value: name.valueType)
# We check if the argument's type is a generic
if name.valueType == nil and argument.valueType.kind == identExpr:
if name.valueType.isNil() and argument.valueType.kind == identExpr:
for gen in node.generics:
if gen.name == IdentExpr(argument.valueType):
name.valueType = Type(kind: Generic)
break
# If it's still nil, it's an error!
if name.valueType == nil:
if name.valueType.isNil():
self.error(&"cannot determine the type of argument '{argument.name.token.lexeme}'")
fn.valueType.args.add((argument.name.token.lexeme, name.valueType))
else:
@ -987,18 +1009,18 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
proc identifier(self: Compiler, node: IdentExpr) =
## Compiles access to identifiers
let s = self.resolve(node)
if s == nil:
if s.isNil():
self.error(&"reference to undeclared name '{node.token.lexeme}'")
elif s.isConst:
# Constants are always emitted as Load* instructions
# no matter the scope depth
self.emitConstant(node, self.inferType(node))
else:
if s.valueType.kind == Function:
self.emitByte(LoadFunction)
self.emitBytes(s.codePos.toTriple())
self.detectClosureVariable(s)
if not s.isClosedOver:
if s.valueType.kind == Function:
self.emitByte(LoadFunctionObj)
self.emitBytes(s.codePos.toTriple())
elif not s.isClosedOver:
# Static name resolution, loads value at index in the stack. Very fast. Much wow.
self.emitByte(LoadVar)
# No need to check for -1 here: we already did a nil-check above!
@ -1009,8 +1031,7 @@ proc identifier(self: Compiler, node: IdentExpr) =
# 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())
if s.valueType.kind == Function:
self.emitByte(PopC)
proc assignment(self: Compiler, node: ASTNode) =
@ -1020,7 +1041,7 @@ proc assignment(self: Compiler, node: ASTNode) =
let node = AssignExpr(node)
let name = IdentExpr(node.name)
let r = self.resolve(name)
if r == nil:
if r.isNil():
self.error(&"assignment to undeclared name '{name.token.lexeme}'")
elif r.isConst:
self.error(&"cannot assign to '{name.token.lexeme}' (constant)")
@ -1040,7 +1061,7 @@ proc assignment(self: Compiler, node: ASTNode) =
of setItemExpr:
let node = SetItemExpr(node)
let typ = self.inferType(node)
if typ == nil:
if typ.isNil():
self.error(&"cannot determine the type of '{node.name.token.lexeme}'")
# TODO
else:
@ -1117,7 +1138,7 @@ proc ifStmt(self: Compiler, node: IfStmt) =
## execution of code
var cond = self.inferType(node.condition)
if not self.compareTypes(cond, Type(kind: Bool)):
if cond == nil:
if cond.isNil():
if node.condition.kind == identExpr:
self.error(&"reference to undeclared identifier '{IdentExpr(node.condition).name.lexeme}'")
elif node.condition.kind == callExpr and CallExpr(node.condition).callee.kind == identExpr:
@ -1138,7 +1159,7 @@ proc ifStmt(self: Compiler, node: IfStmt) =
self.statement(node.thenBranch)
let jump2 = self.emitJump(JumpForwards)
self.patchJump(jump)
if node.elseBranch != nil:
if not node.elseBranch.isNil():
self.statement(node.elseBranch)
self.patchJump(jump2)
@ -1203,7 +1224,7 @@ proc checkCallIsPure(self: Compiler, node: ASTnode): bool =
return false
proc callExpr(self: Compiler, node: CallExpr): Name =
proc callExpr(self: Compiler, node: CallExpr) =
## Compiles code to call a function
var args: seq[tuple[name: string, kind: Type]] = @[]
var argExpr: seq[Expression] = @[]
@ -1211,7 +1232,7 @@ proc callExpr(self: Compiler, node: CallExpr): Name =
# TODO: Keyword arguments
for i, argument in node.arguments.positionals:
kind = self.inferType(argument)
if kind == nil:
if kind.isNil():
if argument.kind == identExpr:
self.error(&"reference to undeclared identifier '{IdentExpr(argument).name.lexeme}'")
self.error(&"cannot infer the type of argument {i + 1} in function call")
@ -1228,26 +1249,26 @@ proc callExpr(self: Compiler, node: CallExpr): Name =
of NodeKind.callExpr:
var node = node.callee
while node.kind == callExpr:
funct = self.callExpr(CallExpr(node))
self.callExpr(CallExpr(node))
node = CallExpr(node).callee
# funct = self.matchImpl(IdentExpr(node.callee).name.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: args))
else:
discard # TODO: Lambdas
self.generateCall(funct, argExpr)
discard # TODO: Calling expressions
if not funct.isNil():
self.generateCall(funct, argExpr)
else:
self.generateObjCall(argExpr)
if self.scopeDepth > 0 and not self.checkCallIsPure(node.callee):
if not self.currentFunction.name.isNil():
self.error(&"cannot make sure that calls to '{self.currentFunction.name.token.lexeme}' are side-effect free")
else:
self.error(&"cannot make sure that call is side-effect free")
result = funct
proc expression(self: Compiler, node: Expression) =
## Compiles all expressions
case node.kind:
of NodeKind.callExpr:
discard self.callExpr(CallExpr(node)) # TODO
self.callExpr(CallExpr(node)) # TODO
of getItemExpr:
discard # TODO: Get rid of this
of pragmaExpr:
@ -1425,13 +1446,12 @@ proc statement(self: Compiler, node: Statement) =
of exprStmt:
var expression = ExprStmt(node).expression
self.expression(expression)
if expression.kind == callExpr and self.inferType(CallExpr(expression).callee).returnType == nil:
if expression.kind == callExpr and self.inferType(CallExpr(expression).callee).returnType.isNil():
# The expression has no type, so we don't have to
# pop anything
discard
else:
# We only print top-level expressions
if self.replMode and self.scopeDepth == 0:
if self.replMode:
self.emitByte(PopRepl)
else:
self.emitByte(Pop)
@ -1478,19 +1498,19 @@ proc varDecl(self: Compiler, node: VarDecl) =
## Compiles variable declarations
let expected = self.inferType(node.valueType)
let actual = self.inferType(node.value)
if expected == nil and actual == nil:
if expected.isNil() and actual.isNil():
if node.value.kind == identExpr:
self.error(&"reference to undeclared identifier '{node.value.token.lexeme}'")
self.error(&"'{node.name.token.lexeme}' has no type")
elif expected != nil and expected.kind == Mutable: # I mean, variables *are* already mutable (some of them anyway)
elif not expected.isNil() and expected.kind == Mutable: # I mean, variables *are* already mutable (some of them anyway)
self.error(&"invalid type '{self.typeToStr(expected)}' for var")
elif not self.compareTypes(expected, actual):
if expected != nil:
if not expected.isNil():
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.declareName(node, mutable=node.token.kind == Var)
self.emitByte(StoreVar)
self.emitBytes(self.names.high().toTriple())
self.emitBytes((self.names.high() + 1).toTriple())
proc typeDecl(self: Compiler, node: TypeDecl) =
@ -1521,7 +1541,7 @@ proc funDecl(self: Compiler, node: FunDecl) =
if not isGeneric:
self.error(&"cannot infer the type of '{node.returnType.token.lexeme}'")
# TODO: Forward declarations
if node.body != nil:
if not node.body.isNil():
if BlockStmt(node.body).code.len() == 0:
self.error("cannot declare function with empty body")
let fnType = self.inferType(node)
@ -1562,12 +1582,12 @@ proc funDecl(self: Compiler, node: FunDecl) =
hasVal = LambdaExpr(Declaration(self.currentFunction)).hasExplicitReturn
else:
discard # Unreachable
if hasVal and self.currentFunction.returnType == nil and typ.returnType != nil:
if hasVal and self.currentFunction.returnType.isNil() and not typ.returnType.isNil():
self.error("non-empty return statement is not allowed in void functions")
elif not hasVal and self.currentFunction.returnType != nil:
elif not hasVal and not self.currentFunction.returnType.isNil():
self.error("function has an explicit return type, but no return statement was found")
self.endFunctionBeforeReturn()
hasVal = hasVal and typ.returnType != nil
hasVal = hasVal and not typ.returnType.isNil()
self.endScope(deleteNames=true, fromFunc=true)
self.emitByte(OpCode.Return)
if hasVal:
@ -1579,7 +1599,7 @@ proc funDecl(self: Compiler, node: FunDecl) =
self.chunk.cfi.add(self.chunk.code.high().toTriple())
self.chunk.cfi.add(self.frames[^1].toTriple())
self.chunk.cfi.add(uint8(node.arguments.len()))
if not system.`==`(node.name, nil):
if not node.name.isNil():
self.chunk.cfi.add(node.name.token.lexeme.len().toDouble())
var s = node.name.token.lexeme
if node.name.token.lexeme.len() >= uint16.high().int:

View File

@ -126,7 +126,9 @@ type
Assert, # Raises an AssertionFailed exception if x is false
NoOp, # Just a no-op
LoadArgument,
PopC
LoadFunctionObj,
PopC,
PushC
# We group instructions by their operation/operand types for easier handling when debugging
@ -139,7 +141,7 @@ const simpleInstructions* = {Return, LoadNil,
BeginTry, FinishTry, Yield,
Await, NoOp, PopClosure,
SetResult, LoadArgument,
PopC}
PopC, LoadFunctionObj, PushC}
# Constant instructions are instructions that operate on the bytecode constant table
const constantInstructions* = {LoadInt64, LoadUInt64,

View File

@ -76,7 +76,7 @@ proc repl(vm: PeonVM = newPeonVM()) =
elif current == "#clear":
stdout.write("\x1Bc")
continue
input &= &"\n{current}"
input &= &"\n{current}\n"
tokens = tokenizer.lex(input, "stdin")
if tokens.len() == 0:
continue