2023-03-27 09:53:56 +02:00
# 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)
2023-05-09 11:00:35 +02:00
value * : TypedNode # The type the reference points to
2023-03-27 09:53:56 +02:00
of Pointer :
# An unmanaged reference. Much
# like a raw pointer in C
2023-05-09 11:00:35 +02:00
data * : TypedNode # The type we point to
2023-03-27 09:53:56 +02:00
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
2023-05-09 11:00:35 +02:00
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
2023-03-27 09:53:56 +02:00
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
2023-05-09 11:00:35 +02:00
return self . compare ( a . value . value , b . value . value )
2023-03-27 09:53:56 +02:00
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 )
2023-05-09 11:00:35 +02:00
proc infer * ( self : Compiler , node : LiteralExpr ) : TypedNode =
2023-03-27 09:53:56 +02:00
## 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 :
2023-05-09 11:00:35 +02:00
return TypedNode ( node : node , value : Type ( kind : Int64 ) )
2023-03-27 09:53:56 +02:00
let typ = size [ 1 ] . toIntrinsic ( )
if not self . compare ( typ , nil ) :
2023-05-09 11:00:35 +02:00
return TypedNode ( node : node , value : typ )
2023-03-27 09:53:56 +02:00
else :
self . error ( & " invalid type specifier ' {size[1]} ' for int " , node )
of floatExpr :
let size = node . token . lexeme . split ( " ' " )
if size . len ( ) = = 1 :
2023-05-09 11:00:35 +02:00
return TypedNode ( node : node , value : Type ( kind : Float64 ) )
2023-03-27 09:53:56 +02:00
let typ = size [ 1 ] . toIntrinsic ( )
if not typ . isNil ( ) :
2023-05-09 11:00:35 +02:00
return TypedNode ( node : node , value : typ )
2023-03-27 09:53:56 +02:00
else :
self . error ( & " invalid type specifier ' {size[1]} ' for float " , node )
of trueExpr :
2023-05-09 11:00:35 +02:00
return TypedNode ( node : node , value : Type ( kind : Bool ) )
2023-03-27 09:53:56 +02:00
of falseExpr :
2023-05-09 11:00:35 +02:00
return TypedNode ( node : node , value : Type ( kind : Bool ) )
2023-03-27 09:53:56 +02:00
of strExpr :
2023-05-09 11:00:35 +02:00
return TypedNode ( node : node , value : Type ( kind : String ) )
2023-03-27 09:53:56 +02:00
else :
discard # Unreachable
2023-05-09 11:00:35 +02:00
proc infer * ( self : Compiler , node : Expression ) : TypedNode =
2023-03-27 09:53:56 +02:00
## 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 :
2023-05-09 11:00:35 +02:00
result = TypedNode ( node : node , value : Type ( kind : Reference , value : self . infer ( Ref ( node ) . value ) ) )
2023-03-27 09:53:56 +02:00
of NodeKind . ptrExpr :
2023-05-09 11:00:35 +02:00
result = TypedNode ( node : node , value : Type ( kind : Pointer , data : self . infer ( Ptr ( node ) . value ) ) )
2023-03-27 09:53:56 +02:00
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
2023-05-09 11:00:35 +02:00
proc inferOrError * ( self : Compiler , node : Expression ) : TypedNode =
2023-03-27 09:53:56 +02:00
## 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 "
2023-05-09 11:00:35 +02:00
case typ . kind :
2023-03-27 09:53:56 +02:00
of Int8 , UInt8 , Int16 , UInt16 , Int32 ,
UInt32 , Int64 , UInt64 , Float32 , Float64 ,
Char , Byte , String , Nil , TypeKind . Nan , Bool ,
TypeKind . Inf , Auto :
2023-05-09 11:00:35 +02:00
result & = ( $ typ . kind ) . toLowerAscii ( )
2023-03-27 09:53:56 +02:00
of Pointer :
2023-05-09 11:00:35 +02:00
result & = & " ptr {self.stringify(typ)} "
2023-03-27 09:53:56 +02:00
of Reference :
2023-05-09 11:00:35 +02:00
result & = & " ref {self.stringify(typ)} "
2023-03-27 09:53:56 +02:00
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 )
2023-05-09 11:00:35 +02:00
if not self . compare ( k . value , kind ) :
2023-03-27 09:53:56 +02:00
self . error ( & " expecting value of type {self.stringify(kind)}, got {self.stringify(k)} " , term )
2023-05-09 11:00:35 +02:00
elif k . value . kind = = Any and kind . kind ! = Any :
2023-03-27 09:53:56 +02:00
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 :
2023-05-09 11:00:35 +02:00
list . add ( ( accept , self . inferOrError ( condition ) . value ) )
2023-03-27 09:53:56 +02:00
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 :
2023-05-09 11:00:35 +02:00
list . add ( ( accept , self . inferOrError ( condition ) . value ) )
2023-03-27 09:53:56 +02:00
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 :
2023-05-09 11:00:35 +02:00
typ = self . infer ( argument . valueType ) . value
2023-03-27 09:53:56 +02:00
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
2023-05-09 11:00:35 +02:00
typ = self . infer ( node . returnType ) . value
2023-03-27 09:53:56 +02:00
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 :
2023-05-09 11:00:35 +02:00
n . obj . value = self . inferOrError ( node . value ) . value
2023-03-27 09:53:56 +02:00
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