It is now possible to close over function arguments

This commit is contained in:
Mattia Giambirtone 2022-08-19 10:45:07 +02:00
parent 885d6e3ea8
commit 85de75a50a
5 changed files with 109 additions and 56 deletions

View File

@ -31,7 +31,7 @@ when debugVM or debugMem or debugGC:
import std/terminal
{.push checks:off.} # The VM is a critical point where checks are deleterious
{.push checks:on.} # The VM is a critical point where checks are deleterious
type
PeonVM* = ref object
@ -54,6 +54,7 @@ type
operands: seq[uint64] # Our operand stack
cache: array[6, uint64] # Singletons cache
frames: seq[uint64] # Stores the bottom of stack frames
closures: seq[uint64] # Stores closure offsets
closedOver: seq[uint64] # Stores variables that do not have stack semantics
results: seq[uint64] # Stores function's results (return values)
gc: PeonGC
@ -62,7 +63,8 @@ type
## peon objects
String, List,
Dict, Tuple,
CustomType
CustomType,
Closure
HeapObject* = object
## A tagged box for a heap-allocated
## peon object
@ -429,11 +431,11 @@ proc peekc(self: PeonVM, distance: int = 0): uint64 {.used.} =
return self.calls[self.calls.high() + distance]
proc getc(self: PeonVM, idx: uint64): uint64 =
proc getc(self: PeonVM, idx: int): uint64 =
## Accessor method that abstracts
## indexing our call stack through stack
## frames
return self.calls[idx + self.frames[^1]]
return self.calls[idx.uint64 + self.frames[^1]]
proc setc(self: PeonVM, idx: uint, val: uint64) =
@ -636,6 +638,13 @@ when debugVM: # So nim shuts up
if i < self.results.high():
stdout.styledWrite(fgYellow, ", ")
styledEcho fgMagenta, "]"
if self.closures.len() !> 0:
stdout.styledWrite(fgBlue, "Closure offsets: ", fgMagenta, "[")
for i, e in self.closures:
stdout.styledWrite(fgYellow, $e)
if i < self.closures.high():
stdout.styledWrite(fgYellow, ", ")
styledEcho fgMagenta, "]"
discard readLine stdin
@ -713,6 +722,7 @@ proc dispatch*(self: PeonVM) =
self.results.add(self.getNil())
# Creates a new call frame
self.frames.add(uint64(self.calls.len() - 2))
self.closures.add(self.closedOver.len().uint64)
# Loads the arguments onto the stack
for _ in 0..<argc:
self.pushc(self.pop())
@ -726,7 +736,7 @@ proc dispatch*(self: PeonVM) =
# Every peon program is wrapped
# in a hidden function, so this
# will also exit the VM if we're
# at the end of the program
# at the end of the program
while self.calls.len().uint64 !> self.frames[^1] + 2'u64:
# Discards the function's local variables,
# if there is any
@ -740,6 +750,7 @@ proc dispatch*(self: PeonVM) =
discard self.results.pop()
# Discard the topmost stack frame
discard self.frames.pop()
discard self.closures.pop()
if self.frames.len() == 0:
# End of the program!
return
@ -757,7 +768,7 @@ proc dispatch*(self: PeonVM) =
# into the given call stack index
let idx = self.readLong()
when debugVM:
assert idx - self.calls.high() <= 1, "StoreVar index is bigger than the length of the call stack"
assert idx.int - self.calls.high() <= 1, "StoreVar index is bigger than the length of the call stack"
if idx + self.frames[^1] <= self.calls.high().uint:
self.setc(idx, self.pop())
else:
@ -775,11 +786,16 @@ proc dispatch*(self: PeonVM) =
of LoadClosure:
# Loads a closed-over variable onto the
# stack
self.push(self.closedOver[self.readLong()])
self.push(self.closedOver[self.readLong() + self.closures[^1] - 1])
of PopClosure:
self.closedOver.delete(self.readLong())
of LiftArgument:
# Lifts a function argument onto the stack
self.closedOver.add(self.getc(self.readLong().int))
of LoadVar:
# Pushes a variable onto the operand
# stack
self.push(self.getc(self.readLong()))
self.push(self.getc(self.readLong().int))
of NoOp:
# Does nothing
continue

View File

