WIP closures

This commit is contained in:
prod2 2022-02-05 12:30:07 +01:00
parent 0c4a6553ff
commit 3e48b1cc52
7 changed files with 134 additions and 25 deletions

View File

@ -6,6 +6,7 @@ import types/value
type
OpCode* = enum
opReturn, opCall, opCheckArity, opFunctionDef, # functions
opClosure, # closures
opPop, opPopSA, opPopA # pop
opNegate, opNot # unary
opAdd, opSubtract, opMultiply, opDivide, # math
@ -111,7 +112,7 @@ const argInstructions = {
opPopA,
opGetLocal, opSetLocal,
opJumpIfFalse, opJump, opLoop, opJumpIfFalsePop,
opFunctionDef,
opFunctionDef, opClosure,
opCreateList, opCreateTable,
}

View File

@ -9,19 +9,21 @@ import bitops # needed for value
import config
type
Local = object
Local = ref object
name: string # name of this local
index: int # what is its index in the stack (0 is the stack bottom - nil in the main function)
depth: int # depth of this local
# if depth is -1, the variable cannot be referenced yet
# its depth will be set once its first ever value is determined
scope: Scope # the innermost scope of this local
Scope = ref object
labels: seq[string]
#depth: int
goalStackIndex: int # the stack count it started with plus 1
jumps: seq[int] # jumps to be patched that jump to the end
function: bool # if true, it is a function
parentFunction: Scope # if not a function, which scope is the innermost function it's in (if it's a function this points to itself)
# if parentFunction is nil, the scope is not inside a function
Compiler = ref object
#
@ -67,6 +69,11 @@ proc newScope(comp: Compiler, function: bool): Scope =
#result.depth = comp.scopes.len + 1
result.function = function
result.goalStackIndex = comp.stackIndex + 1
if function:
result.parentFunction = result
elif comp.scopes[comp.scopes.high()].parentFunction != nil:
result.parentFunction = comp.scopes[comp.scopes.high()].parentFunction
comp.scopes.add(result)
# HELPERS FOR THE COMPILER TYPE
@ -168,7 +175,7 @@ proc addLocal(comp: Compiler, name: string, delta: int) =
# if delta is 0 or negative, it means that it is already on the stack when addLocal is called
# if delta is positive, the first ever value of the local is to the right
comp.locals.add(Local(name: name, depth: if delta > 0: -1 else: comp.scopes.high, index: comp.stackIndex + delta))
comp.locals.add(Local(name: name, depth: if delta > 0: -1 else: comp.scopes.high, index: comp.stackIndex + delta, scope: comp.scopes[comp.scopes.high()]))
proc markInitialized(comp: Compiler) =
comp.locals[comp.locals.high].depth = comp.scopes.high
@ -295,6 +302,8 @@ proc jumpToEnd(comp: Compiler, scope: Scope) =
proc endScope(comp: Compiler) =
# remove locals
let popped = comp.scopes.pop()
if popped.parentFunction == popped:
popped.parentFunction = nil # cleanup cycles
let function = popped.function
when debugCompiler:
debugEcho &"End scope called for depth {comp.scopes.len} function? {function}"
@ -383,15 +392,30 @@ proc expString(comp: Compiler) =
tkString.genRule(expString, nop, pcNone)
proc resolveLocal(comp: Compiler, name: string): int =
## returns the stack index of the local of the name
proc addUpvalue(comp: Compiler, index: int): int =
## This proc takes an index to a local in the locals table
## and creates an upvalue in every function up until the one
## including this local so that all of the function scopes in between
## have the right upvalue in them (compile time)
discard
proc resolveLocal(comp: Compiler, name: string): Tuple[int, bool] =
## the bool arg specifies whether it found an upvalue
## if it's a local: returns the stack index of the local of the name
## if it's an upvalue: returns the upvalue index
## if the number is -1, then the name cannot be resolved at compile time
var i = comp.locals.high
let cfunc = comp.scopes[comp.scopes.high()].parentFunction
while i >= 0:
let local = comp.locals[i]
if local.name == name:
if local.depth == -1:
comp.error("Can't read local variable in its own initializer.")
return local.index
let upvalue = local.scope.parentFunction != cfunc
if not upvalue:
return (local.index, false)
else:
return (comp.addUpvalue(i), true)
i.dec
return -1
@ -403,12 +427,16 @@ proc variable(comp: Compiler) =
let name = comp.previous.text
# try resolving local, set arg to the index on the stack
var arg = comp.resolveLocal(name)
var (arg, upval) = comp.resolveLocal(name)
if arg != -1:
# local
getOp = opGetLocal
setOp = opSetLocal
if upval:
getOp = opGetUpvalue
setOp = opSetUpvalue
else:
# local
getOp = opGetLocal
setOp = opSetLocal
else:
# global
arg = comp.chunk.addConstant(name.fromNimString())

View File

@ -0,0 +1,20 @@
type
UpvalueObj[T] = object
location: ptr T
Upvalue[T]* = ptr UpvalueObj[T]
ClosureObj[T] = object
start: ptr uint8
upvalueCount: int
upvalues: UncheckedArray[Upvalue[T]]
Closure[T]* = ptr ClosureObj[T]
proc newClosure[T]*(start: ptr uint8, upvalueCount: int): Closure[T] =
result = cast[Closure[T]](alloc0(8 * upvalueCount + sizeof(ClosureObj[T])))
result.start = start
result.upvalueCount = upvalueCount
proc getIp*(clos: Closure): ptr uint8 {.inline.} =
clos.start

View File

