Various fixes to closures. Fixed chained calls and local scopes

This commit is contained in:
Mattia Giambirtone 2022-10-07 15:55:41 +02:00
parent a4bccba6cd
commit e759e6cbb2
5 changed files with 103 additions and 55 deletions

View File

@ -648,6 +648,13 @@ when debugVM: # So nim shuts up
if i < self.frames.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, "]"
if self.envs.len() !> 0:
stdout.styledWrite(fgGreen, "Environments: ", fgMagenta, "[")
for i, e in self.envs:
@ -655,13 +662,6 @@ when debugVM: # So nim shuts up
if i < self.envs.high():
stdout.styledWrite(fgYellow, ", ")
styledEcho fgMagenta, "]"
if self.closures.len() !> 0:
stdout.styledWrite(fgGreen, "Environment offsets: ", fgMagenta, "[")
for i, e in self.closures:
stdout.styledWrite(fgYellow, $e)
if i < self.closures.high():
stdout.styledWrite(fgYellow, ", ")
styledEcho fgMagenta, "]"
if self.results.len() !> 0:
stdout.styledWrite(fgYellow, "Function Results: ", fgMagenta, "[")
for i, e in self.results:
@ -669,13 +669,6 @@ 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
@ -771,7 +764,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(offset)
self.closures.add(offset - 1)
# Loads the arguments onto the stack
for _ in 0..<argc:
self.pushc(self.pop())

View File

@ -476,6 +476,7 @@ proc getStackPos(self: Compiler, name: Name): int =
if name == variable:
found = true
break
inc(result)
if not found:
return -1
@ -954,6 +955,32 @@ proc emitFunction(self: Compiler, name: Name) =
self.emitBytes(self.getStackPos(name).toTriple(), name.line)
proc generateCall(self: Compiler, fn: Type, args: seq[Expression], line: int) =
## Version of generateCall that takes Type objects
## instead of Name objects. The function is assumed
## to be on the stack
self.emitByte(LoadUInt32, line)
self.emitBytes(self.chunk.writeConstant(0.toQuad()), line)
let pos = self.chunk.consts.len() - 4
for i, argument in reversed(args):
# We pass the arguments in reverse
# because of how stacks work. They'll
# be reversed again at runtime
self.check(argument, fn.args[^(i + 1)].kind)
self.expression(argument)
# Creates a new call frame and jumps
# to the function's first instruction
# in the code
if not fn.isClosure:
self.emitByte(Call, line)
else:
self.emitByte(CallClosure,line)
self.emitBytes(fn.args.len().toTriple(), line)
if fn.isClosure:
self.emitBytes(fn.envLen.toTriple(), line)
self.patchReturnAddress(pos)
proc generateCall(self: Compiler, fn: Name, args: seq[Expression], onStack: bool = false) =
## Small wrapper that abstracts emitting a call instruction
## for a given function
@ -1008,6 +1035,8 @@ proc beginScope(self: Compiler) =
proc `$`(self: Type): string = $self[]
# Flattens our weird function tree into a linear
# list
proc flattenImpl(self: Type, to: var seq[Type]) =
to.add(self)
for child in self.children:
@ -1028,24 +1057,41 @@ proc endScope(self: Compiler) =
for name in self.names:
if name.depth > self.scopeDepth:
names.add(name)
if name.valueType.kind notin {Generic, CustomType} and not name.isFunDecl:
if name.valueType.kind notin {Generic, CustomType} and not name.isFunDecl and name.depth > 0:
# We don't increase the pop count for these kinds of objects
# because they're not stored the same way as regular variables
# (for types, generics and function declarations). We also don't
# pop for local scopes because the VM takes care of closing a function's
# stack frame
inc(popCount)
if name.isFunDecl and not name.valueType.isClosure and name.valueType.children.len() > 0 and name.depth == 0:
# If a function at the top level contains any closures,
# when it goes out of scope all of the environments that
# belong to its inner functions also go out of
# scope
if name.isFunDecl and name.valueType.children.len() > 0 and name.depth == 0:
# When a closure goes out of scope, its environment is reclaimed.
# This includes the environments of every other closure that may
# have been contained within it, too
var i = 0
let f = name.valueType
for fn in flatten(f):
if fn.isClosure:
for y in 0..f.envLen:
self.closedOver.delete(y + i)
self.emitByte(PopClosure, self.peek().token.line)
self.emitBytes((y + i).toTriple(), self.peek().token.line)
inc(i)
var all = flatten(name.valueType)
# Why this? Well, it's simple: if a function returns
# a closure, that function becomes a closure too. The
# environments of closures are aligned one after the
# other, so if a and b are both closures, but only b
# closes over a value, both a and b will have an envLen
# of 1, which would cause us to emit one extra PopClosure
# instruction than what's actually needed. We can account
# for this easily by checking if the contained function's
# environment is larger than the contained one, which will
# guarantee there actually is some value that the contained
# function is closing over
var envLen = 0
var lastEnvLen = 0
for fn in all:
if fn.isClosure and fn.envLen > lastEnvLen:
envLen += fn.envLen
lastEnvLen = fn.envLen
for y in 0..<envLen:
self.closedOver.delete(y + i)
self.emitByte(PopClosure, self.peek().token.line)
self.emitBytes((y + i).toTriple(), self.peek().token.line)
inc(i)
if popCount > 1:
# If we're popping less than 65535 variables, then
# we can emit a PopN instruction. This is true for
@ -1434,8 +1480,8 @@ proc identifier(self: Compiler, node: IdentExpr) =
else:
if s.valueType.kind == Function and s.isFunDecl:
# Functions have no runtime
# representation, so we need
# to create one on the fly
# representation: they're just
# a location to jump to
self.emitByte(LoadInt64, node.token.line)
self.emitBytes(self.chunk.writeConstant(s.codePos.toLong()), node.token.line)
elif self.scopeDepth > 0 and not self.currentFunction.isNil() and s.depth != self.scopeDepth:
@ -1446,15 +1492,18 @@ proc identifier(self: Compiler, node: IdentExpr) =
var fn = self.currentFunction.valueType
while true:
fn.isClosure = true
fn.envLen += 1
if fn.parent.isNil():
break
fn = fn.parent
s.isClosedOver = true
self.currentFunction.valueType.envLen += 1
self.closedOver.add(s)
let stackIdx = self.getStackPos(s).toTriple()
let closeIdx = self.getClosurePos(s).toTriple()
let oldLen = self.chunk.code.len()
# This madness makes it so that we can insert bytecode
# at arbitrary offsets into our alredy compiled code and
# have our metadata be up to date
self.chunk.code.insert(StoreClosure.uint8, s.belongsTo.codePos)
self.chunk.code.insert(stackIdx[0], s.belongsTo.codePos + 1)
self.chunk.code.insert(stackIdx[1], s.belongsTo.codePos + 2)
@ -1581,7 +1630,12 @@ proc callExpr(self: Compiler, node: CallExpr): Name {.discardable.} =
for exp in reversed(all):
self.callExpr(exp)
# TODO
self.generateCall(result, argExpr, onStack=true)
case all[^1].callee.kind:
of identExpr:
let fn = self.resolve(IdentExpr(all[^1].callee))
self.generateCall(fn.valueType.returnType, argExpr, fn.line)
else:
discard # TODO: Lambdas
# TODO: Calling lambdas on-the-fly (i.e. on the same line)
else:
let typ = self.inferType(node)

