diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 0000000..86f6cab --- /dev/null +++ b/run_tests.py @@ -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()) \ No newline at end of file diff --git a/src/frontend/compiler/compiler.nim b/src/frontend/compiler/compiler.nim index ed1bc84..6c184eb 100644 --- a/src/frontend/compiler/compiler.nim +++ b/src/frontend/compiler/compiler.nim @@ -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) diff --git a/src/frontend/compiler/targets/bytecode/target.nim b/src/frontend/compiler/targets/bytecode/target.nim index 4be5bee..38b3894 100644 --- a/src/frontend/compiler/targets/bytecode/target.nim +++ b/src/frontend/compiler/targets/bytecode/target.nim @@ -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 diff --git a/src/frontend/parsing/ast.nim b/src/frontend/parsing/ast.nim index d06fcae..e9604e6 100644 --- a/src/frontend/parsing/ast.nim +++ b/src/frontend/parsing/ast.nim @@ -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})""" diff --git a/src/frontend/parsing/parser.nim b/src/frontend/parsing/parser.nim index 77d99f0..0f21000 100644 --- a/src/frontend/parsing/parser.nim +++ b/src/frontend/parsing/parser.nim @@ -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) diff --git a/src/frontend/parsing/token.nim b/src/frontend/parsing/token.nim index 46da886..6d5c1d2 100644 --- a/src/frontend/parsing/token.nim +++ b/src/frontend/parsing/token.nim @@ -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, diff --git a/src/util/symbols.nim b/src/util/symbols.nim index 4bee751..e060026 100644 --- a/src/util/symbols.nim +++ b/src/util/symbols.nim @@ -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, diff --git a/tests/generics2.pn b/tests/generics2.pn index e110d99..ceda138 100644 --- a/tests/generics2.pn +++ b/tests/generics2.pn @@ -12,4 +12,3 @@ fn identity(x: int32): int32 { fn nope[T: int32 | int16](x: T): T { return identity(x); } - diff --git a/tests/loops.pn b/tests/loops.pn index dda0ef3..222bb6e 100644 --- a/tests/loops.pn +++ b/tests/loops.pn @@ -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); diff --git a/tests/templates.pn b/tests/templates.pn deleted file mode 100644 index 26e1530..0000000 --- a/tests/templates.pn +++ /dev/null @@ -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