From 90381840af8b70bca1f15e84af48894179e05249 Mon Sep 17 00:00:00 2001 From: Andrew Segavac Date: Tue, 19 Aug 2025 21:54:06 -0600 Subject: [PATCH] add trait checking --- examples/strings.bl | 22 ++++++ packages/boringlang/src/commands/run.ts | 3 + packages/boringlang/src/parse/grammar.ts | 24 +++---- packages/boringlang/src/parse/semantics.ts | 2 +- .../boringlang/src/types/trait_checker.ts | 70 +++++++++++++++++++ prettier.config.js | 2 +- 6 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 packages/boringlang/src/types/trait_checker.ts diff --git a/examples/strings.bl b/examples/strings.bl index 2178a5d..61e863c 100644 --- a/examples/strings.bl +++ b/examples/strings.bl @@ -7,3 +7,25 @@ fn main(): String { a = (b + c.d()); // comment return a.b() ; } + +type User struct { + id: i64 +} + +type TestTrait trait { + fn class_method(id: i64): Self; + fn instance_method(self: Self): i64; + fn default_impl(self: Self): i64; +} + +impl TestTrait for User { + fn class_method(id: i64): Self { + return Self{id: id}; + } + fn instance_method(self: Self): i64 { + return self.get_id(); + } + fn default_impl(self: Self): i64 { + return self.instance_method(); + } +} diff --git a/packages/boringlang/src/commands/run.ts b/packages/boringlang/src/commands/run.ts index 2298e78..3c62900 100644 --- a/packages/boringlang/src/commands/run.ts +++ b/packages/boringlang/src/commands/run.ts @@ -1,6 +1,7 @@ import { defineCommand } from "@bunli/core"; import { boringGrammar } from "../parse/grammar"; import { semantics } from "../parse/semantics"; +import TraitChecker from "../types/trait_checker"; export const run = defineCommand({ name: "run", @@ -20,6 +21,8 @@ export const run = defineCommand({ if (match.succeeded()) { const adapter = semantics(match); const ast = adapter.toAST(); + new TraitChecker().withModule(ast); + console.log(JSON.stringify(ast, null, 2)); } else { console.log(match.message); diff --git a/packages/boringlang/src/parse/grammar.ts b/packages/boringlang/src/parse/grammar.ts index 53d49a8..a2e085a 100644 --- a/packages/boringlang/src/parse/grammar.ts +++ b/packages/boringlang/src/parse/grammar.ts @@ -3,7 +3,7 @@ import * as ohm from "ohm-js"; export const boringGrammar = ohm.grammar(String.raw` Boringlang { ReturnStatement = "return" Expression ";" - LetStatement = "let" Identifier (":" TypeUsage)? "=" Expression ";" + LetStatement = "let" identifier (":" TypeUsage)? "=" Expression ";" AssignmentStatement = VariableUsage "=" Expression ";" | StructGetter "=" Expression ";" ExpressionStatement = Expression ";" @@ -16,12 +16,12 @@ export const boringGrammar = ohm.grammar(String.raw` LiteralBool = "true" | "false" LiteralString = "\"" (~"\"" any)* "\"" | "'" (~"'" any)* "'" - LiteralStructField = Identifier ":" Expression - LiteralStruct = Identifier "{" ListOf "}" - Identifier = (letter | "_")+ (letter | digit | "_")* + LiteralStructField = identifier ":" Expression + LiteralStruct = identifier "{" ListOf "}" + identifier = (letter | "_")+(letter | digit | "_")* FunctionCall = Expression "(" ListOf ")" - StructGetter = Expression "." Identifier - VariableUsage = Identifier + StructGetter = Expression "." identifier + VariableUsage = identifier IfExpression = "if" "(" Expression ")" Block ("else" Block)? Term = LiteralInt | LiteralFloat @@ -41,16 +41,16 @@ export const boringGrammar = ohm.grammar(String.raw` | FunctionCall | Factor Block = "{" Statement* Expression? "}" - NamedTypeUsage = Identifier + NamedTypeUsage = identifier TypeUsage = NamedTypeUsage | "fn" "(" ListOf ")" ":" TypeUsage -- function_tu - FunctionArgument = Identifier ":" TypeUsage - FunctionDeclaration = "fn" Identifier "(" ListOf ")" ":" TypeUsage + FunctionArgument = identifier ":" TypeUsage + FunctionDeclaration = "fn" identifier "(" ListOf ")" ":" TypeUsage Function = FunctionDeclaration Block - StructTypeField = Identifier ":" TypeUsage - StructTypeDeclaration = "type" Identifier "struct" "{" ListOf "}" + StructTypeField = identifier ":" TypeUsage + StructTypeDeclaration = "type" identifier "struct" "{" ListOf "}" TraitMethod = FunctionDeclaration ";" - TraitTypeDeclaration = "type" Identifier "trait" "{" TraitMethod* "}" + TraitTypeDeclaration = "type" identifier "trait" "{" TraitMethod* "}" TypeDeclaration = StructTypeDeclaration | TraitTypeDeclaration Impl = "impl" (NamedTypeUsage "for")? NamedTypeUsage "{" Function* "}" diff --git a/packages/boringlang/src/parse/semantics.ts b/packages/boringlang/src/parse/semantics.ts index baf79f3..e8e8598 100644 --- a/packages/boringlang/src/parse/semantics.ts +++ b/packages/boringlang/src/parse/semantics.ts @@ -98,7 +98,7 @@ semantics.addOperation("toAST", { type: { typeUsage: "NamedTypeUsage", name: identifier.toAST() }, }; }, - Identifier(_1, _2): Identifier { + identifier(_1, _2): Identifier { return { name: this.sourceString, spanStart: this.source.startIdx, diff --git a/packages/boringlang/src/types/trait_checker.ts b/packages/boringlang/src/types/trait_checker.ts new file mode 100644 index 0000000..581b996 --- /dev/null +++ b/packages/boringlang/src/types/trait_checker.ts @@ -0,0 +1,70 @@ +import { Impl, Module, TraitTypeDeclaration, TypeUsage } from "../parse/ast"; + +interface Context { + environment: Record; +} + +const compareTypes = (typeA: TypeUsage, typeB: TypeUsage) => { + if (typeA.typeUsage !== typeB.typeUsage) { + throw Error(`Mismatched types: ${typeA.typeUsage} ${typeB.typeUsage}`); + } + if (typeA.typeUsage == "NamedTypeUsage" && typeB.typeUsage == "NamedTypeUsage") { + if (typeA.name.name !== typeB.name.name) { + throw Error(`Mismatched types: ${typeA.name.name} ${typeB.name.name}`); + } + } + if (typeA.typeUsage == "FunctionTypeUsage" && typeB.typeUsage == "FunctionTypeUsage") { + if (typeA.arguments.length !== typeB.arguments.length) { + throw Error(`Mismatched arg lengths: ${typeA.arguments.length} ${typeB.arguments.length}`); + } + for (let i = 0; i < typeA.arguments.length; i++) { + compareTypes(typeA.arguments[i], typeB.arguments[i]); + } + compareTypes(typeA.returnType, typeB.returnType); + } +}; + +export default class TraitChecker { + withModule = (module: Module) => { + let ctx: Context = { environment: {} }; + for (const item of module.items) { + if (item.moduleItem == "TraitTypeDeclaration") { + ctx.environment[item.name.name] = item; + } + } + for (const item of module.items) { + if (item.moduleItem == "Impl") { + this.withImpl(ctx, item); + } + } + }; + withImpl = (ctx: Context, impl: Impl) => { + if (new Set(impl.functions.map((fn) => fn.declaration.name)).size !== impl.functions.length) { + throw Error(`Duplicate functions in ${impl.struct.name.name}`); + } + if (impl.trait == null) { + return; + } + const trait = ctx.environment[impl.trait.name.name]; + if (!trait) { + throw Error(`No such trait: ${impl.trait.name}`); + } + if (impl.functions.length !== trait.functions.length) { + throw Error(`Mismatched impl/trait len ${impl.trait.name.name} for ${impl.struct.name.name}`); + } + for (let i = 0; i < impl.functions.length; i++) { + if (impl.functions[i].declaration.name.name !== trait.functions[i].name.name) { + throw Error( + `Mismatched impl/trait names ${impl.functions[i].declaration.name} for ${trait.functions[i].name}`, + ); + } + for (let j = 0; j < impl.functions[i].declaration.arguments.length; j++) { + compareTypes( + impl.functions[i].declaration.arguments[j].type, + trait.functions[i].arguments[j].type, + ); + } + compareTypes(impl.functions[i].declaration.returnType, trait.functions[i].returnType); + } + }; +} diff --git a/prettier.config.js b/prettier.config.js index a3ba494..d584e26 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -2,7 +2,7 @@ const config = { trailingComma: "all", singleQuote: false, - printWidth: 80, + printWidth: 100, semi: true, };