View File

@ -150,6 +150,17 @@ proc callInstruction(self: Debugger, instruction: OpCode) =
self.current += 1
proc callClosureInstruction(self: Debugger, instruction: OpCode) =
## Debugs closure 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
var envSize = [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), fgGreen, " with environment of size ", fgYellow, $envSize, fgGreen
self.current += 1
proc constantInstruction(self: Debugger, instruction: OpCode) =
## Debugs instructions that operate on the constant table
var size: uint
@ -203,6 +214,8 @@ proc disassembleInstruction*(self: Debugger) =
self.storeClosureInstruction(opcode)
of Call:
self.callInstruction(opcode)
of CallClosure:
self.callClosureInstruction(opcode)
of jumpInstructions:
self.jumpInstruction(opcode)
else:

View File

@ -5,13 +5,13 @@ fn first(a, b, c: int): int {
return a;
}
fn second(a, b: int): int {
return first(b, a, 0);
fn second(a, b, c: int): int {
return first(b, a, c);
}
fn last(a, b, c: int): int {
return second(a, c);
return second(a, c, b);
}
@ -20,8 +20,9 @@ fn middle(a, b, c: int): int {
}
# These should all print true!
print(first(1, 2, 3) == 1);
var x = first(second(1, 2), 3, 4);
print(x == 2);
print(middle(3, 1, 2) == 1);
print(first(last(second(2, 1), 3, 0), 4, 5) == 0);
print(second(1, 2, 3) == 2);
print(last(1, 2, 3) == 3);
print(first(second(1, 2, 3), 2, 3) == 2);
print(last(1, 2, second(3, 4, 5)) == 4);

View File

@ -11,19 +11,6 @@ fn outer: fn (n: int): int {
}
fn getAdder(a, b: int): fn (): int64 {
var x = a;
var y = b;
fn adder: int {
return x + y;
}
return adder;
}
var x = outer();
print(x(50)); # 50
print(outer()(1)); # 1
var a = 1;
var b = 2;
var adder = getAdder(a, b);
print(adder()); # 3