Added tagged type unions, moved intrinsics to peon's stdlib and fixed issues with generics and parametric polymorphism

This commit is contained in:
Mattia Giambirtone 2022-11-29 16:48:05 +01:00
parent 382a6c141b
commit 63cdda42ce
17 changed files with 367 additions and 245 deletions

View File

@ -77,6 +77,7 @@ Type system:
- Custom types
- Intrinsics ✅
- Type unions ✅
- Generics ✅
- Functions ✅
- Closures ✅
@ -85,8 +86,8 @@ Misc:
- Pragmas ✅
- Attribute resolution ✅
- UFCS
- Import system with namespaces and visibility control ✅
- UFCS (Universal Function Call Syntax)
- Import system (with namespaces and visibility control)
__Note__: Since peon isn't complete yet, the toolchain (lexer, parser, compiler and VM) may change, but since they
@ -101,9 +102,9 @@ have been implemented alredady):
- Easy C/Nim interop via FFI
- C/C++ backend
- Nim backend
- Structured concurrency
- Capability-based programming (i.e. functions are passed objects to act on the real world)
- Parametric Polymorphism (with untagged typed unions)
- Structured concurrency (must-have!)
- Capability-based programming (i.e. functions are passed objects to act on the real world. This is just a maybe)
- Parametric Polymorphism ✅
- Simple OOP (with multiple dispatch!)
- RTTI, with methods that dispatch at runtime based on the true type of a value
- Limited compile-time evaluation (embed the Peon VM in the C/C++/Nim backend and use that to execute peon code at compile time)

View File

