1057 lines
43 KiB
Nim
1057 lines
43 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.
|
|
|
|
# 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, NativeCpp
|
|
|
|
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[Name]
|
|
# 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*: HashSet[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 and returns
|
|
## the first object with the given name. Returns
|
|
## nil when the name can't be found. Note that
|
|
## when a type or function declaration is first
|
|
## resolved, it is also compiled on-the-fly
|
|
for obj in reversed(self.names):
|
|
if obj.ident.token.lexeme == name:
|
|
if obj.owner.path != self.currentModule.path:
|
|
# We don't own this name, but we
|
|
# may still have access to it
|
|
if obj.isPrivate:
|
|
# Name is private in its owner
|
|
# module, so we definitely can't
|
|
# use it
|
|
continue
|
|
elif self.currentModule in obj.exportedTo:
|
|
# The name is public in its owner
|
|
# module and said module has explicitly
|
|
# exported it to us: we can use it
|
|
result = obj
|
|
break
|
|
# If the name is public but not exported in
|
|
# its owner module, then we act as if it's
|
|
# private. This is to avoid namespace pollution
|
|
# from imports (i.e. if module A imports modules
|
|
# C and D and module B imports module A, then B
|
|
# might not want to also have access to C's and D's
|
|
# names as they might clash with its own stuff)
|
|
continue
|
|
result = obj
|
|
result.resolved = true
|
|
break
|
|
|
|
|
|
proc resolve*(self: 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 notin obj.exportedTo:
|
|
continue
|
|
result.add(obj)
|
|
|
|
|
|
method findInModule*(self: Compiler, name: string, module: Name): seq[Name] =
|
|
## Looks for objects that have been already declared as
|
|
## public within the given module with the given name.
|
|
## Returns all objects that apply. If the name is an
|
|
## empty string, returns all objects within the given
|
|
## module, regardless of whether they are exported to
|
|
## the current one or not
|
|
if name == "":
|
|
for obj in reversed(self.names):
|
|
if not obj.isPrivate and obj.owner == module:
|
|
result.add(obj)
|
|
else:
|
|
for obj in self.findInModule("", module):
|
|
if obj.ident.token.lexeme == name and self.currentModule in obj.exportedTo:
|
|
result.add(obj)
|
|
|
|
|
|
method findByType*(self: Compiler, name: string, kind: Type): seq[Name] =
|
|
## Looks for objects that have already been declared
|
|
## with the given name and type. Returns all objects
|
|
## that apply
|
|
for obj in self.findByName(name):
|
|
if self.compare(obj.valueType, kind):
|
|
result.add(obj)
|
|
|
|
|
|
method findAtDepth*(self: Compiler, name: string, depth: int): seq[Name] {.used.} =
|
|
## Looks for objects that have been already declared
|
|
## with the given name at the given scope depth.
|
|
## Returns all objects that apply
|
|
for obj in self.findByName(name):
|
|
if obj.depth == depth:
|
|
result.add(obj)
|
|
|
|
|
|
proc check*(self: Compiler, term: Expression, kind: Type) {.inline.} =
|
|
## Checks the type of term against a known type.
|
|
## Raises an error if appropriate and returns
|
|
## otherwise
|
|
let k = self.inferOrError(term)
|
|
if not self.compare(k, kind):
|
|
self.error(&"expecting value of type {self.stringify(kind)}, got {self.stringify(k)}", term)
|
|
elif k.kind == Any and kind.kind != Any:
|
|
self.error(&"any is not a valid type in this context")
|
|
|
|
|
|
proc isAny*(typ: Type): bool =
|
|
## Returns true if the given type is
|
|
## of (or contains) the any type
|
|
case typ.kind:
|
|
of Any:
|
|
return true
|
|
of Generic:
|
|
for condition in typ.cond:
|
|
if condition.kind.isAny():
|
|
return true
|
|
of Union:
|
|
for condition in typ.types:
|
|
if condition.kind.isAny():
|
|
return true
|
|
else:
|
|
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 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
|