peon/src/frontend/compiler/compiler.nim

1047 lines
42 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/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 ["<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
# Peon's type inference and name resolution system is very flexible
# and can be reused across multiple compilation backends
proc resolve*(self: Compiler, name: string): Name =
## Traverses all existing namespaces in reverse order
## and returns the first object with the given name.
## Returns nil when the name can't be found
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
if self.currentModule.path in obj.exportedTo:
# The name is public in its owner
# module and said module has explicitly
# exported it to us: we can use it
result = obj
result.resolved = true
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
# We own this name, so we can definitely access it
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 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()
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