peon-rewrite/src/frontend/compiler/compiler.nim

1942 lines
80 KiB
Nim

import errors
import frontend/parsing/parser
import frontend/parsing/lexer
import std/os
import std/sets
import std/tables
import std/terminal
import std/strutils
import std/sequtils
import std/algorithm
import std/strformat
import std/parseutils
export ast, errors
type
IntegerSize* = enum
## Integer size enumeration
Tiny = 8
Short = 16
Long = 32
LongLong = 64
FloatSize* = enum
## Float size enumeration
Half = 32
Full = 64
TypeKind* = enum
## Enumeration of compile-time types
Integer,
Float,
String,
NaN,
Infinity,
Boolean,
Any,
Typevar,
Auto,
Byte,
Char,
Nil,
CustomType,
Enum,
Reference,
Pointer,
Generic,
Union,
Function,
Lent
Type* = ref object
## A compile-time type
isBuiltin*: bool
case kind*: TypeKind
of Integer:
signed*: bool
size*: IntegerSize
of Float:
width*: FloatSize
of Infinity:
positive*: bool
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
forwarded*: bool
compiled*: bool
genericArgs*: TableRef[string, Type]
of Typevar:
wrapped*: Type
of CustomType:
typeName*: string
generics*: TableRef[string, Type]
fields*: TableRef[string, Type]
parent*: Type
interfaces*: seq[Type]
of Enum:
members*: seq[Name]
of Reference, Pointer, Lent:
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, value: Expression]]
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, value: Expression]]
else:
discard
WarningKind* {.pure.} = enum
## A warning enumeration type
UnreachableCode, UnusedName, ShadowOuterScope,
MutateOuterScope
CompileError* = ref object of PeonException
## A compilation error with location information
node*: ASTNode
function*: Declaration
compiler*: PeonCompiler
NameKind* {.pure.} = enum
## A name enumeration type
None, Module, Argument, Var, Function, CustomType, Enum
Name* = ref object
## A generic name object
# Type of the identifier (NOT of the value!)
case kind*: NameKind
of Module:
path*: string
# Full absolute path of the module,
# including the extension
absPath*: string
# Just for easier lookup, it's all
# pointers anyway
names*: TableRef[string, Name]
of NameKind.Var:
# If the variable's value is another
# name, this attribute contains its
# name object. This is useful for things
# like assigning functions to variables and
# then calling the variable like it's the
# original function
assignedName*: Name
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*: seq[Name]
# Is this name generated by user code or internally by the compiler?
isReal: bool
## Our typed AST representation
TypedNode* = ref object of RootObj
## A generic typed AST node
node*: ASTNode
TypedExpr* = ref object of TypedNode
## A generic typed expression
kind*: Type
TypedUnaryExpr* = ref object of TypedExpr
## A generic typed unary expression
a*: TypedExpr
TypedBinaryExpr* = ref object of TypedUnaryExpr
## A generic typed binary expression
b*: TypedExpr
TypedIdentExpr* = ref object of TypedExpr
## A typed identifier expression
name*: Name
TypedDecl* = ref object of TypedNode
## A typed declaration node
name*: Name # The declaration's name object
TypedTypeDecl* = ref object of TypedDecl
## A typed type declaration node
fields*: TableRef[string, TypedExpr]
parent*: Name
interfaces*: seq[TypedTypeDecl]
TypedEnumDecl* = ref object of TypedTypeDecl
## A typed enum declaration node
enumeration*: Type
variants: seq[TypedTypeDecl]
TypedFunDecl* = ref object of TypedDecl
## A typed function declaration
args*: seq[tuple[name: Name, default: TypedExpr]]
body*: TypedBlockStmt
TypedStmt* = ref object of TypedNode
## A typed statement node
TypedBlockStmt* = ref object of TypedStmt
## A typed block statement
body*: seq[TypedNode]
TypedIfStmt* = ref object of TypedStmt
## A typed if statement node
thenBranch*: TypedBlockStmt
elseBranch*: TypedBlockStmt
condition*: TypedExpr
TypedWhileStmt* = ref object of TypedStmt
## A typed while statement node
body*: TypedBlockStmt
condition*: TypedExpr
CompilerFunc* = object
## An internal compiler function called
## by pragmas
kind: PragmaKind
handler: proc (self: PeonCompiler, pragma: Pragma, name: Name)
PragmaKind* = enum
## An enumeration of pragma types
Immediate,
Delayed
PeonCompiler* = ref object
## The Peon compiler
current: int # The current node we're looking at
tree: seq[Declaration] # The AST for the current module
scopeDepth*: int # The current scope depth (0 == global, > 0 == local)
# These objects are needed to parse other
# modules
tokenizer: Lexer
parser: Parser
source: string # The module's raw source code
file: string # The module's filename
isMainModule: bool # Are we the main module?
currentFunction: Name # The current function we're compiling
currentModule: Name # The current module we're compiling
disabledWarnings: seq[WarningKind] # List of disabled compiler warnings
names: seq[TableRef[string, seq[Name]]] # Maps scope depths to namespaces
# Compiler procedures called by pragmas
compilerProcs: TableRef[string, CompilerFunc]
# Show full info about type mismatches when dispatching
# function calls fails (we hide this under a boolean flag
# because the output is quite verbose)
showMismatches: bool
## Forward declarations
proc compile(self: PeonCompiler, node: ASTNode): TypedNode
proc toIntrinsic(name: string): Type
proc done(self: PeonCompiler): bool {.inline.}
proc wrap(self: Type): Type {.inline.}
proc unwrap(self: Type): Type {.inline.}
proc toRef(self: Type): Type {.inline.}
proc toPtr(self: Type): Type {.inline.}
proc toLent(self: Type): Type {.inline.}
proc handleErrorPragma(self: PeonCompiler, pragma: Pragma, name: Name)
proc handlePurePragma(self: PeonCompiler, pragma: Pragma, name: Name)
proc handleMagicPragma(self: PeonCompiler, pragma: Pragma, name: Name)
proc warning(self: PeonCompiler, kind: WarningKind, message: string, name: Name = nil, node: ASTNode = nil)
proc stringify*(self: PeonCompiler, typ: Type): string
proc stringify*(self: PeonCompiler, typ: TypedNode): string
proc inferOrError*(self: PeonCompiler, node: Expression): TypedExpr
proc compare(self: PeonCompiler, a, b: Type): bool
proc expression(self: PeonCompiler, node: Expression): TypedExpr
proc specialize(self: PeonCompiler, typ: Type, args: seq[Expression]): Type {.discardable.}
proc funDecl(self: PeonCompiler, node: FunDecl, name: Name = nil): TypedDecl
proc getTypeDistance(self: PeonCompiler, a, b: Type): int
## Public getters for nicer error formatting
proc getCurrentNode*(self: PeonCompiler): ASTNode = (if self.done(): self.tree[^1] else: self.tree[self.current - 1])
proc getCurrentFunction*(self: PeonCompiler): Declaration {.inline.} = (if self.currentFunction.isNil(): nil else: self.currentFunction.node)
proc getSource*(self: PeonCompiler): string {.inline.} = self.source
proc newTypedNode(node: ASTNode): TypedNode =
## Initializes a new typed node
new(result)
result.node = node
proc newTypedExpr(node: Expression, kind: Type): TypedExpr =
## Initializes a new typed expression
result = TypedExpr(node: node, kind: kind)
proc newTypedDecl(node: Declaration, name: Name): TypedDecl =
## Initializes a new typed declaration
result = TypedDecl(node: node, name: name)
proc newTypedTypeDecl(node: TypeDecl, name: Name, fields: TableRef[string, TypedExpr], parent: Name): TypedTypeDecl =
## Initializes a new typed function declaration
result = TypedTypeDecl(node: node, name: name, fields: fields, parent: parent)
proc newTypedEnumDecl(node: TypeDecl, name: Name, variants: seq[TypedTypeDecl], enumeration: Type): TypedEnumDecl =
## Initializes a new typed function declaration
result = TypedEnumDecl(node: node, name: name, variants: variants, enumeration: enumeration)
proc newTypedFunDecl(node: FunDecl, name: Name, body: TypedBlockStmt): TypedFunDecl =
## Initializes a new typed function declaration
result = TypedFunDecl(node: node, name: name, body: body)
proc newTypedIdentExpr(node: IdentExpr, name: Name): TypedIdentExpr =
## Initializes a new typed identifier expression
result = TypedIdentExpr(node: node, name: name, kind: name.valueType)
proc newTypedUnaryExpr(node: UnaryExpr, kind: Type, a: TypedExpr): TypedUnaryExpr =
## Initializes a new typed unary expression
result = TypedUnaryExpr(node: node, a: a, kind: kind)
proc newTypedBinaryExpr(node: UnaryExpr, kind: Type, a, b: TypedExpr): TypedBinaryExpr =
## Initializes a new typed binary expression
result = TypedBinaryExpr(node: node, a: a, b: b, kind: kind)
proc newTypedBlockStmt(node: BlockStmt, body: seq[TypedNode]): TypedBlockStmt =
## Initializes a new typed block statement
result = TypedBlockStmt(node: node, body: body)
proc newTypedWhileStmt(node: WhileStmt, body: TypedBlockStmt, condition: TypedExpr): TypedWhileStmt =
## Initializes a new typed while statement
result = TypedWhileStmt(node: node, body: body, condition: condition)
proc newTypedIfStmt(node: IfStmt, thenBranch, elseBranch: TypedBlockStmt, condition: TypedExpr): TypedIfStmt =
## Initializes a new typed block statement
result = TypedIfStmt(node: node, thenBranch: thenBranch,
elseBranch: elseBranch, condition: condition)
proc getName(self: TypedNode): Name =
## Gets the name object associated with the
## given typed node, if it has any
case self.node.kind:
of identExpr:
result = TypedIdentExpr(self).name
of NodeKind.funDecl, varDecl, typeDecl:
result = TypedDecl(self).name
else:
result = nil # TODO
proc newPeonCompiler*: PeonCompiler =
## Initializes a new compiler instance
new(result)
result.current = 0
result.tree = @[]
result.scopeDepth = 0
result.tokenizer = newLexer()
result.parser = newParser()
result.source = ""
result.file = ""
result.isMainModule = false
result.currentFunction = nil
result.disabledWarnings = @[]
result.names = @[]
result.compilerProcs = newTable[string, CompilerFunc]()
result.compilerProcs["magic"] = CompilerFunc(kind: Immediate, handler: handleMagicPragma)
result.compilerProcs["pure"] = CompilerFunc(kind: Immediate, handler: handlePurePragma)
result.compilerProcs["error"] = CompilerFunc(kind: Delayed, handler: handleErrorPragma)
result.showMismatches = false
proc done(self: PeonCompiler): bool {.inline.} = self.current == self.tree.len()
proc `$`(self: Name): string = $(self[])
proc `$`(self: Type): string = $(self[])
proc peek(self: PeonCompiler): ASTNode {.inline.} =
if self.tree.len() == 0:
return nil
if self.done():
return self.tree[^1]
return self.tree[self.current]
proc step(self: PeonCompiler): ASTNode {.inline.} =
if self.tree.len() == 0:
return nil
if self.done():
return self.tree[^1]
result = self.peek()
inc(self.current)
proc error(self: PeonCompiler, 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 handleMagicPragma(self: PeonCompiler, pragma: Pragma, name: Name) =
## Handles the "magic" pragma. Assumes the given name is already
## declared
if pragma.args.len() != 1:
self.error(&"'magic' pragma: wrong number of arguments (expected 1, got {len(pragma.args)})")
elif pragma.args[0].kind != strExpr:
self.error(&"'magic' pragma: wrong argument type (constant string expected, got {self.stringify(self.inferOrError(pragma.args[0]))})")
elif name.node.kind == NodeKind.funDecl:
name.valueType.isBuiltin = true
name.valueType.builtinOp = pragma.args[0].token.lexeme[1..^2]
name.valueType.compiled = true
elif name.node.kind == NodeKind.typeDecl:
name.valueType = pragma.args[0].token.lexeme[1..^2].toIntrinsic()
if name.valueType.isNil():
self.error("'magic' pragma: wrong argument value", pragma.args[0])
if TypeDecl(name.node).isRef:
name.valueType = name.valueType.toRef()
name.valueType = name.valueType.wrap()
name.valueType.isBuiltin = true
else:
self.error("'magic' pragma is not valid in this context")
proc handleErrorPragma(self: PeonCompiler, pragma: Pragma, name: Name) =
## Handles the "error" pragma
if pragma.args.len() != 1:
self.error("'error' pragma: wrong number of arguments")
elif pragma.args[0].kind != strExpr:
self.error("'error' pragma: wrong type of argument (constant string expected)")
elif not name.isNil() and name.node.kind != NodeKind.funDecl:
self.error("'error' pragma is not valid in this context")
self.error(pragma.args[0].token.lexeme[1..^2])
proc handlePurePragma(self: PeonCompiler, pragma: Pragma, name: Name) =
## Handles the "pure" pragma
case name.node.kind:
of NodeKind.funDecl:
FunDecl(name.node).isPure = true
of NodeKind.lambdaExpr:
LambdaExpr(name.node).isPure = true
else:
self.error("'pure' pragma is not valid in this context")
proc warning(self: PeonCompiler, kind: WarningKind, message: string, name: Name = nil, node: ASTNode = nil) =
## Raises a warning
if 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 = self.source.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 IndexDefect:
# Something probably went wrong (wrong line metadata): bad idea to crash!
stderr.styledwriteLine(resetStyle, fgRed, "Failed to retrieve line information")
proc wrap(self: Type): Type {.inline.} =
## Wraps a type in a typevar if it's not already
## wrapped
if self.kind != Typevar:
return Type(kind: Typevar, wrapped: self)
return self
proc unwrap(self: Type): Type {.inline.} =
## Unwraps a typevar if it's not already
## unwrapped
if self.kind == Typevar:
return self.wrapped
return self
proc toRef(self: Type): Type {.inline.} =
## Wraps a type in a ref
return Type(kind: Reference, value: self, isBuiltin: self.isBuiltin)
proc toPtr(self: Type): Type {.inline} =
## Wraps a type in a ptr
return Type(kind: Pointer, value: self, isBuiltin: self.isBuiltin)
proc toLent(self: Type): Type {.inline.} =
## Turns a type into a lent type
result = "lent".toIntrinsic()
result.value = self
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, isBuiltin: true)
elif name == "auto":
return Type(kind: Auto, isBuiltin: true)
elif name in ["int", "int64", "i64"]:
return Type(kind: Integer, size: LongLong, signed: true, isBuiltin: true)
elif name in ["uint64", "u64", "uint"]:
return Type(kind: Integer, size: LongLong, signed: false, isBuiltin: true)
elif name in ["int32", "i32"]:
return Type(kind: Integer, size: Long, signed: true, isBuiltin: true)
elif name in ["uint32", "u32"]:
return Type(kind: Integer, size: Long, signed: false, isBuiltin: true)
elif name in ["int16", "i16", "short"]:
return Type(kind: Integer, size: Short, signed: true, isBuiltin: true)
elif name in ["uint16", "u16"]:
return Type(kind: Integer, size: Short, signed: false, isBuiltin: true)
elif name in ["int8", "i8"]:
return Type(kind: Integer, size: Tiny, signed: true, isBuiltin: true)
elif name in ["uint8", "u8"]:
return Type(kind: Integer, size: Tiny, signed: false, isBuiltin: true)
elif name in ["f64", "float", "float64"]:
return Type(kind: Float, width: Full, isBuiltin: true)
elif name in ["f32", "float32"]:
return Type(kind: Float, width: Half, isBuiltin: true)
elif name in ["byte", "b"]:
return Type(kind: Byte, isBuiltin: true)
elif name in ["char", "c"]:
return Type(kind: Char, isBuiltin: true)
elif name == "nan":
return Type(kind: TypeKind.Nan, isBuiltin: true)
elif name == "nil":
return Type(kind: Nil, isBuiltin: true)
elif name == "inf":
return Type(kind: Infinity, isBuiltin: true)
elif name == "bool":
return Type(kind: Boolean, isBuiltin: true)
elif name == "typevar":
return Type(kind: Typevar, isBuiltin: true, wrapped: "any".toIntrinsic())
elif name == "string":
return Type(kind: String, isBuiltin: true)
elif name == "lent":
return Type(kind: Lent, isBuiltin: true, value: "any".toIntrinsic())
proc infer(self: PeonCompiler, node: LiteralExpr): TypedExpr =
case node.kind:
of trueExpr, falseExpr:
return newTypedExpr(node, "bool".toIntrinsic())
of strExpr:
return newTypedExpr(node, "string".toIntrinsic())
of intExpr, binExpr, octExpr, hexExpr:
let size = node.token.lexeme.split("'")
if size.len() == 1:
return newTypedExpr(node, "int64".toIntrinsic())
result = newTypedExpr(node, size[1].toIntrinsic())
if result.isNil():
self.error(&"invalid type specifier '{size[1]}' for int", node)
of floatExpr:
let size = node.token.lexeme.split("'")
if size.len() == 1:
return newTypedExpr(node, "float".toIntrinsic())
result = newTypedExpr(node, size[1].toIntrinsic())
if result.isNil():
self.error(&"invalid type specifier '{size[1]}' for float", node)
else:
discard
proc infer(self: PeonCompiler, node: Expression): TypedExpr =
if node.isConst():
return self.infer(LiteralExpr(node))
result = self.expression(node)
proc compareUnions*(self: PeonCompiler, a, b: seq[tuple[match: bool, kind: Type, value: Expression]]): bool =
## Compares type unions between each other
var
long = a
short = b
i = 0
if b.len() > a.len():
long = b
short = a
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 matchUnion(self: PeonCompiler, a: Type, b: seq[tuple[match: bool, kind: Type, value: Expression]]): bool =
## Returns whether a non-union type a matches
## the given untagged union b
assert a.kind != Union
for constraint in b:
if self.compare(constraint.kind, a) and constraint.match:
return true
return false
proc matchGeneric(self: PeonCompiler, a: Type, b: seq[tuple[match: bool, kind: Type, value: Expression]]): bool =
## Returns whether a concrete type matches the
## given generic type b
assert a.kind != Generic
for constraint in b:
if not self.compare(constraint.kind, a) or not constraint.match:
return false
return true
proc isAny(typ: Type): bool =
## Returns true if the given type is
## of (or contains) the any type. Not
## applicable to typevars
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
proc compare(self: PeonCompiler, a, b: Type): bool =
if a.isAny() or b.isAny():
return true
if a.kind == b.kind:
case a.kind:
# TODO: Take interfaces into account
of CustomType:
# First, compare their names (only if both types have names)
if a.typeName != "" and a.typeName != b.typeName:
return false
# Then, compare the number of fields
if a.fields.len() != b.fields.len():
return false
# Then the field names
var fields: HashSet[string] = initHashSet[string]()
for fieldName in a.fields.keys():
fields.incl(fieldName)
for fieldName in b.fields.keys():
if not fields.contains(fieldName):
return false
# From here on out, we know that both a and b have
# the same number of fields and that they share field
# names. All we gotta now do is compare the types of the
# fields
for fieldName in a.fields.keys():
if not self.compare(a.fields[fieldName], b.fields[fieldName]):
return false
# All checks passed, types are equal as
# far as the type system is concerned
return true
of Boolean, Infinity, Integer, Float, Any,
Auto, Char, Byte, String, Nil:
return true
of Lent, Reference, Pointer:
return self.compare(a.value, b.value)
of Enum:
if a.members.len() != b.members.len():
return false
for (memberA, memberB) in zip(a.members, b.members):
if not self.compare(memberA.valueType, memberB.valueType):
return false
return true
of Union:
return self.compareUnions(a.types, b.types)
of Typevar:
return self.compare(a.wrapped, b.wrapped)
of Function:
# Are arities the same?
if a.args.len() != b.args.len():
return false
# Is it a coroutine/generator?
if a.isCoroutine != b.isCoroutine or a.isGenerator != b.isGenerator:
return false
# Do they have compatible return types?
if not self.compare(b.returnType, a.returnType):
return false
# Do they have compatible arguments?
var i = 0
for (argA, argB) in zip(a.args, b.args):
if not self.compare(argA.kind, argB.kind):
return false
if argA.name == "":
continue
if argB.name == "":
continue
if argA.name != argB.name:
return false
return true
else:
# TODO
return false
if a.kind == Union:
return self.matchUnion(b, a.types)
if b.kind == Union:
return self.matchUnion(a, b.types)
if a.kind == Generic:
if a.asUnion:
return self.matchUnion(b, a.cond)
else:
return self.matchGeneric(b, a.cond)
if b.kind == Generic:
if b.asUnion:
return self.matchUnion(a, b.cond)
else:
return self.matchGeneric(a, b.cond)
return false
proc literal(self: PeonCompiler, node: LiteralExpr): TypedExpr =
case node.kind:
of trueExpr, falseExpr:
result = self.infer(node)
of strExpr:
result = self.infer(node)
of intExpr:
let y = IntExpr(node)
result = self.infer(y)
if result.kind.kind == Integer:
var x: int
try:
discard parseInt(y.literal.lexeme, x)
except ValueError:
self.error("integer value out of range")
else:
var x: uint64
try:
discard parseBiggestUInt(y.literal.lexeme, x)
except ValueError:
self.error("integer value out of range")
of hexExpr:
var x: int
var y = HexExpr(node)
result = self.infer(y)
try:
discard parseHex(y.literal.lexeme, x)
except ValueError:
self.error("integer value out of range")
let node = newIntExpr(Token(lexeme: $x, line: y.token.line,
pos: (start: y.token.pos.start,
stop: y.token.pos.start + len($x)),
relPos: (start: y.token.relPos.start, stop: y.token.relPos.start + len($x))
)
)
of binExpr:
var x: int
var y = BinExpr(node)
result = self.infer(y)
try:
discard parseBin(y.literal.lexeme, x)
except ValueError:
self.error("integer value out of range")
let node = newIntExpr(Token(lexeme: $x, line: y.token.line,
pos: (start: y.token.pos.start,
stop: y.token.pos.start + len($x)),
relPos: (start: y.token.relPos.start, stop: y.token.relPos.start + len($x))
)
)
of octExpr:
var x: int
var y = OctExpr(node)
result = self.infer(y)
try:
discard parseOct(y.literal.lexeme, x)
except ValueError:
self.error("integer value out of range")
let node = newIntExpr(Token(lexeme: $x, line: y.token.line,
pos: (start: y.token.pos.start,
stop: y.token.pos.start + len($x)),
relPos: (start: y.token.relPos.start, stop: y.token.relPos.start + len($x))
)
)
of floatExpr:
var x: float
var y = FloatExpr(node)
result = self.infer(y)
try:
discard parseFloat(y.literal.lexeme, x)
except ValueError:
self.error("floating point value out of range")
else:
self.error(&"invalid AST node of kind {node.kind} at literal(): {node} (This is an internal error and most likely a bug!)")
proc find(self: PeonCompiler, name: string, kind: Type = "any".toIntrinsic()): Name =
## Looks up a name in all scopes starting from the current
## one. Optionally matches it to the given type
## A note about how namespaces are implemented. At each scope depth lies
## a dictionary which maps strings to a list of names. Why a list of names
## rather than a single name? Well, that's to allow cool things like overloading
## existing functions with different type signatures. We still disallow most cases
## of re-declaration, though (for example, shadowing a function with a variable in the
## same scope and module) because it would just be confusing
var depth = self.scopeDepth
while depth >= 0:
if self.names[depth].hasKey(name):
for obj in self.names[depth][name]:
if obj.owner.absPath != self.currentModule.absPath:
# We don't own this name, but we
# may still have access to it
if obj.isPrivate or self.currentModule notin obj.exportedTo:
# The name is either private in its owner
# module, so we definitely can't use it, or
# said module has not explicitly exported it
# to us. 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
# If we got here, we can access the name
if self.compare(obj.valueType, kind):
result = obj
result.resolved = true
break
if not result.isNil():
break
dec(depth)
proc findOrError(self: PeonCompiler, name: string, kind: Type = "any".toIntrinsic()): Name =
## Like find(), but raises an error if the name is not found
result = self.find(name, kind)
if result.isNil():
self.error(&"reference to undefined name '{name}'")
proc findAll(self: PeonCompiler, name: string, kind: Type = "any".toIntrinsic()): seq[Name] =
## Like find(), but doesn't stop at the first match. Returns
## a list of matches
var depth = self.scopeDepth
while depth >= 0:
if self.names[depth].hasKey(name):
for obj in self.names[depth][name]:
if obj.owner.absPath != self.currentModule.absPath:
# We don't own this name, but we
# may still have access to it
if obj.isPrivate or self.currentModule notin obj.exportedTo:
# The name is either private in its owner
# module, so we definitely can't use it, or
# said module has not explicitly exported it
# to us. 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
# If we got here, we can access the name
if self.compare(obj.valueType, kind):
result.add(obj)
dec(depth)
proc inferOrError(self: PeonCompiler, node: Expression): TypedExpr =
## 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: PeonCompiler, typ: Type): string =
## Returns the string representation of a
## type object
case typ.kind:
of Char, Byte, String, Nil, TypeKind.Nan,
Auto, Any:
result &= ($typ.kind).toLowerAscii()
of CustomType:
result &= typ.typeName
if typ.generics.len() > 0:
result &= "["
var i = 0
for gen in typ.generics.keys():
result &= &"{gen}: {self.stringify(typ.generics[gen])}"
if i < typ.generics.len() - 1:
result &= ", "
inc(i)
result &= "]"
of Boolean:
result = "bool"
of Infinity:
result = "inf"
of Integer:
if not typ.signed:
result &= "u"
result &= &"int{int(typ.size)}"
of Float:
result &= "float"
case typ.width:
of Half:
result &= "32"
of Full:
result &= "64"
of Typevar:
result = &"typevar[{self.stringify(typ.wrapped)}]"
of Pointer:
result &= &"ptr {self.stringify(typ.value)}"
of Reference:
result &= &"ref {self.stringify(typ.value)}"
of Function:
result &= "fn "
if typ.fun.generics.len() > 0:
result &= "["
for i, gen in typ.fun.generics:
result &= &"{gen.name.token.lexeme}: {self.stringify(typ.genericArgs[gen.name.token.lexeme])}"
if i < typ.fun.generics.len() - 1:
result &= ", "
result &= "]"
result &= "("
for i, (argName, argType, argDefault) in typ.args:
result &= &"{argName}: "
if argType.kind == Generic:
result &= argType.name
else:
result &= self.stringify(argType)
if not argDefault.isNil():
result &= &" = {argDefault}"
if i < typ.args.len() - 1:
result &= ", "
result &= ")"
if not self.compare(typ.returnType, "nil".toIntrinsic()):
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 Lent:
result &= &"lent {self.stringify(typ.value)}"
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 # TODO(?)
proc stringify*(self: PeonCompiler, typ: TypedNode): string =
if typ.node.isConst():
return self.stringify(TypedExpr(typ).kind)
case typ.node.kind:
of NodeKind.funDecl, varDecl, typeDecl:
result = self.stringify(TypedDecl(typ).name.valueType)
of binaryExpr, unaryExpr, identExpr, callExpr:
result = self.stringify(TypedExpr(typ).kind)
else:
discard # TODO
proc beginScope(self: PeonCompiler) =
## Begins a new lexical scope
inc(self.scopeDepth)
self.names.add(newTable[string, seq[Name]]())
proc endScope(self: PeonCompiler) =
## Closes the current lexical
## scope and reverts to the one
## above it
for names in self.names[self.scopeDepth].values():
for name in names:
if not name.resolved:
# We emit warnings for names that are declared but never used
case name.kind:
of NameKind.Var:
# We ignore names that start with an underscore (simply by convention)
# and of course we ignore public names because we can't know if they have
# been exported into or imported from other modules and not used yet
if not name.ident.token.lexeme.startsWith("_") and name.isPrivate:
self.warning(UnusedName, &"'{name.ident.token.lexeme}' is declared but not used (add '_' prefix to silence warning)", name)
of NameKind.Argument:
if not name.ident.token.lexeme.startsWith("_") and name.isPrivate:
if not name.belongsTo.isNil() and not name.belongsTo.valueType.isBuiltin and name.belongsTo.isReal and name.belongsTo.resolved:
# Builtin functions never use their arguments. We also don't emit this
# warning if the function was generated internally by the compiler (for
# example as a result of generic/auto type instantiation) because such
# objects do not exist in the user's code and are likely duplicated anyway
self.warning(UnusedName, &"argument '{name.ident.token.lexeme}' of '{name.belongsTo.ident.token.lexeme}' is unused (add '_' prefix to silence warning)", name)
else:
discard
discard self.names.pop()
dec(self.scopeDepth)
assert self.scopeDepth == self.names.high()
proc check(self: PeonCompiler, term: Expression, expected: Type): TypedExpr {.inline, discardable.} =
## Checks the type of the given expression against a known one.
## Raises an error if appropriate and returns the typed expression
## otherwise
result = self.inferOrError(term)
if not self.compare(result.kind, expected):
self.error(&"expecting value of type {self.stringify(expected)}, got {self.stringify(result)} instead", term)
if not result.kind.isAny() and expected.isAny():
self.error("any is not a valid type in this context", term)
proc getTypeDistance(self: PeonCompiler, a, b: Type): int =
## Gets the type distance of two Peon types. Assumes a and b are already
## compatible (i.e. compare(a, b) returns true). The type distance from a
## to b is defined as their relative distance in their inheritance tree: So
## for example, from type A to type A it would be 0, from type B (child of A)
## to type A it would be 1, from type C of B to B it would also be 1 (and it
## would be 2 from C to A), and so on and so forth
assert self.compare(a, b)
if a.kind != CustomType or b.kind != CustomType:
# TODO: Test
return 0
var parent = b.parent
result = 0
# We already know that the inheritance
# chain is correct (i.e. that a is at the
# root of it) because we assume a and b are
# already compatible, so we really just need
# to walk the tree backwards and keep track of
# how many times we go up one node. We add some
# debugging checks for safety tho
while not parent.isNil():
when defined(debug):
if parent.parent.isNil():
assert self.parent.parent == a
inc(result)
parent = parent.parent
proc match(self: PeonCompiler, name: string, kind: Type, node: ASTNode = nil, allowFwd: bool = true, error: bool = true): Name =
## Tries to find the best matching function implementation
## compatible with the given type and returns its associated
## name object
var
impl: seq[Name] = self.findAll(name, kind)
matches: seq[Name] = @[]
distances: seq[int] = @[]
dst: int = 0
minDst: int = dst
# Compute type distances of all functions and find
# the smallest one
for n in impl:
dst = 0
# Compute cumulative type distance of the function's
# type signature
for (argA, argB) in zip(n.valueType.args, kind.args):
dst += self.getTypeDistance(argA.kind, argB.kind)
dst += self.getTypeDistance(n.valueType.returnType, kind.returnType)
distances.add(dst)
if dst < minDst:
minDst = dst
# Filter implementations by smallest type distance
for i, n in impl:
if distances[i] == minDst:
matches.add(n)
case matches.len():
of 0:
# No matches
let names = self.findAll(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 arg.name != "" and name.valueType.args[i].name != "" and arg.name != name.valueType.args[i].name:
msg &= &": unexpected argument '{arg.name}' at position {i + 1}"
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)
of 1:
# Match found
result = impl[0]
result.resolved = true
if result.kind == NameKind.Var:
# Variables bound to other names must always
# have this field set
assert not result.assignedName.isNil()
# We found a function bound to a variable,
# so we return the original function's name object
result = result.assignedName
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)
if result.valueType.returnType.isAny() and not kind.returnType.isAny():
self.error("any is not a valid type in this context")
else:
# Ambiguity detected
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)
proc specialize(self: PeonCompiler, typ: Type, args: seq[Expression]): Type =
## Instantiates a generic type
var
mapping: TableRef[string, Type] = newTable[string, Type]()
kind: Type
gen: Type
result = deepCopy(typ)
case result.kind:
of TypeKind.Function:
# This loop checks if a user tries to reassign a generic's
# name to a different type
for i, arg in args:
if i > typ.fun.generics.high():
gen = typ.genericArgs[typ.fun.generics[^1].name.token.lexeme]
else:
gen = typ.genericArgs[typ.fun.generics[i].name.token.lexeme]
kind = self.inferOrError(arg).kind
if gen.name in mapping and not self.compare(kind, mapping[gen.name]):
self.error(&"expecting generic argument '{gen.name}' to be of type {self.stringify(mapping[gen.name])}, got {self.stringify(kind)} instead", args[i])
mapping[gen.name] = kind
result.args[i].kind = kind
for arg in result.args.mitems():
# We only replace types we know about. This allows
# us to do partial instantiation
if arg.kind.kind == Generic and arg.kind.name in mapping:
arg.kind = mapping[arg.kind.name]
if not result.returnType.isNil() and result.returnType.kind == Generic:
if result.returnType.name in mapping:
result.returnType = mapping[result.returnType.name]
elif mapping.len() == 0:
# The function has no generic arguments,
# just a generic return type
var typ: Type
for i, gen in result.fun.generics:
if gen.name.token.lexeme == result.returnType.name:
typ = result.args[i].kind
break
if typ.isNil():
self.error(&"unknown generic argument name '{result.returnType.name}'", result.fun)
result.returnType = typ
else:
self.error(&"unknown generic argument name '{result.returnType.name}'", result.fun)
else:
discard
proc unpackTypes*(self: PeonCompiler, condition: Expression, list: var seq[tuple[match: bool, kind: Type, value: Expression]], accept: bool = true, isGeneric: bool = false) =
## Recursively unpacks a type constraint
case condition.kind:
of identExpr:
var typ = self.inferOrError(condition).kind
if not isGeneric and not self.compare(typ, "typevar".toIntrinsic()):
self.error(&"expecting a type name, got value of type {self.stringify(typ)} instead", condition)
typ = typ.unwrap()
if self.compare(typ, "auto".toIntrinsic()):
self.error("automatic types cannot be used within type constraints", condition)
list.add((accept, typ, condition))
of binaryExpr:
let condition = BinaryExpr(condition)
case condition.operator.lexeme:
of "|":
self.unpackTypes(condition.a, list)
self.unpackTypes(condition.b, list)
else:
self.error("invalid type constraint", condition)
of unaryExpr:
let condition = UnaryExpr(condition)
case condition.operator.lexeme:
of "~":
self.unpackTypes(condition.a, list, accept=false)
else:
self.error("invalid type constraint in", condition)
else:
self.error("invalid type constraint", condition)
proc dispatchPragmas(self: PeonCompiler, name: Name) =
## Dispatches pragmas bound to objects
if name.node.isNil():
return
var pragmas: seq[Pragma] = @[]
case name.node.kind:
of NodeKind.funDecl, NodeKind.typeDecl, NodeKind.varDecl:
pragmas = Declaration(name.node).pragmas
of NodeKind.lambdaExpr:
pragmas = LambdaExpr(name.node).pragmas
else:
discard # Unreachable
var f: CompilerFunc
for pragma in pragmas:
if pragma.name.token.lexeme notin self.compilerProcs:
self.error(&"unknown pragma '{pragma.name.token.lexeme}'")
f = self.compilerProcs[pragma.name.token.lexeme]
if f.kind != Immediate:
continue
f.handler(self, pragma, name)
proc dispatchDelayedPragmas(self: PeonCompiler, name: Name) {.used.} =
## Dispatches pragmas bound to objects once they
## are used
if name.node.isNil():
return
var pragmas: seq[Pragma] = @[]
pragmas = Declaration(name.node).pragmas
var f: CompilerFunc
for pragma in pragmas:
if pragma.name.token.lexeme notin self.compilerProcs:
self.error(&"unknown pragma '{pragma.name.token.lexeme}'")
f = self.compilerProcs[pragma.name.token.lexeme]
if f.kind == Immediate:
continue
f.handler(self, pragma, name)
proc addName(self: PeonCompiler, name: Name) =
## Adds a name to the current lexical scope
var scope = self.names[self.scopeDepth]
if scope.hasKey(name.ident.token.lexeme):
for obj in scope[name.ident.token.lexeme]:
if name.kind == NameKind.Function:
# We don't check for name clashes for functions because self.match() does that
continue
if obj.kind in [NameKind.Var, NameKind.Module, NameKind.CustomType, NameKind.Enum, NameKind.Function] and name.owner == obj.owner:
self.error(&"re-declaration of '{obj.ident.token.lexeme}' is not allowed (previously declared in {obj.owner.ident.token.lexeme}:{obj.ident.token.line}:{obj.ident.token.relPos.start})", name.node)
# We emit a bunch of warnings, mostly for QoL
if obj.owner != name.owner and (obj.isPrivate or name.owner notin obj.exportedTo):
continue
if obj.kind in [NameKind.Var, NameKind.Module, NameKind.CustomType, NameKind.Enum]:
if name.depth < obj.depth:
self.warning(WarningKind.ShadowOuterScope, &"'{name.ident.token.lexeme}' shadows a name from an outer scope ({obj.owner.file}:{obj.ident.token.line}:{obj.ident.token.relPos.start})", node=obj.ident)
if name.owner != obj.owner:
self.warning(WarningKind.ShadowOuterScope, &"'{name.ident.token.lexeme}' shadows a name from an outer module ({obj.owner.file}:{obj.ident.token.line}:{obj.ident.token.relPos.start})", node=obj.ident)
else:
scope[name.ident.token.lexeme] = @[]
scope[name.ident.token.lexeme].add(name)
proc declare(self: PeonCompiler, node: ASTNode): Name {.discardable.} =
## Declares a name into the current scope
var scope = self.names[self.scopeDepth]
var name: Name
var declaredName: string = ""
case node.kind:
of NodeKind.varDecl:
var node = VarDecl(node)
declaredName = node.name.token.lexeme
# Creates a new Name entry so that self.identifier can find it later
name = Name(depth: self.scopeDepth,
ident: node.name,
isPrivate: node.isPrivate,
owner: self.currentModule,
file: self.file,
isConst: node.isConst,
valueType: "nil".toIntrinsic(), # Done later in varDecl (for better semantics)
isLet: node.isLet,
line: node.token.line,
belongsTo: self.currentFunction,
kind: NameKind.Var,
node: node,
isReal: true
)
self.addName(name)
of NodeKind.funDecl:
var node = FunDecl(node)
declaredName = node.name.token.lexeme
name = Name(depth: self.scopeDepth,
isPrivate: node.isPrivate,
isConst: false,
owner: self.currentModule,
file: self.file,
valueType: Type(kind: Function,
returnType: "nil".toIntrinsic(), # We check it later
args: @[],
fun: node,
forwarded: node.body.isNil(),
isAuto: false,
genericArgs: newTable[string, Type]()),
ident: node.name,
node: node,
isLet: false,
line: node.token.line,
kind: NameKind.Function,
belongsTo: self.currentFunction,
isReal: true,
)
if node.generics.len() > 0:
name.isGeneric = true
self.addName(name)
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.addName(Name(depth: self.scopeDepth,
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
))
name = scope[node.moduleName.token.lexeme][^1]
declaredName = name.ident.token.lexeme
of NodeKind.typeDecl:
var node = TypeDecl(node)
declaredName = node.name.token.lexeme
var kind = Type(kind: CustomType, typeName: declaredName, generics: newTable[string, Type](), fields: newTable[string, Type](),
interfaces: @[])
if node.isRef:
kind = kind.toRef()
kind = kind.wrap()
self.addName(Name(kind: NameKind.CustomType,
depth: self.scopeDepth,
owner: self.currentModule,
node: node,
ident: node.name,
line: node.name.token.line,
#isPrivate: node.isPrivate,
belongsTo: self.currentFunction,
valueType: kind,
isReal: true,
))
name = scope[declaredName][^1]
else:
discard # TODO: enums
if not name.isNil():
self.dispatchPragmas(name)
return name
proc identifier(self: PeonCompiler, node: IdentExpr): TypedExpr =
## Compiles name resolution
return newTypedIdentExpr(node, self.findOrError(node.name.lexeme))
proc unary(self: PeonCompiler, node: UnaryExpr): TypedUnaryExpr =
## Compiles unary expressions
var
default: Expression
typeOfA = self.infer(node.a)
let fn = Type(kind: Function,
returnType: Type(kind: Any),
args: @[("", typeOfA.kind, default)])
var name = self.match(node.token.lexeme, fn, node)
var impl = name.valueType
if name.isGeneric:
impl = self.specialize(impl, @[node.a])
result = newTypedUnaryExpr(node, impl.returnType, typeOfA)
proc binary(self: PeonCompiler, node: BinaryExpr): TypedBinaryExpr =
## Compiles unary expressions
var
default: Expression
typeOfA = self.infer(node.a)
typeOfB = self.infer(node.b)
let fn = Type(kind: Function,
returnType: Type(kind: Any),
args: @[("", typeOfA.kind, default), ("", typeOfB.kind, default)])
var name = self.match(node.token.lexeme, fn, node)
var impl = name.valueType
if name.isGeneric:
impl = self.specialize(impl, @[node.a])
result = newTypedBinaryExpr(node, impl.returnType, typeOfA, typeOfB)
proc genericExpr(self: PeonCompiler, node: GenericExpr): TypedExpr =
## Compiles generic instantiation
var name = self.findOrError(node.ident.token.lexeme)
if not name.isGeneric:
self.error(&"cannot instantiate concrete type from {self.stringify(name.valueType)}: a generic is required")
var fun = FunDecl(name.node)
if fun.generics.len() != node.args.len():
self.error(&"wrong number of types supplied for generic instantiation (expected {fun.generics.len()}, got {node.args.len()} instead)")
var concrete = deepCopy(name.valueType)
var types: seq[Type] = @[]
var map = newTable[string, Type]()
for arg in node.args:
types.add(self.inferOrError(arg).kind)
if types[^1].kind != Typevar:
self.error(&"expecting type name during generic instantiation, got {self.stringify(types[^1])} instead", arg)
for (gen, value) in zip(fun.generics, node.args):
map[gen.name.token.lexeme] = self.inferOrError(value).kind
for i, argument in concrete.args:
if argument.kind.kind != Generic:
continue
elif argument.name in map:
concrete.args[i].kind = map[argument.name]
else:
self.error(&"unknown generic argument name '{argument.name}'", concrete.fun)
if not concrete.returnType.isNil() and concrete.returnType.kind == Generic:
if concrete.returnType.name in map:
concrete.returnType = map[concrete.returnType.name]
else:
self.error(&"unknown generic argument name '{concrete.returnType.name}'", concrete.fun)
result = newTypedExpr(node, concrete)
proc call(self: PeonCompiler, node: CallExpr): TypedExpr =
## Compiles function calls
var args: seq[tuple[name: string, kind: Type, default: Expression]] = @[]
var argExpr: seq[Expression] = @[]
var default: Expression
var kind: Type
for i, argument in node.arguments.positionals:
kind = self.inferOrError(argument).kind
args.add(("", kind, default))
argExpr.add(argument)
for i, argument in node.arguments.keyword:
kind = self.infer(argument.value).kind
args.add((argument.name.token.lexeme, kind, default))
argExpr.add(argument.value)
case node.callee.kind:
of NodeKind.identExpr:
# Calls like hi()
var impl = self.match(IdentExpr(node.callee).name.lexeme, Type(kind: Function, returnType: "any".toIntrinsic(), args: args), node)
var function = impl.valueType
if impl.isGeneric:
function = self.specialize(function, argExpr)
result = newTypedExpr(node, function.returnType)
if not impl.valueType.compiled:
discard self.funDecl(FunDecl(function.fun), impl)
self.dispatchDelayedPragmas(impl)
of NodeKind.callExpr:
# TODO
# Calling a call expression, like hello()()
var node: Expression = node
var all: seq[CallExpr] = @[]
# Since there can be as many consecutive calls as
# the user wants, we need to "extract" all of them
while CallExpr(node).callee.kind == callExpr:
all.add(CallExpr(CallExpr(node).callee))
node = CallExpr(node).callee
# Now that we know how many call expressions we
# need to compile, we start from the outermost
# one and work our way to the innermost call
for exp in all:
result = self.call(exp)
of NodeKind.getItemExpr:
# Calling a.b()
# TODO
let node = GetItemExpr(node.callee)
of NodeKind.lambdaExpr:
# Calling a lambda on the fly
var node = LambdaExpr(node.callee)
# TODO
of NodeKind.genericExpr:
# Calling a generic expression
# TODO
result = newTypedExpr(node, nil)
let node = GenericExpr(node.callee)
let concrete = self.genericExpr(node)
var impl = self.find(node.ident.token.lexeme).deepCopy()
impl.valueType = concrete.kind
result.kind = impl.valueType.returnType
else:
let typ = self.infer(node)
if typ.isNil():
self.error(&"expression has no type", node)
else:
self.error(&"object of type '{self.stringify(typ)}' is not callable", node)
proc expression(self: PeonCompiler, node: Expression): TypedExpr =
## Compiles expressions
if node.isConst():
return self.literal(LiteralExpr(node))
case node.kind:
of callExpr:
result = self.call(CallExpr(node))
of identExpr:
result = self.identifier(IdentExpr(node))
of groupingExpr:
result = self.expression(GroupingExpr(node).expression)
of unaryExpr:
result = self.unary(UnaryExpr(node))
of binaryExpr:
result = self.binary(BinaryExpr(node))
of NodeKind.genericExpr:
result = self.genericExpr(GenericExpr(node))
of refExpr:
result = self.check(Ref(node).value, "typevar".toIntrinsic())
result.kind = result.kind.toRef()
result.kind = result.kind.wrap()
of ptrExpr:
result = self.check(Ref(node).value, "typevar".toIntrinsic())
result.kind = result.kind.toPtr()
result.kind = result.kind.wrap()
of lentExpr:
result = self.check(ast.Lent(node).value, "typevar".toIntrinsic())
result.kind = result.kind.toLent()
result.kind = result.kind.wrap()
else:
discard # TODO
proc blockStmt(self: PeonCompiler, node: BlockStmt): TypedBlockStmt =
## Compiles block statements
self.beginScope()
var body: seq[TypedNode] = @[]
for decl in node.code:
body.add(self.compile(decl))
self.endScope()
result = newTypedBlockStmt(node, body)
proc ifStmt(self: PeonCompiler, node: IfStmt): TypedNode =
## Compiles if/else statements for conditional
## execution of code
# Check that the condition is a boolean and record it
let condition = self.check(node.condition, "bool".toIntrinsic())
# Compile the "then" part of "if-then-else"
let then = TypedBlockStmt(self.compile(node.thenBranch))
# Compile the "else" part
let otherwise = TypedBlockStmt(self.compile(node.elseBranch))
# Note: Peon enforces the body of loops and conditionals to
# always be a block statement (for a variety of very good reasons),
# so the conversion here is safe
return newTypedIfStmt(node, then, otherwise, condition)
proc whileStmt(self: PeonCompiler, node: WhileStmt): TypedNode =
## Compiles C-style while loops
# Compile and check the condition
let condition = self.check(node.condition, "bool".toIntrinsic())
return newTypedWhileStmt(node, TypedBlockStmt(self.compile(node.body)), condition)
proc varDecl(self: PeonCompiler, node: VarDecl): TypedDecl =
## Compiles variable declarations
var typ: TypedExpr
var kind: Type
# Our parser guarantees that the variable declaration
# will have a type declaration or a value (or both)
if node.value.isNil():
# Variable has no value: the type declaration
# takes over
typ = self.inferOrError(node.valueType)
kind = typ.kind
if self.compare(typ.kind, "auto".toIntrinsic()):
self.error("automatic types require initialization", node)
if not self.compare(typ.kind, "typevar".toIntrinsic()):
self.error(&"expecting type name, got value of type {self.stringify(typ)} instead", node.name)
kind = kind.unwrap()
# TODO: Implement T.default()!
elif node.valueType.isNil():
# Variable has no type declaration: the type
# of its value takes over
typ = self.inferOrError(node.value)
else:
# Variable has both a type declaration and
# a value: the value's type must match the
# type declaration
let expected = self.inferOrError(node.valueType)
if not self.compare(expected.kind, "auto".toIntrinsic()):
typ = self.check(node.value, expected.kind)
else:
# Let the compiler infer the type (this
# is the default behavior already, but
# some users may prefer to be explicit!)
typ = self.inferOrError(node.value)
var name = self.declare(node)
name.valueType = kind
name.assignedName = typ.getName()
return newTypedDecl(node, name)
proc funDecl(self: PeonCompiler, node: FunDecl, name: Name = nil): TypedDecl =
## Compiles function declarations
if node.token.kind == Operator and node.name.token.lexeme in [".", ]:
self.error(&"Due to compiler limitations, the '{node.name.token.lexeme}' operator cannot be currently overridden", node.name)
var name = name
if name.isNil():
name = self.declare(node)
result = newTypedDecl(node, name)
var node = node
# First we declare the function's generics, if it has any
var
constraints: seq[tuple[match: bool, kind: Type, value: Expression]] = @[]
value: Expression
self.beginScope()
for gen in node.generics:
if gen.cond.isNil():
constraints = @[(match: true, kind: "any".toIntrinsic(), value: value)]
else:
self.unpackTypes(gen.cond, constraints)
self.addName(Name(depth: self.scopeDepth,
isPrivate: true,
valueType: Type(kind: Generic, name: gen.name.token.lexeme, cond: constraints),
isLet: false,
line: node.token.line,
belongsTo: name,
ident: gen.name,
owner: self.currentModule,
file: self.file,
isReal: true))
if name.valueType.genericArgs.hasKey(gen.name.token.lexeme):
self.error(&"cannot re-declare generic argument '{gen.name.token.lexeme}'", node=gen.name)
name.valueType.genericArgs[gen.name.token.lexeme] = self.names[self.scopeDepth][gen.name.token.lexeme][^1].valueType
constraints = @[]
# We now declare and typecheck the function's
# arguments
var default: Expression
var i = 0
var typ: Type
var arg: Name
for argument in node.arguments:
typ = self.inferOrError(argument.valueType).kind
if typ.kind != Generic:
typ = self.check(argument.valueType, "typevar".toIntrinsic()).kind.unwrap()
if self.compare(typ, "auto".toIntrinsic()):
name.valueType.isAuto = true
arg = Name(depth: self.scopeDepth,
isPrivate: true,
owner: name.owner,
file: name.file,
isConst: false,
ident: argument.name,
valueType: typ,
isLet: false,
line: argument.name.token.line,
belongsTo: name,
kind: NameKind.Argument,
node: argument.name,
isReal: true)
if node.arguments.high() - node.defaults.high() <= node.arguments.high():
# There's a default argument!
name.valueType.args.add((arg.ident.token.lexeme, typ, node.defaults[i]))
inc(i)
else:
# This argument has no default
name.valueType.args.add((arg.ident.token.lexeme, typ, default))
self.addName(arg)
if not node.returnType.isNil():
# The function needs a return type too!
name.valueType.returnType = self.inferOrError(node.returnType).kind
if name.valueType.returnType.kind != Generic:
name.valueType.returnType = self.check(node.returnType, "typevar".toIntrinsic()).kind.unwrap()
if self.compare(name.valueType.returnType, "auto".toIntrinsic()):
name.valueType.isAuto = true
if node.body.isNil():
# When we stumble across a forward declaration,
# we record it for later so we can look it up at
# the end of the module
# TODO
# self.forwarded.add((name, 0))
name.valueType.forwarded = true
self.endScope()
return
if name.valueType.isBuiltin:
# Builtins are handled at call time
self.endScope()
return
# We store the current function to restore
# it later
let function = self.currentFunction
self.currentFunction = name
name.valueType.compiled = true
if BlockStmt(node.body).code.len() == 0:
self.error("cannot declare function with empty body")
var last: Declaration
for decl in BlockStmt(node.body).code:
if not last.isNil() and last.kind == returnStmt:
self.warning(UnreachableCode, "code after 'return' statement is unreachable", nil, decl)
discard self.compile(decl)
last = decl
self.endScope()
# Restores the enclosing function (if any).
# Makes nested calls work (including recursion)
self.currentFunction = function
proc typeDecl(self: PeonCompiler, node: TypeDecl, name: Name = nil): TypedTypeDecl =
## Compiles type declarations
var name = name
if name.isNil():
name = self.declare(node)
result = newTypedTypeDecl(node, name, newTable[string, TypedExpr](), nil)
self.beginScope()
# Declare the type's generics
var
constraints: seq[tuple[match: bool, kind: Type, value: Expression]] = @[]
value: Expression
for gen in node.generics:
if gen.cond.isNil():
constraints = @[(match: true, kind: "any".toIntrinsic(), value: value)]
else:
self.unpackTypes(gen.cond, constraints)
self.addName(Name(depth: self.scopeDepth,
isPrivate: true,
valueType: Type(kind: Generic, name: gen.name.token.lexeme, cond: constraints),
isLet: false,
line: node.token.line,
belongsTo: name,
ident: gen.name,
owner: self.currentModule,
file: self.file,
isReal: true))
if name.valueType.wrapped.generics.hasKey(gen.name.token.lexeme):
self.error(&"cannot re-declare generic argument '{gen.name.token.lexeme}'", node=gen.name)
name.valueType.wrapped.generics[gen.name.token.lexeme] = self.names[self.scopeDepth][gen.name.token.lexeme][^1].valueType
constraints = @[]
if node.value.isNil():
# Type is not a type union nor type alias
if not node.isEnum:
# Type has fields
var fieldType: TypedExpr
var n: Name
for field in node.fields:
fieldType = self.infer(field.valueType)
if not node.isRef:
# Check for self-recursion of non-ref (aka managed pointer) types (which would require
# infinite memory)
n = fieldType.getName()
if n.isNil():
# Expression has no associated name: cannot self-recurse
continue
if name == n:
self.error(&"illegal self-recursion in member '{field.name.token.lexeme}' for non-ref type '{name.ident.token.lexeme}'", fieldType.node)
result.fields[field.name.token.lexeme] = fieldType
else:
# Type is a variant (aka enum). We'll only declare a single
# object (the enum type itself) so that we don't pollute the
# global namespace. I don't love unqualified enums, but I use
# them because I'm lazy and it often leads to problems, so since
# peon is also meant (among other things) to address my pain points
# with the languages I'm currentlyusing , I'm going to explicitly
# forbid myself from using unqualified enum members ever again :)
var res = newTypedEnumDecl(node, name, @[], Type(kind: Enum, members: @[]))
for member in node.members:
res.enumeration.members.add(self.declare(member))
res.variants.add(self.typeDecl(member, res.enumeration.members[^1]))
result = res
else:
case node.value.kind:
of identExpr:
# Type alias
name.valueType = self.inferOrError(node.value).kind
of binaryExpr, unaryExpr:
# Untagged type union
name.valueType = Type(kind: Union, types: @[])
self.unpackTypes(node.value, name.valueType.types)
else:
# Unreachable
discard
if not node.parent.isNil():
# Ensure parent is actually a type
var subtype = self.check(node.parent, "typevar".toIntrinsic())
# Grab its name object
var parentName = subtype.getName()
# This should *never* be nil
if parentName.isNil():
self.error(&"could not obtain name information for the given object: is this a type?", node.parent)
result.parent = parentName
for field in TypeDecl(result.parent.node).fields:
if result.fields.hasKey(field.name.token.lexeme):
for f in TypeDecl(result.node).fields:
if f.name.token.lexeme == field.name.token.lexeme:
# This always eventually runs
self.error(&"cannot to re-declare type member '{field}'", f.name)
result.fields[field.name.token.lexeme] = newTypedExpr(field.name, result.parent.valueType.fields[field.name.token.lexeme])
# TODO: Check interfaces
self.endScope()
proc compile(self: PeonCompiler, node: ASTNode): TypedNode =
## Dispatches typeless AST nodes to compile them into
## typed ones
case node.kind:
of binaryExpr, unaryExpr, NodeKind.genericExpr, identExpr,
groupingExpr, callExpr:
result = self.expression(Expression(node))
of exprStmt:
result = self.expression(ExprStmt(node).expression)
of NodeKind.whileStmt:
result = self.whileStmt(WhileStmt(node))
of NodeKind.blockStmt:
result = self.blockStmt(BlockStmt(node))
of NodeKind.ifStmt:
result = self.ifStmt(IfStmt(node))
of NodeKind.varDecl:
result = self.varDecl(VarDecl(node))
of NodeKind.funDecl:
result = self.funDecl(FunDecl(node))
of NodeKind.typeDecl:
result = self.typeDecl(TypeDecl(node))
else:
discard
proc compile*(self: PeonCompiler, tree: seq[Declaration], file, source: string, showMismatches: bool = false): seq[TypedNode] =
## Compiles a sequence of typeless AST nodes
## into a sequence of typed AST nodes
self.file = file
self.source = source
self.tree = tree
self.current = 0
self.scopeDepth = -1
self.showMismatches = showMismatches
self.names = @[]
self.beginScope()
var mainModule = Name(kind: NameKind.Module,
depth: 0,
isPrivate: true,
isConst: false,
isLet: false,
owner: nil,
file: self.file,
path: self.file,
ident: newIdentExpr(Token(lexeme: self.file, kind: Identifier)),
resolved: true,
line: 1)
self.addName(mainModule)
self.currentModule = mainModule
# Every peon program has a hidden entry point in
# which user code is wrapped. Think of it as if
# peon is implicitly writing the main() function
# of your program and putting all of your code in
# there
var main = Name(depth: 0,
isPrivate: true,
isConst: false,
isLet: false,
owner: self.currentModule,
file: self.file,
valueType: Type(kind: Function,
returnType: "nil".toIntrinsic(),
compiled: true,
args: @[],
),
ident: newIdentExpr(Token(lexeme: "", kind: Identifier)),
kind: NameKind.Function,
resolved: true,
line: 1)
self.addName(main)
while not self.done():
result.add(self.compile(self.step()))
assert self.scopeDepth == 0
self.endScope()
assert self.scopeDepth == -1