# Copyright 2022 Mattia Giambirtone & All Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import std/tables import std/strformat import std/algorithm import std/parseutils import std/strutils import std/sequtils import std/sets import std/os import std/terminal import std/hashes import errors import config import util/symbols import frontend/parsing/token import frontend/parsing/ast import frontend/parsing/lexer as l import frontend/parsing/parser as p export ast, token, symbols, config, errors type PeonBackend* = enum ## An enumeration of the peon backends Bytecode, NativeC PragmaKind* = enum ## An enumeration of pragma types Immediate, Delayed TypeKind* = enum ## An enumeration of compile-time ## types Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Float32, Float64, Char, Byte, String, Function, CustomType, Nil, Nan, Bool, Inf, Typevar, Generic, Reference, Pointer, Any, All, Union, Auto Type* = ref object ## A wrapper around ## compile-time types case kind*: TypeKind: of Function: isLambda*: bool isGenerator*: bool isCoroutine*: bool isAuto*: bool args*: seq[tuple[name: string, kind: Type, default: Expression]] returnType*: Type builtinOp*: string fun*: Declaration retJumps*: seq[int] forwarded*: bool location*: int compiled*: bool of CustomType: fields*: TableRef[string, Type] of Reference, Pointer: value*: Type of Generic: # cond represents a type constraint. For # example, fn foo[T*: int & ~uint](...) {...} # would map to [(true, int), (false, uint)] cond*: seq[tuple[match: bool, kind: Type]] asUnion*: bool # If this is true, the constraint is treated like a type union name*: string of Union: types*: seq[tuple[match: bool, kind: Type]] else: discard 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[string] # Has the compiler generated this name internally or # does it come from user code? isReal*: bool # Is this name a builtin? isBuiltin*: bool ## BACKEND-SPECIFIC FIELDS # Bytecode backend stuff # 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 location of this name on the stack. # Only makes sense for names that actually # materialize on the call stack at runtime # (except for functions, where we use it to # signal where the function's frame starts) position*: int WarningKind* {.pure.} = enum ## A warning enumeration type UnreachableCode, UnusedName, ShadowOuterScope, MutateOuterScope CompileMode* {.pure.} = enum ## A compilation mode enumeration Debug, Release CompileError* = ref object of PeonException node*: ASTNode function*: Declaration compiler*: Compiler Compiler* = ref object of RootObj ## A wrapper around the Peon compiler's state # The output of our parser (AST) ast*: seq[Declaration] # The current AST node we're looking at current*: int # The current file being compiled (used only for # error reporting) file*: string # The current scope depth. If > 0, we're # in a local scope, otherwise it's global depth*: int # Are we in REPL mode? 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, # used for error reporting source*: string # We store these objects to compile modules lexer*: Lexer parser*: Parser # Are we compiling the main module? isMainModule*: bool # List of disabled warnings disabledWarnings*: seq[WarningKind] # Whether to show detailed info about type # mismatches when we dispatch with self.match showMismatches*: bool # Are we compiling in debug mode? mode*: CompileMode # The current function being compiled currentFunction*: Name # The current module being compiled currentModule*: Name # The module importing us, if any parentModule*: Name # Currently imported modules modules*: TableRef[string, Name] TypedNode* = ref object ## A wapper for AST nodes ## with attached type information kind*: Type node*: ASTNode ## 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) proc getSource*(self: Compiler): string {.inline.} = self.source ## Some forward declarations (some of them arere 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 method prepareFunction*(self: Compiler, name: Name) {.base.} = discard method dispatchPragmas(self: Compiler, name: Name) {.base.} = discard method dispatchDelayedPragmas(self: Compiler, name: Name) {.base.} = discard ## 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, compiler: self) 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 == "all": return Type(kind: All) 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 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.path 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 obj.owner.isNil(): continue if not obj.isPrivate and obj.owner.path == module.path: result.add(obj) else: for obj in self.findInModule("", module): if obj.ident.token.lexeme == name and self.currentModule.path 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: 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: # If we happen to find more than one match, we try again # and ignore forward declarations and automatic functions impl = filterIt(impl, not it.valueType.forwarded and not it.valueType.isAuto) if impl.len() > 1: # If there'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) # This is only true when we're called by self.patchForwardDeclarations() 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 beginScope*(self: Compiler) = ## Begins a new local scope by incrementing the current ## scope's depth inc(self.depth) proc unpackGenerics*(self: Compiler, condition: Expression, list: var seq[tuple[match: bool, kind: Type]], accept: bool = true) = ## Recursively unpacks a type constraint in a generic type case condition.kind: of identExpr: list.add((accept, self.inferOrError(condition))) if list[^1].kind.kind == Auto: self.error("automatic types cannot be used within generics", condition) of binaryExpr: let condition = BinaryExpr(condition) case condition.operator.lexeme: of "|": self.unpackGenerics(condition.a, list) self.unpackGenerics(condition.b, list) else: self.error("invalid type constraint in generic declaration", condition) of unaryExpr: let condition = UnaryExpr(condition) case condition.operator.lexeme: of "~": self.unpackGenerics(condition.a, list, accept=false) else: self.error("invalid type constraint in generic declaration", condition) else: self.error("invalid type constraint in generic declaration", condition) proc unpackUnion*(self: Compiler, condition: Expression, list: var seq[tuple[match: bool, kind: Type]], accept: bool = true) = ## Recursively unpacks a type union case condition.kind: of identExpr: list.add((accept, self.inferOrError(condition))) of binaryExpr: let condition = BinaryExpr(condition) case condition.operator.lexeme: of "|": self.unpackUnion(condition.a, list) self.unpackUnion(condition.b, list) else: self.error("invalid type constraint in type union", condition) of unaryExpr: let condition = UnaryExpr(condition) case condition.operator.lexeme: of "~": self.unpackUnion(condition.a, list, accept=false) else: self.error("invalid type constraint in type union", condition) else: self.error("invalid type constraint in type union", condition) proc declare*(self: Compiler, node: ASTNode): Name {.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() ## correctly return them. There is no code to actually ## declare a variable at runtime: the value is already ## on the stack var declaredName: string = "" var n: Name 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 self.error("cannot declare more than 16777215 names at a time") case node.kind: of NodeKind.varDecl: 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, ident: node.name, isPrivate: node.isPrivate, owner: self.currentModule, file: self.file, isConst: node.isConst, valueType: nil, # Done later isLet: node.isLet, line: node.token.line, belongsTo: self.currentFunction, kind: NameKind.Var, node: node, isReal: true )) n = self.names[^1] of NodeKind.funDecl: var node = FunDecl(node) declaredName = node.name.token.lexeme var fn = Name(depth: self.depth, isPrivate: node.isPrivate, isConst: false, owner: self.currentModule, file: self.file, valueType: Type(kind: Function, returnType: nil, # We check it later args: @[], fun: node, forwarded: node.body.isNil(), isAuto: false), ident: node.name, node: node, isLet: false, line: node.token.line, kind: NameKind.Function, belongsTo: self.currentFunction, isReal: true) if node.isTemplate: fn.valueType.compiled = true if node.generics.len() > 0: fn.isGeneric = true var typ: Type for argument in node.arguments: typ = self.infer(argument.valueType) if not typ.isNil() and typ.kind == Auto: fn.valueType.isAuto = true if fn.isGeneric: self.error("automatic types cannot be used within generics", argument.valueType) break typ = self.infer(node.returnType) if not typ.isNil() and typ.kind == Auto: fn.valueType.isAuto = true if fn.isGeneric: self.error("automatic types cannot be used within generics", node.returnType) self.names.add(fn) self.prepareFunction(fn) n = fn of NodeKind.importStmt: var node = ImportStmt(node) # We change the name of the module internally so that # if you import /path/to/mod, then doing mod.f() will # still work without any extra work on our end. Note how # we don't change the metadata about the identifier's # position so that error messages still highlight the # full path let path = node.moduleName.token.lexeme node.moduleName.token.lexeme = node.moduleName.token.lexeme.extractFilename() self.names.add(Name(depth: self.depth, owner: self.currentModule, file: "", # The file of the module isn't known until it's compiled! path: path, ident: node.moduleName, line: node.moduleName.token.line, kind: NameKind.Module, isPrivate: false, isReal: true, )) n = self.names[^1] declaredName = self.names[^1].ident.token.lexeme of NodeKind.typeDecl: var node = TypeDecl(node) self.names.add(Name(kind: NameKind.CustomType, depth: self.depth, owner: self.currentModule, node: node, ident: node.name, line: node.token.line, isPrivate: node.isPrivate, isReal: true, belongsTo: self.currentFunction, valueType: Type(kind: CustomType) ) ) n = self.names[^1] declaredName = node.name.token.lexeme if node.value.isNil(): discard # TODO: Fields else: case node.value.kind: of identExpr: n.valueType = self.inferOrError(node.value) of binaryExpr: # Type union n.valueType = Type(kind: Union, types: @[]) self.unpackUnion(node.value, n.valueType.types) else: discard else: discard # TODO: enums if not n.isNil(): self.dispatchPragmas(n) for name in self.findByName(declaredName): if name == n: continue # We don't check for name clashes with functions because self.match() does that if name.kind in [NameKind.Var, NameKind.Module, NameKind.CustomType, NameKind.Enum] and name.depth == n.depth and name.owner == n.owner: self.error(&"re-declaration of {declaredName} is not allowed (previously declared in {name.owner.ident.token.lexeme}:{name.ident.token.line}:{name.ident.token.relPos.start})") # We emit a bunch of warnings, mostly for QoL for name in self.names: if name == n: break if name.ident.token.lexeme != declaredName: continue if name.owner != n.owner and (name.isPrivate or n.owner.path notin name.exportedTo): continue if name.kind in [NameKind.Var, NameKind.Module, NameKind.CustomType, NameKind.Enum]: if name.depth < n.depth: self.warning(WarningKind.ShadowOuterScope, &"'{declaredName}' at depth {name.depth} shadows a name from an outer scope ({name.owner.file}.pn:{name.ident.token.line}:{name.ident.token.relPos.start})", n) if name.owner != n.owner: self.warning(WarningKind.ShadowOuterScope, &"'{declaredName}' at depth {name.depth} shadows a name from an outer module ({name.owner.file}.pn:{name.ident.token.line}:{name.ident.token.relPos.start})", n) return n