peon/src/frontend/compiler/newcompiler.nim

1057 lines
46 KiB
Nim

# 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 ["<string>", ""]:
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..<pos.start])
stderr.styledWrite(fgYellow, styleUnderscore, line[pos.start..pos.stop])
stderr.styledWriteLine(fgDefault, line[pos.stop + 1..^1])
except IOError:
discard
except OSError:
discard
except IndexDefect:
# Something probably went wrong (wrong line metadata): bad idea to crash!
discard
proc step*(self: Compiler): ASTNode {.inline.} =
## Steps to the next node and returns
## the consumed one
result = self.peek()
if not self.done():
self.current += 1
# Some forward declarations
proc compareUnions*(self: Compiler, a, b: seq[tuple[match: bool, kind: Type]]): bool
proc expression*(self: Compiler, node: Expression, compile: bool = true): TypedNode {.discardable.} = nil
proc identifier*(self: Compiler, node: IdentExpr, name: Name = nil, compile: bool = true, strict: bool = true): TypedNode {.discardable.} = nil
proc call*(self: Compiler, node: CallExpr, compile: bool = true): TypedNode {.discardable.} = nil
proc getItemExpr*(self: Compiler, node: GetItemExpr, compile: bool = true, matching: Type = nil): TypedNode {.discardable.} = nil
proc unary*(self: Compiler, node: UnaryExpr, compile: bool = true): TypedNode {.discardable.} = nil
proc binary*(self: Compiler, node: BinaryExpr, compile: bool = true): TypedNode {.discardable.} = nil
proc lambdaExpr*(self: Compiler, node: LambdaExpr, compile: bool = true): TypedNode {.discardable.} = nil
proc literal*(self: Compiler, node: ASTNode, compile: bool = true): TypedNode {.discardable.} = nil
proc infer*(self: Compiler, node: LiteralExpr): TypedNode
proc infer*(self: Compiler, node: Expression): TypedNode
proc inferOrError*(self: Compiler, node: Expression): TypedNode
proc findByName*(self: Compiler, name: string): seq[Name]
proc findInModule*(self: Compiler, name: string, module: Name): seq[Name]
proc findByType*(self: Compiler, name: string, kind: Type): seq[Name]
proc compare*(self: Compiler, a, b: Type): bool
proc match*(self: Compiler, name: string, kind: Type, node: ASTNode = nil, allowFwd: bool = true): Name
proc prepareFunction*(self: Compiler, name: Name) = discard
proc resolve*(self: Compiler, 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.exports:
# 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: Compiler, 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: Compiler, 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 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.constraints, b.constraints)
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.value, b.value.value)
of Function:
# Functions are a bit trickier to compare
if a.arguments.len() != b.arguments.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.arguments, b.arguments):
# 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.constraints:
if self.compare(constraint.kind, b) and constraint.match:
return true
return false
else:
for constraint in a.constraints:
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.constraints:
if self.compare(constraint.kind, a) and constraint.match:
return true
return false
else:
for constraint in b.constraints:
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 compareUnions*(self: Compiler, 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 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