@ -14,7 +14,6 @@
import meta/token
import meta/ast
import meta/errors
import ../config
import ../util/multibyte
import ../util/symbols
import lexer as l
@ -63,6 +62,9 @@ type
isBuiltinFunction: bool
builtinOp: string
fun: FunDecl
isClosure: bool
closureBounds: tuple[start, stop: int]
childFunc: Type
of Reference, Pointer:
value: Type
of Generic:
@ -100,6 +102,8 @@ type
codePos: int
# Is the name closed over (i.e. used in a closure)?
isClosedOver: bool
# The function that owns this variable (may be nil!)
belongsTo: Name
# Is this a function argument?
isFunctionArgument: bool
# Where is this node declared in the file?
@ -143,7 +147,7 @@ type
# in a local scope, otherwise it's global
scopeDepth: int
# The current function being compiled
currentFunction: Type
currentFunction: Name
# Are optimizations turned on?
enableOptimizations: bool
# The current loop being compiled (used to
@ -172,7 +176,7 @@ type
# be empty)
deferred: seq[uint8]
# List of closed-over variables
closedOver: seq[Name]
closedOver: seq[tuple[name: Name, count: int]]
# Keeps track of stack frames
frames: seq[int]
# Compiler procedures called by pragmas
@ -239,7 +243,7 @@ proc compileModule(self: Compiler, filename: string)
## Public getter for nicer error formatting
proc getCurrentNode*(self: Compiler): ASTNode = (if self.current >=
self.ast.len(): self.ast[^1] else: self.ast[self.current - 1])
proc getCurrentFunction*(self: Compiler): Declaration {.inline.} = (if self.currentFunction.isNil(): nil else: self.currentFunction.fun)
proc getCurrentFunction*(self: Compiler): Declaration {.inline.} = (if self.currentFunction.valueType.isNil(): nil else: self.currentFunction.valueType.fun)
proc getFile*(self: Compiler): string {.inline.} = self.file
proc getModule*(self: Compiler): string {.inline.} = self.currentModule
proc getLines*(self: Compiler): seq[tuple[start, stop: int]] = self.lines
@ -428,7 +432,7 @@ proc getClosurePos(self: Compiler, name: Name): int =
result = self.closedOver.high()
var found = false
for variable in reversed(self.closedOver):
if name == variable:
if name == variable.name:
found = true
break
dec(result)
@ -464,23 +468,29 @@ proc detectClosureVariable(self: Compiler, name: var Name, depth: int = self.sco
## unpredictably or crash
if name.isNil() or name.depth == 0 or name.isClosedOver:
return
elif name.depth < depth and self.scopes[name.depth - 1] != self.scopes[depth - 1]:
elif name.depth < depth and self.scopes[name.depth - 1] != self.scopes[depth - 1]:
# Ding! The given name is closed over in another function:
# we need to change the Jump instruction that self.declareName
# put in place for us into a StoreClosure. We also update
# the name's isClosedOver field so that self.identifier()
# can emit a LoadClosure instruction instead of a LoadVar
# once this name is referenced in the future
self.closedOver.add((name, 0))
name.isClosedOver = true
if not self.currentFunction.valueType.isClosure:
self.currentFunction.valueType.isClosure = true
self.currentFunction.valueType.closureBounds.start = self.closedOver.high()
self.currentFunction.valueType.closureBounds.stop = self.closedOver.high()
if self.closedOver.len() >= 16777216:
self.error("too many consecutive closed-over variables (max is 16777215)")
if not name.isFunctionArgument:
# We handle closed-over function arguments later
self.closedOver.add(name)
if self.closedOver.len() >= 16777216:
self.error("too many consecutive closed-over variables (max is 16777215)")
name.isClosedOver = true
self.chunk.code[name.codePos] = StoreClosure.uint8()
for i, b in self.closedOver.high().toTriple():
self.chunk.code[name.codePos + i + 1] = b
else:
self.chunk.code[name.codePos] = LiftArgument.uint8()
for i, b in self.getStackPos(name).toTriple():
self.chunk.code[name.codePos + i + 1] = b
proc compareTypes(self: Compiler, a, b: Type): bool =
@ -920,7 +930,7 @@ proc literal(self: Compiler, node: ASTNode) =
var x: float
var y = FloatExpr(node)
try:
discard parseFloat(y.literal.lexeme)
discard parseFloat(y.literal.lexeme, x)
except ValueError:
self.error("floating point value out of range")
self.emitConstant(y, self.inferType(y))
@ -1036,6 +1046,12 @@ proc generateCall(self: Compiler, fn: Name, args: seq[Expression], onStack: bool
# because of how stack semantics
# work. They'll be fixed at runtime
self.expression(argument)
var f = fn.valueType
while not f.isNil():
if f.isClosure:
for i in f.closureBounds.start..f.closureBounds.stop:
self.closedOver[i].count += 1
f = f.childFunc
# Creates a new call frame and jumps
# to the function's first instruction
# in the code
@ -1100,7 +1116,9 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
codePos: self.chunk.code.len(),
isLet: node.isLet,
isClosedOver: false,
line: node.token.line))
line: node.token.line,
belongsTo: self.currentFunction
))
if mutable:
self.names[^1].valueType.mutable = true
# We emit a jump of 0 because this may become a
@ -1163,7 +1181,9 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
isLet: false,
isClosedOver: false,
line: argument.name.token.line,
isFunctionArgument: true)
isFunctionArgument: true,
belongsTo: fn
)
self.names.add(name)
name.valueType = self.inferType(argument.valueType)
# If it's still nil, it's an error!
@ -1201,7 +1221,7 @@ proc identifier(self: Compiler, node: IdentExpr) =
# 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)
self.emitByte(LoadClosure)
self.emitBytes(self.getClosurePos(s).toTriple())
self.emitBytes((self.getClosurePos(s)).toTriple())
proc assignment(self: Compiler, node: ASTNode) =
@ -1242,7 +1262,7 @@ proc beginScope(self: Compiler) =
## Begins a new local scope by incrementing the current
## scope's depth
inc(self.scopeDepth)
self.scopes.add(self.currentFunction)
self.scopes.add(self.currentFunction.valueType)
proc endScope(self: Compiler) =
@ -1261,6 +1281,16 @@ proc endScope(self: Compiler) =
# We don't increase the pop count for these kinds of objects
# because they're not stored the same way as regular variables
inc(popCount)
if name.isFunDecl and not name.valueType.childFunc.isNil() and name.valueType.childFunc.isClosure:
var i = 0
var closure: tuple[name: Name, count: int]
for y in name.valueType.childFunc.closureBounds.start..name.valueType.childFunc.closureBounds.stop:
closure = self.closedOver[y + i]
self.closedOver.delete(y + i)
for _ in 0..<closure.count:
self.emitByte(PopClosure)
self.emitBytes((y + i).toTriple())
inc(i)
if popCount > 1:
# If we're popping less than 65535 variables, then
# we can emit a PopN instruction. This is true for
@ -1379,8 +1409,8 @@ proc callExpr(self: Compiler, node: CallExpr): Name {.discardable.} =
result = funct
self.generateCall(funct, argExpr, onStack)
if not self.checkCallIsPure(node.callee):
if self.currentFunction.name != "":
self.error(&"cannot make sure that calls to '{self.currentFunction.name}' are side-effect free")
if self.currentFunction.valueType.name != "":
self.error(&"cannot make sure that calls to '{self.currentFunction.valueType.name}' are side-effect free")
else:
self.error(&"cannot make sure that call is side-effect free")
@ -1451,7 +1481,7 @@ proc deferStmt(self: Compiler, node: DeferStmt) =
proc returnStmt(self: Compiler, node: ReturnStmt) =
## Compiles return statements
var expected = self.currentFunction.returnType
var expected = self.currentFunction.valueType.returnType
self.check(node.value, expected)
if not node.value.isNil():
self.expression(node.value)
@ -1683,21 +1713,20 @@ proc dispatchPragmas(self: Compiler, node: ASTnode) =
self.compilerProcs[pragma.name.token.lexeme](self, pragma, node)
proc fixGenericFunc(self: Compiler, name: Name, args: seq[Expression]): Type =
proc fixGenericFunc(self: Compiler, name: Name, args: seq[Expression]): Name =
## Specializes generic arguments in functions
var fn = name.valueType.deepCopy()
var fn = name.deepCopy()
result = fn
var typ: Type
for i in 0..args.high():
if fn.args[i].kind.kind == Generic:
if fn.valueType.args[i].kind.kind == Generic:
typ = self.inferType(args[i])
fn.args[i].kind = typ
self.resolve(fn.args[i].name).valueType = typ
if fn.args[i].kind.isNil():
fn.valueType.args[i].kind = typ
self.resolve(fn.valueType.args[i].name).valueType = typ
if fn.valueType.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] = @[]) =
## Compiles function declarations
#[if not node.isNil():
@ -1710,6 +1739,7 @@ proc funDecl(self: Compiler, node: FunDecl, fn: Name = nil, args: seq[Expression
self.dispatchPragmas(node)
var node = node
var fn = if fn.isNil(): self.names[^(node.arguments.len() + 1)] else: fn
var names = self.names[^(node.arguments.len())..^1]
if fn.valueType.isBuiltinFunction:
# We take the arguments off of our name list
# because they become temporaries on the stack.
@ -1727,13 +1757,20 @@ proc funDecl(self: Compiler, node: FunDecl, fn: Name = nil, args: seq[Expression
jmp = self.emitJump(JumpForwards)
# Function's code starts after the jump
fn.codePos = self.chunk.code.len()
# We let our debugger know a function is starting
let start = self.chunk.code.high()
for name in names:
self.emitBytes([NoOp, NoOp, NoOp, NoOp])
name.codePos = self.chunk.code.len() - 4
# We store the current function
self.currentFunction = fn.valueType
if not self.currentFunction.isNil():
self.currentFunction.valueType.childFunc = fn.valueType
self.currentFunction = fn
if node.isNil():
# We got called back with more specific type
# arguments: time to fix them!
self.currentFunction = self.fixGenericFunc(fn, args)
node = self.currentFunction.fun
node = self.currentFunction.valueType.fun
elif not node.body.isNil():
if BlockStmt(node.body).code.len() == 0:
self.error("cannot declare function with empty body")
@ -1758,18 +1795,16 @@ proc funDecl(self: Compiler, node: FunDecl, fn: Name = nil, args: seq[Expression
# the try/finally block with the deferred
# code
var deferStart = self.deferred.len()
# We let our debugger know a function is starting
let start = self.chunk.code.high()
self.beginScope()
for decl in BlockStmt(node.body).code:
self.declaration(decl)
let typ = self.currentFunction.returnType
let typ = self.currentFunction.valueType.returnType
var hasVal: bool = false
case self.currentFunction.fun.kind:
case self.currentFunction.valueType.fun.kind:
of NodeKind.funDecl:
hasVal = self.currentFunction.fun.hasExplicitReturn
hasVal = self.currentFunction.valueType.fun.hasExplicitReturn
of NodeKind.lambdaExpr:
hasVal = LambdaExpr(Declaration(self.currentFunction.fun)).hasExplicitReturn
hasVal = LambdaExpr(Declaration(self.currentFunction.valueType.fun)).hasExplicitReturn
else:
discard # Unreachable
if not hasVal and not typ.isNil():

View File

@ -152,6 +152,8 @@ type
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
StoreClosure, # Stores the value of b at position a in the closure array
LiftArgument, # Closes over a function argument
PopClosure,
## Looping and jumping
Jump, # Absolute, unconditional jump into the bytecode
JumpForwards, # Relative, unconditional, positive jump in the bytecode
@ -253,7 +255,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, LoadCLosure, }
const stackTripleInstructions* = {StoreVar, LoadVar, LoadCLosure, LiftArgument, PopClosure}
# Stack double instructions operate on the stack at arbitrary offsets and pop arguments off of it in the form
# of 16 bit integers
@ -265,9 +267,6 @@ const argumentDoubleInstructions* = {PopN, }
# Argument double argument instructions take hardcoded arguments as 24 bit integers
const argumentTripleInstructions* = {StoreClosure}
# Instructions that call functions
const callInstructions* = {Call, }
# Jump instructions jump at relative or absolute bytecode offsets
const jumpInstructions* = {Jump, JumpIfFalse, JumpIfFalsePop,
JumpForwards, JumpBackwards,

View File

@ -99,7 +99,6 @@ proc simpleInstruction(self: Debugger, instruction: OpCode) =
self.checkFrameEnd(self.current)
proc stackTripleInstruction(self: Debugger, instruction: OpCode) =
## Debugs instructions that operate on a single value on the stack using a 24-bit operand
var slot = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple()
@ -136,9 +135,10 @@ proc argumentTripleInstruction(self: Debugger, instruction: OpCode) =
proc callInstruction(self: Debugger, instruction: OpCode) =
## Debugs function calls
var size = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple()
self.current += 3
printInstruction(instruction)
styledEcho fgGreen, &", creates frame of size ", fgYellow, $(size + 2)
self.current += 4
styledEcho fgGreen, &", creates frame of size ", fgYellow, $(size + 2), fgGreen
self.current += 1
proc functionInstruction(self: Debugger, instruction: OpCode) =
@ -183,6 +183,8 @@ proc jumpInstruction(self: Debugger, instruction: OpCode) =
stdout.styledWrite(fgYellow, $jump)
nl()
self.current += 4
while self.chunk.code[self.current] == NoOp.uint8:
inc(self.current)
for i in countup(orig, self.current + 1):
self.checkFrameStart(i)
@ -207,7 +209,7 @@ proc disassembleInstruction*(self: Debugger) =
self.argumentDoubleInstruction(opcode)
of argumentTripleInstructions:
self.argumentTripleInstruction(opcode)
of callInstructions:
of Call:
self.callInstruction(opcode)
of jumpInstructions:
self.jumpInstruction(opcode)

View File

@ -3,14 +3,15 @@ import std;
fn makeClosure(n: int): fn: int {
let n = n; # Workaround
fn inner: int {
return n;
}
return inner;
}
var closure = makeClosure(1)();
print(closure); # 1
print(makeClosure(2)()); # 2
var closed = makeClosure(1)();
print(closed); # 1
print(makeClosure(2)()); # 2
var closure = makeClosure(3);
print(closure()); # 3
print(closure()); # 3