@ -135,7 +135,6 @@ genericSum(3.14, 0.1);
genericSum(1'u8, 250'u8);
```
__Note__: The generic `Number` type is currently not defined. You can use type unions instead
#### More generics!

View File

@ -46,7 +46,7 @@ type
UInt32, Int64, UInt64, Float32, Float64,
Char, Byte, String, Function, CustomType,
Nil, Nan, Bool, Inf, Typevar, Generic,
Reference, Pointer, Any, All
Reference, Pointer, Any, All, Union
Type = ref object
## A wrapper around
## compile-time types
@ -77,6 +77,8 @@ type
# would map to [(true, int), (false, uint)]
cond: seq[tuple[match: bool, kind: Type]]
name: string
of Union:
types: seq[tuple[match: bool, kind: Type]]
else:
discard
@ -261,14 +263,17 @@ proc expression(self: Compiler, node: Expression)
proc statement(self: Compiler, node: Statement)
proc declaration(self: Compiler, node: Declaration)
proc peek(self: Compiler, distance: int = 0): ASTNode
proc identifier(self: Compiler, node: IdentExpr, name: Name = nil)
proc identifier(self: Compiler, node: IdentExpr, name: Name = nil, compile: bool = true): Name {.discardable.}
proc varDecl(self: Compiler, node: VarDecl)
proc specialize(self: Compiler, name: Name, args: seq[Expression]): Name
proc match(self: Compiler, name: string, kind: Type, node: ASTNode = nil, args: seq[Expression] = @[], allowFwd: bool = true): Name
proc call(self: Compiler, node: CallExpr, compile: bool = true): Name {.discardable.}
proc getItemExpr(self: Compiler, node: GetItemExpr, compile: bool = true): Type {.discardable.}
proc infer(self: Compiler, node: LiteralExpr, allowGeneric: bool = false): Type
proc infer(self: Compiler, node: Expression, allowGeneric: bool = false): Type
proc inferOrError(self: Compiler, node: Expression, allowGeneric: bool = false): Type
proc unary(self: Compiler, node: UnaryExpr, compile: bool = true): Name {.discardable.}
proc binary(self: Compiler, node: BinaryExpr, compile: bool = true): Name {.discardable.}
proc infer(self: Compiler, node: LiteralExpr): Type
proc infer(self: Compiler, node: Expression): Type
proc inferOrError(self: Compiler, node: Expression): Type
proc findByName(self: Compiler, name: string): seq[Name]
proc findInModule(self: Compiler, name: string, module: Name): seq[Name]
proc findByType(self: Compiler, name: string, kind: Type): seq[Name]
@ -723,8 +728,11 @@ proc getStackPos(self: Compiler, name: Name): int =
# Arguments to builtin functions are optimized away to stack
# temporaries. There is no stack frame for builtins, so we skip
# these names too
elif variable.kind == Argument and variable.belongsTo.valueType.isBuiltinFunction:
continue
elif variable.kind == Argument:
if variable.belongsTo.valueType.isBuiltinFunction:
continue
elif not variable.belongsTo.resolved:
continue
# This variable isn't in declared in our module,
# but we may still have access to it
elif variable.owner != name.owner:
@ -734,9 +742,11 @@ proc getStackPos(self: Compiler, name: Name): int =
if variable.isPrivate or name.owner notin variable.exportedTo:
inc(result)
continue
# Note: this MUST be an if, NOT an elif!
if name.ident == variable.ident and variable.depth == name.depth and name.owner == variable.owner:
if not name.belongsTo.isNil() and not variable.belongsTo.isNil():
if name.belongsTo != variable.belongsTo and variable.belongsTo.depth > name.belongsTo.depth:
# Argument with the same name but in a different function: ignore it
dec(result)
continue
found = true
@ -758,6 +768,28 @@ proc getClosurePos(self: Compiler, name: Name): int =
return -1
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 matched = false
for cond1 in long:
for cond2 in short:
if not self.compare(cond2.kind, cond1.kind) or cond2.match != cond1.match:
continue
else:
matched = true
break
if not matched:
return false
matched = false
return true
proc compare(self: Compiler, a, b: Type): bool =
## Compares two type objects
## for equality (works with nil!)
@ -765,36 +797,19 @@ proc compare(self: Compiler, a, b: Type): bool =
# The nil code here is for void functions (when
# we compare their return types)
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():
result = b.isNil() or b.kind == All
elif b.isNil():
result = a.isNil() or a.kind == All
elif a.kind == All:
result = true
elif b.kind == All:
result = true
# 'Any' is an type internal to
# the peon compiler that cannot
# be produced from regular user
# code. We use it mostly when
# resolving function calls (where
# we don't care about the type of
# the function's return value, as
# long as it has one) because peon
# doesn't have return type inference
# (yet :)). A seemingly identical type
# 'All' also exists, but while 'Any'
# means 'any type', 'All' means 'any
# type or no type at all'
elif a.kind == Any:
result = not b.isNil()
elif b.kind == Any:
result = not a.isNil()
# Note: the elif chain here is important!
# If we used regular if statements, we'd run
# the risk of running more checks than we
# need to and incorrectly modifying the result
elif a.kind != Generic and b.kind != Generic and a.kind == b.kind:
return b.isNil() or b.kind == All
if b.isNil():
return a.isNil() or a.kind == All
if a.kind == All or b.kind == All:
return true
if a.kind != Generic and b.kind != Generic and a.kind == b.kind:
# Here we make sure to compare only concrete
# types
case a.kind:
@ -803,7 +818,9 @@ proc compare(self: Compiler, a, b: Type): bool =
Char, Byte, String, Nil, Nan, Bool, Inf:
# A value's type is always equal to
# another one's
result = true
return true
of Union:
return self.compareUnions(a.types, b.types)
of Reference, Pointer:
# Here we already know that both
# a and b are of either of the two
@ -856,24 +873,28 @@ proc compare(self: Compiler, a, b: Type): bool =
if not self.compare(argA.kind, argB.kind):
temp = false
break
result = temp
return temp
else:
discard # TODO: Custom types
elif a.kind == Generic:
discard # TODO: Custom types, enums
if a.kind == Union:
for constraint in a.types:
if self.compare(constraint.kind, b):
if not constraint.match:
return false
else:
return false
return true
if b.kind == Union:
for constraint in b.types:
if self.compare(constraint.kind, a):
if not constraint.match:
return false
else:
return false
return true
if a.kind == Generic:
if b.kind == Generic:
for c1 in a.cond:
for c2 in b.cond:
if self.compare(c1.kind, c2.kind):
# Here return is fine, because there's
# no more checks after this one!
if c1.match != c2.match:
return false
else:
return false
# We only return at the end because when matching
# generics we want to match *all* constraints at
# once, not just find the first match
return true
return self.compareUnions(a.cond, b.cond)
else:
for constraint in a.cond:
if self.compare(constraint.kind, b):
@ -882,16 +903,9 @@ proc compare(self: Compiler, a, b: Type): bool =
else:
return false
return true
elif b.kind == Generic:
if b.kind == Generic:
if a.kind == Generic:
for c1 in a.cond:
for c2 in b.cond:
if self.compare(c1.kind, c2.kind):
if c1.match != c2.match:
return false
else:
return false
return true
return self.compareUnions(a.cond, b.cond)
else:
for constraint in b.cond:
if self.compare(constraint.kind, a):
@ -900,9 +914,18 @@ proc compare(self: Compiler, a, b: Type): bool =
else:
return false
return true
# TODO: Is this ok?
else:
result = false
if a.kind == Any:
# Why this difference? The reason is that
# while, for example, int is a subtype of
# any, meaning that passing int to a function
# that takes any is fine, this means that any
# cannot be a subtype of int! Basically, you
# can't pass any where int is expected, because
# that would break the type system in ways I'd
# rather not talk about
return true
if b.kind == Any:
return a.kind == Any
proc toIntrinsic(name: string): Type =
@ -951,7 +974,7 @@ proc toIntrinsic(name: string): Type =
return nil
proc infer(self: Compiler, node: LiteralExpr, allowGeneric: bool = false): Type =
proc infer(self: Compiler, node: LiteralExpr): Type =
## Infers the type of a given literal expression
## (if the expression is nil, nil is returned)
if node.isNil():
@ -995,88 +1018,26 @@ proc infer(self: Compiler, node: LiteralExpr, allowGeneric: bool = false): Type
discard # TODO
proc infer(self: Compiler, node: Expression, allowGeneric: bool = false): Type =
proc infer(self: Compiler, node: Expression): Type =
## Infers the type of a given expression and
## returns it (if the node is nil, nil is
## returned). Always returns a concrete type
## unless allowGeneric is set to true
## returned)
if node.isNil():
return nil
if node.isLiteral():
return self.infer(LiteralExpr(node), allowGeneric)
case node.kind:
of identExpr:
let node = IdentExpr(node)
var name = self.resolve(node)
if not name.isNil():
result = name.valueType
if not result.isNil() and result.kind == Generic and not allowGeneric:
if name.belongsTo.isNil():
name = self.resolve(result.name)
if not name.isNil():
result = name.valueType
else:
for arg in name.belongsTo.valueType.args:
if node.token.lexeme == arg.name:
result = arg.kind
else:
result = node.name.lexeme.toIntrinsic()
result = self.identifier(IdentExpr(node), compile=false).valueType
of unaryExpr:
let node = UnaryExpr(node)
var default: Expression
let impl = self.match(node.operator.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", self.infer(node.a), default)]), node)
result = impl.valueType.returnType
if result.kind == Generic and not allowGeneric:
result = self.specialize(impl, @[node.a]).valueType.returnType
result = self.unary(UnaryExpr(node), compile=false).valueType.returnType
of binaryExpr:
let node = BinaryExpr(node)
var default: Expression
let impl = self.match(node.operator.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", self.infer(node.a), default), ("", self.infer(node.b), default)]), node)
result = impl.valueType.returnType
if result.kind == Generic and not allowGeneric:
result = self.specialize(impl, @[node.a, node.b]).valueType.returnType
result = self.binary(BinaryExpr(node), compile=false).valueType.returnType
of {intExpr, hexExpr, binExpr, octExpr,
strExpr, falseExpr, trueExpr, infExpr,
nanExpr, floatExpr, nilExpr
}:
result = self.infer(LiteralExpr(node))
of lambdaExpr:
var node = LambdaExpr(node)
result = Type(kind: Function, returnType: nil, args: @[], isLambda: true, fun: node)
var default: Expression
if not node.returnType.isNil():
result.returnType = self.infer(node.returnType)
for argument in node.arguments:
result.args.add((argument.name.token.lexeme, self.infer(argument.valueType), default))
of callExpr:
var node = CallExpr(node)
case node.callee.kind:
of identExpr:
let resolved = self.resolve(IdentExpr(node.callee))
if not resolved.isNil():
case resolved.valueType.kind:
of Function:
if resolved.isGeneric and not allowGeneric:
var args: seq[Expression] = @[]
for argument in node.arguments.positionals:
args.add(argument)
for argument in node.arguments.keyword:
args.add(argument.value)
result = self.specialize(resolved, args).valueType.returnType
else:
result = resolved.valueType.returnType
else:
result = resolved.valueType
else:
result = nil
of lambdaExpr:
result = self.infer(LambdaExpr(node.callee).returnType)
of callExpr:
result = self.infer(CallExpr(node.callee))
if not result.isNil():
result = result.returnType
else:
discard # Unreachable
of NodeKind.callExpr:
result = self.call(CallExpr(node), compile=false).valueType.returnType
of varExpr:
result = self.infer(Var(node).value)
result.mutable = true
@ -1089,15 +1050,15 @@ proc infer(self: Compiler, node: Expression, allowGeneric: bool = false): Type =
of NodeKind.getItemExpr:
result = self.getItemExpr(GetItemExpr(node), compile=false)
else:
discard # Unreachable
discard # TODO
proc inferOrError(self: Compiler, node: Expression, allowGeneric: bool = false): Type =
proc inferOrError(self: Compiler, node: Expression): Type =
## Attempts to infer the type of
## the given expression and raises an
## error with an appropriate message if
## it fails
result = self.infer(node, allowGeneric)
result = self.infer(node)
if result.isNil():
self.error("expression has no type", node)
@ -1145,6 +1106,15 @@ proc typeToStr(self: Compiler, typ: Type): string =
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.typeToStr(condition.kind)
of Generic:
for i, condition in typ.cond:
if i > 0:
@ -1204,12 +1174,12 @@ proc match(self: Compiler, name: string, kind: Type, node: ASTNode = nil, args:
var impl: seq[Name] = @[]
var temp: Name
for obj in self.findByName(name):
if obj.isGeneric:
if obj.isGeneric and args.len() > 0:
temp = self.specialize(obj, args)
else:
temp = obj
if self.compare(kind, temp.valueType):
impl.add(temp)
impl.add(self.match(name, temp.valueType, node))
if impl.len() == 0:
var msg = &"failed to find a suitable implementation for '{name}'"
let names = self.findByName(name)
@ -1423,7 +1393,7 @@ proc endScope(self: Compiler) =
# Arguments to builtin functions become temporaries on the
# stack and are popped automatically
continue
if not name.belongsTo.resolved or not name.isReal:
if not name.belongsTo.resolved or not name.belongsTo.isReal:
# Function hasn't been compiled yet,
# so we can't get rid of its arguments
# (it may need them later)
@ -1492,7 +1462,31 @@ proc unpackGenerics(self: Compiler, condition: Expression, list: var seq[tuple[m
self.error("invalid type constraint in generic declaration", condition)
proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name {.discardable.} =
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.unpackGenerics(condition.a, list)
self.unpackGenerics(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.unpackGenerics(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, mutable: bool = false): 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()
@ -1555,8 +1549,8 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name {.d
of NodeKind.importStmt:
var node = ImportStmt(node)
# We change the name of the module internally so that
# if you do import /path/to/mod, then doing mod.f()
# will work without any extra work on our end. Note how
# 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
@ -1564,7 +1558,7 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name {.d
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 compiled!
file: "", # The file of the module isn't known until it's compiled!
path: path,
ident: node.moduleName,
line: node.moduleName.token.line,
@ -1574,8 +1568,34 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name {.d
))
n = self.names[^1]
declaredName = self.names[^1].ident.token.lexeme
of NodeKind.typeDecl:
var node = TypeDecl(node)
self.names.add(Name(kind: NameKind.CustomType,
depth: self.depth,
owner: self.currentModule,
node: node,
ident: node.name,
line: node.token.line,
isPrivate: node.isPrivate,
isReal: true,
belongsTo: self.currentFunction
)
)
n = self.names[^1]
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: Types, enums
discard # TODO: enums
self.dispatchPragmas(n)
case n.kind:
of NameKind.Function:
@ -1629,12 +1649,43 @@ proc handleMagicPragma(self: Compiler, pragma: Pragma, name: Name) =
self.error("'magic' pragma: wrong number of arguments")
elif pragma.args[0].kind != strExpr:
self.error("'magic' pragma: wrong type of argument (constant string expected)")
elif name.node.kind != NodeKind.funDecl:
elif name.node.kind == NodeKind.funDecl:
name.valueType.isBuiltinFunction = true
name.valueType.builtinOp = pragma.args[0].token.lexeme[1..^2]
elif name.node.kind == NodeKind.typeDecl:
case pragma.args[0].token.lexeme[1..^2]:
of "int64":
name.valueType = Type(kind: Int64)
of "int32":
name.valueType = Type(kind: Int32)
of "int16":
name.valueType = Type(kind: Int16)
of "int8":
name.valueType = Type(kind: Int8)
of "uint64":
name.valueType = Type(kind: UInt64)
of "uint32":
name.valueType = Type(kind: UInt32)
of "uint16":
name.valueType = Type(kind: UInt16)
of "uint8":
name.valueType = Type(kind: UInt8)
of "float64":
name.valueType = Type(kind: Float64)
of "float32":
name.valueType = Type(kind: Float32)
of "bool":
name.valueType = Type(kind: Bool)
of "string":
name.valueType = Type(kind: String)
of "any":
name.valueType = Type(kind: Any)
of "all":
self.error("don't even think about it (compiler-chan is angry at you)", pragma)
else:
self.error("'magic' pragma: wrong argument value", pragma.args[0])
else:
self.error("'magic' pragma is not valid in this context")
var node = FunDecl(name.node)
name.valueType.isBuiltinFunction = true
name.valueType.builtinOp = pragma.args[0].token.lexeme[1..^2]
proc handleErrorPragma(self: Compiler, pragma: Pragma, name: Name) =
@ -1869,31 +1920,36 @@ proc literal(self: Compiler, node: ASTNode) =
self.error(&"invalid AST node of kind {node.kind} at literal(): {node} (This is an internal error and most likely a bug!)")
proc unary(self: Compiler, node: UnaryExpr) {.inline.} =
proc unary(self: Compiler, node: UnaryExpr, compile: bool = true): Name {.discardable.} =
## Compiles all unary expressions
var default: Expression
let fn = Type(kind: Function,
returnType: Type(kind: Any),
args: @[("", self.inferOrError(node.a), default)])
let impl = self.match(node.token.lexeme, fn, node, @[node.a])
self.generateCall(impl, @[node.a], impl.line)
result = self.match(node.token.lexeme, fn, node, @[node.a])
if compile:
self.generateCall(result, @[node.a], result.line)
proc binary(self: Compiler, node: BinaryExpr) {.inline.} =
proc binary(self: Compiler, node: BinaryExpr, compile: bool = true): Name {.discardable.} =
## Compiles all binary expressions
var default: Expression
let fn = Type(kind: Function, returnType: Type(kind: Any), args: @[("", self.inferOrError(node.a), default), ("", self.inferOrError(node.b), default)])
let impl = self.match(node.token.lexeme, fn, node, @[node.a, node.b])
self.generateCall(impl, @[node.a, node.b], impl.line)
result = self.match(node.token.lexeme, fn, node, @[node.a, node.b])
if compile:
self.generateCall(result, @[node.a, node.b], result.line)
proc identifier(self: Compiler, node: IdentExpr, name: Name = nil) =
proc identifier(self: Compiler, node: IdentExpr, name: Name = nil, compile: bool = true): Name {.discardable.} =
## Compiles access to identifiers
var s = name
if s.isNil():
s = self.resolveOrError(node)
var t = self.findByType(s.ident.token.lexeme, Type(kind: All))
s = t[0] # Shadowing!
result = s
if not compile:
return
if s.isConst:
# Constants are always emitted as Load* instructions
# no matter the scope depth
@ -1943,7 +1999,7 @@ proc identifier(self: Compiler, node: IdentExpr, name: Name = nil) =
self.emitBytes(self.getStackPos(s).toTriple(), s.ident.token.line)
proc assignment(self: Compiler, node: ASTNode) =
proc assignment(self: Compiler, node: ASTNode, compile: bool = true): Name {.discardable.} =
## Compiles assignment expressions
case node.kind:
of assignExpr:
@ -1956,6 +2012,8 @@ proc assignment(self: Compiler, node: ASTNode) =
self.error(&"cannot reassign '{name.token.lexeme}' (value is immutable)", name)
self.check(node.value, r.valueType)
self.expression(node.value)
if not compile:
return
if not r.isClosedOver:
self.emitByte(StoreVar, node.token.line)
self.emitBytes(self.getStackPos(r).toTriple(), node.token.line)
@ -1966,8 +2024,7 @@ proc assignment(self: Compiler, node: ASTNode) =
self.emitByte(StoreClosure, node.token.line)
self.emitBytes(self.getClosurePos(r).toTriple(), node.token.line)
of setItemExpr:
let typ = self.inferOrError(SetItemExpr(node))
discard # TODO
else:
self.error(&"invalid AST node of kind {node.kind} at assignment(): {node} (This is an internal error and most likely a bug)")
@ -2043,6 +2100,9 @@ proc generateCall(self: Compiler, fn: Type, args: seq[Expression], line: int) {.
proc prepareFunction(self: Compiler, fn: Name) =
## "Prepares" a function declaration by declaring
## its arguments and typechecking it
# First we declare the function's generics, if it has any.
# This is because the function's return type may in itself
# be a generic, so it needs to exist first
@ -2074,7 +2134,7 @@ proc prepareFunction(self: Compiler, fn: Name) =
file: fn.file,
isConst: false,
ident: argument.name,
valueType: self.inferOrError(argument.valueType, allowGeneric=true),
valueType: self.inferOrError(argument.valueType),
codePos: 0,
isLet: false,
line: argument.name.token.line,
@ -2091,7 +2151,7 @@ proc prepareFunction(self: Compiler, fn: Name) =
fn.valueType.args.add((self.names[^1].ident.token.lexeme, self.names[^1].valueType, default))
# The function needs a return type too!
if not FunDecl(fn.node).returnType.isNil():
fn.valueType.returnType = self.inferOrError(FunDecl(fn.node).returnType, allowGeneric=true)
fn.valueType.returnType = self.inferOrError(FunDecl(fn.node).returnType)
proc generateCall(self: Compiler, fn: Name, args: seq[Expression], line: int) =
@ -2108,7 +2168,7 @@ proc generateCall(self: Compiler, fn: Name, args: seq[Expression], line: int) =
self.emitByte(LoadUInt64, line)
self.emitBytes(self.chunk.writeConstant(fn.codePos.toLong()), line)
else:
discard
discard # Unreachable
if fn.valueType.forwarded:
self.forwarded.add((fn, self.chunk.consts.high() - 7))
self.emitByte(LoadUInt64, line)
@ -2172,14 +2232,14 @@ proc specialize(self: Compiler, name: Name, args: seq[Expression]): Name =
discard # TODO: Custom user-defined types
proc callExpr(self: Compiler, node: CallExpr): Name {.discardable.} =
proc call(self: Compiler, node: CallExpr, compile: bool = true): Name {.discardable.} =
## Compiles code to call a chain of 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.infer(argument, allowGeneric=false) # We don't use inferOrError so that we can raise a more appropriate error message
kind = self.infer(argument) # We don't use inferOrError so that we can raise a more appropriate error message
if kind.isNil():
if argument.kind == NodeKind.identExpr:
self.error(&"reference to undeclared name '{argument.token.lexeme}'", argument)
@ -2187,7 +2247,7 @@ proc callExpr(self: Compiler, node: CallExpr): Name {.discardable.} =
args.add(("", kind, default))
argExpr.add(argument)
for i, argument in node.arguments.keyword:
kind = self.infer(argument.value, allowGeneric=false)
kind = self.infer(argument.value)
if kind.isNil():
if argument.value.kind == NodeKind.identExpr:
self.error(&"reference to undeclared name '{argument.value.token.lexeme}'", argument.value)
@ -2199,7 +2259,8 @@ proc callExpr(self: Compiler, node: CallExpr): Name {.discardable.} =
# Calls like hi()
result = self.match(IdentExpr(node.callee).name.lexeme, Type(kind: Function, returnType: Type(kind: All), args: args), node, argExpr)
# Now we call it
self.generateCall(result, argExpr, node.token.line)
if compile:
self.generateCall(result, argExpr, node.token.line)
of NodeKind.callExpr:
# Calling a call expression, like hello()()
var node: Expression = node
@ -2215,7 +2276,7 @@ proc callExpr(self: Compiler, node: CallExpr): Name {.discardable.} =
# the way back to the first one earlier) and work
# our way to the innermost call
for exp in reversed(all):
self.callExpr(exp)
self.call(exp, compile)
# TODO: Calling lambdas on-the-fly (i.e. on the same line)
else:
let typ = self.infer(node)
@ -2252,7 +2313,7 @@ proc expression(self: Compiler, node: Expression) =
## Compiles all expressions
case node.kind:
of NodeKind.callExpr:
self.callExpr(CallExpr(node))
self.call(CallExpr(node))
of NodeKind.getItemExpr:
self.getItemExpr(GetItemExpr(node))
of NodeKind.pragmaExpr:
@ -2377,8 +2438,7 @@ proc breakStmt(self: Compiler, node: BreakStmt) =
proc assertStmt(self: Compiler, node: AssertStmt) =
## Compiles assert statements (raise
## AssertionError if the expression is falsey)
self.expression(node.expression)
self.emitByte(OpCode.Assert, node.token.line)
# TODO
proc forEachStmt(self: Compiler, node: ForEachStmt) =
@ -2388,7 +2448,7 @@ proc forEachStmt(self: Compiler, node: ForEachStmt) =
proc importStmt(self: Compiler, node: ImportStmt) =
## Imports a module at compile time
self.declareName(node)
self.declare(node)
var module = self.names[^1]
try:
self.compileModule(module)
@ -2540,7 +2600,7 @@ proc varDecl(self: Compiler, node: VarDecl) =
typ = expected
self.expression(node.value)
self.emitByte(AddVar, node.token.line)
self.declareName(node)
self.declare(node)
var name = self.names[^1]
name.valueType = typ
@ -2587,7 +2647,6 @@ proc funDecl(self: Compiler, node: FunDecl, name: Name) =
else:
self.chunk.functions.add(0.toDouble())
if BlockStmt(node.body).code.len() == 0:
raise newException(IndexDefect, "")
self.error("cannot declare function with empty body")
# Since the deferred array is a linear
# sequence of instructions and we want
@ -2655,12 +2714,12 @@ proc declaration(self: Compiler, node: Declaration) =
## the first time
case node.kind:
of NodeKind.funDecl:
var name = self.declareName(node)
var name = self.declare(node)
if name.isGeneric:
# We typecheck generics immediately
self.funDecl(FunDecl(node), name)
of NodeKind.typeDecl:
self.declareName(node)
self.declare(node)
of NodeKind.varDecl:
# We compile this immediately because we
# need to keep the stack in the right state
@ -2708,7 +2767,7 @@ proc compileModule(self: Compiler, module: Name) =
var moduleName = module.path & ".pn"
for i, searchPath in moduleLookupPaths:
if searchPath == "":
path = joinPath(getCurrentDir(), joinPath(splitPath(self.file).head, moduleName))
path = absolutePath(joinPath(splitPath(self.file).head, moduleName))
else:
path = joinPath(searchPath, moduleName)
if fileExists(path):

View File

@ -276,6 +276,7 @@ type
isEnum*: bool
isRef*: bool
parent*: IdentExpr
value*: Expression
Pragma* = ref object of Expression
name*: IdentExpr

View File

@ -139,9 +139,8 @@ proc newOperatorTable: OperatorTable =
result.tokens = @[]
for prec in Precedence:
result.precedence[prec] = @[]
# Assignment and attribute accessing are (currently)
# the only builtins that cannot be easily implemented
# from within peon itself
# These operators are currently not built-in
# due to compiler limitations
result.addOperator("=")
result.addOperator(".")
@ -735,7 +734,7 @@ proc importStmt(self: Parser, fromStmt: bool = false): Statement =
var path = ""
for i, searchPath in moduleLookupPaths:
if searchPath == "":
path = joinPath(getCurrentDir(), joinPath(splitPath(self.file).head, moduleName))
path = absolutePath(joinPath(splitPath(self.file).head, moduleName))
else:
path = joinPath(searchPath, moduleName)
if fileExists(path):
@ -986,6 +985,8 @@ proc parseGenericConstraint(self: Parser): Expression =
result = newBinaryExpr(result, self.step(), self.parseGenericConstraint())
of "~":
result = newUnaryExpr(self.step(), result)
of ",":
discard # Comma is handled in parseGenerics()
else:
self.error("invalid type constraint in generic declaration")
@ -1183,9 +1184,9 @@ proc typeDecl(self: Parser): TypeDecl =
## Parses type declarations
let token = self.peek(-1)
self.expect(Identifier, "expecting type name after 'type'")
var name = newIdentExpr(self.peek(-1), self.scopeDepth)
let isPrivate = not self.match("*")
self.checkDecl(isPrivate)
var name = newIdentExpr(self.peek(-1), self.scopeDepth)
var fields: seq[tuple[name: IdentExpr, valueType: Expression, isPrivate: bool]] = @[]
var defaults: seq[Expression] = @[]
var generics: seq[tuple[name: IdentExpr, cond: Expression]] = @[]
@ -1194,44 +1195,58 @@ proc typeDecl(self: Parser): TypeDecl =
if self.match(LeftBracket):
self.parseGenerics(result)
self.expect("=", "expecting '=' after type name")
case self.step().lexeme:
var hasNone = false
case self.peek().lexeme:
of "ref":
discard self.step()
self.expect("object", "expecting 'object' after 'ref'")
result.isRef = true
of "enum":
discard self.step()
result.isEnum = true
of "object":
discard self.step()
discard # Default case
else:
self.error("invalid syntax")
if not result.isEnum and self.match("of"):
self.expect(Identifier, "expecting parent type name after 'of'")
result.parent = newIdentExpr(self.peek(-1))
self.expect(LeftBrace, "expecting '{' after type declaration")
if self.match(TokenType.Pragma):
for pragma in self.parsePragmas():
pragmas.add(pragma)
var
argName: IdentExpr
argPrivate: bool
argType: Expression
while not self.match(RightBrace) and not self.done():
self.expect(Identifier, "expecting field name")
argName = newIdentExpr(self.peek(-1), self.scopeDepth)
if not result.isEnum:
argPrivate = not self.match("*")
self.expect(":", "expecting ':' after field name")
argType = self.expression()
result.fields.add((argName, argType, argPrivate))
if self.match("="):
result.defaults.add(self.expression())
else:
result.fields.add((argName, nil, false))
if not result.isEnum:
self.expect(";", "expecting semicolon after type field declaration")
else:
if not self.check(RightBrace):
self.expect(",", "expecting comma after enum field declaration")
hasNone = true
if hasNone:
result.value = self.expression()
while not self.check(";"):
case self.peek().lexeme:
of "|": # Type unions!
result.value = newBinaryExpr(result.value, self.step(), self.expression())
else:
discard
else:
if not result.isEnum and not hasNone and self.match("of"):
self.expect(Identifier, "expecting parent type name after 'of'")
result.parent = newIdentExpr(self.peek(-1))
if not self.match(";"): # Type has no fields. Usually for unions or aliases
self.expect(LeftBrace, "expecting '{' after type declaration")
if self.match(TokenType.Pragma):
for pragma in self.parsePragmas():
pragmas.add(pragma)
var
argName: IdentExpr
argPrivate: bool
argType: Expression
while not self.match(RightBrace) and not self.done():
self.expect(Identifier, "expecting field name")
argName = newIdentExpr(self.peek(-1), self.scopeDepth)
if not result.isEnum:
argPrivate = not self.match("*")
self.expect(":", "expecting ':' after field name")
argType = self.expression()
result.fields.add((argName, argType, argPrivate))
if self.match("="):
result.defaults.add(self.expression())
else:
result.fields.add((argName, nil, false))
if not result.isEnum:
self.expect(";", "expecting semicolon after type field declaration")
else:
if not self.check(RightBrace):
self.expect(",", "expecting comma after enum field declaration")
result.pragmas = pragmas

View File

@ -85,6 +85,8 @@ proc repl =
elif input == "#clear":
stdout.write("\x1Bc")
continue
elif input == "":
continue
else:
current &= input & "\n"
if current.len() == 0:

View File

@ -1,12 +1,14 @@
# Builtin arithmetic operators for Peon
# Note: Most of these do nothing on their own. All they do
# Note: These stubs do nothing on their own. All they do
# is serve as placeholders for emitting specific VM
# instructions. They're implemented this way because:
# - They tie into the existing type system nicely
# - It makes the implementation easier and more flexible
import values;
operator `+`*[T: int | uint64 | int32 | uint32 | int16 | uint16 | int8 | uint8](a, b: T): T {
operator `+`*[T: Integer](a, b: T): T {
#pragma[magic: "Add", pure]
}
@ -21,7 +23,7 @@ operator `+`(a, b: float32): float32 {
}
operator `-`*[T: int | uint64 | int32 | uint32 | int16 | uint16 | int8 | uint8](a, b: T): T {
operator `-`*[T: Integer](a, b: T): T {
#pragma[magic: "Subtract", pure]
}
@ -36,12 +38,12 @@ operator `-`*(a, b: float32): float32 {
}
operator `-`*[T: int | int32 | int16 | int8](a: T): T {
operator `-`*[T: SignedInteger](a: T): T {
#pragma[magic: "Negate", pure]
}
operator `-`*[T: uint64 | uint32 | uint16 | uint8](a: T): T {
operator `-`*[T: UnsignedInteger](a: T): T {
#pragma[magic: "", error: "unsigned integer cannot be negative"]
}
@ -61,7 +63,7 @@ operator `-`*(a: inf): inf {
}
operator `*`*[T: int | uint64 | int32 | uint32 | int16 | uint16 | int8 | uint8](a, b: T): T {
operator `*`*[T: Integer](a, b: T): T {
#pragma[magic: "Multiply", pure]
}
@ -76,12 +78,12 @@ operator `*`*(a, b: float32): float32 {
}
operator `/`*[T: int | int32 | int16 | int8](a, b: T): T {
operator `/`*[T: SignedInteger](a, b: T): T {
#pragma[magic: "SignedDivide", pure]
}
operator `/`*[T: uint64 | uint32 | uint16 | uint8](a, b: T): T {
operator `/`*[T: UnsignedInteger](a, b: T): T {
#pragma[magic: "Divide", pure]
}
@ -96,16 +98,16 @@ operator `/`*(a, b: float32): float32 {
}
operator `**`*[T: int | int32 | int16 | int8](a, b: T): T {
operator `**`*[T: SignedInteger](a, b: T): T {
#pragma[magic: "SignedPow", pure]
}
operator `**`*[T: uint64 | uint32 | uint16 | uint8](a, b: T): T {
operator `**`*[T: UnsignedInteger](a, b: T): T {
#pragma[magic: "Pow", pure]
}
operator `%`*(a, b: int64): int64 {
operator `%`*[T: SignedInteger](a, b: T): T {
#pragma[magic: "SignedMod", pure]
}

View File

@ -1,4 +1,6 @@
# Bitwise operations on primitive types
import values;
operator `&`*[T: int | uint64 | int32 | uint32 | int16 | uint16 | int8 | uint8](a, b: T): T {
#pragma[magic: "And", pure]

View File

@ -1,30 +1,31 @@
# Comparison operators
import values;
operator `>`*[T: int | uint64 | int32 | uint32 | int16 | uint16 | int8 | uint8 | float | float32](a, b: T): bool {
operator `>`*[T: Number](a, b: T): bool {
#pragma[magic: "GreaterThan", pure]
}
operator `<`*[T: int | uint64 | int32 | uint32 | int16 | uint16 | int8 | uint8 | float | float32](a, b: T): bool {
operator `<`*[T: Number](a, b: T): bool {
#pragma[magic: "LessThan", pure]
}
operator `==`*[T: int | uint64 | int32 | uint32 | int16 | uint16 | int8 | uint8 | float | float32](a, b: T): bool {
operator `==`*[T: Number](a, b: T): bool {
#pragma[magic: "Equal", pure]
}
operator `!=`*[T: int | uint64 | int32 | uint32 | int16 | uint16 | int8 | uint8 | float | float32](a, b: T): bool {
operator `!=`*[T: Number](a, b: T): bool {
#pragma[magic: "NotEqual", pure]
}
operator `>=`*[T: int | uint64 | int32 | uint32 | int16 | uint16 | int8 | uint8 | float | float32](a, b: T): bool {
operator `>=`*[T: Number](a, b: T): bool {
#pragma[magic: "GreaterOrEqual", pure]
}
operator `<=`*[T: int | uint64 | int32 | uint32 | int16 | uint16 | int8 | uint8 | float | float32](a, b: T): bool {
operator `<=`*[T: Number](a, b: T): bool {
#pragma[magic: "LessOrEqual", pure]
}

View File

@ -1,4 +1,6 @@
# Logical operators
import values;
operator `and`*(a, b: bool): bool {
#pragma[magic: "LogicalAnd", pure]

View File

@ -1,4 +1,5 @@
# Various miscellaneous utilities
import values;
# Some useful builtins

View File

@ -0,0 +1,24 @@
# Stub type declarations for peon's intrinsic types
type int64* = object {#pragma[magic: "int64"]}
type int32* = object {#pragma[magic: "int32"]}
type int16* = object {#pragma[magic: "int16"]}
type int8* = object {#pragma[magic: "int8"]}
type uint64* = object {#pragma[magic: "uint64"]}
type uint32* = object {#pragma[magic: "uint32"]}
type uint16* = object {#pragma[magic: "uint16"]}
type uint8* = object {#pragma[magic: "uint8"]}
type float64* = object {#pragma[magic: "float64"]}
type float32* = object {#pragma[magic: "float32"]}
type bool* = object {#pragma[magic: "bool"]}
type string* = object {#pragma[magic: "string"]}
type any* = object {#pragma[magic: "any"]}
# Some convenience aliases
type int* = int64;
type float* = float64;
type SignedInteger* = int64 | int32 | int16 | int8;
type UnsignedInteger* = uint64 | uint32 | uint16 | uint8;
type Integer* = SignedInteger | UnsignedInteger;
type Number* = Integer | float64 | float32;

View File

@ -1,18 +1,20 @@
## The peon standard library
import builtins/values;
import builtins/arithmetics;
import builtins/bitwise;
import builtins/logical;
import builtins/misc;
import builtins/comparisons;
export values;
export arithmetics;
export bitwise;
export logical;
export misc;
export comparisons;
var version* = 1;
var _private = 5; # Invisible outside the module (underscore is to silence warning)
var test* = 0x60;

View File

@ -32,9 +32,10 @@ proc printError(file, line: string, lineNo: int, pos: tuple[start, stop: int], f
if not fn.isNil() and fn.kind == funDecl:
stderr.styledWrite(fgRed, styleBright, " in function ", fgYellow, FunDecl(fn).name.token.lexeme)
stderr.styledWriteLine(styleBright, fgDefault, ": ", msg)
stderr.styledWrite(fgRed, styleBright, "Source line: ", resetStyle, fgDefault, line[0..<pos.start])
stderr.styledWrite(fgRed, styleUnderscore, line[pos.start..pos.stop])
stderr.styledWriteLine(fgDefault, line[pos.stop + 1..^1])
if line.len() > 0:
stderr.styledWrite(fgRed, styleBright, "Source line: ", resetStyle, fgDefault, line[0..<pos.start])
stderr.styledWrite(fgRed, styleUnderscore, line[pos.start..pos.stop])
stderr.styledWriteLine(fgDefault, line[pos.stop + 1..^1])
proc print*(exc: CompileError) =

View File

@ -5,15 +5,18 @@ import std;
fn noReturn(n: int) {
var n = n;
var `17` = 17;
print(n == n); # true
print(`17` == 17); # true
return;
}
fn fooBar(a, b: int): int {
var baz = 38;
print(baz == 38);
return a;
}
noReturn(1);
print(fooBar(1, 3) == 1); # true
print(fooBar(1, 3) == 1); # true

View File

@ -1,12 +1,12 @@
# Tests generic functions
# Another test for generic functions
import std;
fn sum[T: any](a, b: T): T {
fn sum[T: int | int32](a, b: T): T {
return a + b;
}
print(sum(1, 2)); # Prints 3
print(sum(1'i32, 2'i32)); # Also prints 3!
# print(sum(1'i16, 2'i16)); # Will not work if uncommented: print is not defined for i16!
#print(sum(1.0, 2.0)); # Will fail to compile

7
tests/generics3.pn Normal file
View File

@ -0,0 +1,7 @@
# Another test for generic functions
import std;
fn sum[T: any](a: T, b: T): T {
return a + b;
}