diff --git a/README.md b/README.md index 1cd2acd..c0ae2b6 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,11 @@ Work in progress for Peon 0.2.x ## Build -Just run `nimble build`. It should grab all the dependencies for you and produce a `peon` binary in your current working directory +Just run `nimble build`. It should grab all the dependencies, run the test suite and produce a `peon` binary in your current working directory ## Tests Peon is starting to get large enough for it to need an automated test suite (wow, much fancy, such cool), so you can run `nimble test` -to run that. The actual tests aren't using testament because I have a severe case of NIH syndrome, sorry folks! \ No newline at end of file +to run that (it's also ran automatically at every `nimble build`). The tests aren't using testament because I have a severe case of NIH syndrome, +sorry folks! \ No newline at end of file diff --git a/peon.nimble b/peon.nimble index 313f93f..470ab63 100644 --- a/peon.nimble +++ b/peon.nimble @@ -12,7 +12,7 @@ binDir = "bin" # Dependencies requires "nim >= 2.1.1" -requires "jale >= 0.1.1" +# requires "jale >= 0.1.1" before build: exec "nimble test" @@ -20,3 +20,4 @@ before build: task test, "Runs the test suite": exec "nim r tests/tokenize.nim" + exec "nim r tests/parse.nim" diff --git a/src/frontend/compiler/typechecker.nim b/src/frontend/compiler/typechecker.nim index 0a35ee0..7a83468 100644 --- a/src/frontend/compiler/typechecker.nim +++ b/src/frontend/compiler/typechecker.nim @@ -60,7 +60,7 @@ type ## Forward declarations -proc validate(self: TypeChecker, node: ASTNode): TypedNode +proc check(self: TypeChecker, node: ASTNode): TypedNode proc toIntrinsic(name: string): Type proc done(self: TypeChecker): bool {.inline.} proc toRef(self: Type): Type {.inline.} @@ -1015,14 +1015,6 @@ proc match(self: TypeChecker, name: string, sig: TypeSignature, node: ASTNode = of 1: # There's just one match. We're done result = matches[0] - if result.kind == NameKind.Var: - # Variables bound to other names must always - # have this field set - assert not result.assignedName.isNil() - # We found a name bound to a variable, so we - # return the original name object rather than - # the wrapper - result = result.assignedName # Extra checks case result.valueType.kind: of Function: @@ -1097,10 +1089,9 @@ proc replaceGenerics(self: TypeChecker, typ: Type, generics: TableRef[string, Ty else: self.error(&"unable to perform generic instantiation for object of type {self.stringify(typ)}") - proc concretize(self: TypeChecker, name: Name, args: seq[TypedExpr], node: ASTNode = nil): Type = - ## Takes in a generic type and its arguments, returning a concrete instantiation + ## Takes in a generic type and its arguments and returns a concrete instantiation ## of it let typ = name.valueType.unwrapType() @@ -1126,7 +1117,6 @@ proc concretize(self: TypeChecker, name: Name, args: seq[TypedExpr], node: ASTNo self.replaceGenerics(typ, replaced) - proc expandTypeConstraints(self: TypeChecker, condition: Expression, list: var seq[tuple[match: bool, kind: Type, value: Expression]], accept: bool = true) = ## Recursively unpacks a type constraint case condition.kind: @@ -1249,7 +1239,8 @@ proc declare(self: TypeChecker, node: ASTNode): Name {.discardable.} = valueType: kind) self.addName(name) of NodeKind.importStmt: - discard + # TODO + self.error("not implemented") of NodeKind.typeDecl: var node = TypeDecl(node) declaredName = node.name.token.lexeme @@ -1272,7 +1263,8 @@ proc declare(self: TypeChecker, node: ASTNode): Name {.discardable.} = valueType: kind) self.addName(name) else: - discard # TODO: enums + self.error("not implemented") + # TODO: enums if not name.isNil(): self.dispatchPragmas(name) return name @@ -1360,7 +1352,7 @@ proc call(self: TypeChecker, node: CallExpr): TypedExpr = result = newTypedCallExpr(node, impl, typedArgs) else: # TODO? - discard + self.error("not implemented") of NodeKind.callExpr: # Calling a call expression, like hello()() # TODO @@ -1378,18 +1370,20 @@ proc call(self: TypeChecker, node: CallExpr): TypedExpr = for exp in all: result = self.call(exp) ]# - discard + self.error("not implemented") of NodeKind.getterExpr: # Calling a.b() # TODO let node = GetterExpr(node.callee) + self.error("not implemented") of NodeKind.lambdaExpr: # Calling a lambda on the fly var node = LambdaExpr(node.callee) + self.error("not implemented") # TODO of NodeKind.genericExpr: # Calling a generic expression - # TODO + self.error("not implemented") var node = GenericExpr(node.callee) # TODO else: @@ -1400,14 +1394,30 @@ proc call(self: TypeChecker, node: CallExpr): TypedExpr = self.error(&"object of type '{self.stringify(typ)}' is not callable", node) +proc ensureBoolean(self: TypeChecker, node: Expression): TypedExpr {.inline.} = + ## Shorthand for checking that a given expression is + ## of type bool + return self.check(node, "bool".toIntrinsic()) + + +proc ensureType(self: TypeChecker, node: Expression): TypedExpr {.inline.} = + ## Ensures the given expression resolves to a type (rather + ## than a value) and returns it + return self.check(node, "typevar".toIntrinsic()) + + proc refExpr(self: TypeChecker, node: Ref): TypedExpr = ## Typechecks ref expressions - result = self.check(node.value, "typevar".toIntrinsic()) + + # "ref" only makes sense with types + result = self.ensureType(node.value) result.kind = result.kind.toRef() proc constExpr(self: TypeChecker, node: ast.Const): TypedExpr = ## Typechecks const expressions + + # "const" makes a type immutable var kind = "any".toIntrinsic().toConst() result = self.check(node.value, kind) result.kind = result.kind.toConst() @@ -1416,7 +1426,7 @@ proc constExpr(self: TypeChecker, node: ast.Const): TypedExpr = proc lentExpr(self: TypeChecker, node: ast.Lent): TypedExpr = ## Typechecks lent expressions - # Only match references + # "lent" only makes sense for references var kind = "any".toIntrinsic().toRef() result = self.check(node.value, kind) # Wrap the result back @@ -1458,29 +1468,23 @@ proc blockStmt(self: TypeChecker, node: BlockStmt): TypedBlockStmt = ## Typechecks block statements self.beginScope() var body: seq[TypedNode] = @[] - for decl in node.code: - body.add(self.validate(decl)) + for decl in node.body: + body.add(self.check(decl)) self.endScope() result = newTypedBlockStmt(node, body) -proc ensureBoolean(self: TypeChecker, node: Expression): TypedExpr {.inline.} = - ## Shorthand for checking that a given expression is - ## of type bool - return self.check(node, "bool".toIntrinsic()) - - proc ifStmt(self: TypeChecker, node: IfStmt): TypedNode = ## Typechecks if/else statements # Check the condition let condition = self.ensureBoolean(node.condition) # Check the body - let then = TypedBlockStmt(self.validate(node.thenBranch)) + let then = TypedBlockStmt(self.check(node.thenBranch)) var otherwise: TypedBlockStmt = nil if not node.elseBranch.isNil(): # Check the else clause, if it exists - otherwise = TypedBlockStmt(self.validate(node.elseBranch)) + otherwise = TypedBlockStmt(self.check(node.elseBranch)) # Note: Peon enforces the body of loops and conditionals to # always be a block statement (for a variety of very good reasons, # as it avoids mistakes like the infamous GOTO fail), so the @@ -1490,11 +1494,11 @@ proc ifStmt(self: TypeChecker, node: IfStmt): TypedNode = proc whileStmt(self: TypeChecker, node: WhileStmt): TypedNode = ## Typechecks C-style while loops + let + condition = self.ensureBoolean(node.condition) # Check the condition + body = TypedBlockStmt(self.check(node.body)) # Check the body + return newTypedWhileStmt(node, body, condition) - # Check the condition - let condition = self.ensureBoolean(node.condition) - # Check the body - return newTypedWhileStmt(node, TypedBlockStmt(self.validate(node.body)), condition) proc varDecl(self: TypeChecker, node: VarDecl): TypedVarDecl = @@ -1510,8 +1514,8 @@ proc varDecl(self: TypeChecker, node: VarDecl): TypedVarDecl = self.error("const declaration requires an initializer", node) else: if node.constant and not node.value.isConst(): - self.error("constant initializer is not a constant", node.value) - init = TypedExpr(self.validate(node.value)) + self.error("const initializer must be a value of constant type", node.value) + init = TypedExpr(self.check(node.value)) typ = init.kind if not node.valueType.isNil(): # Explicit type declaration always takes over @@ -1519,9 +1523,9 @@ proc varDecl(self: TypeChecker, node: VarDecl): TypedVarDecl = # Check that the inferred expression represents a type # and not a value. This is to guard against things # like "var x: 1 = 1;". We unwrap it immediately because - # we don't actually want the valueType field of the variable - # to be a wrapped type, we just do this step as a safety check - typ = self.check(node.valueType, "typevar".toIntrinsic()).kind.unwrapType() + # we don't actually want the returned expression to be + # a wrapped type, we just do this step as a safety check + typ = self.ensureType(node.valueType).kind.unwrapType() if typ.isNil(): self.error("expecting either a type declaration or an initializer value, but neither was found", node) # Now check that the type of the initializer, if it exists, @@ -1535,9 +1539,9 @@ proc varDecl(self: TypeChecker, node: VarDecl): TypedVarDecl = proc funDecl(self: TypeChecker, node: FunDecl, name: Name = nil): TypedFunDecl = ## Typechecks function declarations - # Some things are just not possible + # Some things are just not possible in life 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) + self.error(&"due to compiler limitations, the '{node.name.token.lexeme}' operator cannot be overridden", node.name) var name = name if name.isNil(): name = self.declare(node) @@ -1567,17 +1571,17 @@ proc funDecl(self: TypeChecker, node: FunDecl, name: Name = nil): TypedFunDecl = return if node.body.isNil(): # Forward declaration - # TODO self.endScope() - return - if BlockStmt(node.body).code.len() == 0: + # TODO + self.error("not implemented") + if BlockStmt(node.body).body.len() == 0: self.error("cannot declare function with empty body") # We store the current function to restore # it later let function = self.currentFunction self.currentFunction = name - for decl in BlockStmt(node.body).code: - result.body.body.add(self.validate(decl)) + for decl in BlockStmt(node.body).body: + result.body.body.add(self.check(decl)) self.endScope() # Restores the enclosing function (if any). # Makes nested calls work (including recursion) @@ -1684,9 +1688,9 @@ proc typeDecl(self: TypeChecker, node: TypeDecl, name: Name = nil): TypedTypeDec self.error(&"got node of unexpected type {node.value.kind} at typeDecl()") if not node.parent.isNil(): # Ensure parent is actually a type - var subtype = self.check(node.parent, "typevar".toIntrinsic()) + let subtype = self.ensureType(node.parent) # Grab its name object - var parentName = subtype.getName() + let parentName = subtype.getName() # This should *never* be nil if parentName.isNil(): self.error(&"could not obtain name information for the given object: is it a type?", node.parent) @@ -1713,7 +1717,7 @@ proc pragmaExpr(self: TypeChecker, pragma: Pragma) = self.pragmas[pragma.name.token.lexeme].handler(self, pragma, nil) -proc validate(self: TypeChecker, node: ASTNode): TypedNode = +proc check(self: TypeChecker, node: ASTNode): TypedNode = ## Dispatches typeless AST nodes to typecheck them and turn ## them into typed ones case node.kind: @@ -1723,9 +1727,9 @@ proc validate(self: TypeChecker, node: ASTNode): TypedNode = NodeKind.binExpr, NodeKind.hexExpr, NodeKind.trueExpr, NodeKind.falseExpr, NodeKind.nanExpr, NodeKind.infExpr: result = self.expression(Expression(node)) - of exprStmt: + of NodeKind.exprStmt: let statement = ExprStmt(node) - result = TypedExprStmt(node: statement, expression: TypedExpr(self.validate(statement.expression))) + result = TypedExprStmt(node: statement, expression: TypedExpr(self.check(statement.expression))) of NodeKind.whileStmt: result = self.whileStmt(WhileStmt(node)) of NodeKind.blockStmt: @@ -1746,7 +1750,7 @@ proc validate(self: TypeChecker, node: ASTNode): TypedNode = self.error(&"failed to dispatch node of type {node.kind}", node) -proc validate*(self: TypeChecker, tree: ParseTree, file, source: string, showMismatches: bool = false, +proc check*(self: TypeChecker, tree: ParseTree, file, source: string, showMismatches: bool = false, disabledWarnings: seq[WarningKind] = @[]): seq[TypedNode] = ## Transforms a sequence of typeless AST nodes ## into a sequence of typed AST nodes @@ -1785,10 +1789,12 @@ proc validate*(self: TypeChecker, tree: ParseTree, file, source: string, showMis ident: newIdentExpr(Token(lexeme: "", kind: Identifier)), line: 1) self.addName(main) + var node: TypedNode while not self.done(): - result.add(self.validate(self.step())) - if result[^1].isNil(): - result.delete(result.high()) + node = self.check(self.step()) + if node.isNil(): + continue + result.add(node) assert self.scopeDepth == 0 # Do not close the global scope if # we're being imported diff --git a/src/frontend/compiler/typesystem.nim b/src/frontend/compiler/typesystem.nim index 8df1d21..c204a36 100644 --- a/src/frontend/compiler/typesystem.nim +++ b/src/frontend/compiler/typesystem.nim @@ -114,14 +114,6 @@ type # 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 diff --git a/src/frontend/parsing/ast.nim b/src/frontend/parsing/ast.nim index b195d48..cfae31e 100644 --- a/src/frontend/parsing/ast.nim +++ b/src/frontend/parsing/ast.nim @@ -226,9 +226,8 @@ type ## A block statement. This is basically ## the equivalent of a C scope - # We allow declarations inside a block statement. - # Kinda confusing, but it makes sense, I promise (TM) - code*: seq[Declaration] + + body*: seq[ASTNode] NamedBlockStmt* = ref object of BlockStmt ## Identical to a block statement, except @@ -580,17 +579,17 @@ proc newAssertStmt*(expression: Expression, token: Token): AssertStmt = result.token = token -proc newBlockStmt*(code: seq[Declaration], token: Token): BlockStmt = +proc newBlockStmt*(body: seq[ASTNode], token: Token): BlockStmt = new(result) result.kind = NodeKind.blockStmt - result.code = code + result.body = body result.token = token -proc newNamedBlockStmt*(code: seq[Declaration], name: IdentExpr, token: Token): NamedBlockStmt = +proc newNamedBlockStmt*(body: seq[ASTNode], name: IdentExpr, token: Token): NamedBlockStmt = new(result) result.kind = NodeKind.namedBlockStmt - result.code = code + result.body = body result.token = token result.name = name @@ -745,10 +744,10 @@ proc `$`*(self: ASTNode): string = result &= &"Assert({self.expression})" of blockStmt: var self = BlockStmt(self) - result &= &"""Block([{self.code.join(", ")}])""" + result &= &"""Block([{self.body.join(", ")}])""" of namedBlockStmt: var self = NamedBlockStmt(self) - result &= &"""Block(name={self.name}, [{self.code.join(", ")}])""" + result &= &"""Block(name={self.name}, [{self.body.join(", ")}])""" of whileStmt: var self = WhileStmt(self) result &= &"While(condition={self.condition}, body={self.body})" diff --git a/src/frontend/parsing/parser.nim b/src/frontend/parsing/parser.nim index af393c1..b4090b8 100644 --- a/src/frontend/parsing/parser.nim +++ b/src/frontend/parsing/parser.nim @@ -174,7 +174,7 @@ template endOfLine(msg: string, tok: Token = nil) = self.expect(Semicolon, msg, # Utility functions - +proc dispatch(self: Parser): ASTNode proc beginScope(self: Parser) {.inline.} = ## Begins a new lexical scope @@ -449,7 +449,7 @@ proc makeCall(self: Parser, callee: Expression): CallExpr = if not self.check(RightParen): while true: if argCount >= 255: - self.error("can not pass more than 255 arguments in function call") + self.error("cannot pass more than 255 arguments in call") argument = self.expression() if argument.kind == assignExpr: var assign = AssignExpr(argument) @@ -460,7 +460,7 @@ proc makeCall(self: Parser, callee: Expression): CallExpr = elif arguments.keyword.len() == 0: arguments.positionals.add(argument) else: - self.error("positional arguments cannot follow keyword arguments in function calls") + self.error("positional argument cannot follow keyword argument in call", token=argument.token) if not self.match(Comma): break argCount += 1 @@ -495,7 +495,7 @@ proc call(self: Parser): Expression = result.file = self.file elif self.match(LeftBracket): if self.peek(-2).kind != Identifier: - self.error("invalid syntax") + self.error("expecting identifier before '['") result = self.parseGenericArgs() else: break @@ -611,7 +611,7 @@ proc parseAssign(self: Parser): Expression = result = newSetterExpr(GetterExpr(result).obj, GetterExpr(result).name, value, tok) result.file = self.file else: - self.error("invalid assignment target", tok) + self.error("invalid assignment target", result.token) proc parseArrow(self: Parser): Expression = @@ -645,13 +645,16 @@ proc blockStmt(self: Parser): BlockStmt = ## scope self.beginScope() let tok = self.peek(-1) - var code: seq[Declaration] = @[] + var + body: seq[ASTNode] = @[] + node: ASTNode while not self.check(RightBrace) and not self.done(): - code.add(self.declaration()) - if code[^1].isNil(): - code.delete(code.high()) + node = self.dispatch() + if node.isNil(): + continue + body.add(node) self.expect(RightBrace, "expecting '}'") - result = newBlockStmt(code, tok) + result = newBlockStmt(body, tok) result.file = self.file self.endScope() @@ -660,18 +663,21 @@ proc namedBlockStmt(self: Parser): Statement = ## Parses named block statement self.beginScope() let tok = self.peek(-1) - var code: seq[Declaration] = @[] self.expect(Identifier, "expecting block name after 'block'") var name = newIdentExpr(self.peek(-1)) name.file = self.file inc(self.loopDepth) - self.expect(LeftBrace, "expecting '{' after 'block'") + self.expect(LeftBrace, "expecting '{' after block name") + var + body: seq[ASTNode] = @[] + node: ASTNode while not self.check(RightBrace) and not self.done(): - code.add(self.declaration()) - if code[^1].isNil(): - code.delete(code.high()) + node = self.dispatch() + if node.isNil(): + continue + body.add(node) self.expect(RightBrace, "expecting '}'") - result = newNamedBlockStmt(code, name, tok) + result = newNamedBlockStmt(body, name, tok) result.file = self.file self.endScope() dec(self.loopDepth) diff --git a/src/peon.nim b/src/peon.nim index d867433..d729a69 100644 --- a/src/peon.nim +++ b/src/peon.nim @@ -116,7 +116,7 @@ proc runFile(filename: string, fromString: bool = false, dump: bool = true, gene echo "" if not typeCheck: return - typedNodes = typeChecker.validate(tree, filename, tokenizer.getSource(), mismatches, disabledWarnings) + typedNodes = typeChecker.check(tree, filename, tokenizer.getSource(), mismatches, disabledWarnings) if debugTypeChecker: styledEcho fgCyan, "Typechecker output:" for typedNode in typedNodes: diff --git a/src/util/astchecker.nim b/src/util/astchecker.nim index 36872b6..6f991c5 100644 --- a/src/util/astchecker.nim +++ b/src/util/astchecker.nim @@ -59,7 +59,7 @@ proc compare(provided, expected: LiteralExpr): Comparison = result.match = false result.reason = &"expected lexeme to be {expected.literal.lexeme.escape()}, got {provided.literal.lexeme.escape()}" else: - raise newException(ValueError, &"literal comparison attempted with non-literal node of type {provided.kind}") + raise newException(ValueError, &"literal comparison attempted with non-literal nodes of type {provided.kind}") proc compare(provided, expected: ASTNode): Comparison = @@ -68,6 +68,8 @@ proc compare(provided, expected: ASTNode): Comparison = ensureEqualType(provided, expected) if provided.isLiteral(): return compare(LiteralExpr(provided), LiteralExpr(expected)) + else: + raise newException(ValueError, &"comparisons for nodes of type {provided.kind} are not supported yet") proc check*(self: ASTChecker, code: string, structure: ParseTree): Comparison = diff --git a/src/util/fmterr.nim b/src/util/fmterr.nim index 40ac17e..65d3f7e 100644 --- a/src/util/fmterr.nim +++ b/src/util/fmterr.nim @@ -42,7 +42,7 @@ proc formatError*(errKind: string = "", outFile = stderr, file, line: string, li # Print the line where the error occurred and underline the exact node that caused # the error. Might be inaccurate, but definitely better than nothing outFile.styledWrite(fgRed, styleBright, "Source line: ", resetStyle, fgDefault, line[0..