From cc0aab850e6a2bd17b9ad503197797bade61cd65 Mon Sep 17 00:00:00 2001 From: Mattia Giambirtone Date: Sat, 9 Jul 2022 12:47:53 +0200 Subject: [PATCH] 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 --- src/backend/vm.nim | 126 ++++++---- src/config.nim | 2 +- src/frontend/compiler.nim | 408 ++++++++++++++------------------- src/frontend/meta/bytecode.nim | 14 +- src/frontend/parser.nim | 31 ++- src/main.nim | 9 +- src/util/debugger.nim | 9 +- 7 files changed, 283 insertions(+), 316 deletions(-) diff --git a/src/backend/vm.nim b/src/backend/vm.nim index bde82cc..db55717 100644 --- a/src/backend/vm.nim +++ b/src/backend/vm.nim @@ -287,6 +287,41 @@ proc constReadFloat64(self: PeonVM, idx: int): PeonObject = 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) = ## Main bytecode dispatch loop var instruction {.register.}: OpCode @@ -342,11 +377,13 @@ proc dispatch*(self: PeonVM) = of LoadFloat64: self.push(self.constReadFloat64(int(self.readLong()))) of LoadFunction: - self.pushc(PeonObject(kind: Function, ip: self.readLong())) - of LoadFunctionObj: + # Loads a function onto the operand stack by reading its + # instruction pointer self.push(PeonObject(kind: Function, ip: self.readLong())) 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: # Calls a function. The calling convention for peon # 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 # return address. After that, all arguments and locals # follow - var size {.used.} = self.readLong().int - self.frames.add(self.calls.len() - 2) - self.ip = self.peekc(-1).ip + var argc {.used.} = self.readLong().int + let retAddr = self.peek(-argc) # Return address + 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()) - # 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.. 0: - dec(size) + #[while argc > 0: + dec(argc) self.pushc(self.getNil()) ]# - of LoadArgument: - self.pushc(self.pop()) - of OpCode.Return: + of Return: # Returns from a function. # Every peon program is wrapped # in a hidden function, so this @@ -380,6 +431,7 @@ proc dispatch*(self: PeonVM) = self.push(self.results.pop()) else: discard self.results.pop() + # Discard a stack frame discard self.frames.pop() if self.frames.len() == 0: # End of the program! @@ -392,11 +444,11 @@ proc dispatch*(self: PeonVM) = of StoreVar: # Stores the value at the top of the operand stack # into the given call stack index - let idx = int(self.readLong()) + self.frames[^1] - if idx <= self.calls.high(): + let idx = int(self.readLong()) + if idx + self.frames[^1] <= self.calls.high(): self.setc(idx, self.pop()) else: - self.calls.add(self.pop()) + self.pushc(self.pop()) of StoreClosure: # Stores/updates the value of a closed-over # variable @@ -411,56 +463,30 @@ proc dispatch*(self: PeonVM) = # Loads a closed-over variable onto the # stack self.push(self.closedOver[self.readLong()]) - of PopClosure: - # Removes a closed-over variable from the closure - # array - discard self.closedOver.pop() of LoadVar: + # Pushes a variable onto the operand + # stack self.push(self.getc(int(self.readLong()))) of NoOp: + # Does nothing continue of PopC: + # Pops a value off the call stack discard self.popc() of Pop: + # Pops a value off the operand stack discard self.pop() of PushC: + # Pushes a value from the operand stack + # onto the call stack self.pushc(self.pop()) of PopRepl: if self.frames.len() > 1: discard self.pop() continue - let popped = 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 + echo self.pop() of PopN: + # Pops N elements off the call stack for _ in 0.. 255: {.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_ALLOCATION* = false # Traces memory allocation/deallocation const DEBUG_TRACE_COMPILER* = false # Traces the compiler diff --git a/src/frontend/compiler.nim b/src/frontend/compiler.nim index 266a27a..b43d8e5 100644 --- a/src/frontend/compiler.nim +++ b/src/frontend/compiler.nim @@ -102,6 +102,10 @@ type isFunctionArgument: bool # Where is this node declared in the file? 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 ## A "loop object" used ## by the compiler to emit @@ -181,11 +185,11 @@ proc declaration(self: Compiler, node: Declaration) proc peek(self: Compiler, distance: int = 0): ASTNode proc identifier(self: Compiler, node: IdentExpr) proc varDecl(self: Compiler, node: VarDecl) -proc inferType(self: Compiler, node: LiteralExpr, strictMutable: bool = true): Type -proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Type +proc inferType(self: Compiler, node: LiteralExpr): Type +proc inferType(self: Compiler, node: Expression): Type proc findByName(self: Compiler, name: string): seq[Name] -proc findByType(self: Compiler, name: string, kind: Type, strictMutable: bool = true): seq[Name] -proc compareTypes(self: Compiler, a, b: Type, strictMutable: bool = true): bool +proc findByType(self: Compiler, name: string, kind: Type): seq[Name] +proc compareTypes(self: Compiler, a, b: Type): bool proc patchReturnAddress(self: Compiler, pos: int) proc handleMagicPragma(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 = ## Returns the predicted call stack position of a given name, relative ## to the current frame - result = 2 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 variable.isPrivate and variable.owner != self.currentModule: continue - if variable.depth == depth or variable.depth == 0: + elif variable.depth == depth or variable.depth == 0: # variable.depth == 0 for globals! found = true + dec(result) break - inc(result) if not found: 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 ## returns the predicted closure array position of a given name. ## 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 variable.isPrivate and variable.owner != self.currentModule: continue - elif variable.depth == depth: + else: found = true break dec(result) @@ -423,7 +430,7 @@ proc resolve(self: Compiler, name: string, 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 ## 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 @@ -434,27 +441,25 @@ proc detectClosureVariable(self: Compiler, name: Name, depth: int = self.scopeDe ## unpredictably or crash if name.isNil() or name.depth == 0: return - elif name.depth < depth and not name.isClosedOver: + elif name.depth < depth: # Ding! The given name is closed over: we need to # change the dummy 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 - self.closedOver.add(name) - let idx = self.closedOver.high().toTriple() + if not name.isClosedOver: + self.closedOver.add(name) if self.closedOver.len() >= 16777216: 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 + 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 ## for equality (works with nil!) - # The nil code here is for void functions (when # we compare their return types) 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 # be the same type! return false - elif a.mutable != b.mutable and strictMutable: - # Are they both (im)mutable? If not, - # they're different - return false case a.kind: # If all previous checks pass, it's time # 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): return false 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 true else: + # TODO: Custom types discard @@ -553,7 +555,7 @@ proc toIntrinsic(name: string): Type = 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 if node.isNil(): return nil @@ -565,7 +567,7 @@ proc inferType(self: Compiler, node: LiteralExpr, strictMutable: bool = true): T if size.len() == 1: return Type(kind: Int64) let typ = size[1].toIntrinsic() - if not self.compareTypes(typ, nil, strictMutable): + if not self.compareTypes(typ, nil): return typ else: 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": return Type(kind: Float64) let typ = size[1].toIntrinsic() - if not self.compareTypes(typ, nil, strictMutable): + if not self.compareTypes(typ, nil): return typ else: 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 -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 ## returns it if node.isNil(): @@ -611,9 +613,9 @@ proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Ty return self.inferType(UnaryExpr(node).a) of binaryExpr: let node = BinaryExpr(node) - var a = self.inferType(node.a, strictMutable) - var b = self.inferType(node.b, strictMutable) - if not self.compareTypes(a, b, strictMutable): + var a = self.inferType(node.a) + var b = self.inferType(node.b) + if not self.compareTypes(a, b): return nil return a of {intExpr, hexExpr, binExpr, octExpr, @@ -627,7 +629,7 @@ proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Ty 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, strictMutable))) + result.args.add((argument.name.token.lexeme, self.inferType(argument.valueType))) of callExpr: var node = CallExpr(node) case node.callee.kind: @@ -640,16 +642,16 @@ proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Ty else: result = nil of lambdaExpr: - result = self.inferType(LambdaExpr(node.callee).returnType, strictMutable) + result = self.inferType(LambdaExpr(node.callee).returnType) else: discard # Unreachable of varExpr: result = self.inferType(Var(node).value) result.mutable = true 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: - result = Type(kind: Pointer, value: self.inferType(Ptr(node).value, strictMutable)) + result = Type(kind: Pointer, value: self.inferType(Ptr(node).value)) else: discard # Unreachable @@ -715,19 +717,29 @@ proc findByName(self: Compiler, name: string): seq[Name] = 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 ## with the given name and type for obj in self.findByName(name): - if self.compareTypes(obj.valueType, kind, strictMutable): + if self.compareTypes(obj.valueType, kind): 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 ## compatible with the given type and returns its ## name object - let impl = self.findByType(name, kind, strictMutable) + let impl = self.findByType(name, kind) if impl.len() == 0: var msg = &"cannot find a suitable implementation for '{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) = ## Wrapper to emit LoadFunction instructions - self.emitByte(LoadFunction) - self.emitBytes(name.codePos.toTriple()) - + if name.isFunDecl: + 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 @@ -850,11 +873,11 @@ proc literal(self: Compiler, node: ASTNode) = 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 if fn.valueType.builtinOp notin ["GenericLogicalOr", "GenericLogicalAnd"]: - for argument in args: - self.expression(argument) + self.expression(args[1]) + self.expression(args[0]) case fn.valueType.builtinOp: of "AddInt64": self.emitByte(AddInt64) @@ -936,29 +959,27 @@ proc handleBuiltinFunction(self: Compiler, fn: Name, args: seq[Expression]) = self.emitByte(DivFloat32) of "MulFloat32": self.emitByte(MulFloat32) - of "GenericLogicalOr": + of "LogicalOr": self.expression(args[0]) let jump = self.emitJump(JumpIfTrue) self.expression(args[1]) self.patchJump(jump) - of "GenericLogicalAnd": + of "LogicalAnd": self.expression(args[0]) - var jump: int - if self.enableOptimizations: - jump = self.emitJump(JumpIfFalseOrPop) - else: - jump = self.emitJump(JumpIfFalse) - self.emitByte(Pop) + var jump = self.emitJump(JumpIfFalseOrPop) self.expression(args[1]) self.patchJump(jump) else: - discard # Unreachable + self.error(&"unknown built-in: '{fn.valueType.builtinOp}'") proc generateCall(self: Compiler, fn: Name, args: seq[Expression]) = ## Small wrapper that abstracts emitting a call instruction ## for a given function if fn.valueType.isBuiltinFunction: + # Builtins map to individual instructions + # (usually 1, but some use more) so we handle + # them differently self.handleBuiltinFunction(fn, args) return 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.emitByte(LoadReturnAddress) let pos = self.chunk.code.len() + # We initially emit a dummy return + # address. It is patched later 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.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 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()) + # Creates a new call frame and jumps + # to the function's first instruction + # in the code + self.emitByte(Call) + self.emitBytes(fn.valueType.args.len().toTriple()) self.patchReturnAddress(pos) @@ -1013,23 +1012,22 @@ proc callUnaryOp(self: Compiler, fn: Name, op: UnaryExpr) = proc callBinaryOp(self: Compiler, fn: Name, op: BinaryExpr) = ## Emits the code to call a binary operator - # Pushes the return address self.generateCall(fn, @[op.a, op.b]) proc unary(self: Compiler, node: UnaryExpr) = ## Compiles unary expressions such as decimal ## and bitwise negation - let valueType = self.inferType(node.a, strictMutable=false) - let funct = self.matchImpl(node.token.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", valueType)]), strictMutable=false) + let valueType = self.inferType(node.a) + let funct = self.matchImpl(node.token.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", valueType)])) self.callUnaryOp(funct, node) proc binary(self: Compiler, node: BinaryExpr) = ## Compiles all binary expressions - let typeOfA = self.inferType(node.a, strictMutable=false) - let typeOfB = self.inferType(node.b, strictMutable=false) - let funct = self.matchImpl(node.token.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", typeOfA), ("", typeOfB)]), strictMutable=false) + let typeOfA = self.inferType(node.a) + let typeOfB = self.inferType(node.b) + let funct = self.matchImpl(node.token.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", typeOfA), ("", typeOfB)])) 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 self.error("cannot declare more than 16777215 variables at a time") 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: - # Trying to redeclare a variable in the same module is an error, but it's okay + if name.depth == self.scopeDepth and not name.isFunctionArgument: + # 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 # mutate it) 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)) if mutable: 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: var node = FunDecl(node) # 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, isLet: false, isClosedOver: false, - line: node.token.line)) + line: node.token.line, + isFunDecl: true)) let fn = self.names[^1] var name: Name for argument in node.arguments: if self.names.high() > 16777215: self.error("cannot declare more than 16777215 variables at a time") # 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 name = Name(depth: self.scopeDepth + 1, isPrivate: true, @@ -1138,7 +1122,7 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) = proc identifier(self: Compiler, node: IdentExpr) = ## Compiles access to identifiers - let s = self.resolve(node) + var s = self.resolve(node) if s.isNil(): self.error(&"reference to undeclared name '{node.token.lexeme}'") elif s.isConst: @@ -1148,11 +1132,11 @@ proc identifier(self: Compiler, node: IdentExpr) = else: self.detectClosureVariable(s) if s.valueType.kind == Function: - if not s.valueType.isBuiltinFunction: - self.emitByte(LoadFunctionObj) - self.emitBytes(s.codePos.toTriple()) - else: - self.emitByte(LoadNil) + # Functions have no runtime + # representation, so we need + # to create one on the fly + self.emitByte(LoadFunction) + 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) @@ -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 # 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()) + self.emitBytes(self.getClosurePos(s.name).toTriple()) @@ -1173,7 +1157,7 @@ proc assignment(self: Compiler, node: ASTNode) = of assignExpr: let node = AssignExpr(node) let name = IdentExpr(node.name) - let r = self.resolve(name) + var r = self.resolve(name) if r.isNil(): self.error(&"assignment to undeclared name '{name.token.lexeme}'") elif r.isConst: @@ -1207,7 +1191,7 @@ proc beginScope(self: Compiler) = inc(self.scopeDepth) -proc endScope(self: Compiler, deleteNames: bool = true, fromFunc: bool = false) = +proc endScope(self: Compiler) = ## Ends the current local scope if self.scopeDepth < 0: 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: if name.depth > self.scopeDepth: names.add(name) - if not self.enableOptimizations and not fromFunc: - # 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 len(names) > 1: # If we're popping less than 65535 variables, then # we can emit a PopN instruction. This is true for # 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()): if self.names[i].depth > self.scopeDepth: 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 self.emitByte(PopC) # This seems *really* slow, but # what else should I do? Nim doesn't # allow the removal of items during # seq iteration so ¯\_(ツ)_/¯ - if deleteNames: - var idx = 0 - while idx < self.names.len(): - for name in names: - if self.names[idx] == name: - self.names.delete(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) + var idx = 0 + while idx < self.names.len(): + for name in names: + if self.names[idx] == name: + self.names.delete(idx) + inc(idx) + proc blockStmt(self: Compiler, node: BlockStmt) = ## Compiles block statements, which create a new - ## local scope. + ## local scope self.beginScope() for decl in node.code: self.declaration(decl) @@ -1270,25 +1243,13 @@ proc ifStmt(self: Compiler, node: IfStmt) = ## Compiles if/else statements for conditional ## execution of code var cond = self.inferType(node.condition) + self.expression(node.condition) if not self.compareTypes(cond, Type(kind: Bool)): 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: - 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") + self.error(&"expecting value of type 'bool', but expression has no type") else: self.error(&"expecting value of type 'bool', got '{self.typeToStr(cond)}' instead") - self.expression(node.condition) - var jumpCode: OpCode - if self.enableOptimizations: - jumpCode = JumpIfFalsePop - else: - jumpCode = JumpIfFalse - let jump = self.emitJump(jumpCode) - if not self.enableOptimizations: - self.emitByte(Pop) + let jump = self.emitJump(JumpIfFalsePop) self.statement(node.thenBranch) let jump2 = self.emitJump(JumpForwards) self.patchJump(jump) @@ -1310,51 +1271,23 @@ proc emitLoop(self: Compiler, begin: int) = proc whileStmt(self: Compiler, node: WhileStmt) = ## Compiles C-style while loops and ## desugared C-style for loops - let start = self.chunk.code.len() + var cond = self.inferType(node.condition) self.expression(node.condition) - var jump: int - if self.enableOptimizations: - jump = self.emitJump(JumpIfFalsePop) - else: - jump = self.emitJump(JumpIfFalse) - self.emitByte(Pop) + if not self.compareTypes(cond, Type(kind: Bool)): + if cond.isNil(): + self.error(&"expecting value of type 'bool', but expression has no type") + else: + self.error(&"expecting value of type 'bool', got '{self.typeToStr(cond)}' instead") + let start = self.chunk.code.len() + var jump = self.emitJump(JumpIfFalsePop) self.statement(node.body) self.patchJump(jump) 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 = ## Checks if a call has any side effects - if not self.isPure(node): - 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 + return true # TODO 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 argExpr: seq[Expression] = @[] var kind: Type - var strictMutable = true # TODO: Keyword arguments for i, argument in node.arguments.positionals: kind = self.inferType(argument) @@ -1370,8 +1302,6 @@ proc callExpr(self: Compiler, node: CallExpr) = 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") - if kind.mutable: - strictMutable = false args.add(("", kind)) argExpr.add(argument) for argument in node.arguments.keyword: @@ -1381,7 +1311,7 @@ proc callExpr(self: Compiler, node: CallExpr) = var funct: Name case node.callee.kind: 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: var node = node.callee while node.kind == callExpr: @@ -1394,13 +1324,15 @@ proc callExpr(self: Compiler, node: CallExpr) = self.error(&"expression has no type") else: self.error(&"object of type '{self.typeToStr(typ)}' is not callable") - if not funct.isNil(): - if funct.valueType.isBuiltinFunction: - self.handleBuiltinFunction(funct, argExpr) - else: - self.generateCall(funct, argExpr) + if any(funct.valueType.args, proc (arg: tuple[name: string, kind: Type]): bool = arg[1].kind == Generic): + # The function has generic arguments! We need to compile a version + # of it with the right type data + self.funDecl(nil, funct, argExpr) + # TODO: What next? + elif funct.valueType.isBuiltinFunction: + self.handleBuiltinFunction(funct, argExpr) else: - self.generateObjCall(argExpr) + self.generateCall(funct, argExpr) if self.scopeDepth > 0 and not self.checkCallIsPure(node.callee): if self.currentFunction.name != "": 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 var popped = 0 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) - if self.enableOptimizations and popped > 1: + if popped > 1: self.emitByte(PopN) self.emitBytes(popped.toDouble()) dec(popped, uint16.high().int) @@ -1491,17 +1423,19 @@ proc endFunctionBeforeReturn(self: Compiler) = proc returnStmt(self: Compiler, node: ReturnStmt) = ## Compiles return statements let actual = self.inferType(node.value) - let expected = self.currentFunction - if actual.isNil() and not expected.returnType.isNil(): + var expected = self.currentFunction.returnType + if not expected.isNil() and expected.kind == Generic: + expected = actual + if actual.isNil() and not expected.isNil(): if not node.value.isNil(): if node.value.kind == identExpr: self.error(&"reference to undeclared identifier '{node.value.token.lexeme}'") 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(&"expected return value of type '{self.typeToStr(expected.returnType)}', but expression has no type") - elif expected.returnType.isNil() and not actual.isNil(): - self.error("non-empty return statement is not allowed in void functions") - elif not self.compareTypes(actual, expected.returnType): + self.error(&"expected return value of type '{self.typeToStr(expected)}', but expression has no type") + elif expected.isNil() and not actual.isNil(): + self.error("empty return statement is only allowed in void functions") + elif not self.compareTypes(actual, expected): self.error(&"expected return value of type '{self.typeToStr(expected)}', got '{self.typeToStr(actual)}' instead") if not node.value.isNil(): 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.expression(node.value) self.declareName(node, mutable=node.token.kind == TokenType.Var) + var r = self.names[^1] + self.detectClosureVariable(r, decl=true) self.emitByte(StoreVar) - self.emitBytes(self.names.len().toTriple()) + self.emitBytes((self.getStackPos(r.name) + 1).toTriple()) 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 = ## Specializes generic arguments in functions - var fn = name.valueType - result = fn.deepCopy() - var node = fn.fun + var fn = name.valueType.deepCopy() + result = fn + var typ: Type for i in 0..args.high(): 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] = @[]) = @@ -1728,9 +1668,12 @@ 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 - if fn.valueType.returnType.isNil(): - self.error(&"cannot infer the type of '{node.returnType.token.lexeme}'") - if not fn.valueType.isBuiltinFunction: + if fn.valueType.isBuiltinFunction: + # We take the arguments off of our name list + # 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 jmp: int # 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() # We store the current function self.currentFunction = fn.valueType - let argLen = if node.isNil(): fn.valueType.args.len() else: node.arguments.len() - for _ in 0.. 0: + path &= "/" + path &= splitFile(f).name & ".pbc" + serializer.dumpFile(compiled, f, path) + serialized = serializer.loadFile(path) when debugSerializer: var hashMatches = computeSHA256(input).toHex().toLowerAscii() == serialized.fileHash styledEcho fgCyan, "Serialization step: " diff --git a/src/util/debugger.nim b/src/util/debugger.nim index 51d5289..62aaa97 100644 --- a/src/util/debugger.nim +++ b/src/util/debugger.nim @@ -63,8 +63,8 @@ proc printInstruction(instruction: OpCode, newline: bool = false) = proc checkFrameStart(self: Debugger, n: int) = - ## Todo: Checking frame end offsets needs - ## fixes + ## Checks if a call frame begins at the given + ## bytecode offset for i, e in self.cfiData: if n == e.start and not (e.started or e.stopped): e.started = true @@ -76,6 +76,8 @@ proc checkFrameStart(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: if n == e.stop and e.started and not e.stopped: e.stopped = true @@ -83,6 +85,7 @@ proc checkFrameEnd(self: Debugger, n: int) = proc simpleInstruction(self: Debugger, instruction: OpCode) = + ## Debugs simple instructions printInstruction(instruction, true) self.current += 1 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], self.chunk.code[self.current + 4]].fromQuad() printInstruction(instruction) - styledEcho fgGreen, &" loads address ", fgYellow, $address + styledEcho fgGreen, &", loads address ", fgYellow, $address self.current += 5