2022-12-15 11:48:36 +01: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.
2023-03-27 09:53:56 +02:00
2022-12-15 11:48:36 +01:00
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
2022-12-16 15:11:06 +01:00
PeonBackend * = enum
## An enumeration of the peon backends
2023-05-22 13:10:15 +02:00
Bytecode , NativeC
2023-01-17 12:53:23 +01:00
2022-12-15 16:49:27 +01:00
PragmaKind * = enum
## An enumeration of pragma types
Immediate ,
Delayed
2023-01-17 12:53:23 +01:00
2022-12-15 11:48:36 +01:00
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
2023-01-17 12:53:23 +01:00
2022-12-15 11:48:36 +01:00
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
2023-01-05 12:44:11 +01:00
2022-12-15 13:22:34 +01:00
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)
2023-05-22 12:57:38 +02:00
exportedTo * : HashSet [ string ]
2023-01-17 12:53:23 +01:00
# Has the compiler generated this name internally or
2022-12-15 13:22:34 +01:00
# does it come from user code?
isReal * : bool
# Is this name a builtin?
isBuiltin * : bool
2023-01-17 12:53:23 +01:00
## 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
2022-12-15 11:48:36 +01:00
WarningKind * {. pure . } = enum
## A warning enumeration type
UnreachableCode , UnusedName , ShadowOuterScope ,
MutateOuterScope
2022-12-15 13:22:34 +01:00
2022-12-15 11:48:36 +01:00
CompileMode * {. pure . } = enum
## A compilation mode enumeration
Debug , Release
CompileError * = ref object of PeonException
node * : ASTNode
function * : Declaration
2022-12-16 15:11:06 +01:00
compiler * : Compiler
2022-12-15 11:48:36 +01:00
2022-12-15 13:22:34 +01:00
Compiler * = ref object of RootObj
2022-12-15 11:48:36 +01:00
## 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
2023-01-05 12:44:11 +01:00
# Are we in REPL mode?
2022-12-15 11:48:36 +01:00
replMode * : bool
2022-12-15 13:22:34 +01:00
# List of all compile-time names
names * : seq [ Name ]
2022-12-15 11:48:36 +01:00
# 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
2023-01-05 12:44:11 +01:00
# mismatches when we dispatch with self.match
2022-12-15 11:48:36 +01:00
showMismatches * : bool
# Are we compiling in debug mode?
2022-12-15 13:22:34 +01:00
mode * : CompileMode
# The current function being compiled
currentFunction * : Name
# The current module being compiled
currentModule * : Name
# The module importing us, if any
parentModule * : Name
2022-12-15 16:49:27 +01:00
# Currently imported modules
2023-05-22 14:38:03 +02:00
modules * : TableRef [ string , Name ]
2022-12-15 13:22:34 +01:00
2023-01-05 12:44:11 +01:00
TypedNode * = ref object
## A wapper for AST nodes
## with attached type information
kind * : Type
node * : ASTNode
2022-12-15 13:22:34 +01:00
## 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 )
2022-12-16 15:11:06 +01:00
proc getSource * ( self : Compiler ) : string {. inline . } = self . source
2022-12-15 13:22:34 +01:00
2022-12-15 16:49:27 +01:00
## Some forward declarations (some of them arere actually stubs because nim forces forward declarations to be
2022-12-15 13:22:34 +01:00
## 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
2022-12-16 14:32:20 +01:00
method prepareFunction * ( self : Compiler , name : Name ) {. base . } = discard
method dispatchPragmas ( self : Compiler , name : Name ) {. base . } = discard
method dispatchDelayedPragmas ( self : Compiler , name : Name ) {. base . } = discard
2022-12-15 13:22:34 +01:00
## 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
2022-12-16 15:11:06 +01:00
raise CompileError ( msg : message , node : node , line : node . token . line , file : node . file , compiler : self )
2022-12-15 13:22:34 +01:00
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
2022-12-15 16:49:27 +01:00
# Peon's type inference and name resolution system is very flexible
# and can be reused across multiple compilation backends
2022-12-15 13:22:34 +01:00
proc resolve * ( self : Compiler , name : string ) : Name =
2023-05-01 15:30:36 +02:00
## 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
2022-12-15 13:22:34 +01:00
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
2023-05-22 14:38:03 +02:00
if self . currentModule . path in obj . exportedTo :
2022-12-15 13:22:34 +01:00
# The name is public in its owner
# module and said module has explicitly
# exported it to us: we can use it
result = obj
2023-05-01 15:30:36 +02:00
result . resolved = true
2022-12-15 13:22:34 +01:00
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
2023-05-01 15:30:36 +02:00
# We own this name, so we can definitely access it
2022-12-15 13:22:34 +01:00
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 )
2023-03-05 16:49:14 +01:00
elif name = = " all " :
return Type ( kind : All )
2022-12-15 13:22:34 +01:00
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
2023-01-22 17:58:32 +01:00
method infer * ( self : Compiler , node : Expression ) : Type =
2022-12-15 13:22:34 +01: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 :
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
2023-01-22 18:02:31 +01:00
## error if it fails
2022-12-15 13:22:34 +01:00
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 :
2023-05-22 12:57:38 +02:00
if obj . isPrivate or self . currentModule . path notin obj . exportedTo :
2022-12-15 13:22:34 +01:00
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 ) :
2023-05-22 12:57:38 +02:00
if obj . owner . isNil ( ) :
continue
if not obj . isPrivate and obj . owner . path = = module . path :
2022-12-15 13:22:34 +01:00
result . add ( obj )
else :
for obj in self . findInModule ( " " , module ) :
2023-05-22 12:57:38 +02:00
if obj . ident . token . lexeme = = name and self . currentModule . path in obj . exportedTo :
2022-12-15 13:22:34 +01:00
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 :
2023-01-24 12:22:26 +01:00
return false
2022-12-15 13:22:34 +01:00
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 :
2023-01-24 12:19:06 +01:00
# If we happen to find more than one match, we try again
# and ignore forward declarations and automatic functions
2022-12-15 13:22:34 +01:00
impl = filterIt ( impl , not it . valueType . forwarded and not it . valueType . isAuto )
if impl . len ( ) > 1 :
2023-01-24 12:19:06 +01:00
# If there's *still* more than one match, then it's an error
2022-12-15 13:22:34 +01:00
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 )
2023-01-24 12:19:06 +01:00
# This is only true when we're called by self.patchForwardDeclarations()
2022-12-15 13:22:34 +01:00
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 )
2022-12-16 14:32:20 +01:00
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 ,
2023-05-22 14:38:03 +02:00
isReal : true ,
2022-12-16 14:32:20 +01:00
) )
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 ,
2023-01-22 17:58:32 +01:00
belongsTo : self . currentFunction ,
valueType : Type ( kind : CustomType )
2022-12-16 14:32:20 +01:00
)
)
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}) " )
2023-01-24 12:19:06 +01:00
# We emit a bunch of warnings, mostly for QoL
2022-12-16 14:32:20 +01:00
for name in self . names :
if name = = n :
break
if name . ident . token . lexeme ! = declaredName :
continue
2023-05-22 12:57:38 +02:00
if name . owner ! = n . owner and ( name . isPrivate or n . owner . path notin name . exportedTo ) :
2022-12-16 14:32:20 +01:00
continue
if name . kind in [ NameKind . Var , NameKind . Module , NameKind . CustomType , NameKind . Enum ] :
if name . depth < n . depth :
2023-01-17 12:53:23 +01:00
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 )
2022-12-16 14:32:20 +01:00
return n