Fixed test suite and added test suite runner. Also yeeted templates

This commit is contained in:
Mattia Giambirtone 2023-07-04 14:33:18 +02:00
parent 20a2f07eba
commit 88073194f2
Signed by: nocturn9x
GPG Key ID: 8270F9F467971E59
10 changed files with 138 additions and 133 deletions

40
run_tests.py Normal file
View File

@ -0,0 +1,40 @@
import sys
from tqdm import tqdm
import shlex
import subprocess
from pathlib import Path
NIM_FLAGS = "-d:debug"
PEON_FLAGS = "-n --showMismatches"
EXCLUDE = ["fib.pn", "gc.pn", "import_a.pn", "import_b.pn", "fizzbuzz.pn"]
def main() -> int:
tests: set[Path] = set()
skipped: set[Path] = set()
failed: set[Path] = set()
# We consume the generator now because I want tqdm to show the progress bar!
test_files = list((Path.cwd() / "tests").resolve(strict=True).glob("*.pn"))
for test_file in tqdm(test_files):
tests.add(test_file)
if test_file.name in EXCLUDE:
skipped.add(test_file)
continue
try:
cmd = f"nim {NIM_FLAGS} r src/main.nim {test_file} {PEON_FLAGS}"
out = subprocess.run(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except subprocess.CalledProcessError as e:
print(f"An error occurred while executing test -> {type(e).__name__}: {e}")
failed.add(test_file)
continue
if not all(map(lambda s: s == b"true", out.stdout.splitlines())):
failed.add(test_file)
total = len(tests)
successful = len(tests - failed - skipped)
print(f"Collected {total} tests ({successful}/{total}) passed, {len(failed)}/{total} failed, {len(skipped)}/{total} skipped)")
return len(failed) == 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -489,9 +489,11 @@ proc compare*(self: Compiler, a, b: Type): bool =
continue
if argA.name != argB.name:
return false
if a.forwarded or b.forwarded:
if (a.forwarded or b.forwarded) and (not a.fun.isNil() and not b.fun.isNil()):
# We need to be more strict when checking forward
# declarations
# declarations. Also, if either of the nodes is null,
# then it means that object is coming from compiler internals
# and we don't really care about its pragmas
if b.fun.pragmas.len() != a.fun.pragmas.len():
return false
for (pragA, pragB) in zip(a.fun.pragmas, b.fun.pragmas):
@ -535,7 +537,7 @@ proc compare*(self: Compiler, a, b: Type): bool =
for constraint in b.cond:
if not self.compare(constraint.kind, a) or not constraint.match:
return false
return true
return true
if a.kind == Any or b.kind == Any:
# Here we already know that neither of
# these types are nil, so we can always
@ -870,7 +872,7 @@ proc match*(self: Compiler, name: string, kind: Type, node: ASTNode = nil, allow
self.error(&"expecting an implementation for function '{impl[0].ident.token.lexeme}' declared in module '{impl[0].owner.ident.token.lexeme}' at line {impl[0].ident.token.line} of type '{self.stringify(impl[0].valueType)}'")
result = impl[0]
result.resolved = true
if result.kind == NameKind.Var:
if result.kind == NameKind.Var and not result.valueType.nameObj.isNil():
# We found a function bound to a variable,
# so we return the original function's name object
result = result.valueType.nameObj
@ -969,8 +971,6 @@ proc declare*(self: Compiler, node: ASTNode): Name {.discardable.} =
kind: NameKind.Function,
belongsTo: self.currentFunction,
isReal: true)
if node.isTemplate:
fn.valueType.compiled = true
if node.generics.len() > 0:
fn.isGeneric = true
self.names.add(fn)

View File

@ -826,8 +826,12 @@ method prepareFunction(self: BytecodeCompiler, fn: Name) =
self.error("cannot declare more than 16777215 variables at a time")
inc(self.stackIndex)
typ = self.inferOrError(argument.valueType)
if self.compare(typ, "auto".toIntrinsic()):
# We can't use self.compare(), because it would
# always just return true
if typ.kind == Auto:
fn.valueType.isAuto = true
# Magic trick! We turn auto into any, just
# to make our lives easier
typ = "any".toIntrinsic()
self.names.add(Name(depth: fn.depth + 1,
isPrivate: true,
@ -843,7 +847,7 @@ method prepareFunction(self: BytecodeCompiler, fn: Name) =
kind: NameKind.Argument,
node: argument.name,
position: self.stackIndex,
isReal: not node.isTemplate
isReal: true
))
if node.arguments.high() - node.defaults.high() <= node.arguments.high():
# There's a default argument!
@ -855,12 +859,14 @@ method prepareFunction(self: BytecodeCompiler, fn: Name) =
# The function needs a return type too!
if not node.returnType.isNil():
fn.valueType.returnType = self.inferOrError(node.returnType)
if self.compare(fn.valueType.returnType, "auto".toIntrinsic()):
if fn.valueType.returnType.kind == Auto:
fn.valueType.isAuto = true
# Here we don't bother changing the return type
# to any because returnStmt() will see the auto
# type and change it accordingly once we know what
# we're trying to return for the first time
fn.position = self.stackIndex
self.stackIndex = idx
if node.isTemplate:
fn.valueType.compiled = true
proc prepareAutoFunction(self: BytecodeCompiler, fn: Name, args: seq[tuple[name: string, kind: Type, default: Expression]]): Name =
@ -896,10 +902,8 @@ proc prepareAutoFunction(self: BytecodeCompiler, fn: Name, args: seq[tuple[name:
kind: NameKind.Argument,
node: argument.name,
position: self.stackIndex,
isReal: not node.isTemplate
isReal: true
))
if node.isTemplate:
fn.valueType.compiled = true
fn.valueType.args = args
fn.position = self.stackIndex
self.stackIndex = idx
@ -1331,24 +1335,7 @@ method call(self: BytecodeCompiler, node: CallExpr, compile: bool = true): Type
result = result.returnType
self.dispatchDelayedPragmas(impl)
if compile:
# Lambdas can't be templates :P
if impl.valueType.fun.kind == funDecl and FunDecl(impl.valueType.fun).isTemplate:
for arg in reversed(argExpr):
self.expression(arg)
let code = BlockStmt(FunDecl(impl.valueType.fun).body).code
for i, decl in code:
if i < code.high():
self.declaration(decl)
else:
# The last expression in a template
# is its "return value", so we compute
# it, but don't pop it off the stack
if decl.kind == exprStmt:
self.expression(ExprStmt(decl).expression)
else:
self.declaration(decl)
else:
self.generateCall(impl, argExpr, node.token.line)
self.generateCall(impl, argExpr, node.token.line)
of NodeKind.callExpr:
# Calling a call expression, like hello()()
var node: Expression = node
@ -1707,6 +1694,8 @@ proc continueStmt(self: BytecodeCompiler, node: ContinueStmt, compile: bool = tr
break
if not found:
self.error(&"unknown block name '{node.label.token.lexeme}'", node.label)
if blocks[^1].start > 16777215:
self.error("too much code to jump over in continue statement")
if compile:
self.emitByte(Jump, node.token.line)
self.emitBytes(blocks[^1].start.toTriple(), node.token.line)
@ -1979,71 +1968,69 @@ proc funDecl(self: BytecodeCompiler, node: FunDecl, name: Name) =
self.currentFunction = name
let stackIdx = self.stackIndex
self.stackIndex = name.position
if not node.isTemplate:
# A function's code is just compiled linearly
# and then jumped over
name.valueType.compiled = true
jmp = self.emitJump(JumpForwards, node.token.line)
name.codePos = self.chunk.code.len()
name.valueType.location = name.codePos
# We let our debugger know this function's boundaries
self.chunk.functions.add(self.chunk.code.len().toTriple())
self.functions.add((start: self.chunk.code.len(), stop: 0, pos: self.chunk.functions.len() - 3, fn: name))
var offset = self.functions[^1]
var idx = self.chunk.functions.len()
self.chunk.functions.add(0.toTriple()) # Patched it later
self.chunk.functions.add(uint8(node.arguments.len()))
if not node.name.isNil():
self.chunk.functions.add(name.ident.token.lexeme.len().toDouble())
var s = name.ident.token.lexeme
if s.len() >= uint16.high().int:
s = node.name.token.lexeme[0..uint16.high()]
self.chunk.functions.add(s.toBytes())
# A function's code is just compiled linearly
# and then jumped over
name.valueType.compiled = true
jmp = self.emitJump(JumpForwards, node.token.line)
name.codePos = self.chunk.code.len()
name.valueType.location = name.codePos
# We let our debugger know this function's boundaries
self.chunk.functions.add(self.chunk.code.len().toTriple())
self.functions.add((start: self.chunk.code.len(), stop: 0, pos: self.chunk.functions.len() - 3, fn: name))
var offset = self.functions[^1]
var idx = self.chunk.functions.len()
self.chunk.functions.add(0.toTriple()) # Patched it later
self.chunk.functions.add(uint8(node.arguments.len()))
if not node.name.isNil():
self.chunk.functions.add(name.ident.token.lexeme.len().toDouble())
var s = name.ident.token.lexeme
if s.len() >= uint16.high().int:
s = node.name.token.lexeme[0..uint16.high()]
self.chunk.functions.add(s.toBytes())
else:
self.chunk.functions.add(0.toDouble())
if BlockStmt(node.body).code.len() == 0:
self.error("cannot declare function with empty body")
var last: Declaration
self.beginScope()
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)
self.declaration(decl)
last = decl
let typ = self.currentFunction.valueType.returnType
var hasVal: bool = false
case self.currentFunction.valueType.fun.kind:
of NodeKind.funDecl:
hasVal = FunDecl(self.currentFunction.valueType.fun).hasExplicitReturn
of NodeKind.lambdaExpr:
hasVal = LambdaExpr(self.currentFunction.valueType.fun).hasExplicitReturn
else:
self.chunk.functions.add(0.toDouble())
if BlockStmt(node.body).code.len() == 0:
self.error("cannot declare function with empty body")
var last: Declaration
self.beginScope()
for decl in BlockStmt(node.body).code:
if not last.isNil():
if last.kind == returnStmt:
self.warning(UnreachableCode, "code after 'return' statement is unreachable", nil, decl)
self.declaration(decl)
last = decl
let typ = self.currentFunction.valueType.returnType
var hasVal: bool = false
case self.currentFunction.valueType.fun.kind:
of NodeKind.funDecl:
hasVal = FunDecl(self.currentFunction.valueType.fun).hasExplicitReturn
of NodeKind.lambdaExpr:
hasVal = LambdaExpr(self.currentFunction.valueType.fun).hasExplicitReturn
else:
discard # Unreachable
if not hasVal and not typ.isNil():
# There is no explicit return statement anywhere in the function's
# body: while this is not a tremendously useful piece of information
# (since the presence of at least one doesn't mean all control flow
# cases are covered), it definitely is an error worth reporting
self.error("function has an explicit return type, but no return statement was found", node)
hasVal = hasVal and not typ.isNil()
for jump in self.currentFunction.valueType.retJumps:
self.patchJump(jump)
self.endScope()
# Terminates the function's context
let stop = self.chunk.code.len().toTriple()
self.emitByte(OpCode.Return, self.peek().token.line)
if hasVal:
self.emitByte(1, self.peek().token.line)
else:
self.emitByte(0, self.peek().token.line)
self.chunk.functions[idx] = stop[0]
self.chunk.functions[idx + 1] = stop[1]
self.chunk.functions[idx + 2] = stop[2]
offset.stop = self.chunk.code.len()
# Well, we've compiled everything: time to patch
# the jump offset
self.patchJump(jmp)
discard # Unreachable
if not hasVal and not typ.isNil():
# There is no explicit return statement anywhere in the function's
# body: while this is not a tremendously useful piece of information
# (since the presence of at least one doesn't mean all control flow
# cases are covered), it definitely is an error worth reporting
self.error("function has an explicit return type, but no return statement was found", node)
hasVal = hasVal and not typ.isNil()
for jump in self.currentFunction.valueType.retJumps:
self.patchJump(jump)
self.endScope()
# Terminates the function's context
let stop = self.chunk.code.len().toTriple()
self.emitByte(OpCode.Return, self.peek().token.line)
if hasVal:
self.emitByte(1, self.peek().token.line)
else:
self.emitByte(0, self.peek().token.line)
self.chunk.functions[idx] = stop[0]
self.chunk.functions[idx + 1] = stop[1]
self.chunk.functions[idx + 2] = stop[2]
offset.stop = self.chunk.code.len()
# Well, we've compiled everything: time to patch
# the jump offset
self.patchJump(jmp)
# Restores the enclosing function (if any).
# Makes nested calls work (including recursion)
self.currentFunction = function

View File

@ -272,7 +272,6 @@ type
defaults*: seq[Expression]
isAsync*: bool
isGenerator*: bool
isTemplate*: bool
isPure*: bool
returnType*: Expression
hasExplicitReturn*: bool
@ -767,7 +766,7 @@ proc `$`*(self: ASTNode): string =
result &= &"Var(name={self.name}, value={self.value}, const={self.isConst}, private={self.isPrivate}, type={self.valueType}, pragmas={self.pragmas})"
of funDecl:
var self = FunDecl(self)
result &= &"""FunDecl(name={self.name}, body={self.body}, type={self.returnType}, arguments=[{self.arguments.join(", ")}], defaults=[{self.defaults.join(", ")}], generics=[{self.generics.join(", ")}], async={self.isAsync}, generator={self.isGenerator}, template={self.isTemplate}, private={self.isPrivate}, pragmas={self.pragmas})"""
result &= &"""FunDecl(name={self.name}, body={self.body}, type={self.returnType}, arguments=[{self.arguments.join(", ")}], defaults=[{self.defaults.join(", ")}], generics=[{self.generics.join(", ")}], async={self.isAsync}, generator={self.isGenerator}, private={self.isPrivate}, pragmas={self.pragmas})"""
of typeDecl:
var self = TypeDecl(self)
result &= &"""TypeDecl(name={self.name}, fields={self.fields}, defaults={self.defaults}, private={self.isPrivate}, pragmas={self.pragmas}, generics={self.generics}, parent={self.parent}, ref={self.isRef}, enum={self.isEnum}, value={self.value})"""

View File

@ -306,7 +306,7 @@ proc varDecl(self: Parser, isLet: bool = false,
isConst: bool = false): Declaration
proc parseFunExpr(self: Parser): LambdaExpr
proc funDecl(self: Parser, isAsync: bool = false, isGenerator: bool = false,
isLambda: bool = false, isOperator: bool = false, isTemplate: bool = false): Declaration
isLambda: bool = false, isOperator: bool = false): Declaration
proc declaration(self: Parser): Declaration
proc parse*(self: Parser, tokens: seq[Token], file: string, lines: seq[tuple[start, stop: int]], source: string, persist: bool = false): seq[Declaration]
proc findOperators(self: Parser, tokens: seq[Token])
@ -341,7 +341,7 @@ proc primary(self: Parser): Expression =
self.expect(RightParen, "unterminated parenthesized expression")
of Yield:
let tok = self.step()
if self.currentFunction.isNil() or (self.currentFunction.kind == funDecl and FunDecl(self.currentFunction).isTemplate):
if self.currentFunction.isNil():
self.error("'yield' cannot be used outside functions", tok)
elif self.currentFunction.token.kind != Generator:
# It's easier than doing conversions for lambda/funDecl
@ -355,7 +355,7 @@ proc primary(self: Parser): Expression =
result.file = self.file
of Await:
let tok = self.step()
if self.currentFunction.isNil() or (self.currentFunction.kind == funDecl and FunDecl(self.currentFunction).isTemplate):
if self.currentFunction.isNil():
self.error("'await' cannot be used outside functions", tok)
if self.currentFunction.token.kind != Coroutine:
self.error("'await' can only be used inside coroutines", tok)
@ -674,7 +674,7 @@ proc breakStmt(self: Parser): Statement =
proc deferStmt(self: Parser): Statement =
## Parses defer statements
let tok = self.peek(-1)
if self.currentFunction.isNil() or (self.currentFunction.kind == funDecl and FunDecl(self.currentFunction).isTemplate):
if self.currentFunction.isNil():
self.error("'defer' cannot be used outside functions")
endOfLine("missing semicolon after 'defer'")
result = newDeferStmt(self.expression(), tok)
@ -698,7 +698,7 @@ proc continueStmt(self: Parser): Statement =
proc returnStmt(self: Parser): Statement =
## Parses return statements
let tok = self.peek(-1)
if self.currentFunction.isNil() or (self.currentFunction.kind == funDecl and FunDecl(self.currentFunction).isTemplate):
if self.currentFunction.isNil():
self.error("'return' cannot be used outside functions")
var value: Expression
if not self.check(Semicolon):
@ -719,7 +719,7 @@ proc returnStmt(self: Parser): Statement =
proc yieldStmt(self: Parser): Statement =
## Parses yield statements
let tok = self.peek(-1)
if self.currentFunction.isNil() or (self.currentFunction.kind == funDecl and FunDecl(self.currentFunction).isTemplate):
if self.currentFunction.isNil():
self.error("'yield' cannot be outside functions")
elif self.currentFunction.token.kind != Generator:
self.error("'yield' can only be used inside generators")
@ -734,7 +734,7 @@ proc yieldStmt(self: Parser): Statement =
proc awaitStmt(self: Parser): Statement =
## Parses await statements
let tok = self.peek(-1)
if self.currentFunction.isNil() or (self.currentFunction.kind == funDecl and FunDecl(self.currentFunction).isTemplate):
if self.currentFunction.isNil():
self.error("'await' cannot be used outside functions")
if self.currentFunction.token.kind != Coroutine:
self.error("'await' can only be used inside coroutines")
@ -1052,7 +1052,7 @@ proc parseFunExpr(self: Parser): LambdaExpr =
if self.match(LeftParen):
self.parseDeclArguments(arguments, parameter, defaults)
if self.match(":"):
if self.match([Function, Coroutine, Generator, Template]):
if self.match([Function, Coroutine, Generator]):
result.returnType = self.parseFunExpr()
else:
result.returnType = self.expression()
@ -1095,7 +1095,7 @@ proc parseGenerics(self: Parser, decl: Declaration) =
proc funDecl(self: Parser, isAsync: bool = false, isGenerator: bool = false,
isLambda: bool = false, isOperator: bool = false, isTemplate: bool = false): Declaration = # Can't use just FunDecl because it can also return LambdaExpr!
isLambda: bool = false, isOperator: bool = false): Declaration = # Can't use just FunDecl because it can also return LambdaExpr!
## Parses all types of functions, coroutines, generators and operators
## (with or without a name, where applicable)
let tok = self.peek(-1)
@ -1141,7 +1141,7 @@ proc funDecl(self: Parser, isAsync: bool = false, isGenerator: bool = false,
returnType=nil, depth=self.scopeDepth)
if self.match(":"):
# Function has explicit return type
if self.match([Function, Coroutine, Generator, Template]):
if self.match([Function, Coroutine, Generator]):
# The function's return type is another
# function. We specialize this case because
# the type declaration for a function lacks
@ -1155,7 +1155,7 @@ proc funDecl(self: Parser, isAsync: bool = false, isGenerator: bool = false,
self.parseDeclArguments(arguments, parameter, defaults)
if self.match(":"):
# Function's return type
if self.match([Function, Coroutine, Generator, Template]):
if self.match([Function, Coroutine, Generator]):
returnType = self.parseFunExpr()
else:
returnType = self.expression()
@ -1167,7 +1167,6 @@ proc funDecl(self: Parser, isAsync: bool = false, isGenerator: bool = false,
if self.match(TokenType.Pragma):
for pragma in self.parsePragmas():
pragmas.add(pragma)
FunDecl(self.currentFunction).isTemplate = isTemplate
FunDecl(self.currentFunction).body = self.blockStmt()
else:
# This is a forward declaration, so we explicitly
@ -1381,9 +1380,6 @@ proc declaration(self: Parser): Declaration =
of Function:
discard self.step()
result = self.funDecl()
of Template:
discard self.step()
result = self.funDecl(isTemplate=true)
of Coroutine:
discard self.step()
result = self.funDecl(isAsync=true)

View File

@ -38,7 +38,7 @@ type
Yield, Defer, Try, Except,
Finally, Type, Operator, Case,
Enum, From, Ptr, Ref, Object,
Export, Block, Template, Switch,
Export, Block, Switch,
# Literal types
Integer, Float, String, Identifier,

View File

@ -47,7 +47,6 @@ proc fillSymbolTable*(tokenizer: Lexer) =
tokenizer.symbols.addKeyword("object", Object)
tokenizer.symbols.addKeyword("export", Export)
tokenizer.symbols.addKeyword("block", TokenType.Block)
tokenizer.symbols.addKeyword("template", TokenType.Template)
tokenizer.symbols.addKeyword("switch", TokenType.Switch)
# These are more like expressions with a reserved
# name that produce a value of a builtin type,

View File

@ -12,4 +12,3 @@ fn identity(x: int32): int32 {
fn nope[T: int32 | int16](x: T): T {
return identity(x);
}

View File

@ -1,22 +1,18 @@
import std;
print("Counting down...");
var from = 10;
let to = 0;
while from > to {
print(from);
from = from - 1;
}
print("Done!");
print(from == to);
print("Counting up...");
var start = 0;
let stop = 10;
while start < stop {
print(start);
start = start + 1;
}
print("Done!");
print(start == stop);

View File

@ -1,11 +0,0 @@
# A test for templates
import std;
template sum[T: Integer](a, b: T): T {
a + b;
}
print(sum(1, 2) == 3); # true
print(sum(1'i32, 2'i32) == 3'i32); # true