@ -12,11 +12,12 @@ proc free*(ndStr: var NdString) =
# hashes
proc fnv1a*(ndStr: NdString): int {.inline.} =
var hash = 2166136261'u32
for i in countup(0, ndStr.len.int - 1):
hash = hash xor (ndStr.chars[i]).uint32
hash *= 16777619
return hash.int
return ndStr.hash.int
#var hash = 2166136261'u32
#for i in countup(0, ndStr.len.int - 1):
# hash = hash xor (ndStr.chars[i]).uint32
# hash *= 16777619
#return hash.int
proc fnv1a*(str: string): int {.inline.} = # SHOULD RETURN THE SAME HASH AS AN NDSTRING WITH THE SAME CONTENTS
var hash = 2166136261'u32

View File

@ -4,6 +4,7 @@ import bitops
import ndstring
import ndlist
import hashtable
import closure
type
NdValue* = uint
@ -23,7 +24,7 @@ type
# 00 -> nil or bool (singletons)
# 01 -> string
# 10 -> funct
# 11 -> closure (to be implemented later)
# 11 -> closure
# if bit 63 is 1:
# 00 -> list
# 01 -> table
@ -42,6 +43,8 @@ const tagString* = 0x7ffd000000000000'u
# 0111 1111 1111 11*10* 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
const tagFunct* = 0x7ffe000000000000'u
const tagClosure* = 0x7fff000000000000'u
const tagList* = 0xfffc000000000000'u
const tagTable* = 0xfffd000000000000'u
@ -64,6 +67,9 @@ template isString*(val: NdValue): bool =
template isFunct*(val: NdValue): bool =
val.bitand(mask48) == tagFunct
template isClosure*(val: NdValue): bool =
val.bitand(mask48) == tagClosure
template isList*(val: NdValue): bool =
val.bitand(mask48) == tagList
@ -83,6 +89,9 @@ template asString*(val: NdValue): NdString =
template asFunct*(val: NdValue): ptr uint8 =
cast[ptr uint8](val.bitand(mask48.bitnot()))
template asClosure*(val: NdValue): Closure[NdValue] =
cast[Closure[NdValue]](val.bitand(mask48.bitnot()))
template asList*(val: NdValue): List[NdValue] =
cast[List[NdValue]](val.bitand(mask48.bitnot()))
@ -107,6 +116,9 @@ template fromNimString*(sval: string): NdValue =
template fromFunct*(val: ptr uint8): NdValue =
cast[uint](val).bitor(tagFunct)
template fromClosure*(val: Closure[NdValue]): NdValue =
cast[uint](val).bitor(tagClosure)
template fromList*(val: List[NdValue]): NdValue =
cast[uint](val).bitor(tagList)

View File

@ -10,6 +10,7 @@ import bitops # needed for value's templates
import types/value
import types/hashtable
import types/ndlist
import types/closure
when profileInstructions:
@ -64,6 +65,17 @@ proc run*(chunk: Chunk): InterpretResult =
template frameBottom: int = frames.peek().stackBottom
template call(funct: NdValue, argcount: int, error: untyped) =
if funct.isFunct():
# create new frame
frames.add(Frame(stackBottom: stack.high - argcount, returnIp: ip))
ip = funct.asFunct() # jump to the entry point
elif funct.isClosure():
frames.add(Frame(stackBottom: stack.high - argcount, returnIp: ip))
ip = funct.asClosure().getIp()
else:
error
while true:
{.computedgoto.} # See https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma
@ -204,6 +216,11 @@ proc run*(chunk: Chunk): InterpretResult =
let faddr: ptr uint8 = ip
ip = ip.padd(offset)
stack.push(faddr.fromFunct())
of opClosure:
let offset = readDU8()
let faddr: ptr uint8 = ip
ip = ip.padd(offset)
stack.push(newClosure(faddr).fromClosure())
of opCheckArity:
let arity = readUI8()
let argcount = stack.high() - frameBottom
@ -218,16 +235,12 @@ proc run*(chunk: Chunk): InterpretResult =
# ... <ret val> <arg1> <arg2> <arg3>
let argcount = readUI8()
let funct = stack.getIndexNeg(argcount)
if not funct.isFunct():
stack.setIndexNeg(argcount, fromNil()) # replace the function with nil: this is the return value slot
funct.call(argcount):
runtimeError("Attempt to call a non-funct (a defunct?).") # here is a bad defunct joke
break
stack.setIndexNeg(argcount, fromNil()) # replace the function with nil: this is the return value slot
# create new frame
frames.add(Frame(stackBottom: stack.high - argcount, returnIp: ip))
ip = funct.asFunct() # jump to the entry point
of opCreateList:
let listLen = readDU8()
if listLen == 0:

View File

@ -29,4 +29,38 @@ var f2 = funct() {
};
//expect:5.0
f2()();
f2()();
// oop: constructors, getters, setters
var newAnimal = funct(species, color) {
var species = species; // copy it so that it's mutable, if args ever get immutable
var animal = @{
"getSpecies" = funct() :result = species,
"setSpecies" = funct(newSpecies) species = newSpecies,
"getColor" = funct() :result = color, // this captures an argument directly
};
:result = animal;
};
var horse1 = newAnimal("horse", "brown");
var horse2 = newAnimal("horse", "white");
var turtle = newAnimal("turtle", "brown");
//expect:horse
print horse1["getSpecies"]();
horse1["setSpecies"]("zebra");
//expect:zebra
print horse1["getSpecies"]();
//expect:brown
print horse1["getColor"]();
//expect:horse
print horse2["getSpecies"]();
//expect:white
print horse2["getColor"]();
//expect:turtle
print turtle["getSpecies"]();
// TODO: tests for enclosing upvalues multiple levels up