diff --git a/src/frontend/compiler/compiler.nim b/src/frontend/compiler/compiler.nim index f59a231..a1dc9fb 100644 --- a/src/frontend/compiler/compiler.nim +++ b/src/frontend/compiler/compiler.nim @@ -90,10 +90,65 @@ type types*: seq[tuple[match: bool, kind: Type]] else: discard +type + + NameKind* {.pure.} = enum + ## A name enumeration type + None, Module, Argument, Var, Function, CustomType, Enum + + Name* = ref object of RootObj + ## A generic name object + + # Type of the identifier (NOT of the value!) + case kind*: NameKind + of NameKind.Module: + path*: string + else: + discard + # The name's identifier + ident*: IdentExpr + # Owner of the identifier (module) + owner*: Name + # File where the name is declared + file*: string + # Scope depth + depth*: int + # Is this name private? + isPrivate*: bool + # Is this a constant? + isConst*: bool + # Can this name's value be mutated? + isLet*: bool + # Is this name a generic type? + isGeneric*: bool + # The type of the name's associated + # value + valueType*: Type + # The function that owns this name (may be nil!) + belongsTo*: Name + # Where is this node declared in its file? + line*: int + # Has this name been referenced at least once? + resolved*: bool + # The AST node associated with this node. This + # is needed because we compile function and type + # declarations only if, and when, they're actually + # used + node*: Declaration + # Who is this name exported to? (Only makes sense if isPrivate + # equals false) + exportedTo*: HashSet[Name] + # Has the compiler generates this name internally or + # does it come from user code? + isReal*: bool + # Is this name a builtin? + isBuiltin*: bool + WarningKind* {.pure.} = enum ## A warning enumeration type UnreachableCode, UnusedName, ShadowOuterScope, MutateOuterScope + CompileMode* {.pure.} = enum ## A compilation mode enumeration Debug, Release @@ -102,7 +157,7 @@ type node*: ASTNode function*: Declaration - Compiler* = ref object {.inheritable.} + Compiler* = ref object of RootObj ## A wrapper around the Peon compiler's state # The output of our parser (AST) @@ -120,6 +175,8 @@ type # swapped for a special instruction that prints # the result of the expression once it is evaluated replMode*: bool + # List of all compile-time names + names*: seq[Name] # Stores line data for error reporting lines*: seq[tuple[start, stop: int]] # The source of the current module, @@ -136,4 +193,618 @@ type # mismatches when we dispatch with match() showMismatches*: bool # Are we compiling in debug mode? - mode*: CompileMode \ No newline at end of file + mode*: CompileMode + # The current function being compiled + currentFunction*: Name + # The current module being compiled + currentModule*: Name + # The module importing us, if any + parentModule*: Name + + +## Public getters 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.valueType.fun) + +## Some "forward declarations" here (they're actually stubs because nim forces forward declarations to be +## implemented in the same module). They are methods because we need to dispatch to their actual specific +## implementations inside each target module, so we need the runtime type of the compiler object to be +## taken into account +method expression*(self: Compiler, node: Expression, compile: bool = true): Type {.discardable, base.} = nil +method identifier*(self: Compiler, node: IdentExpr, name: Name = nil, compile: bool = true, strict: bool = true): Type {.discardable, base.} = nil +method call*(self: Compiler, node: CallExpr, compile: bool = true): Type {.discardable, base.} = nil +method getItemExpr*(self: Compiler, node: GetItemExpr, compile: bool = true, matching: Type = nil): Type {.discardable, base.} = nil +method unary*(self: Compiler, node: UnaryExpr, compile: bool = true): Type {.discardable, base.} = nil +method binary*(self: Compiler, node: BinaryExpr, compile: bool = true): Type {.discardable, base.} = nil +method lambdaExpr*(self: Compiler, node: LambdaExpr, compile: bool = true): Type {.discardable, base.} = nil +method literal*(self: Compiler, node: ASTNode, compile: bool = true): Type {.discardable, base.} = nil +method infer*(self: Compiler, node: LiteralExpr): Type +method infer*(self: Compiler, node: Expression): Type +method inferOrError*(self: Compiler, node: Expression): Type +method findByName*(self: Compiler, name: string): seq[Name] +method findInModule*(self: Compiler, name: string, module: Name): seq[Name] +method findByType*(self: Compiler, name: string, kind: Type): seq[Name] +method compare*(self: Compiler, a, b: Type): bool +method match*(self: Compiler, name: string, kind: Type, node: ASTNode = nil, allowFwd: bool = true): Name +## End of forward declarations + +## Utility functions + +proc `$`*(self: Name): string = $(self[]) +proc `$`(self: Type): string = $(self[]) +proc hash(self: Name): Hash = self.ident.token.lexeme.hash() + + +proc peek*(self: Compiler, distance: int = 0): ASTNode = + ## Peeks at the AST node at the given distance. + ## If the distance is out of bounds, the last + ## AST node in the tree is returned. A negative + ## distance may be used to retrieve previously + ## consumed AST nodes + if self.ast.high() == -1 or self.current + distance > self.ast.high() or self.current + distance < 0: + result = self.ast[^1] + else: + result = self.ast[self.current + distance] + + +proc done*(self: Compiler): bool {.inline.} = + ## Returns true if the compiler is done + ## compiling, false otherwise + result = self.current > self.ast.high() + + +proc error*(self: Compiler, message: string, node: ASTNode = nil) {.inline.} = + ## Raises a CompileError exception + let node = if node.isNil(): self.getCurrentNode() else: node + raise CompileError(msg: message, node: node, line: node.token.line, file: node.file) + + +proc warning*(self: Compiler, kind: WarningKind, message: string, name: Name = nil, node: ASTNode = nil) = + ## Raises a warning. Note that warnings are always disabled in REPL mode + if self.replMode or kind in self.disabledWarnings: + return + var node: ASTNode = node + var fn: Declaration + if name.isNil(): + if node.isNil(): + node = self.getCurrentNode() + fn = self.getCurrentFunction() + else: + node = name.node + if node.isNil(): + node = self.getCurrentNode() + if not name.belongsTo.isNil(): + fn = name.belongsTo.node + else: + fn = self.getCurrentFunction() + var file = self.file + if not name.isNil(): + file = name.owner.file + var pos = node.getRelativeBoundaries() + if file notin ["", ""]: + file = relativePath(file, getCurrentDir()) + stderr.styledWrite(fgYellow, styleBright, "Warning in ", fgRed, &"{file}:{node.token.line}:{pos.start}") + if not fn.isNil() and fn.kind == funDecl: + stderr.styledWrite(fgYellow, styleBright, " in function ", fgRed, FunDecl(fn).name.token.lexeme) + stderr.styledWriteLine(styleBright, fgDefault, ": ", message) + try: + # We try to be as specific as possible with the warning message, pointing to the + # line it belongs to, but since warnings are not always raised from the source + # file they're generated in, we take into account the fact that retrieving the + # exact warning location may fail and bail out silently if it does + let line = readFile(file).splitLines()[node.token.line - 1].strip(chars={'\n'}) + stderr.styledWrite(fgYellow, styleBright, "Source line: ", resetStyle, fgDefault, line[0.. a.len(): + long = b + short = a + var i = 0 + for cond1 in short: + for cond2 in long: + if not self.compare(cond1.kind, cond2.kind) or cond1.match != cond2.match: + continue + inc(i) + return i >= short.len() + + +method compare*(self: Compiler, a, b: Type): bool = + ## Compares two type objects + ## for equality + result = false + # Note: 'All' is a type internal to the peon + # compiler that cannot be generated from user + # code in any way. It's used mostly for matching + # function return types (at least until we don't + # have return type inference) and it matches any + # type, including nil + if a.isNil(): + return b.isNil() or b.kind == All + elif b.isNil(): + return a.isNil() or a.kind == All + elif a.kind == All or b.kind == All: + return true + elif a.kind == b.kind: + # Here we compare types with the same kind discriminant + case a.kind: + of Int8, UInt8, Int16, UInt16, Int32, + UInt32, Int64, UInt64, Float32, Float64, + Char, Byte, String, Nil, TypeKind.Nan, Bool, TypeKind.Inf, Any: + return true + of Union: + return self.compareUnions(a.types, b.types) + of Generic: + return self.compareUnions(a.cond, b.cond) + of Reference, Pointer: + # Here we already know that both + # a and b are of either of the two + # types in this branch, so we just need + # to compare their values + return self.compare(a.value, b.value) + of Function: + # Functions are a bit trickier to compare + if a.args.len() != b.args.len(): + return false + if a.isCoroutine != b.isCoroutine or a.isGenerator != b.isGenerator: + return false + if not self.compare(b.returnType, a.returnType): + return false + var i = 0 + for (argA, argB) in zip(a.args, b.args): + # When we compare functions with forward + # declarations, or forward declarations + # between each other, we need to be more + # strict (as in: check argument names and + # their default values, any pragma associated + # with the function, and whether they are pure) + if a.forwarded: + if b.forwarded: + if argA.name != argB.name: + return false + else: + if argB.name == "": + # An empty argument name means + # we crafted this type object + # manually, so we don't need + # to match the argument name + continue + if argA.name != argB.name: + return false + elif b.forwarded: + if a.forwarded: + if argA.name != argB.name: + return false + else: + if argA.name == "": + continue + if argA.name != argB.name: + return false + if not self.compare(argA.kind, argB.kind): + return false + return true + else: + discard # TODO: Custom types, enums + elif a.kind == Union: + for constraint in a.types: + if self.compare(constraint.kind, b) and constraint.match: + return true + return false + elif b.kind == Union: + for constraint in b.types: + if self.compare(constraint.kind, a) and constraint.match: + return true + return false + elif a.kind == Generic: + if a.asUnion: + for constraint in a.cond: + if self.compare(constraint.kind, b) and constraint.match: + return true + return false + else: + for constraint in a.cond: + if not self.compare(constraint.kind, b) or not constraint.match: + return false + return true + elif b.kind == Generic: + if b.asUnion: + for constraint in b.cond: + if self.compare(constraint.kind, a) and constraint.match: + return true + return false + else: + for constraint in b.cond: + if not self.compare(constraint.kind, a) or not constraint.match: + return false + return true + elif a.kind == Any or b.kind == Any: + # Here we already know that neither of + # these types are nil, so we can always + # just return true + return true + return false + + +proc toIntrinsic*(name: string): Type = + ## Converts a string to an intrinsic + ## type if it is valid and returns nil + ## otherwise + if name == "any": + return Type(kind: Any) + elif name == "auto": + return Type(kind: Auto) + elif name in ["int", "int64", "i64"]: + return Type(kind: Int64) + elif name in ["uint64", "u64", "uint"]: + return Type(kind: UInt64) + elif name in ["int32", "i32"]: + return Type(kind: Int32) + elif name in ["uint32", "u32"]: + return Type(kind: UInt32) + elif name in ["int16", "i16", "short"]: + return Type(kind: Int16) + elif name in ["uint16", "u16"]: + return Type(kind: UInt16) + elif name in ["int8", "i8"]: + return Type(kind: Int8) + elif name in ["uint8", "u8"]: + return Type(kind: UInt8) + elif name in ["f64", "float", "float64"]: + return Type(kind: Float64) + elif name in ["f32", "float32"]: + return Type(kind: Float32) + elif name in ["byte", "b"]: + return Type(kind: Byte) + elif name in ["char", "c"]: + return Type(kind: Char) + elif name == "nan": + return Type(kind: TypeKind.Nan) + elif name == "nil": + return Type(kind: Nil) + elif name == "inf": + return Type(kind: TypeKind.Inf) + elif name == "bool": + return Type(kind: Bool) + elif name == "typevar": + return Type(kind: Typevar) + elif name == "string": + return Type(kind: String) + + +method infer*(self: Compiler, node: LiteralExpr): Type = + ## Infers the type of a given literal expression + if node.isNil(): + return nil + case node.kind: + of intExpr, binExpr, octExpr, hexExpr: + let size = node.token.lexeme.split("'") + if size.len() == 1: + return Type(kind: Int64) + let typ = size[1].toIntrinsic() + if not self.compare(typ, nil): + return typ + else: + self.error(&"invalid type specifier '{size[1]}' for int", node) + of floatExpr: + let size = node.token.lexeme.split("'") + if size.len() == 1: + return Type(kind: Float64) + let typ = size[1].toIntrinsic() + if not typ.isNil(): + return typ + else: + self.error(&"invalid type specifier '{size[1]}' for float", node) + of trueExpr: + return Type(kind: Bool) + of falseExpr: + return Type(kind: Bool) + of strExpr: + return Type(kind: String) + else: + discard # Unreachable + + +method infer*(self: Compiler, node: Expression): Type = + ## Infers the type of a given expression and + ## returns it + if node.isNil(): + return nil + case node.kind: + of NodeKind.identExpr: + result = self.identifier(IdentExpr(node), compile=false, strict=false) + of NodeKind.unaryExpr: + result = self.unary(UnaryExpr(node), compile=false) + of NodeKind.binaryExpr: + result = self.binary(BinaryExpr(node), compile=false) + of {NodeKind.intExpr, NodeKind.hexExpr, NodeKind.binExpr, NodeKind.octExpr, + NodeKind.strExpr, NodeKind.falseExpr, NodeKind.trueExpr, NodeKind.floatExpr + }: + result = self.infer(LiteralExpr(node)) + of NodeKind.callExpr: + result = self.call(CallExpr(node), compile=false) + of NodeKind.refExpr: + result = Type(kind: Reference, value: self.infer(Ref(node).value)) + of NodeKind.ptrExpr: + result = Type(kind: Pointer, value: self.infer(Ptr(node).value)) + of NodeKind.groupingExpr: + result = self.infer(GroupingExpr(node).expression) + of NodeKind.getItemExpr: + result = self.getItemExpr(GetItemExpr(node), compile=false) + of NodeKind.lambdaExpr: + result = self.lambdaExpr(LambdaExpr(node), compile=false) + else: + discard # TODO + + +method inferOrError*(self: Compiler, node: Expression): Type = + ## Attempts to infer the type of + ## the given expression and raises an + ## error with if it fails + result = self.infer(node) + if result.isNil(): + self.error("expression has no type", node) + + +method stringify*(self: Compiler, typ: Type): string = + ## Returns the string representation of a + ## type object + if typ.isNil(): + return "nil" + case typ.kind: + of Int8, UInt8, Int16, UInt16, Int32, + UInt32, Int64, UInt64, Float32, Float64, + Char, Byte, String, Nil, TypeKind.Nan, Bool, + TypeKind.Inf, Auto: + result &= ($typ.kind).toLowerAscii() + of Pointer: + result &= &"ptr {self.stringify(typ.value)}" + of Reference: + result &= &"ref {self.stringify(typ.value)}" + of Function: + result &= "fn (" + for i, (argName, argType, argDefault) in typ.args: + result &= &"{argName}: {self.stringify(argType)}" + if not argDefault.isNil(): + result &= &" = {argDefault}" + if i < typ.args.len() - 1: + result &= ", " + result &= ")" + if not typ.returnType.isNil(): + result &= &": {self.stringify(typ.returnType)}" + if typ.fun.pragmas.len() > 0: + result &= " {" + for i, pragma in typ.fun.pragmas: + result &= &"{pragma.name.token.lexeme}" + if pragma.args.len() > 0: + result &= ": " + for j, arg in pragma.args: + result &= arg.token.lexeme + if j < pragma.args.high(): + result &= ", " + if i < typ.fun.pragmas.high(): + result &= ", " + else: + result &= "}" + of Any: + return "any" + of Union: + for i, condition in typ.types: + if i > 0: + result &= " | " + if not condition.match: + result &= "~" + result &= self.stringify(condition.kind) + of Generic: + for i, condition in typ.cond: + if i > 0: + result &= " | " + if not condition.match: + result &= "~" + result &= self.stringify(condition.kind) + else: + discard + + +method findByName*(self: Compiler, name: string): seq[Name] = + ## Looks for objects that have been already declared + ## with the given name. Returns all objects that apply. + for obj in reversed(self.names): + if obj.ident.token.lexeme == name: + if obj.owner.path != self.currentModule.path: + if obj.isPrivate or self.currentModule notin obj.exportedTo: + continue + result.add(obj) + + +method findInModule*(self: Compiler, name: string, module: Name): seq[Name] = + ## Looks for objects that have been already declared as + ## public within the given module with the given name. + ## Returns all objects that apply. If the name is an + ## empty string, returns all objects within the given + ## module, regardless of whether they are exported to + ## the current one or not + if name == "": + for obj in reversed(self.names): + if not obj.isPrivate and obj.owner == module: + result.add(obj) + else: + for obj in self.findInModule("", module): + if obj.ident.token.lexeme == name and self.currentModule in obj.exportedTo: + result.add(obj) + + +method findByType*(self: Compiler, name: string, kind: Type): seq[Name] = + ## Looks for objects that have already been declared + ## with the given name and type. Returns all objects + ## that apply + for obj in self.findByName(name): + if self.compare(obj.valueType, kind): + result.add(obj) + + +method findAtDepth*(self: Compiler, name: string, depth: int): seq[Name] {.used.} = + ## 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 check*(self: Compiler, term: Expression, kind: Type) {.inline.} = + ## Checks the type of term against a known type. + ## Raises an error if appropriate and returns + ## otherwise + let k = self.inferOrError(term) + if not self.compare(k, kind): + self.error(&"expecting value of type {self.stringify(kind)}, got {self.stringify(k)}", term) + elif k.kind == Any and kind.kind != Any: + self.error(&"any is not a valid type in this context") + + +proc isAny*(typ: Type): bool = + ## Returns true if the given type is + ## of (or contains) the any type + case typ.kind: + of Any: + return true + of Generic: + for condition in typ.cond: + if condition.kind.isAny(): + return true + of Union: + for condition in typ.types: + if condition.kind.isAny(): + return true + else: + discard + return false + + +method match*(self: Compiler, name: string, kind: Type, node: ASTNode = nil, allowFwd: bool = true): Name = + ## Tries to find a matching function implementation + ## compatible with the given type and returns its + ## name object + var impl: seq[Name] = @[] + for obj in self.findByName(name): + if self.compare(kind, obj.valueType): + impl.add(obj) + if impl.len() == 0: + let names = self.findByName(name) + var msg = &"failed to find a suitable implementation for '{name}'" + if names.len() > 0: + msg &= &", found {len(names)} potential candidate" + if names.len() > 1: + msg &= "s" + if self.showMismatches: + msg &= ":" + for name in names: + msg &= &"\n - in {relativePath(name.file, getCurrentDir())}:{name.ident.token.line}:{name.ident.token.relPos.start} -> {self.stringify(name.valueType)}" + if name.valueType.kind != Function: + msg &= ": not a callable" + elif kind.args.len() != name.valueType.args.len(): + msg &= &": wrong number of arguments (expected {name.valueType.args.len()}, got {kind.args.len()})" + else: + for i, arg in kind.args: + if not self.compare(arg.kind, name.valueType.args[i].kind): + msg &= &": first mismatch at position {i + 1}: (expected {self.stringify(name.valueType.args[i].kind)}, got {self.stringify(arg.kind)})" + break + else: + msg &= " (compile with --showMismatches for more details)" + else: + msg = &"call to undefined function '{name}'" + self.error(msg, node) + elif impl.len() > 1: + impl = filterIt(impl, not it.valueType.forwarded and not it.valueType.isAuto) + if impl.len() > 1: + # If it's *still* more than one match, then it's an error + var msg = &"multiple matching implementations of '{name}' found" + if self.showMismatches: + msg &= ":" + for fn in reversed(impl): + msg &= &"\n- in {relativePath(fn.file, getCurrentDir())}, line {fn.line} of type {self.stringify(fn.valueType)}" + else: + msg &= " (compile with --showMismatches for more details)" + self.error(msg, node) + if impl[0].valueType.forwarded and not allowFwd: + self.error(&"expecting an implementation for function '{impl[0].ident.token.lexeme}' declared in module '{impl[0].owner.ident.token.lexeme}' at line {impl[0].ident.token.line} of type '{self.stringify(impl[0].valueType)}'") + result = impl[0] + for (a, b) in zip(result.valueType.args, kind.args): + if not a.kind.isAny() and b.kind.isAny(): + self.error("any is not a valid type in this context", node) diff --git a/src/frontend/compiler/targets/bytecode/target.nim b/src/frontend/compiler/targets/bytecode/target.nim index b64d8db..256ef53 100644 --- a/src/frontend/compiler/targets/bytecode/target.nim +++ b/src/frontend/compiler/targets/bytecode/target.nim @@ -44,64 +44,15 @@ type ## An internal compiler function called ## by pragmas kind: PragmaKind - handler: proc (self: BytecodeCompiler, pragma: Pragma, name: Name) + handler: proc (self: BytecodeCompiler, pragma: Pragma, name: BytecodeName) - NameKind {.pure.} = enum - ## A name enumeration type - None, Module, Argument, Var, Function, CustomType, Enum - - Name = ref object - ## A name object - - # Type of the identifier (NOT of the value!) - case kind: NameKind - of NameKind.Module: - path: string - else: - discard - # The name's identifier - ident: IdentExpr - # Owner of the identifier (module) - owner: Name - # File where the name is declared - file: string - # Scope depth - depth: int - # Is this name private? - isPrivate: bool - # Is this a constant? - isConst: bool - # Can this name's value be mutated? - isLet: bool - # Is this name a generic type? - isGeneric: bool - # The type of the name's associated - # value - valueType: Type + BytecodeName = ref object of Name + ## A name object in our bytecode target # For functions, this marks where their body's # code begins. For variables, it represents the # instruction after which they will be available # for use. Other name types don't use this value codePos: int - # The function that owns this name (may be nil!) - belongsTo: Name - # Where is this node declared in its file? - line: int - # Has this name been referenced at least once? - resolved: bool - # The AST node associated with this node. This - # is needed because we compile function and type - # declarations only if, and when, they're actually - # used - node: Declaration - # Who is this name exported to? (Only makes sense if isPrivate - # equals false) - exportedTo: HashSet[Name] - # Has the compiler generates this name internally or - # does it come from user code? - isReal: bool - # Is this name a builtin? - isBuiltin: bool # The location of this name on the stack. # Only makes sense for names that actually # materialize on the call stack at runtime @@ -138,26 +89,11 @@ type # The bytecode chunk where we write code to chunk: Chunk - # Compile-time "simulation" of the stack at - # runtime to load variables that have stack - # behavior more efficiently - names: seq[Name] - # The current function being compiled - currentFunction: Name # The current loop being compiled (used to # keep track of where to jump) currentLoop: Loop # Stack of named blocks namedBlocks: seq[NamedBlock] - # The current module being compiled - # (used to restrict access to statically - # defined variables at compile time) - currentModule: Name - # If we're a sub-compiler working on a module - # that is not the main one, we need to know - # where we're coming from so export statements - # work as expected - parentModule: Name # Compiler procedures called by pragmas compilerProcs: TableRef[string, CompilerFunc] # Currently imported modules @@ -165,8 +101,8 @@ type # Stores the position of all jumps jumps: seq[tuple[patched: bool, offset: int]] # Metadata about function locations - functions: seq[tuple[start, stop, pos: int, fn: Name]] - forwarded: seq[tuple[name: Name, pos: int]] + functions: seq[tuple[start, stop, pos: int, fn: BytecodeName]] + forwarded: seq[tuple[name: BytecodeName, pos: int]] # The topmost occupied stack slot # in the current frame (0-indexed) stackIndex: int @@ -176,36 +112,20 @@ type proc compile*(self: BytecodeCompiler, ast: seq[Declaration], file: string, lines: seq[tuple[start, stop: int]], source: string, chunk: Chunk = nil, incremental: bool = false, isMainModule: bool = true, disabledWarnings: seq[WarningKind] = @[], showMismatches: bool = false, mode: CompileMode = Debug): Chunk -proc expression(self: BytecodeCompiler, node: Expression, compile: bool = true): Type {.discardable.} proc statement(self: BytecodeCompiler, node: Statement) proc declaration(self: BytecodeCompiler, node: Declaration) -proc peek(self: BytecodeCompiler, distance: int = 0): ASTNode -proc identifier(self: BytecodeCompiler, node: IdentExpr, name: Name = nil, compile: bool = true, strict: bool = true): Type {.discardable.} proc varDecl(self: BytecodeCompiler, node: VarDecl) -proc match(self: BytecodeCompiler, name: string, kind: Type, node: ASTNode = nil, allowFwd: bool = true): Name proc specialize(self: BytecodeCompiler, typ: Type, args: seq[Expression]): Type {.discardable.} -proc call(self: BytecodeCompiler, node: CallExpr, compile: bool = true): Type {.discardable.} -proc getItemExpr(self: BytecodeCompiler, node: GetItemExpr, compile: bool = true, matching: Type = nil): Type {.discardable.} -proc unary(self: BytecodeCompiler, node: UnaryExpr, compile: bool = true): Type {.discardable.} -proc binary(self: BytecodeCompiler, node: BinaryExpr, compile: bool = true): Type {.discardable.} -proc infer(self: BytecodeCompiler, node: LiteralExpr): Type -proc infer(self: BytecodeCompiler, node: Expression): Type -proc inferOrError(self: BytecodeCompiler, node: Expression): Type -proc findByName(self: BytecodeCompiler, name: string): seq[Name] -proc findInModule(self: BytecodeCompiler, name: string, module: Name): seq[Name] -proc findByType(self: BytecodeCompiler, name: string, kind: Type): seq[Name] -proc compare(self: BytecodeCompiler, a, b: Type): bool proc patchReturnAddress(self: BytecodeCompiler, pos: int) -proc handleMagicPragma(self: BytecodeCompiler, pragma: Pragma, name: Name) -proc handlePurePragma(self: BytecodeCompiler, pragma: Pragma, name: Name) -proc handleErrorPragma(self: BytecodeCompiler, pragma: Pragma, name: Name) -proc dispatchPragmas(self: BytecodeCompiler, name: Name) -proc dispatchDelayedPragmas(self: BytecodeCompiler, name: Name) -proc funDecl(self: BytecodeCompiler, node: FunDecl, name: Name) -proc compileModule(self: BytecodeCompiler, module: Name) -proc generateCall(self: BytecodeCompiler, fn: Name, args: seq[Expression], line: int) -proc prepareFunction(self: BytecodeCompiler, fn: Name) -proc lambdaExpr(self: BytecodeCompiler, node: LambdaExpr, compile: bool = true): Type {.discardable.} +proc handleMagicPragma(self: BytecodeCompiler, pragma: Pragma, name: BytecodeName) +proc handlePurePragma(self: BytecodeCompiler, pragma: Pragma, name: BytecodeName) +proc handleErrorPragma(self: BytecodeCompiler, pragma: Pragma, name: BytecodeName) +proc dispatchPragmas(self: BytecodeCompiler, name: BytecodeName) +proc dispatchDelayedPragmas(self: BytecodeCompiler, name: BytecodeName) +proc funDecl(self: BytecodeCompiler, node: FunDecl, name: BytecodeName) +proc compileModule(self: BytecodeCompiler, module: BytecodeName) +proc generateCall(self: BytecodeCompiler, fn: BytecodeName, args: seq[Expression], line: int) +proc prepareFunction(self: BytecodeCompiler, fn: BytecodeName) # End of forward declarations @@ -236,96 +156,7 @@ proc newBytecodeCompiler*(replMode: bool = false): BytecodeCompiler = result.functions = @[] result.stackIndex = 1 - -## Public getters for nicer error formatting -proc getCurrentNode*(self: BytecodeCompiler): ASTNode = (if self.current >= - self.ast.len(): self.ast[^1] else: self.ast[self.current - 1]) -proc getCurrentFunction*(self: BytecodeCompiler): Declaration {.inline.} = (if self.currentFunction.isNil(): nil else: self.currentFunction.valueType.fun) - -## Utility functions - -proc `$`*(self: Name): string = $(self[]) -proc `$`(self: Type): string = $(self[]) -proc hash(self: Name): Hash = self.ident.token.lexeme.hash() - - -proc peek(self: BytecodeCompiler, distance: int = 0): ASTNode = - ## Peeks at the AST node at the given distance. - ## If the distance is out of bounds, the last - ## AST node in the tree is returned. A negative - ## distance may be used to retrieve previously - ## consumed AST nodes - if self.ast.high() == -1 or self.current + distance > self.ast.high() or self.current + distance < 0: - result = self.ast[^1] - else: - result = self.ast[self.current + distance] - - -proc done(self: BytecodeCompiler): bool {.inline.} = - ## Returns true if the compiler is done - ## compiling, false otherwise - result = self.current > self.ast.high() - - -proc error(self: BytecodeCompiler, message: string, node: ASTNode = nil) {.inline.} = - ## Raises a CompileError exception - let node = if node.isNil(): self.getCurrentNode() else: node - raise CompileError(msg: message, node: node, line: node.token.line, file: node.file) - - -proc warning(self: BytecodeCompiler, kind: WarningKind, message: string, name: Name = nil, node: ASTNode = nil) = - ## Raises a warning. Note that warnings are always disabled in REPL mode - if self.replMode or kind in self.disabledWarnings: - return - var node: ASTNode = node - var fn: Declaration - if name.isNil(): - if node.isNil(): - node = self.getCurrentNode() - fn = self.getCurrentFunction() - else: - node = name.node - if node.isNil(): - node = self.getCurrentNode() - if not name.belongsTo.isNil(): - fn = name.belongsTo.node - else: - fn = self.getCurrentFunction() - var file = self.file - if not name.isNil(): - file = name.owner.file - var pos = node.getRelativeBoundaries() - if file notin ["", ""]: - file = relativePath(file, getCurrentDir()) - stderr.styledWrite(fgYellow, styleBright, "Warning in ", fgRed, &"{file}:{node.token.line}:{pos.start}") - if not fn.isNil() and fn.kind == funDecl: - stderr.styledWrite(fgYellow, styleBright, " in function ", fgRed, FunDecl(fn).name.token.lexeme) - stderr.styledWriteLine(styleBright, fgDefault, ": ", message) - try: - # We try to be as specific as possible with the warning message, pointing to the - # line it belongs to, but since warnings are not always raised from the source - # file they're generated in, we take into account the fact that retrieving the - # exact warning location may fail and bail out silently if it does - let line = readFile(file).splitLines()[node.token.line - 1].strip(chars={'\n'}) - stderr.styledWrite(fgYellow, styleBright, "Source line: ", resetStyle, fgDefault, line[0.. where: - name.codePos += offset + if BytecodeName(name).codePos > where: + BytecodeName(name).codePos += offset if name.valueType.kind == Function: name.valueType.location += offset @@ -591,500 +421,6 @@ proc insertAt(self: BytecodeCompiler, where: int, opcode: OpCode, data: openarra self.fixFunctionOffsets(oldLen, where) -proc resolve(self: BytecodeCompiler, name: string): Name = - ## Traverses all existing namespaces and returns - ## the first object with the given name. Returns - ## nil when the name can't be found. Note that - ## when a type or function declaration is first - ## resolved, it is also compiled on-the-fly - for obj in reversed(self.names): - if obj.ident.token.lexeme == name: - if obj.owner.path != self.currentModule.path: - # We don't own this name, but we - # may still have access to it - if obj.isPrivate: - # Name is private in its owner - # module, so we definitely can't - # use it - continue - elif self.currentModule in obj.exportedTo: - # The name is public in its owner - # module and said module has explicitly - # exported it to us: we can use it - result = obj - break - # If the name is public but not exported in - # its owner module, then we act as if it's - # private. This is to avoid namespace pollution - # from imports (i.e. if module A imports modules - # C and D and module B imports module A, then B - # might not want to also have access to C's and D's - # names as they might clash with its own stuff) - continue - result = obj - result.resolved = true - break - - -proc resolve(self: BytecodeCompiler, name: IdentExpr): Name = - ## Version of resolve that takes Identifier - ## AST nodes instead of strings - return self.resolve(name.token.lexeme) - - -proc resolveOrError[T: IdentExpr | string](self: BytecodeCompiler, name: T): Name = - ## Calls self.resolve() and errors out with an appropriate - ## message if it returns nil - result = self.resolve(name) - if result.isNil(): - when T is IdentExpr: - self.error(&"reference to undefined name '{name.token.lexeme}'", name) - when T is string: - self.error(&"reference to undefined name '{name}'") - - -proc compareUnions(self: BytecodeCompiler, a, b: seq[tuple[match: bool, kind: Type]]): bool = - ## Compares type unions between each other - var - long = a - short = b - if b.len() > a.len(): - long = b - short = a - var i = 0 - for cond1 in short: - for cond2 in long: - if not self.compare(cond1.kind, cond2.kind) or cond1.match != cond2.match: - continue - inc(i) - return i >= short.len() - - -proc compare(self: BytecodeCompiler, a, b: Type): bool = - ## Compares two type objects - ## for equality - result = false - # Note: 'All' is a type internal to the peon - # compiler that cannot be generated from user - # code in any way. It's used mostly for matching - # function return types (at least until we don't - # have return type inference) and it matches any - # type, including nil - if a.isNil(): - return b.isNil() or b.kind == All - elif b.isNil(): - return a.isNil() or a.kind == All - elif a.kind == All or b.kind == All: - return true - elif a.kind == b.kind: - # Here we compare types with the same kind discriminant - case a.kind: - of Int8, UInt8, Int16, UInt16, Int32, - UInt32, Int64, UInt64, Float32, Float64, - Char, Byte, String, Nil, TypeKind.Nan, Bool, TypeKind.Inf, Any: - return true - of Union: - return self.compareUnions(a.types, b.types) - of Generic: - return self.compareUnions(a.cond, b.cond) - of Reference, Pointer: - # Here we already know that both - # a and b are of either of the two - # types in this branch, so we just need - # to compare their values - return self.compare(a.value, b.value) - of Function: - # Functions are a bit trickier to compare - if a.args.len() != b.args.len(): - return false - if a.isCoroutine != b.isCoroutine or a.isGenerator != b.isGenerator: - return false - if not self.compare(b.returnType, a.returnType): - return false - var i = 0 - for (argA, argB) in zip(a.args, b.args): - # When we compare functions with forward - # declarations, or forward declarations - # between each other, we need to be more - # strict (as in: check argument names and - # their default values, any pragma associated - # with the function, and whether they are pure) - if a.forwarded: - if b.forwarded: - if argA.name != argB.name: - return false - else: - if argB.name == "": - # An empty argument name means - # we crafted this type object - # manually, so we don't need - # to match the argument name - continue - if argA.name != argB.name: - return false - elif b.forwarded: - if a.forwarded: - if argA.name != argB.name: - return false - else: - if argA.name == "": - continue - if argA.name != argB.name: - return false - if not self.compare(argA.kind, argB.kind): - return false - return true - else: - discard # TODO: Custom types, enums - elif a.kind == Union: - for constraint in a.types: - if self.compare(constraint.kind, b) and constraint.match: - return true - return false - elif b.kind == Union: - for constraint in b.types: - if self.compare(constraint.kind, a) and constraint.match: - return true - return false - elif a.kind == Generic: - if a.asUnion: - for constraint in a.cond: - if self.compare(constraint.kind, b) and constraint.match: - return true - return false - else: - for constraint in a.cond: - if not self.compare(constraint.kind, b) or not constraint.match: - return false - return true - elif b.kind == Generic: - if b.asUnion: - for constraint in b.cond: - if self.compare(constraint.kind, a) and constraint.match: - return true - return false - else: - for constraint in b.cond: - if not self.compare(constraint.kind, a) or not constraint.match: - return false - return true - elif a.kind == Any or b.kind == Any: - # Here we already know that neither of - # these types are nil, so we can always - # just return true - return true - return false - - -proc toIntrinsic(name: string): Type = - ## Converts a string to an intrinsic - ## type if it is valid and returns nil - ## otherwise - if name == "any": - return Type(kind: Any) - elif name == "auto": - return Type(kind: Auto) - elif name in ["int", "int64", "i64"]: - return Type(kind: Int64) - elif name in ["uint64", "u64", "uint"]: - return Type(kind: UInt64) - elif name in ["int32", "i32"]: - return Type(kind: Int32) - elif name in ["uint32", "u32"]: - return Type(kind: UInt32) - elif name in ["int16", "i16", "short"]: - return Type(kind: Int16) - elif name in ["uint16", "u16"]: - return Type(kind: UInt16) - elif name in ["int8", "i8"]: - return Type(kind: Int8) - elif name in ["uint8", "u8"]: - return Type(kind: UInt8) - elif name in ["f64", "float", "float64"]: - return Type(kind: Float64) - elif name in ["f32", "float32"]: - return Type(kind: Float32) - elif name in ["byte", "b"]: - return Type(kind: Byte) - elif name in ["char", "c"]: - return Type(kind: Char) - elif name == "nan": - return Type(kind: TypeKind.Nan) - elif name == "nil": - return Type(kind: Nil) - elif name == "inf": - return Type(kind: TypeKind.Inf) - elif name == "bool": - return Type(kind: Bool) - elif name == "typevar": - return Type(kind: Typevar) - elif name == "string": - return Type(kind: String) - - -proc infer(self: BytecodeCompiler, node: LiteralExpr): Type = - ## Infers the type of a given literal expression - if node.isNil(): - return nil - case node.kind: - of intExpr, binExpr, octExpr, hexExpr: - let size = node.token.lexeme.split("'") - if size.len() == 1: - return Type(kind: Int64) - let typ = size[1].toIntrinsic() - if not self.compare(typ, nil): - return typ - else: - self.error(&"invalid type specifier '{size[1]}' for int", node) - of floatExpr: - let size = node.token.lexeme.split("'") - if size.len() == 1: - return Type(kind: Float64) - let typ = size[1].toIntrinsic() - if not typ.isNil(): - return typ - else: - self.error(&"invalid type specifier '{size[1]}' for float", node) - of trueExpr: - return Type(kind: Bool) - of falseExpr: - return Type(kind: Bool) - of strExpr: - return Type(kind: String) - else: - discard # Unreachable - - -proc infer(self: BytecodeCompiler, node: Expression): Type = - ## Infers the type of a given expression and - ## returns it - if node.isNil(): - return nil - case node.kind: - of NodeKind.identExpr: - result = self.identifier(IdentExpr(node), compile=false, strict=false) - of NodeKind.unaryExpr: - result = self.unary(UnaryExpr(node), compile=false) - of NodeKind.binaryExpr: - result = self.binary(BinaryExpr(node), compile=false) - of {NodeKind.intExpr, NodeKind.hexExpr, NodeKind.binExpr, NodeKind.octExpr, - NodeKind.strExpr, NodeKind.falseExpr, NodeKind.trueExpr, NodeKind.floatExpr - }: - result = self.infer(LiteralExpr(node)) - of NodeKind.callExpr: - result = self.call(CallExpr(node), compile=false) - of NodeKind.refExpr: - result = Type(kind: Reference, value: self.infer(Ref(node).value)) - of NodeKind.ptrExpr: - result = Type(kind: Pointer, value: self.infer(Ptr(node).value)) - of NodeKind.groupingExpr: - result = self.infer(GroupingExpr(node).expression) - of NodeKind.getItemExpr: - result = self.getItemExpr(GetItemExpr(node), compile=false) - of NodeKind.lambdaExpr: - result = self.lambdaExpr(LambdaExpr(node), compile=false) - else: - discard # TODO - - -proc inferOrError(self: BytecodeCompiler, node: Expression): Type = - ## Attempts to infer the type of - ## the given expression and raises an - ## error with if it fails - result = self.infer(node) - if result.isNil(): - self.error("expression has no type", node) - - -proc stringify(self: BytecodeCompiler, typ: Type): string = - ## Returns the string representation of a - ## type object - if typ.isNil(): - return "nil" - case typ.kind: - of Int8, UInt8, Int16, UInt16, Int32, - UInt32, Int64, UInt64, Float32, Float64, - Char, Byte, String, Nil, TypeKind.Nan, Bool, - TypeKind.Inf, Auto: - result &= ($typ.kind).toLowerAscii() - of Pointer: - result &= &"ptr {self.stringify(typ.value)}" - of Reference: - result &= &"ref {self.stringify(typ.value)}" - of Function: - result &= "fn (" - for i, (argName, argType, argDefault) in typ.args: - result &= &"{argName}: {self.stringify(argType)}" - if not argDefault.isNil(): - result &= &" = {argDefault}" - if i < typ.args.len() - 1: - result &= ", " - result &= ")" - if not typ.returnType.isNil(): - result &= &": {self.stringify(typ.returnType)}" - if typ.fun.pragmas.len() > 0: - result &= " {" - for i, pragma in typ.fun.pragmas: - result &= &"{pragma.name.token.lexeme}" - if pragma.args.len() > 0: - result &= ": " - for j, arg in pragma.args: - result &= arg.token.lexeme - if j < pragma.args.high(): - result &= ", " - if i < typ.fun.pragmas.high(): - result &= ", " - else: - result &= "}" - of Any: - return "any" - of Union: - for i, condition in typ.types: - if i > 0: - result &= " | " - if not condition.match: - result &= "~" - result &= self.stringify(condition.kind) - of Generic: - for i, condition in typ.cond: - if i > 0: - result &= " | " - if not condition.match: - result &= "~" - result &= self.stringify(condition.kind) - else: - discard - - -proc findByName(self: BytecodeCompiler, name: string): seq[Name] = - ## Looks for objects that have been already declared - ## with the given name. Returns all objects that apply. - for obj in reversed(self.names): - if obj.ident.token.lexeme == name: - if obj.owner.path != self.currentModule.path: - if obj.isPrivate or self.currentModule notin obj.exportedTo: - continue - result.add(obj) - - -proc findInModule(self: BytecodeCompiler, name: string, module: Name): seq[Name] = - ## Looks for objects that have been already declared as - ## public within the given module with the given name. - ## Returns all objects that apply. If the name is an - ## empty string, returns all objects within the given - ## module, regardless of whether they are exported to - ## the current one or not - if name == "": - for obj in reversed(self.names): - if not obj.isPrivate and obj.owner == module: - result.add(obj) - else: - for obj in self.findInModule("", module): - if obj.ident.token.lexeme == name and self.currentModule in obj.exportedTo: - result.add(obj) - - -proc findByType(self: BytecodeCompiler, name: string, kind: Type): seq[Name] = - ## Looks for objects that have already been declared - ## with the given name and type. Returns all objects - ## that apply - for obj in self.findByName(name): - if self.compare(obj.valueType, kind): - result.add(obj) - - -proc findAtDepth(self: BytecodeCompiler, name: string, depth: int): seq[Name] {.used.} = - ## 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 check(self: BytecodeCompiler, term: Expression, kind: Type) {.inline.} = - ## Checks the type of term against a known type. - ## Raises an error if appropriate and returns - ## otherwise - let k = self.inferOrError(term) - if not self.compare(k, kind): - self.error(&"expecting value of type {self.stringify(kind)}, got {self.stringify(k)}", term) - elif k.kind == Any and kind.kind != Any: - self.error(&"any is not a valid type in this context") - - -proc isAny(typ: Type): bool = - ## Returns true if the given type is - ## of (or contains) the any type - case typ.kind: - of Any: - return true - of Generic: - for condition in typ.cond: - if condition.kind.isAny(): - return true - of Union: - for condition in typ.types: - if condition.kind.isAny(): - return true - else: - discard - return false - - -proc match(self: BytecodeCompiler, name: string, kind: Type, node: ASTNode = nil, allowFwd: bool = true): Name = - ## Tries to find a matching function implementation - ## compatible with the given type and returns its - ## name object - var impl: seq[Name] = @[] - for obj in self.findByName(name): - if self.compare(kind, obj.valueType): - impl.add(obj) - if impl.len() == 0: - let names = self.findByName(name) - var msg = &"failed to find a suitable implementation for '{name}'" - if names.len() > 0: - msg &= &", found {len(names)} potential candidate" - if names.len() > 1: - msg &= "s" - if self.showMismatches: - msg &= ":" - for name in names: - msg &= &"\n - in {relativePath(name.file, getCurrentDir())}:{name.ident.token.line}:{name.ident.token.relPos.start} -> {self.stringify(name.valueType)}" - if name.valueType.kind != Function: - msg &= ": not a callable" - elif kind.args.len() != name.valueType.args.len(): - msg &= &": wrong number of arguments (expected {name.valueType.args.len()}, got {kind.args.len()})" - else: - for i, arg in kind.args: - if not self.compare(arg.kind, name.valueType.args[i].kind): - msg &= &": first mismatch at position {i + 1}: (expected {self.stringify(name.valueType.args[i].kind)}, got {self.stringify(arg.kind)})" - break - else: - msg &= " (compile with --showMismatches for more details)" - else: - msg = &"call to undefined function '{name}'" - self.error(msg, node) - elif impl.len() > 1: - impl = filterIt(impl, not it.valueType.forwarded and not it.valueType.isAuto) - if impl.len() > 1: - # If it's *still* more than one match, then it's an error - var msg = &"multiple matching implementations of '{name}' found" - if self.showMismatches: - msg &= ":" - for fn in reversed(impl): - msg &= &"\n- in {relativePath(fn.file, getCurrentDir())}, line {fn.line} of type {self.stringify(fn.valueType)}" - else: - msg &= " (compile with --showMismatches for more details)" - self.error(msg, node) - if impl[0].valueType.forwarded and not allowFwd: - self.error(&"expecting an implementation for function '{impl[0].ident.token.lexeme}' declared in module '{impl[0].owner.ident.token.lexeme}' at line {impl[0].ident.token.line} of type '{self.stringify(impl[0].valueType)}'") - result = impl[0] - for (a, b) in zip(result.valueType.args, kind.args): - if not a.kind.isAny() and b.kind.isAny(): - self.error("any is not a valid type in this context", node) - - proc handleBuiltinFunction(self: BytecodeCompiler, fn: Type, args: seq[Expression], line: int) = ## Emits instructions for builtin functions ## such as addition or subtraction @@ -1224,10 +560,10 @@ proc patchForwardDeclarations(self: BytecodeCompiler) = ## Patches forward declarations and looks ## for their implementations so that calls ## to them work properly - var impl: Name + var impl: BytecodeName var pos: array[8, uint8] for (forwarded, position) in self.forwarded: - impl = self.match(forwarded.ident.token.lexeme, forwarded.valueType, allowFwd=false) + impl = BytecodeName(self.match(forwarded.ident.token.lexeme, forwarded.valueType, allowFwd=false)) if position == 0: continue pos = impl.codePos.toLong() @@ -1369,7 +705,7 @@ proc unpackUnion(self: BytecodeCompiler, condition: Expression, list: var seq[tu self.error("invalid type constraint in type union", condition) -proc declare(self: BytecodeCompiler, node: ASTNode): Name {.discardable.} = +proc declare(self: BytecodeCompiler, node: ASTNode): BytecodeName {.discardable.} = ## Statically declares a name into the current scope. ## "Declaring" a name only means updating our internal ## list of identifiers so that further calls to resolve() @@ -1377,7 +713,7 @@ proc declare(self: BytecodeCompiler, node: ASTNode): Name {.discardable.} = ## declare a variable at runtime: the value is already ## on the stack var declaredName: string = "" - var n: Name + var n: BytecodeName if self.names.high() > 16777215: # If someone ever hits this limit in real-world scenarios, I swear I'll # slap myself 100 times with a sign saying "I'm dumb". Mark my words @@ -1387,7 +723,7 @@ proc declare(self: BytecodeCompiler, node: ASTNode): Name {.discardable.} = var node = VarDecl(node) declaredName = node.name.token.lexeme # Creates a new Name entry so that self.identifier emits the proper stack offset - self.names.add(Name(depth: self.depth, + self.names.add(BytecodeName(depth: self.depth, ident: node.name, isPrivate: node.isPrivate, owner: self.currentModule, @@ -1401,11 +737,11 @@ proc declare(self: BytecodeCompiler, node: ASTNode): Name {.discardable.} = node: node, isReal: true )) - n = self.names[^1] + n = BytecodeName(self.names[^1]) of NodeKind.funDecl: var node = FunDecl(node) declaredName = node.name.token.lexeme - var fn = Name(depth: self.depth, + var fn = BytecodeName(depth: self.depth, isPrivate: node.isPrivate, isConst: false, owner: self.currentModule, @@ -1441,8 +777,8 @@ proc declare(self: BytecodeCompiler, node: ASTNode): Name {.discardable.} = if fn.isGeneric: self.error("automatic types cannot be used within generics", node.returnType) self.names.add(fn) - self.prepareFunction(fn) - n = fn + self.prepareFunction(BytecodeName(fn)) + n = BytecodeName(fn) of NodeKind.importStmt: var node = ImportStmt(node) # We change the name of the module internally so that @@ -1453,7 +789,7 @@ proc declare(self: BytecodeCompiler, node: ASTNode): Name {.discardable.} = # full path let path = node.moduleName.token.lexeme node.moduleName.token.lexeme = node.moduleName.token.lexeme.extractFilename() - self.names.add(Name(depth: self.depth, + self.names.add(BytecodeName(depth: self.depth, owner: self.currentModule, file: "", # The file of the module isn't known until it's compiled! path: path, @@ -1463,11 +799,11 @@ proc declare(self: BytecodeCompiler, node: ASTNode): Name {.discardable.} = isPrivate: false, isReal: true )) - n = self.names[^1] + n = BytecodeName(self.names[^1]) declaredName = self.names[^1].ident.token.lexeme of NodeKind.typeDecl: var node = TypeDecl(node) - self.names.add(Name(kind: NameKind.CustomType, + self.names.add(BytecodeName(kind: NameKind.CustomType, depth: self.depth, owner: self.currentModule, node: node, @@ -1478,7 +814,7 @@ proc declare(self: BytecodeCompiler, node: ASTNode): Name {.discardable.} = belongsTo: self.currentFunction ) ) - n = self.names[^1] + n = BytecodeName(self.names[^1]) declaredName = node.name.token.lexeme if node.value.isNil(): discard # TODO: Fields @@ -1541,7 +877,7 @@ proc patchBreaks(self: BytecodeCompiler) = self.patchJump(brk) -proc handleMagicPragma(self: BytecodeCompiler, pragma: Pragma, name: Name) = +proc handleMagicPragma(self: BytecodeCompiler, pragma: Pragma, name: BytecodeName) = ## Handles the "magic" pragma. Assumes the given name is already ## declared if pragma.args.len() != 1: @@ -1562,7 +898,7 @@ proc handleMagicPragma(self: BytecodeCompiler, pragma: Pragma, name: Name) = self.error("'magic' pragma is not valid in this context") -proc handleErrorPragma(self: BytecodeCompiler, pragma: Pragma, name: Name) = +proc handleErrorPragma(self: BytecodeCompiler, pragma: Pragma, name: BytecodeName) = ## Handles the "error" pragma if pragma.args.len() != 1: self.error("'error' pragma: wrong number of arguments") @@ -1573,7 +909,7 @@ proc handleErrorPragma(self: BytecodeCompiler, pragma: Pragma, name: Name) = self.error(pragma.args[0].token.lexeme[1..^2]) -proc handlePurePragma(self: BytecodeCompiler, pragma: Pragma, name: Name) = +proc handlePurePragma(self: BytecodeCompiler, pragma: Pragma, name: BytecodeName) = ## Handles the "pure" pragma case name.node.kind: of NodeKind.funDecl: @@ -1584,7 +920,7 @@ proc handlePurePragma(self: BytecodeCompiler, pragma: Pragma, name: Name) = self.error("'pure' pragma is not valid in this context") -proc dispatchPragmas(self: BytecodeCompiler, name: Name) = +proc dispatchPragmas(self: BytecodeCompiler, name: BytecodeName) = ## Dispatches pragmas bound to objects if name.node.isNil(): return @@ -1606,7 +942,7 @@ proc dispatchPragmas(self: BytecodeCompiler, name: Name) = f.handler(self, pragma, name) -proc dispatchDelayedPragmas(self: BytecodeCompiler, name: Name) = +proc dispatchDelayedPragmas(self: BytecodeCompiler, name: BytecodeName) = ## Dispatches pragmas bound to objects once they ## are called. Only applies to functions if name.node.isNil(): @@ -1637,6 +973,196 @@ proc patchReturnAddress(self: BytecodeCompiler, pos: int) = self.chunk.consts[pos + 7] = address[7] +proc generateCall(self: BytecodeCompiler, fn: Type, args: seq[Expression], line: int) {.used.} = + ## Version of generateCall that takes Type objects + ## instead of Name objects (used for lambdas and + ## consequent calls). The function's address is + ## assumed to be on the stack + self.emitByte(LoadUInt64, line) + self.emitBytes(self.chunk.writeConstant(0.toLong()), line) + let pos = self.chunk.consts.len() - 8 + 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 + self.emitByte(Call, line) + self.emitBytes(args.len().toTriple(), line) + self.patchReturnAddress(pos) + + +proc prepareFunction(self: BytecodeCompiler, fn: BytecodeName) = + ## "Prepares" a function declaration by declaring + ## its arguments and typechecking it + + # First we declare the function's generics, if it has any. + # This is because the function's return type may in itself + # be a generic, so it needs to exist first + var constraints: seq[tuple[match: bool, kind: Type]] = @[] + for gen in fn.node.generics: + self.unpackGenerics(gen.cond, constraints) + self.names.add(BytecodeName(depth: fn.depth + 1, + isPrivate: true, + valueType: Type(kind: Generic, name: gen.name.token.lexeme, cond: constraints), + codePos: 0, + isLet: false, + line: fn.node.token.line, + belongsTo: fn, + ident: gen.name, + owner: self.currentModule, + file: self.file)) + constraints = @[] + # We now declare and typecheck the function's + # arguments + let idx = self.stackIndex + self.stackIndex = 1 + var default: Expression + var node = FunDecl(fn.node) + var i = 0 + for argument in node.arguments: + if self.names.high() > 16777215: + self.error("cannot declare more than 16777215 variables at a time") + inc(self.stackIndex) + self.names.add(BytecodeName(depth: fn.depth + 1, + isPrivate: true, + owner: fn.owner, + file: fn.file, + isConst: false, + ident: argument.name, + valueType: if not fn.valueType.isAuto: self.inferOrError(argument.valueType) else: Type(kind: Any), + codePos: 0, + isLet: false, + line: argument.name.token.line, + belongsTo: fn, + kind: NameKind.Argument, + node: argument.name, + position: self.stackIndex, + isReal: not node.isTemplate + )) + if node.arguments.high() - node.defaults.high() <= node.arguments.high(): + # There's a default argument! + fn.valueType.args.add((self.names[^1].ident.token.lexeme, self.names[^1].valueType, node.defaults[i])) + inc(i) + else: + # This argument has no default + fn.valueType.args.add((self.names[^1].ident.token.lexeme, self.names[^1].valueType, default)) + # The function needs a return type too! + if not FunDecl(fn.node).returnType.isNil(): + fn.valueType.returnType = self.inferOrError(FunDecl(fn.node).returnType) + fn.position = self.stackIndex + self.stackIndex = idx + if node.isTemplate: + fn.valueType.compiled = true + + +proc prepareAutoFunction(self: BytecodeCompiler, fn: BytecodeName, args: seq[tuple[name: string, kind: Type, default: Expression]]): BytecodeName = + ## "Prepares" an automatic function declaration + ## by declaring a concrete version of it along + ## with its arguments + + # First we declare the function's generics, if it has any. + # This is because the function's return type may in itself + # be a generic, so it needs to exist first + # We now declare and typecheck the function's + # arguments + let idx = self.stackIndex + self.stackIndex = 1 + var default: Expression + var node = FunDecl(fn.node) + var fn = deepCopy(fn) + fn.valueType.isAuto = false + fn.valueType.compiled = false + self.names.add(fn) + for (argument, val) in zip(node.arguments, args): + if self.names.high() > 16777215: + self.error("cannot declare more than 16777215 variables at a time") + inc(self.stackIndex) + self.names.add(BytecodeName(depth: fn.depth + 1, + isPrivate: true, + owner: fn.owner, + file: fn.file, + isConst: false, + ident: argument.name, + valueType: val.kind, + codePos: 0, + isLet: false, + line: argument.name.token.line, + belongsTo: fn, + kind: NameKind.Argument, + node: argument.name, + position: self.stackIndex, + isReal: not node.isTemplate + )) + if node.isTemplate: + fn.valueType.compiled = true + fn.valueType.args = args + fn.position = self.stackIndex + self.stackIndex = idx + return fn + + +proc generateCall(self: BytecodeCompiler, fn: BytecodeName, args: seq[Expression], line: int) = + ## Small wrapper that abstracts emitting a call instruction + ## for a given function + self.dispatchDelayedPragmas(fn) + if fn.isBuiltin: + self.handleBuiltinFunction(fn.valueType, args, line) + return + case fn.kind: + of NameKind.Var: + self.identifier(VarDecl(fn.node).name) + of NameKind.Function: + self.emitByte(LoadUInt64, line) + self.emitBytes(self.chunk.writeConstant(fn.codePos.toLong()), line) + else: + discard # Unreachable + if fn.valueType.forwarded: + self.forwarded.add((fn, self.chunk.consts.high() - 7)) + self.emitByte(LoadUInt64, line) + self.emitBytes(self.chunk.writeConstant(0.toLong()), line) + let pos = self.chunk.consts.len() - 8 + for arg in reversed(args): + self.expression(arg) + # Creates a new call frame and jumps + # to the function's first instruction + # in the code + self.emitByte(Call, line) + self.emitBytes(args.len().toTriple(), line) + self.patchReturnAddress(pos) + + +proc specialize(self: BytecodeCompiler, typ: Type, args: seq[Expression]): Type {.discardable.} = + ## Specializes a generic type. + ## Used for typechecking at the + ## call site + var mapping: TableRef[string, Type] = newTable[string, Type]() + var kind: Type + result = deepCopy(typ) + case result.kind: + of TypeKind.Function: + # This first loop checks if a user tries to reassign a generic's + # name to a different type + for i, (name, typ, default) in result.args: + if typ.kind != Generic: + continue + kind = self.inferOrError(args[i]) + if typ.name in mapping and not self.compare(kind, mapping[typ.name]): + self.error(&"expecting generic argument '{typ.name}' to be of type {self.stringify(mapping[typ.name])}, got {self.stringify(kind)}", args[i]) + mapping[typ.name] = kind + result.args[i].kind = kind + if not result.returnType.isNil() and result.returnType.kind == Generic: + if result.returnType.name in mapping: + result.returnType = mapping[result.returnType.name] + else: + self.error(&"unknown generic argument name '{result.returnType.name}'", result.fun) + else: + discard # TODO: Custom user-defined types + + proc terminateProgram(self: BytecodeCompiler, pos: int) = ## Utility to terminate a peon program self.patchForwardDeclarations() @@ -1654,7 +1180,7 @@ proc beginProgram(self: BytecodeCompiler): int = if self.currentModule.isNil(): # We declare the program's main module - var mainModule = Name(kind: NameKind.Module, + var mainModule = BytecodeName(kind: NameKind.Module, depth: 0, isPrivate: true, isConst: false, @@ -1678,7 +1204,7 @@ proc beginProgram(self: BytecodeCompiler): int = # of where our program ends (which we don't know yet). # To fix this, we emit dummy offsets and patch them # later, once we know the boundaries of our hidden main() - var main = Name(depth: 0, + var main = BytecodeName(depth: 0, isPrivate: true, isConst: false, isLet: false, @@ -1702,9 +1228,8 @@ proc beginProgram(self: BytecodeCompiler): int = self.emitByte(Call, 1) self.emitBytes(0.toTriple(), 1) -## End of utility functions -proc literal(self: BytecodeCompiler, node: ASTNode, compile: bool = true): Type {.discardable.} = +method literal(self: BytecodeCompiler, node: ASTNode, compile: bool = true): Type {.discardable.} = ## Emits instructions for literals such ## as singletons, strings and numbers case node.kind: @@ -1802,7 +1327,7 @@ proc literal(self: BytecodeCompiler, node: ASTNode, compile: bool = true): Type self.error(&"invalid AST node of kind {node.kind} at literal(): {node} (This is an internal error and most likely a bug!)") -proc unary(self: BytecodeCompiler, node: UnaryExpr, compile: bool = true): Type {.discardable.} = +method unary(self: BytecodeCompiler, node: UnaryExpr, compile: bool = true): Type {.discardable.} = ## Compiles all unary expressions var default: Expression let fn = Type(kind: Function, @@ -1814,10 +1339,10 @@ proc unary(self: BytecodeCompiler, node: UnaryExpr, compile: bool = true): Type result = self.specialize(result, @[node.a]) result = result.returnType if compile: - self.generateCall(impl, @[node.a], impl.line) + self.generateCall(BytecodeName(impl), @[node.a], impl.line) -proc binary(self: BytecodeCompiler, node: BinaryExpr, compile: bool = true): Type {.discardable.} = +method binary(self: BytecodeCompiler, node: BinaryExpr, compile: bool = true): Type {.discardable.} = ## Compiles all binary expressions var default: Expression let fn = Type(kind: Function, returnType: Type(kind: Any), args: @[("", self.inferOrError(node.a), default), ("", self.inferOrError(node.b), default)]) @@ -1827,17 +1352,17 @@ proc binary(self: BytecodeCompiler, node: BinaryExpr, compile: bool = true): Typ result = self.specialize(result, @[node.a, node.b]) result = result.returnType if compile: - self.generateCall(impl, @[node.a, node.b], impl.line) + self.generateCall(BytecodeName(impl), @[node.a, node.b], impl.line) -proc identifier(self: BytecodeCompiler, node: IdentExpr, name: Name = nil, compile: bool = true, strict: bool = true): Type {.discardable.} = +method identifier(self: BytecodeCompiler, node: IdentExpr, name: Name = nil, compile: bool = true, strict: bool = true): Type {.discardable.} = ## Compiles access to identifiers var s = name if s.isNil(): if strict: - s = self.resolveOrError(node) + s = BytecodeName(self.resolveOrError(node)) else: - s = self.resolve(node) + s = BytecodeName(self.resolve(node)) if s.isNil() and not strict: return nil result = s.valueType @@ -1872,16 +1397,16 @@ proc identifier(self: BytecodeCompiler, node: IdentExpr, name: Name = nil, compi # Loads a regular variable from the current frame self.emitByte(LoadVar, s.ident.token.line) # No need to check for -1 here: we already did a nil check above! - self.emitBytes(s.position.toTriple(), s.ident.token.line) + self.emitBytes(BytecodeName(s).position.toTriple(), s.ident.token.line) -proc assignment(self: BytecodeCompiler, node: ASTNode, compile: bool = true): Type {.discardable.} = +method assignment(self: BytecodeCompiler, node: ASTNode, compile: bool = true): Type {.discardable.} = ## Compiles assignment expressions case node.kind: of assignExpr: let node = AssignExpr(node) let name = IdentExpr(node.name) - var r = self.resolveOrError(name) + var r = BytecodeName(self.resolveOrError(name)) if r.isConst: self.error(&"cannot assign to '{name.token.lexeme}' (value is a constant)", name) elif r.isLet: @@ -1910,6 +1435,223 @@ proc assignment(self: BytecodeCompiler, node: ASTNode, compile: bool = true): Ty self.error(&"invalid AST node of kind {node.kind} at assignment(): {node} (This is an internal error and most likely a bug)") +method call(self: BytecodeCompiler, node: CallExpr, compile: bool = true): Type {.discardable.} = + ## Compiles code to call a chain of function calls + var args: seq[tuple[name: string, kind: Type, default: Expression]] = @[] + var argExpr: seq[Expression] = @[] + var default: Expression + var kind: Type + for i, argument in node.arguments.positionals: + kind = self.infer(argument) # We don't use inferOrError so that we can raise a more appropriate error message + if kind.isNil(): + if argument.kind == NodeKind.identExpr: + self.error(&"reference to undefined name '{argument.token.lexeme}'", argument) + self.error(&"positional argument {i + 1} in function call has no type", argument) + args.add(("", kind, default)) + argExpr.add(argument) + for i, argument in node.arguments.keyword: + kind = self.infer(argument.value) + if kind.isNil(): + if argument.value.kind == NodeKind.identExpr: + self.error(&"reference to undefined name '{argument.value.token.lexeme}'", argument.value) + self.error(&"keyword argument '{argument.name.token.lexeme}' in function call has no type", argument.value) + args.add((argument.name.token.lexeme, kind, default)) + argExpr.add(argument.value) + case node.callee.kind: + of NodeKind.identExpr: + # Calls like hi() + var impl = self.match(IdentExpr(node.callee).name.lexeme, Type(kind: Function, returnType: Type(kind: All), args: args), node) + result = impl.valueType + if impl.isGeneric: + result = self.specialize(result, argExpr) + if impl.valueType.isAuto: + impl = self.prepareAutoFunction(BytecodeName(impl), args) + result = impl.valueType + if not impl.valueType.compiled: + self.funDecl(FunDecl(result.fun), BytecodeName(impl)) + result = result.returnType + if compile: + if impl.valueType.fun.kind == funDecl and FunDecl(impl.valueType.fun).isTemplate: + for arg in reversed(argExpr): + self.expression(arg) + let code = BlockStmt(FunDecl(impl.valueType.fun).body).code + for i, decl in code: + if i < code.high(): + self.declaration(decl) + else: + # The last expression in a template + # is its return type, so we compute + # it, but don't pop it off the stack + if decl.kind == exprStmt: + self.expression(ExprStmt(decl).expression) + else: + self.declaration(decl) + else: + self.generateCall(BytecodeName(impl), argExpr, node.token.line) + of NodeKind.callExpr: + # Calling a call expression, like hello()() + var node: Expression = node + var all: seq[CallExpr] = @[] + # Since there can be as many consecutive calls as + # the user wants, we need to "extract" all of them + while CallExpr(node).callee.kind == callExpr: + all.add(CallExpr(CallExpr(node).callee)) + node = CallExpr(node).callee + # Now that we know how many call expressions we + # need to compile, we start from the outermost + # one and work our way to the innermost call + for exp in all: + result = self.call(exp, compile) + if compile and result.kind == Function: + self.generateCall(result, argExpr, node.token.line) + result = result.returnType + of NodeKind.getItemExpr: + var node = GetItemExpr(node.callee) + let impl = self.match(node.name.token.lexeme, + self.getItemExpr(node, compile=false, matching=Type(kind: Function, args: args, returnType: Type(kind: All))), node) + result = impl.valueType + if impl.isGeneric: + result = self.specialize(result, argExpr) + result = result.returnType + self.generateCall(BytecodeName(impl), argExpr, node.token.line) + # TODO: Calling lambdas on-the-fly (i.e. on the same line) + else: + let typ = self.infer(node) + if typ.isNil(): + self.error(&"expression has no type", node) + else: + self.error(&"object of type '{self.stringify(typ)}' is not callable", node) + + +method getItemExpr(self: BytecodeCompiler, node: GetItemExpr, compile: bool = true, matching: Type = nil): Type {.discardable.} = + ## Compiles accessing to fields of a type or + ## module namespace. If the compile flag is set + ## to false, no code is generated for resolving + ## the attribute. Returns the type of the object + ## that is resolved + case node.obj.kind: + of identExpr: + let name = self.resolveOrError(IdentExpr(node.obj)) + case name.kind: + of NameKind.Module: + var values = self.findInModule(node.name.token.lexeme, name) + if len(values) == 0: + self.error(&"reference to undefined name '{node.name.token.lexeme}' in module '{name.ident.token.lexeme}'") + elif len(values) > 1 and matching.isNil(): + self.error(&"ambiguous reference for '{node.name.token.lexeme}' in module '{name.ident.token.lexeme}'") + if not matching.isNil(): + for name in values: + if self.compare(name.valueType, matching): + result = name.valueType + return + if len(values) == 1: + result = values[0].valueType + else: + self.error(&"ambiguous reference for '{node.name.token.lexeme}' in module '{name.ident.token.lexeme}'") + if compile: + self.identifier(nil, BytecodeName(values[0])) + else: + self.error("invalid syntax", node.obj) + else: + self.error("invalid syntax", node) + + +method lambdaExpr(self: BytecodeCompiler, node: LambdaExpr, compile: bool = true): Type {.discardable.} = + ## Compiles lambda functions as expressions + result = Type(kind: Function, isLambda: true, fun: node, location: self.chunk.code.high(), compiled: true) + self.beginScope() + var constraints: seq[tuple[match: bool, kind: Type]] = @[] + for gen in node.generics: + self.unpackGenerics(gen.cond, constraints) + self.names.add(BytecodeName(depth: self.depth, + isPrivate: true, + valueType: Type(kind: Generic, name: gen.name.token.lexeme, cond: constraints), + codePos: 0, + isLet: false, + line: node.token.line, + belongsTo: nil, # TODO + ident: gen.name, + owner: self.currentModule, + file: self.file)) + constraints = @[] + var default: Expression + var name: BytecodeName + var i = 0 + for argument in node.arguments: + if self.names.high() > 16777215: + self.error("cannot declare more than 16777215 variables at a time") + name = BytecodeName(depth: self.depth, + isPrivate: true, + owner: self.currentModule, + file: self.currentModule.file, + isConst: false, + ident: argument.name, + valueType: self.inferOrError(argument.valueType), + codePos: 0, + isLet: false, + line: argument.name.token.line, + belongsTo: nil, # TODO + kind: NameKind.Argument, + node: argument.name + ) + if compile: + self.names.add(name) + if node.arguments.high() - node.defaults.high() <= node.arguments.high(): + # There's a default argument! + result.args.add((name.ident.token.lexeme, name.valueType, node.defaults[i])) + inc(i) + else: + # This argument has no default + result.args.add((name.ident.token.lexeme, name.valueType, default)) + # The function needs a return type too! + if not node.returnType.isNil(): + result.returnType = self.inferOrError(node.returnType) + self.endScope() + if not compile: + return + # TODO + + +method expression(self: BytecodeCompiler, node: Expression, compile: bool = true): Type {.discardable.} = + ## Compiles all expressions + case node.kind: + of NodeKind.callExpr: + return self.call(CallExpr(node), compile) + of NodeKind.getItemExpr: + return self.getItemExpr(GetItemExpr(node), compile) + of NodeKind.pragmaExpr: + discard # TODO + # Note that for setItem and assign we don't convert + # the node to its true type because that type information + # would be lost in the call anyway. The differentiation + # happens in self.assignment() + of NodeKind.setItemExpr, NodeKind.assignExpr: + return self.assignment(node, compile) + of NodeKind.identExpr: + return self.identifier(IdentExpr(node), compile=compile) + of NodeKind.unaryExpr: + # Unary expressions such as ~5 and -3 + return self.unary(UnaryExpr(node), compile) + of NodeKind.groupingExpr: + # Grouping expressions like (2 + 1) + return self.expression(GroupingExpr(node).expression, compile) + of NodeKind.binaryExpr: + # Binary expressions such as 2 ^ 5 and 0.66 * 3.14 + return self.binary(BinaryExpr(node)) + of NodeKind.intExpr, NodeKind.hexExpr, NodeKind.binExpr, NodeKind.octExpr, + NodeKind.strExpr, NodeKind.falseExpr, NodeKind.trueExpr, NodeKind.floatExpr: + # Since all of these AST nodes share the + # same overall structure and the kind + # field is enough to tell one from the + # other, why bother with specialized + # cases when one is enough? + return self.literal(node, compile) + of NodeKind.lambdaExpr: + return self.lambdaExpr(LambdaExpr(node), compile) + else: + self.error(&"invalid AST node of kind {node.kind} at expression(): {node} (This is an internal error and most likely a bug)") + + proc blockStmt(self: BytecodeCompiler, node: BlockStmt, compile: bool = true) = ## Compiles block statements, which create ## a new local scope @@ -1952,415 +1694,6 @@ proc whileStmt(self: BytecodeCompiler, node: WhileStmt) = self.emitLoop(start, node.token.line) self.patchJump(jump) - -proc generateCall(self: BytecodeCompiler, fn: Type, args: seq[Expression], line: int) {.used.} = - ## Version of generateCall that takes Type objects - ## instead of Name objects (used for lambdas and - ## consequent calls). The function's address is - ## assumed to be on the stack - self.emitByte(LoadUInt64, line) - self.emitBytes(self.chunk.writeConstant(0.toLong()), line) - let pos = self.chunk.consts.len() - 8 - 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 - self.emitByte(Call, line) - self.emitBytes(args.len().toTriple(), line) - self.patchReturnAddress(pos) - - -proc prepareFunction(self: BytecodeCompiler, fn: Name) = - ## "Prepares" a function declaration by declaring - ## its arguments and typechecking it - - # First we declare the function's generics, if it has any. - # This is because the function's return type may in itself - # be a generic, so it needs to exist first - var constraints: seq[tuple[match: bool, kind: Type]] = @[] - for gen in fn.node.generics: - self.unpackGenerics(gen.cond, constraints) - self.names.add(Name(depth: fn.depth + 1, - isPrivate: true, - valueType: Type(kind: Generic, name: gen.name.token.lexeme, cond: constraints), - codePos: 0, - isLet: false, - line: fn.node.token.line, - belongsTo: fn, - ident: gen.name, - owner: self.currentModule, - file: self.file)) - constraints = @[] - # We now declare and typecheck the function's - # arguments - let idx = self.stackIndex - self.stackIndex = 1 - var default: Expression - var node = FunDecl(fn.node) - var i = 0 - for argument in node.arguments: - if self.names.high() > 16777215: - self.error("cannot declare more than 16777215 variables at a time") - inc(self.stackIndex) - self.names.add(Name(depth: fn.depth + 1, - isPrivate: true, - owner: fn.owner, - file: fn.file, - isConst: false, - ident: argument.name, - valueType: if not fn.valueType.isAuto: self.inferOrError(argument.valueType) else: Type(kind: Any), - codePos: 0, - isLet: false, - line: argument.name.token.line, - belongsTo: fn, - kind: NameKind.Argument, - node: argument.name, - position: self.stackIndex, - isReal: not node.isTemplate - )) - if node.arguments.high() - node.defaults.high() <= node.arguments.high(): - # There's a default argument! - fn.valueType.args.add((self.names[^1].ident.token.lexeme, self.names[^1].valueType, node.defaults[i])) - inc(i) - else: - # This argument has no default - fn.valueType.args.add((self.names[^1].ident.token.lexeme, self.names[^1].valueType, default)) - # The function needs a return type too! - if not FunDecl(fn.node).returnType.isNil(): - fn.valueType.returnType = self.inferOrError(FunDecl(fn.node).returnType) - fn.position = self.stackIndex - self.stackIndex = idx - if node.isTemplate: - fn.valueType.compiled = true - - -proc prepareAutoFunction(self: BytecodeCompiler, fn: Name, args: seq[tuple[name: string, kind: Type, default: Expression]]): Name = - ## "Prepares" an automatic function declaration - ## by declaring a concrete version of it along - ## with its arguments - - # First we declare the function's generics, if it has any. - # This is because the function's return type may in itself - # be a generic, so it needs to exist first - # We now declare and typecheck the function's - # arguments - let idx = self.stackIndex - self.stackIndex = 1 - var default: Expression - var node = FunDecl(fn.node) - var fn = deepCopy(fn) - fn.valueType.isAuto = false - fn.valueType.compiled = false - self.names.add(fn) - for (argument, val) in zip(node.arguments, args): - if self.names.high() > 16777215: - self.error("cannot declare more than 16777215 variables at a time") - inc(self.stackIndex) - self.names.add(Name(depth: fn.depth + 1, - isPrivate: true, - owner: fn.owner, - file: fn.file, - isConst: false, - ident: argument.name, - valueType: val.kind, - codePos: 0, - isLet: false, - line: argument.name.token.line, - belongsTo: fn, - kind: NameKind.Argument, - node: argument.name, - position: self.stackIndex, - isReal: not node.isTemplate - )) - if node.isTemplate: - fn.valueType.compiled = true - fn.valueType.args = args - fn.position = self.stackIndex - self.stackIndex = idx - return fn - - - -proc generateCall(self: BytecodeCompiler, fn: Name, args: seq[Expression], line: int) = - ## Small wrapper that abstracts emitting a call instruction - ## for a given function - self.dispatchDelayedPragmas(fn) - if fn.isBuiltin: - self.handleBuiltinFunction(fn.valueType, args, line) - return - case fn.kind: - of NameKind.Var: - self.identifier(VarDecl(fn.node).name) - of NameKind.Function: - self.emitByte(LoadUInt64, line) - self.emitBytes(self.chunk.writeConstant(fn.codePos.toLong()), line) - else: - discard # Unreachable - if fn.valueType.forwarded: - self.forwarded.add((fn, self.chunk.consts.high() - 7)) - self.emitByte(LoadUInt64, line) - self.emitBytes(self.chunk.writeConstant(0.toLong()), line) - let pos = self.chunk.consts.len() - 8 - for arg in reversed(args): - self.expression(arg) - # Creates a new call frame and jumps - # to the function's first instruction - # in the code - self.emitByte(Call, line) - self.emitBytes(args.len().toTriple(), line) - self.patchReturnAddress(pos) - - -proc specialize(self: BytecodeCompiler, typ: Type, args: seq[Expression]): Type {.discardable.} = - ## Specializes a generic type. - ## Used for typechecking at the - ## call site - var mapping: TableRef[string, Type] = newTable[string, Type]() - var kind: Type - result = deepCopy(typ) - case result.kind: - of TypeKind.Function: - # This first loop checks if a user tries to reassign a generic's - # name to a different type - for i, (name, typ, default) in result.args: - if typ.kind != Generic: - continue - kind = self.inferOrError(args[i]) - if typ.name in mapping and not self.compare(kind, mapping[typ.name]): - self.error(&"expecting generic argument '{typ.name}' to be of type {self.stringify(mapping[typ.name])}, got {self.stringify(kind)}", args[i]) - mapping[typ.name] = kind - result.args[i].kind = kind - if not result.returnType.isNil() and result.returnType.kind == Generic: - if result.returnType.name in mapping: - result.returnType = mapping[result.returnType.name] - else: - self.error(&"unknown generic argument name '{result.returnType.name}'", result.fun) - else: - discard # TODO: Custom user-defined types - - -proc call(self: BytecodeCompiler, node: CallExpr, compile: bool = true): Type {.discardable.} = - ## Compiles code to call a chain of function calls - var args: seq[tuple[name: string, kind: Type, default: Expression]] = @[] - var argExpr: seq[Expression] = @[] - var default: Expression - var kind: Type - for i, argument in node.arguments.positionals: - kind = self.infer(argument) # We don't use inferOrError so that we can raise a more appropriate error message - if kind.isNil(): - if argument.kind == NodeKind.identExpr: - self.error(&"reference to undefined name '{argument.token.lexeme}'", argument) - self.error(&"positional argument {i + 1} in function call has no type", argument) - args.add(("", kind, default)) - argExpr.add(argument) - for i, argument in node.arguments.keyword: - kind = self.infer(argument.value) - if kind.isNil(): - if argument.value.kind == NodeKind.identExpr: - self.error(&"reference to undefined name '{argument.value.token.lexeme}'", argument.value) - self.error(&"keyword argument '{argument.name.token.lexeme}' in function call has no type", argument.value) - args.add((argument.name.token.lexeme, kind, default)) - argExpr.add(argument.value) - case node.callee.kind: - of NodeKind.identExpr: - # Calls like hi() - var impl = self.match(IdentExpr(node.callee).name.lexeme, Type(kind: Function, returnType: Type(kind: All), args: args), node) - result = impl.valueType - if impl.isGeneric: - result = self.specialize(result, argExpr) - if impl.valueType.isAuto: - impl = self.prepareAutoFunction(impl, args) - result = impl.valueType - if not impl.valueType.compiled: - self.funDecl(FunDecl(result.fun), impl) - result = result.returnType - if compile: - if impl.valueType.fun.kind == funDecl and FunDecl(impl.valueType.fun).isTemplate: - for arg in reversed(argExpr): - self.expression(arg) - let code = BlockStmt(FunDecl(impl.valueType.fun).body).code - for i, decl in code: - if i < code.high(): - self.declaration(decl) - else: - # The last expression in a template - # is its return type, so we compute - # it, but don't pop it off the stack - if decl.kind == exprStmt: - self.expression(ExprStmt(decl).expression) - else: - self.declaration(decl) - else: - self.generateCall(impl, argExpr, node.token.line) - of NodeKind.callExpr: - # Calling a call expression, like hello()() - var node: Expression = node - var all: seq[CallExpr] = @[] - # Since there can be as many consecutive calls as - # the user wants, we need to "extract" all of them - while CallExpr(node).callee.kind == callExpr: - all.add(CallExpr(CallExpr(node).callee)) - node = CallExpr(node).callee - # Now that we know how many call expressions we - # need to compile, we start from the outermost - # one and work our way to the innermost call - for exp in all: - result = self.call(exp, compile) - if compile and result.kind == Function: - self.generateCall(result, argExpr, node.token.line) - result = result.returnType - of NodeKind.getItemExpr: - var node = GetItemExpr(node.callee) - let impl = self.match(node.name.token.lexeme, - self.getItemExpr(node, compile=false, matching=Type(kind: Function, args: args, returnType: Type(kind: All))), node) - result = impl.valueType - if impl.isGeneric: - result = self.specialize(result, argExpr) - result = result.returnType - self.generateCall(impl, argExpr, node.token.line) - # TODO: Calling lambdas on-the-fly (i.e. on the same line) - else: - let typ = self.infer(node) - if typ.isNil(): - self.error(&"expression has no type", node) - else: - self.error(&"object of type '{self.stringify(typ)}' is not callable", node) - - -proc getItemExpr(self: BytecodeCompiler, node: GetItemExpr, compile: bool = true, matching: Type = nil): Type {.discardable.} = - ## Compiles accessing to fields of a type or - ## module namespace. If the compile flag is set - ## to false, no code is generated for resolving - ## the attribute. Returns the type of the object - ## that is resolved - case node.obj.kind: - of identExpr: - let name = self.resolveOrError(IdentExpr(node.obj)) - case name.kind: - of NameKind.Module: - var values = self.findInModule(node.name.token.lexeme, name) - if len(values) == 0: - self.error(&"reference to undefined name '{node.name.token.lexeme}' in module '{name.ident.token.lexeme}'") - elif len(values) > 1 and matching.isNil(): - self.error(&"ambiguous reference for '{node.name.token.lexeme}' in module '{name.ident.token.lexeme}'") - if not matching.isNil(): - for name in values: - if self.compare(name.valueType, matching): - result = name.valueType - return - if len(values) == 1: - result = values[0].valueType - else: - self.error(&"ambiguous reference for '{node.name.token.lexeme}' in module '{name.ident.token.lexeme}'") - if compile: - self.identifier(nil, values[0]) - else: - self.error("invalid syntax", node.obj) - else: - self.error("invalid syntax", node) - - -proc lambdaExpr(self: BytecodeCompiler, node: LambdaExpr, compile: bool = true): Type {.discardable.} = - ## Compiles lambda functions as expressions - result = Type(kind: Function, isLambda: true, fun: node, location: self.chunk.code.high(), compiled: true) - self.beginScope() - var constraints: seq[tuple[match: bool, kind: Type]] = @[] - for gen in node.generics: - self.unpackGenerics(gen.cond, constraints) - self.names.add(Name(depth: self.depth, - isPrivate: true, - valueType: Type(kind: Generic, name: gen.name.token.lexeme, cond: constraints), - codePos: 0, - isLet: false, - line: node.token.line, - belongsTo: nil, # TODO - ident: gen.name, - owner: self.currentModule, - file: self.file)) - constraints = @[] - var default: Expression - var name: Name - var i = 0 - for argument in node.arguments: - if self.names.high() > 16777215: - self.error("cannot declare more than 16777215 variables at a time") - name = Name(depth: self.depth, - isPrivate: true, - owner: self.currentModule, - file: self.currentModule.file, - isConst: false, - ident: argument.name, - valueType: self.inferOrError(argument.valueType), - codePos: 0, - isLet: false, - line: argument.name.token.line, - belongsTo: nil, # TODO - kind: NameKind.Argument, - node: argument.name - ) - if compile: - self.names.add(name) - if node.arguments.high() - node.defaults.high() <= node.arguments.high(): - # There's a default argument! - result.args.add((name.ident.token.lexeme, name.valueType, node.defaults[i])) - inc(i) - else: - # This argument has no default - result.args.add((name.ident.token.lexeme, name.valueType, default)) - # The function needs a return type too! - if not node.returnType.isNil(): - result.returnType = self.inferOrError(node.returnType) - self.endScope() - if not compile: - return - # TODO - - -proc expression(self: BytecodeCompiler, node: Expression, compile: bool = true): Type {.discardable.} = - ## Compiles all expressions - case node.kind: - of NodeKind.callExpr: - return self.call(CallExpr(node), compile) - of NodeKind.getItemExpr: - return self.getItemExpr(GetItemExpr(node), compile) - of NodeKind.pragmaExpr: - discard # TODO - # Note that for setItem and assign we don't convert - # the node to its true type because that type information - # would be lost in the call anyway. The differentiation - # happens in self.assignment() - of NodeKind.setItemExpr, NodeKind.assignExpr: - return self.assignment(node, compile) - of NodeKind.identExpr: - return self.identifier(IdentExpr(node), compile=compile) - of NodeKind.unaryExpr: - # Unary expressions such as ~5 and -3 - return self.unary(UnaryExpr(node), compile) - of NodeKind.groupingExpr: - # Grouping expressions like (2 + 1) - return self.expression(GroupingExpr(node).expression, compile) - of NodeKind.binaryExpr: - # Binary expressions such as 2 ^ 5 and 0.66 * 3.14 - return self.binary(BinaryExpr(node)) - of NodeKind.intExpr, NodeKind.hexExpr, NodeKind.binExpr, NodeKind.octExpr, - NodeKind.strExpr, NodeKind.falseExpr, NodeKind.trueExpr, NodeKind.floatExpr: - # Since all of these AST nodes share the - # same overall structure and the kind - # field is enough to tell one from the - # other, why bother with specialized - # cases when one is enough? - return self.literal(node, compile) - of NodeKind.lambdaExpr: - return self.lambdaExpr(LambdaExpr(node), compile) - else: - self.error(&"invalid AST node of kind {node.kind} at expression(): {node} (This is an internal error and most likely a bug)") - - # TODO proc awaitStmt(self: BytecodeCompiler, node: AwaitStmt) = ## Compiles await statements @@ -2448,7 +1781,7 @@ proc importStmt(self: BytecodeCompiler, node: ImportStmt, compile: bool = true) var module = self.names[^1] try: if compile: - self.compileModule(module) + self.compileModule(BytecodeName(module)) # Importing a module automatically exports # its public names to us for name in self.findInModule("", module): @@ -2638,13 +1971,13 @@ proc varDecl(self: BytecodeCompiler, node: VarDecl) = self.expression(node.value) self.emitByte(AddVar, node.token.line) self.declare(node) - var name = self.names[^1] + var name = BytecodeName(self.names[^1]) inc(self.stackIndex) name.position = self.stackIndex name.valueType = typ -proc funDecl(self: BytecodeCompiler, node: FunDecl, name: Name) = +proc funDecl(self: BytecodeCompiler, node: FunDecl, name: BytecodeName) = ## Compiles function declarations if node.token.kind == Operator and node.name.token.lexeme in [".", "="]: self.error(&"Due to compiler limitations, the '{node.name.token.lexeme}' operator cannot be currently overridden", node.name) @@ -2814,7 +2147,7 @@ proc compile*(self: BytecodeCompiler, ast: seq[Declaration], file: string, lines result = self.chunk -proc compileModule(self: BytecodeCompiler, module: Name) = +proc compileModule(self: BytecodeCompiler, module: BytecodeName) = ## Compiles an imported module into an existing chunk ## using the compiler's internal parser and lexer objects var path = ""