# 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/os import std/sets import std/tables import std/hashes import std/strutils import std/terminal import std/sequtils import std/algorithm import std/strformat import errors import config import frontend/parsing/token import frontend/parsing/ast import frontend/parsing/lexer as l import frontend/parsing/parser as p type # Just a bunch of convenience type aliases TypedArgument* = tuple[name: string, kind: Type, default: TypedNode] TypeConstraint* = tuple[match: bool, kind: Type] PragmaKind* = enum ## A pragma type enumeration # "Immediate" pragmas are processed right # when they are encountered. This is useful # for some types of pragmas, such as those # that flick compile-time switches on or off # or that mark objects with some compile-time # property. "Delayed" pragmas, on the other hand, # are processed when their associated object is # used in some way. For example, the "error" # pragma, when associated to a function, causes # the compiler to raise a static error when # attempting to call said function, rather than # doing so at declaration time. This allows the # detection of errors such as trying to negate # unsigned integers without explicitly hardcoding # the check into the compiler (all that's needed # is a definition of the `-` operator that will # raise a static error once called) Immediate, Delayed Pragma* = object ## A pragma object. Pragmas are ## (usually) hooks into compiler ## functions or serve as markers ## for objects (for example, to ## signal that a function has no ## side effects or that a type is ## nullable) kind*: PragmaKind name*: string # All pragmas have names arguments*: seq[Type] # The arguments to the pragma. Must be values computable at compile time TypeKind* = enum ## An enumeration of compile-time ## types # Intrinsic (aka "built-in") types # Signed and unsigned integer types Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, # Floating point types Float32, Float64, Char, # A single ASCII character Byte, # Basically an alias for char String, # A string. No encoding is specified Function, # A function TypeDecl, # A type declaration Nil, # The nil type (aka null) Nan, # The value of NaN (Not a Number) Bool, # Booleans (true and false) Inf, # Negative and positive infinity # Note: nil, nan, true, false and inf are all singletons Typevar, # A type variable is the type of a type. For example, the type of `int` is typevar Generic, # A parametrically polymorphic generic type Reference, # A managed (aka GC'ed) reference Pointer, # An unmanaged (aka malloc()-ed) reference Any, # The "any" type is a placeholder for a single type (similar to Python's builtins.object) All, # The all type means "any type or no type at all". It is not exposed outside of the compiler Union, # An untagged type union (acts like an exclusive "logical or" constraint) Auto, # An automatic type. The compiler infers the true type of the object from its value when necessary Enum, # An enumeration type Type* = ref object ## A compile-time type case kind: TypeKind: of Generic: # A generic type constraints*: seq[TypeConstraint] # The type's generic constraints. For example, # fn foo[T*: int & ~uint](...) {...} would map to [(true, int), (false, uint)] name*: IdentExpr # The generic's name (in our example above, this would be "T") asUnion*: bool # A generic constraint is treated like a "logical and", which means all # of its constraints must be satisfied. This allows for parametric polymorphism to work, # but it woudln't allow to make use of the type with only one of the types of the constraint, # which is pretty useless. When this value is set to true, which it isn't by default, the # constraints turn into an exclusive "logical or" instead, meaning that any type in the constraints # is a valid instance of the type itself. This allows the compiler to typecheck the type for all # possible types in the constraint and then let the user instantiate said type with any of the types # in said constraint. The field's name means "treat this generic constraint like a type union" of Union: # A type union types*: seq[TypeConstraint] of Reference: # A managed reference nullable*: bool # Is null a valid value for this type? (false by default) value*: TypedNode # The type the reference points to of Pointer: # An unmanaged reference. Much # like a raw pointer in C data*: TypedNode # The type we point to of TypeDecl: # A user-defined type fields*: seq[TypedArgument] # List of fields in the object. May be empty parent*: Type # The parent of this object if inheritance is used. May be nil implements*: seq[Type] # The interfaces this object implements. May be empty of Function: # A function-like object. Wraps regular # functions, lambdas, coroutines and generators isLambda*: bool # Is this a lambda (aka anonymous) function? isCoroutine*: bool # Is this a coroutine? isGenerator*: bool # Is this a generator? isAuto*: bool # Is this an automatic function? arguments*: seq[TypedArgument] # The function's arguments forwarded*: bool # Is this a forward declaration? returnType*: Type # The function's return type else: discard # Can this type be mutated? mutable: bool TypedNode* = ref object ## A typed AST node node*: ASTNode # The original (typeless) AST node value*: Type # The node's type NameKind* = enum ## A name enumeration type DeclType, # Any type declaration Module Name* = ref object ## A name object. Name objects associate ## peon objects to identifiers case kind*: NameKind of Module: path*: string # The module's path else: discard ident*: IdentExpr # The name's identifier file*: string # The file where this name is declared in belongsTo*: Name # The function owning this name, if any obj*: TypedNode # The name's associated object owner*: Name # The module owning this name depth*: int # The name's scope depth isPrivate*: bool # Is this name private? isConst*: bool # Is this name a constant? isLet*: bool # Can this name's value be mutated? isGeneric*: bool # Is this a generic type? line*: int # The line where this name is declared resolved*: bool # Has this name ever been used? node*: Declaration # The declaration associated with this name exports*: HashSet[Name] # The modules to which this name is visible to isBuiltin*: bool # Is this name a built-in? isReal*: bool # Is this an actual name in user code? (The compiler # generates some names for its internal use and they may even duplicate existing # ones, so that is why we need this attribute) 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 ## The peon compiler ast: seq[Declaration] # The (typeless) AST of the current module current: int # Index into self.ast of the current node we're compiling file*: string # The current file being compiled (used only for error reporting) depth*: int # The current scope depth. If > 0, we're in a local scope, otherwise it's global replMode*: bool # Are we in REPL mode? names*: seq[Name] # List of all currently declared names lines*: seq[tuple[start, stop: int]] # Stores line data for error reporting source*: string # The source of the current module, used for error reporting # We store these objects to compile modules lexer*: Lexer parser*: Parser isMainModule*: bool # Are we compiling the main module? disabledWarnings*: seq[WarningKind] # List of disabled warnings showMismatches*: bool # Whether to show detailed info about type mismatches when we dispatch mode*: CompileMode # Are we compiling in debug mode or release mode? currentFunction*: Name # The current function being compiled currentModule*: Name # The current module being compiled parentModule*: Name # The module importing us, if any modules*: HashSet[Name] # Currently imported modules # Makes our name objects hashable func hash(self: Name): Hash {.inline.} = self.ident.token.lexeme.hash() proc `$`*(self: Name): string = $(self[]) proc `$`(self: Type): string = $(self[]) proc `$`(self: TypedNode): string = $(self[]) # Public getters for nicer error formatting func getCurrentNode*(self: Compiler): ASTNode {.inline.} = (if self.current >= self.ast.len(): self.ast[^1] else: self.ast[self.current - 1]) func getCurrentFunction*(self: Compiler): Declaration {.inline.} = (if self.currentFunction.isNil(): nil else: self.currentFunction.node) func getSource*(self: Compiler): string {.inline.} = self.source # Utility functions 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() 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) proc infer*(self: Compiler, node: LiteralExpr): TypedNode = ## 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 TypedNode(node: node, value: Type(kind: Int64)) let typ = size[1].toIntrinsic() if not self.compare(typ, nil): return TypedNode(node: node, value: 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 TypedNode(node: node, value: Type(kind: Float64)) let typ = size[1].toIntrinsic() if not typ.isNil(): return TypedNode(node: node, value: typ) else: self.error(&"invalid type specifier '{size[1]}' for float", node) of trueExpr: return TypedNode(node: node, value: Type(kind: Bool)) of falseExpr: return TypedNode(node: node, value: Type(kind: Bool)) of strExpr: return TypedNode(node: node, value: Type(kind: String)) else: discard # Unreachable proc infer*(self: Compiler, node: Expression): TypedNode = ## 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 = TypedNode(node: node, value: Type(kind: Reference, value: self.infer(Ref(node).value))) of NodeKind.ptrExpr: result = TypedNode(node: node, value: Type(kind: Pointer, data: 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: Compiler, node: Expression): TypedNode = ## 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) proc 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)}" of Reference: result &= &"ref {self.stringify(typ)}" 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.constraints: if i > 0: result &= " | " if not condition.match: result &= "~" result &= self.stringify(condition.kind) else: discard proc stringify*(self: Compiler, typ: TypedNode): string = ## Returns the string representation of a ## type object if typ.isNil(): return "nil" case typ.value.kind: of Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Float32, Float64, Char, Byte, String, Nil, TypeKind.Nan, Bool, TypeKind.Inf, Auto, Pointer, Reference, Any, Union, Generic: result &= self.stringify(typ.value) of Function: result &= "fn (" for i, (argName, argType, argDefault) in typ.value.arguments: result &= &"{argName}: {self.stringify(argType)}" if not argDefault.isNil(): result &= &" = {argDefault}" if i < typ.value.arguments.len() - 1: result &= ", " result &= ")" if not typ.value.returnType.isNil(): result &= &": {self.stringify(typ.value.returnType)}" var node = Declaration(typ.node) if node.pragmas.len() > 0: result &= " {" for i, pragma in node.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 < node.pragmas.high(): result &= ", " else: result &= "}" else: discard proc 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.exports: continue result.add(obj) proc 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.exports: result.add(obj) proc 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 name in self.findByName(name): if self.compare(name.obj.value, kind): result.add(name) proc 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.value, kind): self.error(&"expecting value of type {self.stringify(kind)}, got {self.stringify(k)}", term) elif k.value.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.constraints: if condition.kind.isAny(): return true of Union: for condition in typ.types: if condition.kind.isAny(): return true else: return false proc 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] = self.findByType(name, kind) 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.obj.value)}" if name.obj.value.kind != Function: msg &= ": not a callable" elif kind.arguments.len() != name.obj.value.arguments.len(): msg &= &": wrong number of arguments (expected {name.obj.value.arguments.len()}, got {kind.arguments.len()})" else: for i, arg in kind.arguments: if not self.compare(arg.kind, name.obj.value.arguments[i].kind): msg &= &": first mismatch at position {i + 1}: (expected {self.stringify(name.obj.value.arguments[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.obj.value.forwarded and not it.obj.value.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.obj.value)}" 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].obj.value.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].obj.value)}'") result = impl[0] for (a, b) in zip(result.obj.value.arguments, kind.arguments): 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).value)) 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).value)) 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 dispatchPragmas(self: Compiler, name: Name) = discard proc dispatchDelayedPragmas(self: Compiler, name: Name) = discard 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, obj: nil, # Done later isLet: node.isLet, line: node.token.line, belongsTo: self.currentFunction, kind: DeclType, 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, obj: TypedNode(node: node, value: Type(kind: Function, returnType: nil, # We check it later arguments: @[], forwarded: node.body.isNil(), isAuto: false) ), ident: node.name, node: node, isLet: false, line: node.token.line, kind: DeclType, belongsTo: self.currentFunction, isReal: true) if node.generics.len() > 0: n.isGeneric = true var typ: Type for argument in node.arguments: typ = self.infer(argument.valueType).value if not typ.isNil() and typ.kind == Auto: n.obj.value.isAuto = true if n.isGeneric: self.error("automatic types cannot be used within generics", argument.valueType) break typ = self.infer(node.returnType).value if not typ.isNil() and typ.kind == Auto: n.obj.value.isAuto = true if n.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 = ast.TypeDecl(node) self.names.add(Name(kind: DeclType, depth: self.depth, owner: self.currentModule, node: node, ident: node.name, line: node.token.line, isPrivate: node.isPrivate, isReal: true, belongsTo: self.currentFunction, obj: TypedNode(node: node, value: Type(kind: TypeDecl)) ) ) n = self.names[^1] declaredName = node.name.token.lexeme if node.value.isNil(): discard # TODO: Fields else: case node.value.kind: of identExpr: n.obj.value = self.inferOrError(node.value).value of binaryExpr: # Type union n.obj.value = Type(kind: Union, types: @[]) self.unpackUnion(node.value, n.obj.value.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 == DeclType 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 notin name.exports): continue if name.kind == DeclType: 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