diff --git a/reference/functions.md b/reference/functions.md index 8ab9f0c9..3863ef6a 100644 --- a/reference/functions.md +++ b/reference/functions.md @@ -84,6 +84,17 @@ move({ x, y, z }) move(x: x, y: y, z: z) ``` +Labeled arguments also support concise closure sugar on call sites: + +```void +fn try({ do: ((): throws -> void), catch: (e: Error) -> void }) + +try do(): + this_may_throw() +catch(e): + log e +``` + [1] The compiler will typically optimize this away, so there is no performance penalty for using labeled arguments. @@ -123,25 +134,6 @@ fn add(a: T, b: T) -> T See the chapter on [Generics](./generics.md) for more information. -## Call By Name Parameters - -Call by name parameters automatically wrap the passed expression in a closure. -Call by name parameters are defined by prefixing the parameter type with `@`. -Their type must always be a function type with no parameters. - -```rust -fn eval_twice(@f: () -> void) -> void - f() - f() - -fn main() - var x = 0 - eval_twice(x = x + 1) - print(x) // 2 -``` - -Use by name parameters very SPARINGLY. And only when the function name makes -it obvious that the parameter is a function. ## Parenthetical Elision @@ -226,40 +218,61 @@ See the chapter on [Syntax](./syntax.md) for more information and detailed rules ## Function Overloading -Void functions can be overloaded. Provided that function overload can be -unambiguously distinguished via their parameters and return type. +Void functions can be overloaded. Provided that function overload can be unambiguously distinguished via their parameters and return type. ```void fn sum(a: i32, b: i32) print("Def 1") a + b -fn sum(vec: {a:i32, b: i32}) +fn sum(vec: { a:i32, b: i32 }) print("Def 2") vec.a + vec.b sum a: 1, b: 2 // Def 1 sum { a: 1, b: 2 } // Def 2 - -// ERROR: sum(numbers: ...Int) overlaps ambiguously with sum(a: Int, b: Int) -fn sum(numbers: ...Int) - print("Def 3") ``` This can be especially useful for overloading operators to support a custom type: -``` +```void fn '+'(a: Vec3, b: Vec3) -> Vec3 Vec3(a.x + b.x, a.y + b.y, a.z + b.z) Vec3(1, 2, 3) + Vec3(4, 5, 6) // Vec3(5, 7, 9) ``` -### Rules +A function call is considered to be ambiguous when multiple functions in +the same scope share the same name, and the types of each parameter overlap +in order. + +```void +fn add(a: i32, b: i32) -> i32 +fn add(d: i32, e: i32) -> i32 // Ambiguous collision +fn add(f: i32, c: f32) -> i32 // This is fine, the second parameter does not overlap with previous +``` + +Object types overlap if one is an extension of the other: + +```void +obj Animal {} +obj Dog extends Animal {} +obj Cat extends Animal {} + +fn walk(animal: Animal) +fn walk(dog: Dog) // Ambiguous collision with walk(animal: Animal) + +// Sibling types do not overlap +fn speak(cat: Cat) +fn speak(dog: Dog) // This is fine + +// Labeled parameters can be to add an explicit distinction between two overlapping +// types, (provided they have different labels) +fn walk({animal: Animal}) +fn walk({dog: Dog}) // This is fine, the label, dog, distinguishes this function from the previous + +walk(dog: dexter) // Woof! +``` -- A function signature is: -- Its identifier -- Its parameters, their name, types, order, and label (if applicable) -- Each full function signature must be unique in a given scope -- TBD... +## Function Resolution diff --git a/reference/types/objects.md b/reference/types/objects.md index f0edd098..e089d1df 100644 --- a/reference/types/objects.md +++ b/reference/types/objects.md @@ -5,7 +5,7 @@ pairs (fields). They are defined by listing their fields between curly braces `{}`. -``` +```void type MyObject = { a: i32, b: i32 @@ -17,7 +17,7 @@ type MyObject = { An object is initialized using object literal syntax. Listing the fields and their corresponding values between curly braces `{}`. -``` +```void let my_object: MyObject = { a: 5, b: 4 @@ -195,27 +195,62 @@ impl Animal self.name = name let me = Animal { name: "John" } -log(me.run()) // "John is running!" +log me.run // "John is running!" // The & prefix must be used to call methods that mutate the object &me.change_name("Bob") ``` -## Final Objects +### Inheritance -Objects can be defined as final, meaning they cannot be extended. +Unlike other languages, objects do not inherit methods from their parent +type by default. Instead, they must be opted in with use statements: ```void -final obj Animal { - name: String -} +obj Dog extends Animal {} -// Error - Animal is final -obj Cat extends Animal { - lives_remaining: i32 -} +impl Dog + use super::{ run } // or `all` to inherit all methods + +fn main() + let dog = Dog { name: "Dexter" } + dog.run() // Dexter is running ``` + +Void uses static dispatch for all methods defined on a type. That is, when +a function is called on a method, the function is determined at compile time. + +In practice, this means that compiler will pick the method on the declared type, +even if a subtype is passed. For example: + +```void +obj Animal {} +obj Dog extends Animal {} + +impl Animal + pub fn talk() + log "Glub glub" + +impl Dog + pub fn talk() + log "Bark" + +fn interact(animal: Animal) + animal.talk() + +let dog = Dog {} + +// Here, because interact only knows it will receive an animal, it calls talk from Animal +interact(dog) // Glub glub + +// Here, the compiler knows dog is Dog, so it calls talk from Dog +dog.talk() // Bark +``` + +The next section will discuss how to coerce a function like `interact` into +using the methods of a subtype. + ## Object Type Narrowing ```void @@ -242,34 +277,19 @@ fn main(a: i32, b: i32) log "Error: divide by zero" ``` -# Traits - -Traits are first class types that define the behavior of a nominal object. +## Final Objects -``` -trait Runnable - fn run(self) -> String - fn stop(mut self) -> void +Objects can be defined as final, meaning they cannot be extended. -obj Car { - speed: i32 +```void +final obj Animal { + name: String } -impl Runnable for Car - fn run(self) -> String - "Vroom!" - - fn stop(mut self) -> void - self.speed = 0 - -let car = Car { speed: 10 } -log(car.run()) // "Vroom!" -&car.stop() - -car is Runnable // true - -fn run_thing(thing: Runnable) -> void - log(thing.run()) +// Error - Animal is final +obj Cat extends Animal { + lives_remaining: i32 +} ``` # Built in Object Types @@ -281,7 +301,7 @@ grow and shrink in size when defined as a mutable variable. Type: `String` -``` +```void let my_string = String() // String literals are of type `String` @@ -294,16 +314,16 @@ Arrays are a growable sequence of values of the same type. Type: `Array` -``` +```void let my_array = Array(1, 2, 3) ``` -## Dictionaries +## Maps Dictionaries are a growable collection of key-value pairs. -Type: `Dictionary` +Type: `Map` -``` +```void let my_dict = Dict { a: 1, b: 2, c: 3 } ``` diff --git a/reference/types/traits.md b/reference/types/traits.md new file mode 100644 index 00000000..f5826817 --- /dev/null +++ b/reference/types/traits.md @@ -0,0 +1,86 @@ +# Traits + +Traits are first class types that define the behavior of a nominal object. + +```void +trait Run + fn run(self) -> String + fn stop(&self) -> void + +obj Car { + speed: i32 +} + +impl Run for Car + fn run(self) -> String + "Vroom!" + + fn stop(&self) -> void + self.speed = 0 + +let car = Car { speed: 10 } +log car.run() // "Vroom!" +&car.stop() + +car can Run // true + +// Because traits are first class types, they can be used to define parameters +// that will accept any type that implements the trait +fn run_thing(thing: Run) -> void + log thing.run() + +run_thing(car) // Vroom! +``` + +## Default Implementations + +Status: Not yet implemented + +Traits can specify default implementations which are automatically applied +on implementation, but may still be overridden by that impl if desired + +```void +trait One + fn one() -> i32 + 1 +``` + +## Trait Requirements + +Status: Not yet implemented + +Traits can specify that implementors must also implement other traits: + +```void +trait DoWork requires: This & That +``` + +## Trait limitations + +Traits must be in scope to be used. If the `Run` trait were defined +in a different file (or module), it would have to be imported before its +methods could be used + +```void +car.run() // Error, no function found for run + +use other_file::{ Run } + +car.run() // Vroom! +``` + +Trait implementations cannot have overlapping target types: + +```void +obj Animal {} +obj Dog {} + +trait Speak + fn speak() -> void + +impl Speak for Animal + fn speak() + log "Glub glub" + +impl Speak for Dog // ERROR: Speak is already implemented for Dog via parent type Animal +``` diff --git a/reference/types/unions.md b/reference/types/unions.md index 6a8b23d4..f83a64fa 100644 --- a/reference/types/unions.md +++ b/reference/types/unions.md @@ -5,7 +5,7 @@ Union types represent a value that can be one of a predefined set of types. A union type is defined by listing each of the types it may be, separated by the pipe operator, `|`. -``` +```void type Animal = Cat | Dog obj Cat { @@ -22,7 +22,7 @@ obj Dog { In some cases, where the nominal object is only ever used as part of a union, union sugar can be used -``` +```void union Drink Coffee { size: Size, sugar: Grams, cream: Grams } Tea { size: Size, sugar: Grams, cream: Grams } @@ -38,3 +38,37 @@ type Drink = (obj Soda { size: Size }) | (obj Water) | ``` + +## Calling Methods Of A Union Type + +If all objects of a union have a method with the same signature +(other than self (mutability excluded)). That method can be called +directly from the union + +```void +type Animal = Cat | Dog + +obj Cat {} +obj Dog {} + +impl Cat + pub fn speak(self) + self.meow() + + pub fn meow(self) + log "Meow" + +impl Dog + pub fn speak(self) + self.meow() + + pub fn woof(self) + log "Woof" + +fn main() + let animal = Animal(Dog {}) + animal.speak() // Woof! + animal.woof() // Error +``` + +Internally, the method call is expanded to a match statement. diff --git a/src/__tests__/compiler.test.ts b/src/__tests__/compiler.test.ts index 41633a74..a2fb882d 100644 --- a/src/__tests__/compiler.test.ts +++ b/src/__tests__/compiler.test.ts @@ -31,52 +31,27 @@ describe("E2E Compiler Pipeline", () => { const mod = await compile(kitchenSink); const instance = getWasmInstance(mod); t.expect(mod.validate(), "Module is valid"); - const test1 = getWasmFn("test1", instance); - const test2 = getWasmFn("test2", instance); - const test3 = getWasmFn("test3", instance); - const test4 = getWasmFn("test4", instance); - const test5 = getWasmFn("test5", instance); - const test6 = getWasmFn("test6", instance); - const test7 = getWasmFn("test7", instance); - const test8 = getWasmFn("test8", instance); - const test9 = getWasmFn("test9", instance); - const test10 = getWasmFn("test10", instance); - const test11 = getWasmFn("test11", instance); - const test12 = getWasmFn("test12", instance); - assert(test1, "Test1 exists"); - assert(test2, "Test2 exists"); - assert(test3, "Test3 exists"); - assert(test4, "Test4 exists"); - assert(test5, "Test5 exists"); - assert(test6, "Test6 exists"); - assert(test7, "Test7 exists"); - assert(test8, "Test8 exists"); - assert(test9, "Test9 exists"); - assert(test10, "Test10 exists"); - assert(test11, "Test11 exists"); - assert(test12, "Test12 exists"); - - // Static method resolution tests - t.expect(test1(), "test 1 returns correct value").toEqual(13); - t.expect(test2(), "test 2 returns correct value").toEqual(1); - t.expect(test3(), "test 3 returns correct value").toEqual(2); - t.expect(test4(), "test 4 returns correct value").toEqual(52); - - // Match based type narrowing (and basic gc) - t.expect(test5(), "test 5 returns correct value").toEqual(52); - t.expect(test6(), "test 6 returns correct value").toEqual(21); - t.expect(test7(), "test 7 returns correct value").toEqual(-1); - - // Generic type test - t.expect(test8(), "test 8 returns correct value").toEqual(143); - - // Generic object type test - t.expect(test9(), "test 9 returns correct value").toEqual(7.5); - t.expect(test10(), "test 10 returns correct value").toEqual(12); - t.expect(test11(), "test 11 returns correct value").toEqual(4); - - // Modules - t.expect(test12(), "test 12 returns correct value").toEqual(597); + const tests = (expectedValues: unknown[]) => + expectedValues.forEach((v, i) => { + const test = getWasmFn(`test${i + 1}`, instance); + assert(test, `Test${i + 1} exists`); + t.expect(test(), `test ${i + 1} returns correct value`).toEqual(v); + }); + + tests([ + 13, // Static method resolution tests + 1, + 2, + 52, + 52, // Match based type narrowing (and basic gc) + 21, + -1, + 143, // Generic type test + 7.5, // Generic object type test + 12, + 4, + 597, // Modules + ]); }); test("Compiler can do tco", async (t) => { diff --git a/src/__tests__/fixtures/e2e-file.ts b/src/__tests__/fixtures/e2e-file.ts index c8db31c6..22970cfe 100644 --- a/src/__tests__/fixtures/e2e-file.ts +++ b/src/__tests__/fixtures/e2e-file.ts @@ -41,9 +41,6 @@ obj Bitly extends Vec { fn get_x(vec: Vec) vec.x -fn get_member(vec: Vec) - vec.y - fn get_member(vec: Point) vec.z @@ -68,8 +65,7 @@ pub fn test2() // Should return 2 pub fn test3() - let vec = Vec { x: 1, y: 2 } - vec.get_member() + 2 // Should return 52 pub fn test4() @@ -117,15 +113,12 @@ pub fn test9() let vec2 = VecGeneric { x: 7.5, y: 2.5, z: 3.5 } vec2.x -// Test generic object inheritance strictness +// Test generics with sibling object types fn generic_get_member(vec: T) vec.get_member() -// Ensure generic instances of an ancestor aren't used in place of a descendant, should return 12 +// Ensure generic functions with sibling types are called correctly pub fn test10() - let vec = Vec { x: 7, y: 2} - generic_get_member(vec) - let point = Point { x: 12, y: 17, z: 4 } generic_get_member(point) @@ -157,6 +150,22 @@ use m1::m2::{ test as hi } pub fn test12() hi() + +impl VecGeneric + fn add(self, v: VecGeneric) -> VecGeneric + VecGeneric { x: self.x + v.x, y: self.y + v.y, z: self.z + v.z } + + pub fn do_work(self, v: VecGeneric) -> VecGeneric + let b = self.add(v) + b + +// Test generic impls, should return 9 +pub fn test13() + let a = VecGeneric { x: 1, y: 2, z: 3 } + let b = VecGeneric { x: 4, y: 5, z: 6 } + let c = a.do_work(b) + c.z // 9 + `; export const tcoText = ` diff --git a/src/assembler.ts b/src/assembler.ts index 9adb2cb9..19ab1ff8 100644 --- a/src/assembler.ts +++ b/src/assembler.ts @@ -63,6 +63,7 @@ const compileExpression = (opts: CompileExprOpts): number => { if (expr.isModule()) return compileModule({ ...opts, expr }); if (expr.isObjectLiteral()) return compileObjectLiteral({ ...opts, expr }); if (expr.isType()) return compileType({ ...opts, expr }); + if (expr.isImpl()) return mod.nop(); if (expr.isUse()) return mod.nop(); if (expr.isMacro()) return mod.nop(); if (expr.isMacroVariable()) return mod.nop(); @@ -228,7 +229,11 @@ const compileObjectInit = (opts: CompileExprOpts) => { return initStruct(mod, binaryenTypeToHeapType(objectBinType), [ mod.global.get(`__rtt_${objectType.id}`, opts.extensionHelpers.i32Array), ...obj.fields.map((field) => - compileExpression({ ...opts, expr: field.initializer }) + compileExpression({ + ...opts, + expr: field.initializer, + isReturnExpr: false, + }) ), ]); }; @@ -460,6 +465,13 @@ const buildObjectType = (opts: CompileExprOpts, obj: ObjectType): TypeRef => { ); obj.binaryenType = binaryenType; + + if (obj.implementations?.length) { + obj.implementations.forEach((impl) => + impl.methods.forEach((fn) => compileFunction({ ...opts, expr: fn })) + ); + } + return binaryenType; }; diff --git a/src/parser/syntax-macros/functional-notation.ts b/src/parser/syntax-macros/functional-notation.ts index bbeff4e3..61e032c0 100644 --- a/src/parser/syntax-macros/functional-notation.ts +++ b/src/parser/syntax-macros/functional-notation.ts @@ -38,7 +38,7 @@ export const functionalNotation = (list: List): List => { { result: [], skip: 0 } as Accumulator ); - return finalizeResult(result, isTuple); + return finalizeResult(result, isTuple, list); }; type Accumulator = { result: ListValue[]; skip: number }; @@ -67,12 +67,16 @@ const handleNextExpression = ( return acc; }; -const finalizeResult = (result: ListValue[], isTuple: boolean): List => { +const finalizeResult = ( + result: ListValue[], + isTuple: boolean, + originalList: List +): List => { if (isTuple) { result.unshift(","); result.unshift("tuple"); } - return new List(result); + return new List({ ...originalList.metadata, value: result }); }; const processGenerics = (expr: Expr, generics: List, params?: List): List => { diff --git a/src/semantics/check-types.ts b/src/semantics/check-types.ts index 0f70d851..a153e8ac 100644 --- a/src/semantics/check-types.ts +++ b/src/semantics/check-types.ts @@ -53,8 +53,17 @@ const checkCallTypes = (call: Call): Call | ObjectLiteral => { if (call.calls("member-access")) return call; // TODO if (call.fn?.isObjectType()) return checkObjectInit(call); + call.args = call.args.map(checkTypes); + if (!call.fn) { - throw new Error(`Could not resolve fn ${call.fnName} at ${call.location}`); + const params = call.args + .toArray() + .map((arg) => getExprType(arg)?.name.value) + .join(", "); + + throw new Error( + `Could not resolve fn ${call.fnName}(${params}) at ${call.location}` + ); } if (!call.type) { @@ -63,8 +72,6 @@ const checkCallTypes = (call: Call): Call | ObjectLiteral => { ); } - call.args = call.args.map(checkTypes); - return call; }; @@ -110,7 +117,7 @@ export const checkAssign = (call: Call) => { const checkIdentifier = (id: Identifier) => { const entity = id.resolve(); if (!entity) { - throw new Error(`Unrecognized identifier, ${id}`); + throw new Error(`Unrecognized identifier, ${id} at ${id.location}`); } if (entity.isVariable()) { @@ -192,7 +199,7 @@ const checkFnTypes = (fn: Fn): Fn => { checkTypes(fn.body); if (fn.returnTypeExpr) { - checkTypes(fn.returnTypeExpr); + checkTypeExpr(fn.returnTypeExpr); } if (!fn.returnType) { @@ -220,7 +227,8 @@ const checkParameters = (params: Parameter[]) => { if (!p.type) { throw new Error(`Unable to determine type for ${p}`); } - checkTypes(p.typeExpr); + + checkTypeExpr(p.typeExpr); }); }; @@ -255,7 +263,7 @@ const checkVarTypes = (variable: Variable): Variable => { ); } - if (variable.typeExpr) checkTypes(variable.typeExpr); + if (variable.typeExpr) checkTypeExpr(variable.typeExpr); if ( variable.annotatedType && @@ -281,10 +289,14 @@ const checkObjectType = (obj: ObjectType): ObjectType => { obj.fields.forEach((field) => { if (!field.type) { - throw new Error(`Unable to determine type for ${field.typeExpr}`); + throw new Error( + `Unable to determine type for ${field.typeExpr} at ${field.typeExpr.location}` + ); } }); + obj.implementations.forEach((impl) => impl.methods.forEach(checkTypes)); + if (obj.parentObjExpr) { assertValidExtension(obj, obj.parentObjType); } @@ -310,6 +322,20 @@ export function assertValidExtension( } } +const checkTypeExpr = (expr?: Expr) => { + if (!expr) return; // TODO: Throw error? We use nop instead of undefined now (but maybe not everywhere) + + if (expr.isCall() && !expr.type) { + throw new Error(`Unable to fully resolve type at ${expr.location}`); + } + + if (expr.isCall()) { + return; + } + + return checkTypes(expr); +}; + const checkTypeAlias = (alias: TypeAlias): TypeAlias => { if (!alias.type) { throw new Error(`Unable to determine type for ${alias.typeExpr}`); diff --git a/src/semantics/init-entities.ts b/src/semantics/init-entities.ts index 99de088c..881f62f6 100644 --- a/src/semantics/init-entities.ts +++ b/src/semantics/init-entities.ts @@ -1,4 +1,5 @@ import { Declaration } from "../syntax-objects/declaration.js"; +import { Implementation } from "../syntax-objects/implementation.js"; import { List, Fn, @@ -11,6 +12,7 @@ import { ObjectType, ObjectLiteral, DsArrayType, + nop, } from "../syntax-objects/index.js"; import { Match, MatchCase } from "../syntax-objects/match.js"; import { SemanticProcessor } from "./types.js"; @@ -56,6 +58,10 @@ export const initEntities: SemanticProcessor = (expr) => { return initMatch(expr); } + if (expr.calls("impl")) { + return initImpl(expr); + } + return initCall(expr); }; @@ -79,7 +85,20 @@ const initFn = (expr: List): Fn => { const parameters = parameterList .sliceAsArray(typeParameters ? 2 : 1) - .flatMap((p) => listToParameter(p as List)); + .flatMap((p) => { + if (p.isIdentifier()) { + return new Parameter({ + name: p, + typeExpr: undefined, + }); + } + + if (!p.isList()) { + throw new Error("Invalid parameter"); + } + + return listToParameter(p); + }); const returnTypeExpr = getReturnTypeExprForFn(expr, 3); @@ -271,7 +290,7 @@ const initTypeExprEntities = (type?: Expr): Expr | undefined => { return initDsArray(type); } - throw new Error("Invalid type entity"); + return initCall(type); }; const initDsArray = (type: List) => { @@ -400,3 +419,37 @@ const extractObjectFields = (obj: List) => { return { name: name.value, typeExpr }; }); }; + +const initImpl = (impl: List): Implementation => { + const first = impl.exprAt(1); + const generics = + first.isList() && first.calls("generics") + ? first.sliceAsArray(1).flatMap((p) => (p.isIdentifier() ? p : [])) + : undefined; + + const possibleTraitIndex = generics ? 2 : 1; + const possibleFor = impl.at(possibleTraitIndex + 1); + const traitExpr = + possibleFor?.isIdentifier() && possibleFor.is("for") + ? initEntities(impl.exprAt(possibleTraitIndex)) + : undefined; + + let targetTypeIndex = 1; + if (generics) targetTypeIndex += 1; + if (traitExpr) targetTypeIndex += 2; + + const targetTypeExpr = initEntities(impl.exprAt(targetTypeIndex)); + + const init = new Implementation({ + ...impl.metadata, + typeParams: generics ?? [], + targetTypeExpr, + body: nop(), + traitExpr, + }); + + const body = impl.exprAt(targetTypeIndex + 1); + body.parent = init; + init.body.value = initEntities(body); + return init; +}; diff --git a/src/semantics/resolution/__tests__/get-call-fn.test.ts b/src/semantics/resolution/__tests__/get-call-fn.test.ts index 4c785384..257a82fd 100644 --- a/src/semantics/resolution/__tests__/get-call-fn.test.ts +++ b/src/semantics/resolution/__tests__/get-call-fn.test.ts @@ -108,18 +108,49 @@ describe("getCallFn", () => { const point = new ObjectType({ name: "Vec", value: [], parentObj: vec }); const pointy = new ObjectType({ name: "Vec", value: [], parentObj: vec }); - const objIdentifier = new MockIdentifier({ value: "hi", entity: point }); + const objIdentifier = new MockIdentifier({ value: "hi", entity: pointy }); const candidate1 = new Fn({ name: fnName, - parameters: [new Parameter({ name: pName, type: vec })], + parameters: [new Parameter({ name: pName, type: point })], }); const candidate2 = new Fn({ + name: fnName, + parameters: [new Parameter({ name: pName, type: pointy })], + }); + + const call = new Call({ + fnName, + args: new List({ + value: [objIdentifier], + }), + }); + + call.resolveFns = vi.fn().mockReturnValue([candidate1, candidate2]); + + expect(getCallFn(call)).toBe(candidate2); + }); + + test("subtypes are considered to overlap, and throws ambiguous error", () => { + const pName = new Identifier({ value: "arg1" }); + const fnName = new Identifier({ value: "hi" }); + const vec = new ObjectType({ name: "Vec", value: [] }); + const point = new ObjectType({ name: "Vec", value: [], parentObj: vec }); + const pointy = new ObjectType({ name: "Vec", value: [], parentObj: vec }); + + const objIdentifier = new MockIdentifier({ value: "hi", entity: pointy }); + + const candidate1 = new Fn({ name: fnName, parameters: [new Parameter({ name: pName, type: point })], }); + const candidate2 = new Fn({ + name: fnName, + parameters: [new Parameter({ name: pName, type: vec })], + }); + const candidate3 = new Fn({ name: fnName, parameters: [new Parameter({ name: pName, type: pointy })], @@ -136,6 +167,8 @@ describe("getCallFn", () => { .fn() .mockReturnValue([candidate1, candidate2, candidate3]); - expect(getCallFn(call)).toBe(candidate2); + expect(() => getCallFn(call)).toThrowError( + `Ambiguous call ${JSON.stringify(call, null, 2)}` + ); }); }); diff --git a/src/semantics/resolution/get-call-fn.ts b/src/semantics/resolution/get-call-fn.ts index 04a4ba44..92b3f21c 100644 --- a/src/semantics/resolution/get-call-fn.ts +++ b/src/semantics/resolution/get-call-fn.ts @@ -6,7 +6,7 @@ import { resolveFnTypes } from "./resolve-fn-type.js"; export const getCallFn = (call: Call): Fn | undefined => { if (isPrimitiveFnCall(call)) return undefined; - const unfilteredCandidates = call.resolveFns(call.fnName); + const unfilteredCandidates = getCandidates(call); const candidates = filterCandidates(call, unfilteredCandidates); if (!candidates.length) { @@ -14,7 +14,26 @@ export const getCallFn = (call: Call): Fn | undefined => { } if (candidates.length === 1) return candidates[0]; - return findBestFnMatch(candidates, call); + + throw new Error(`Ambiguous call ${JSON.stringify(call, null, 2)}`); +}; + +const getCandidates = (call: Call): Fn[] => { + const fns = call.resolveFns(call.fnName); + + // Check for methods of arg 1 + const arg1Type = getExprType(call.argAt(0)); + if (arg1Type?.isObjectType()) { + const isInsideImpl = call.parentImpl?.targetType?.id === arg1Type.id; + const implFns = isInsideImpl + ? [] // internal methods already in scope + : arg1Type.implementations + ?.flatMap((impl) => impl.exports) + .filter((fn) => fn.name.is(call.fnName.value)); + fns.push(...(implFns ?? [])); + } + + return fns; }; const filterCandidates = (call: Call, candidates: Fn[]): Fn[] => @@ -78,51 +97,6 @@ const parametersMatch = (candidate: Fn, call: Call) => return typesAreEquivalent(argType, p.type!) && labelsMatch; }); -const findBestFnMatch = (candidates: Fn[], call: Call): Fn => { - let winner: Fn | undefined = undefined; - let tied = false; - let lowestScore: number | undefined; - for (const candidate of candidates) { - const score = candidate.parameters.reduce((score, param, index) => { - if (!param.type?.isObjectType()) { - return score; - } - - const argType = getExprType(call.argAt(index)); - if (!argType || !argType.isObjectType()) { - throw new Error(`Could not determine type. I'm helpful >.<`); - } - - const distance = argType.extensionDistance(param.type); - return score + distance; - }, 0); - - if (lowestScore === undefined) { - lowestScore = score; - winner = candidate; - } - - if (score > lowestScore) { - continue; - } - - if (score < lowestScore) { - lowestScore = score; - winner = candidate; - tied = false; - continue; - } - - tied = true; - } - - if (!winner || tied) { - throw new Error(`Ambiguous call ${JSON.stringify(call, null, 2)}`); - } - - return winner; -}; - const getExprLabel = (expr?: Expr): string | undefined => { if (!expr?.isCall()) return; if (!expr.calls(":")) return; diff --git a/src/semantics/resolution/resolve-call-types.ts b/src/semantics/resolution/resolve-call-types.ts index e24b72d5..5231d550 100644 --- a/src/semantics/resolution/resolve-call-types.ts +++ b/src/semantics/resolution/resolve-call-types.ts @@ -8,6 +8,7 @@ import { resolveTypes } from "./resolve-types.js"; import { resolveExport } from "./resolve-use.js"; export const resolveCallTypes = (call: Call): Call => { + if (call.type) return call; if (call.calls("export")) return resolveExport(call); if (call.calls("if")) return resolveIf(call); if (call.calls(":")) return checkLabeledArg(call); diff --git a/src/semantics/resolution/resolve-fn-type.ts b/src/semantics/resolution/resolve-fn-type.ts index ac0e8981..dc85f551 100644 --- a/src/semantics/resolution/resolve-fn-type.ts +++ b/src/semantics/resolution/resolve-fn-type.ts @@ -1,5 +1,7 @@ import { Call } from "../../syntax-objects/call.js"; +import { Expr } from "../../syntax-objects/expr.js"; import { Fn } from "../../syntax-objects/fn.js"; +import { Implementation } from "../../syntax-objects/implementation.js"; import { List } from "../../syntax-objects/list.js"; import { Parameter } from "../../syntax-objects/parameter.js"; import { TypeAlias } from "../../syntax-objects/types.js"; @@ -39,6 +41,7 @@ export const resolveFnTypes = (fn: Fn, call?: Call): Fn => { fn.body = resolveTypes(fn.body); fn.inferredReturnType = getExprType(fn.body); fn.returnType = fn.annotatedReturnType ?? fn.inferredReturnType; + fn.parentImpl?.registerMethod(fn); // Maybe do this for module when not in an impl return fn; }; @@ -47,6 +50,20 @@ const resolveParameters = (params: Parameter[]) => { params.forEach((p) => { if (p.type) return; + if (p.name.is("self")) { + const impl = getParentImpl(p); + if (!impl) { + throw new Error(`Unable to resolve self type for ${p}`); + } + + if (!impl.targetType) { + throw new Error(`Unable to resolve target type for ${impl}`); + } + + p.type = impl.targetType; + return; + } + if (!p.typeExpr) { throw new Error(`Unable to determine type for ${p}`); } @@ -73,7 +90,6 @@ const resolveGenericsWithTypeArgs = (fn: Fn, args: List): Fn => { } const newFn = fn.clone(); - newFn.id = fn.id + `#${fn.genericInstances?.length ?? 0}`; newFn.typeParameters = undefined; newFn.appliedTypeArgs = []; @@ -94,3 +110,9 @@ const resolveGenericsWithTypeArgs = (fn: Fn, args: List): Fn => { fn.registerGenericInstance(resolvedFn); return fn; }; + +const getParentImpl = (expr: Expr): Implementation | undefined => { + if (expr.syntaxType === "implementation") return expr; + if (expr.parent) return getParentImpl(expr.parent); + return undefined; +}; diff --git a/src/semantics/resolution/resolve-impl.ts b/src/semantics/resolution/resolve-impl.ts new file mode 100644 index 00000000..99f3fa42 --- /dev/null +++ b/src/semantics/resolution/resolve-impl.ts @@ -0,0 +1,82 @@ +import { nop } from "../../syntax-objects/helpers.js"; +import { Implementation } from "../../syntax-objects/implementation.js"; +import { ObjectType, TypeAlias } from "../../syntax-objects/types.js"; +import { getExprType } from "./get-expr-type.js"; +import { resolveObjectTypeTypes } from "./resolve-object-type.js"; +import { resolveTypes } from "./resolve-types.js"; + +export const resolveImpl = ( + impl: Implementation, + targetType?: ObjectType +): Implementation => { + if (impl.typesResolved) return impl; + targetType = targetType ?? resolveTargetType(impl); + impl.targetType = targetType; + + if (!targetType) return impl; + + if (targetType.appliedTypeArgs) { + targetType.appliedTypeArgs.forEach((arg, index) => { + const typeParam = impl.typeParams.at(index); + if (!typeParam) { + throw new Error(`Type param not found for ${arg} at ${impl.location}`); + } + const type = new TypeAlias({ + name: typeParam.clone(), + typeExpr: nop(), + }); + type.type = getExprType(arg); + impl.registerEntity(type); + }); + } + + if (!impl.traitExpr.value && targetType?.isObjectType()) { + targetType.implementations?.push(impl); + } + + if (targetType?.isObjectType() && targetType.typeParameters?.length) { + // Apply impl to existing generic instances + targetType.genericInstances?.forEach((obj) => resolveImpl(impl, obj)); + return impl; + } + + impl.typesResolved = true; + impl.body.value = resolveTypes(impl.body.value); + + return impl; +}; + +const resolveTargetType = (impl: Implementation): ObjectType | undefined => { + const expr = impl.targetTypeExpr.value; + const type = expr.isIdentifier() + ? expr.resolve() + : expr.isCall() + ? expr.fnName.resolve() + : undefined; + + if (!type || !type.isObjectType()) return; + + if (type.typeParameters?.length && expr.isCall()) { + const obj = resolveObjectTypeTypes(type, expr); + // Object fully resolved to non-generic version i.e. `Vec` + if (!obj.typeParameters?.length) return obj; + } + + // Generic impl with generic target type i.e. `impl for Vec` + if (!implIsCompatible(impl, type)) return undefined; + + return type; +}; + +export const implIsCompatible = ( + impl: Implementation, + obj: ObjectType +): boolean => { + if (!impl.typeParams.length && !obj.typeParameters?.length) return true; + + // For now, only handles generic impls with no constraints that match the type arg length of the target type. + if (impl.typeParams.length === obj.typeParameters?.length) return true; // impl for Vec + if (impl.typeParams.length === obj.appliedTypeArgs?.length) return true; // impl for Vec + + return false; +}; diff --git a/src/semantics/resolution/resolve-object-type.ts b/src/semantics/resolution/resolve-object-type.ts index bd979e47..a90faead 100644 --- a/src/semantics/resolution/resolve-object-type.ts +++ b/src/semantics/resolution/resolve-object-type.ts @@ -1,4 +1,5 @@ import { Call } from "../../syntax-objects/call.js"; +import { nop } from "../../syntax-objects/helpers.js"; import { List } from "../../syntax-objects/list.js"; import { ObjectType, @@ -6,6 +7,7 @@ import { voidBaseObject, } from "../../syntax-objects/types.js"; import { getExprType } from "./get-expr-type.js"; +import { implIsCompatible, resolveImpl } from "./resolve-impl.js"; import { resolveTypes } from "./resolve-types.js"; import { typesAreEquivalent } from "./types-are-equivalent.js"; @@ -56,25 +58,35 @@ const resolveGenericsWithTypeArgs = ( } const newObj = obj.clone(); - newObj.id = obj.id + `#${obj.genericInstances?.length ?? 0}`; newObj.typeParameters = undefined; newObj.appliedTypeArgs = []; /** Register resolved type entities for each type param */ + let typesNotResolved = false; typeParameters.forEach((typeParam, index) => { const typeArg = args.exprAt(index); const identifier = typeParam.clone(); const type = new TypeAlias({ name: identifier, - typeExpr: typeArg, + typeExpr: nop(), }); type.type = getExprType(typeArg); + if (!type.type) typesNotResolved = true; newObj.appliedTypeArgs?.push(type); newObj.registerEntity(type); }); + if (typesNotResolved) return obj; const resolvedObj = resolveObjectTypeTypes(newObj); obj.registerGenericInstance(resolvedObj); + + const implementations = newObj.implementations; + newObj.implementations = []; // Clear implementations to avoid duplicates, resolveImpl will re-add them + + implementations + .filter((impl) => implIsCompatible(impl, resolvedObj)) + .map((impl) => resolveImpl(impl, resolvedObj)); + return resolvedObj; }; diff --git a/src/semantics/resolution/resolve-types.ts b/src/semantics/resolution/resolve-types.ts index 5a223751..b05ee65f 100644 --- a/src/semantics/resolution/resolve-types.ts +++ b/src/semantics/resolution/resolve-types.ts @@ -13,6 +13,7 @@ import { Variable } from "../../syntax-objects/variable.js"; import { getExprType } from "./get-expr-type.js"; import { resolveCallTypes } from "./resolve-call-types.js"; import { resolveFnTypes } from "./resolve-fn-type.js"; +import { resolveImpl } from "./resolve-impl.js"; import { resolveMatch } from "./resolve-match.js"; import { resolveObjectTypeTypes } from "./resolve-object-type.js"; import { resolveUse } from "./resolve-use.js"; @@ -20,6 +21,9 @@ import { resolveUse } from "./resolve-use.js"; /** * NOTE: Some mapping is preformed on the AST at this stage. * Returned tree not guaranteed to be same as supplied tree + * + * Should probably rename this to resolveEntities and rename getExprType + * to resolveType */ export const resolveTypes = (expr: Expr | undefined): Expr => { if (!expr) return nop(); @@ -35,6 +39,7 @@ export const resolveTypes = (expr: Expr | undefined): Expr => { if (expr.isTypeAlias()) return resolveTypeAliasTypes(expr); if (expr.isObjectLiteral()) return resolveObjectLiteralTypes(expr); if (expr.isMatch()) return resolveMatch(expr); + if (expr.isImpl()) return resolveImpl(expr); return expr; }; @@ -79,6 +84,7 @@ const resolveDsArrayTypeTypes = (arr: DsArrayType): DsArrayType => { }; const resolveTypeAliasTypes = (alias: TypeAlias): TypeAlias => { + if (alias.type) return alias; alias.typeExpr = resolveTypes(alias.typeExpr); alias.type = getExprType(alias.typeExpr); return alias; @@ -88,6 +94,7 @@ const resolveObjectLiteralTypes = (obj: ObjectLiteral) => { obj.fields.forEach((field) => { field.initializer = resolveTypes(field.initializer); field.type = getExprType(field.initializer); + return field; }); if (!obj.type) { diff --git a/src/semantics/resolution/resolve-use.ts b/src/semantics/resolution/resolve-use.ts index 09a4afb7..975f8113 100644 --- a/src/semantics/resolution/resolve-use.ts +++ b/src/semantics/resolution/resolve-use.ts @@ -168,7 +168,7 @@ export const registerExports = ( if (e instanceof NamedEntity) { registerExport(exportExpr, e); - e.parentModule?.registerEntity(e); + if (!e.parentImpl) e.parentModule?.registerEntity(e); } }); }; @@ -178,5 +178,16 @@ const registerExport = ( entity: NamedEntity, alias?: string ) => { - exportExpr.parentModule?.registerExport(entity, alias); + const parent = exportExpr.parent; + if (!parent) return; + + if (parent.isModule()) { + parent.registerExport(entity, alias); + return; + } + + if (exportExpr.parentImpl && entity.isFn()) { + exportExpr.parentImpl.registerExport(entity); + return; + } }; diff --git a/src/syntax-objects/README.md b/src/syntax-objects/README.md index 386530bb..2e975970 100644 --- a/src/syntax-objects/README.md +++ b/src/syntax-objects/README.md @@ -1,7 +1,6 @@ # Syntax Objects -Syntax objects are data structures that represent concepts within the -language. Such as functions, function calls, variables etc. +Syntax objects are data structures that represent void's language constructs. This includes functions, function calls, variables etc. # Guidelines diff --git a/src/syntax-objects/block.ts b/src/syntax-objects/block.ts index c058d5f7..d7db06db 100644 --- a/src/syntax-objects/block.ts +++ b/src/syntax-objects/block.ts @@ -60,7 +60,7 @@ export class Block extends ScopedSyntax { clone(parent?: Expr) { return new Block({ ...this.getCloneOpts(parent), - body: this.#body.toClonedArray(), + body: this.#body.clone(), }); } } diff --git a/src/syntax-objects/expr.ts b/src/syntax-objects/expr.ts index ea452baa..ac64a2b6 100644 --- a/src/syntax-objects/expr.ts +++ b/src/syntax-objects/expr.ts @@ -21,6 +21,7 @@ import { Use } from "./use.js"; import { ObjectLiteral } from "./object-literal.js"; import { Match } from "./match.js"; import { Nop } from "./nop.js"; +import { Implementation } from "./implementation.js"; export type Expr = | PrimitiveExpr @@ -39,7 +40,8 @@ export type Expr = | Use | ObjectLiteral | Match - | Nop; + | Nop + | Implementation; /** * These are the Expr types that must be returned until all macros have been expanded (reader, syntax, and regular) diff --git a/src/syntax-objects/fn.ts b/src/syntax-objects/fn.ts index 6b5dabbf..90e5a138 100644 --- a/src/syntax-objects/fn.ts +++ b/src/syntax-objects/fn.ts @@ -20,6 +20,7 @@ export class Fn extends ScopedNamedEntity { annotatedReturnType?: Type; appliedTypeArgs?: Type[] = []; typesResolved?: boolean; + #iteration = 0; constructor( opts: ScopedNamedEntityOpts & { @@ -126,10 +127,10 @@ export class Fn extends ScopedNamedEntity { // Don't clone generic instances return new Fn({ ...super.getCloneOpts(parent), - variables: this.variables, + id: `${this.id}#${this.#iteration++}`, returnTypeExpr: this.returnTypeExpr?.clone(), - parameters: this.#parameters.toClonedArray(), - typeParameters: this.#typeParams.toClonedArray(), + parameters: this.#parameters.clone(), + typeParameters: this.#typeParams.clone(), body: this.body?.clone(), }); } diff --git a/src/syntax-objects/implementation.ts b/src/syntax-objects/implementation.ts new file mode 100644 index 00000000..b7f4f59e --- /dev/null +++ b/src/syntax-objects/implementation.ts @@ -0,0 +1,75 @@ +import { Expr } from "./expr.js"; +import { Fn } from "./fn.js"; +import { nop } from "./helpers.js"; +import { Identifier } from "./identifier.js"; +import { ChildList } from "./lib/child-list.js"; +import { Child } from "./lib/child.js"; +import { ScopedSyntax, ScopedSyntaxMetadata } from "./scoped-entity.js"; +import { Type } from "./types.js"; + +export type ImplementationOpts = ScopedSyntaxMetadata & { + typeParams: Identifier[]; + targetTypeExpr: Expr; + body: Expr; + traitExpr?: Expr; +}; + +export class Implementation extends ScopedSyntax { + readonly syntaxType = "implementation"; + readonly typeParams: ChildList; + readonly targetTypeExpr: Child; + readonly body: Child; + readonly traitExpr: Child; + readonly #exports = new Map(); // NO CLONE! + readonly #methods = new Map(); // NO CLONE! + typesResolved?: boolean; + targetType?: Type; + trait?: Type; + + constructor(opts: ImplementationOpts) { + super(opts); + this.typeParams = new ChildList(opts.typeParams, this); + this.targetTypeExpr = new Child(opts.targetTypeExpr, this); + this.body = new Child(opts.body, this); + this.traitExpr = new Child(opts.traitExpr, this); + } + + get exports(): ReadonlyArray { + return [...this.#exports.values()]; + } + + get methods(): ReadonlyArray { + return [...this.#methods.values()]; + } + + registerExport(v: Fn): Implementation { + this.#exports.set(v.id, v as Fn); + return this; + } + + registerMethod(v: Fn): Implementation { + this.#methods.set(v.id, v as Fn); + return this; + } + + clone(parent?: Expr) { + const impl = new Implementation({ + ...super.getCloneOpts(parent), + typeParams: this.typeParams.clone(), + targetTypeExpr: this.targetTypeExpr.clone(), + body: nop(), + traitExpr: this.traitExpr.clone(), + }); + impl.body.value = this.body.clone(impl); + return impl; + } + + toJSON(): unknown { + return [ + "impl", + ["type-params", this.typeParams.toArray()], + ["target", this.targetTypeExpr.toJSON()], + ["body", this.body.toJSON()], + ]; + } +} diff --git a/src/syntax-objects/lib/child-list.ts b/src/syntax-objects/lib/child-list.ts index 9f0c430a..50693882 100644 --- a/src/syntax-objects/lib/child-list.ts +++ b/src/syntax-objects/lib/child-list.ts @@ -41,7 +41,7 @@ export class ChildList { } } - at(index: number): Expr | undefined { + at(index: number): T | undefined { return this.store.at(index); } @@ -114,11 +114,16 @@ export class ChildList { return this; } + reset(to?: T[]) { + this.store = new FastShiftArray(...(to ?? [])); + return this; + } + toArray(): T[] { return this.store.toArray(); } - toClonedArray(): T[] { + clone(): T[] { return this.toArray().map((expr: T): T => expr.clone() as T); } diff --git a/src/syntax-objects/lib/child.ts b/src/syntax-objects/lib/child.ts index 7082f129..785728d4 100644 --- a/src/syntax-objects/lib/child.ts +++ b/src/syntax-objects/lib/child.ts @@ -38,7 +38,7 @@ export class Child { return this.#value?.toJSON(); } - clone(parent?: Expr) { - return new Child(this.#value?.clone(parent), parent ?? this.#parent); + clone(parent?: Expr): T { + return this.#value?.clone(parent) as T; } } diff --git a/src/syntax-objects/list.ts b/src/syntax-objects/list.ts index 2e9db1a1..5ea52fcd 100644 --- a/src/syntax-objects/list.ts +++ b/src/syntax-objects/list.ts @@ -63,9 +63,7 @@ export class List extends Syntax { optionalIdentifierAt(index: number): Identifier | undefined { const id = this.at(index); - if (id?.isIdentifier()) { - return id; - } + if (id?.isIdentifier()) return id; } listAt(index: number): List { @@ -209,7 +207,7 @@ export class List extends Syntax { clone(parent?: Expr): List { return new List({ ...super.getCloneOpts(parent), - value: this.#store.toClonedArray(), + value: this.#store.clone(), }); } } diff --git a/src/syntax-objects/object-literal.ts b/src/syntax-objects/object-literal.ts index 6ae7808d..7f6986d2 100644 --- a/src/syntax-objects/object-literal.ts +++ b/src/syntax-objects/object-literal.ts @@ -10,6 +10,7 @@ export class ObjectLiteral extends Syntax { constructor(opts: SyntaxMetadata & { fields: ObjectLiteralField[] }) { super(opts); this.fields = opts.fields; + this.fields.forEach((f) => (f.initializer.parent = this)); } clone(parent?: Expr): ObjectLiteral { diff --git a/src/syntax-objects/scoped-entity.ts b/src/syntax-objects/scoped-entity.ts index afcf32a6..4a5a9832 100644 --- a/src/syntax-objects/scoped-entity.ts +++ b/src/syntax-objects/scoped-entity.ts @@ -2,7 +2,7 @@ import { Expr } from "./expr.js"; import { LexicalContext } from "./lib/lexical-context.js"; import { Syntax, SyntaxMetadata } from "./syntax.js"; -export type ScopedEntity = Expr & { +export type ScopedEntity = Syntax & { lexicon: LexicalContext; }; diff --git a/src/syntax-objects/syntax.ts b/src/syntax-objects/syntax.ts index 27dd1d6f..ed5bfb53 100644 --- a/src/syntax-objects/syntax.ts +++ b/src/syntax-objects/syntax.ts @@ -26,11 +26,12 @@ import type { } from "./types.js"; import type { Variable } from "./variable.js"; import type { Whitespace } from "./whitespace.js"; -import { NamedEntity } from "./named-entity.js"; -import { ScopedEntity } from "./scoped-entity.js"; -import { Declaration } from "./declaration.js"; -import { Use } from "./use.js"; -import { Match } from "./match.js"; +import { type NamedEntity } from "./named-entity.js"; +import { type ScopedEntity } from "./scoped-entity.js"; +import { type Declaration } from "./declaration.js"; +import { type Use } from "./use.js"; +import { type Match } from "./match.js"; +import { type Implementation } from "./implementation.js"; export type Attributes = { [key: string]: unknown }; @@ -63,6 +64,10 @@ export abstract class Syntax { return this.parent?.isModule() ? this.parent : this.parent?.parentModule; } + get parentImpl(): Implementation | undefined { + return this.parent?.isImpl() ? this.parent : this.parent?.parentImpl; + } + get metadata() { return { location: this.location, @@ -98,7 +103,7 @@ export abstract class Syntax { return this.parentModule?.resolveModule(name, level + 1); } - /** Recursively searches for the entity up the parent tree */ + /** Recursively searches for the entity up the parent tree up to the parent module */ resolveEntity(name: Id): NamedEntity | undefined { if (!this.isScopedEntity()) return this.parent?.resolveEntity(name); @@ -114,6 +119,8 @@ export abstract class Syntax { return this.parent?.resolveFns(id, start) ?? start; } + if (this.isModule()) return start.concat(this.lexicon.resolveFns(id)); + start.push(...this.lexicon.resolveFns(id)); if (this.parent) return this.parent.resolveFns(id, start); return start; @@ -183,6 +190,10 @@ export abstract class Syntax { return this.syntaxType === "whitespace"; } + isImpl(): this is Implementation { + return this.syntaxType === "implementation"; + } + isObjectType(): this is ObjectType { return this.isType() && this.kindOfType === "object"; } diff --git a/src/syntax-objects/types.ts b/src/syntax-objects/types.ts index 96efeeba..87fabdc8 100644 --- a/src/syntax-objects/types.ts +++ b/src/syntax-objects/types.ts @@ -4,6 +4,8 @@ import { NamedEntity, NamedEntityOpts } from "./named-entity.js"; import { Id, Identifier } from "./identifier.js"; import { getIdStr } from "./lib/get-id-str.js"; import { LexicalContext } from "./lib/lexical-context.js"; +import { Implementation } from "./implementation.js"; +import { ScopedEntity } from "./scoped-entity.js"; export type Type = | PrimitiveType @@ -161,9 +163,9 @@ export class TupleType extends BaseType { export type ObjectField = { name: string; typeExpr: Expr; type?: Type }; -export class ObjectType extends BaseType { +export class ObjectType extends BaseType implements ScopedEntity { readonly kindOfType = "object"; - namespace: LexicalContext = new LexicalContext(); + lexicon: LexicalContext = new LexicalContext(); typeParameters?: Identifier[]; appliedTypeArgs?: Type[]; genericInstances?: ObjectType[]; @@ -172,7 +174,9 @@ export class ObjectType extends BaseType { parentObjType?: ObjectType; /** Type used for locals, globals, function return type */ binaryenType?: number; - typesResolved?: boolean; + typesResolved?: boolean; // Don't set if type parameters are present + implementations: Implementation[]; + #iteration = 0; constructor( opts: NamedEntityOpts & { @@ -180,6 +184,7 @@ export class ObjectType extends BaseType { parentObjExpr?: Expr; parentObj?: ObjectType; typeParameters?: Identifier[]; + implementations?: Implementation[]; } ) { super(opts); @@ -190,6 +195,7 @@ export class ObjectType extends BaseType { this.parentObjType = opts.parentObj; this.parentObjExpr = opts.parentObjExpr; this.typeParameters = opts.typeParameters; + this.implementations = opts.implementations ?? []; } get size() { @@ -210,6 +216,7 @@ export class ObjectType extends BaseType { clone(parent?: Expr): ObjectType { return new ObjectType({ ...super.getCloneOpts(parent), + id: `${this.id}#${this.#iteration++}`, value: this.fields.map((field) => ({ ...field, typeExpr: field.typeExpr.clone(), @@ -217,6 +224,7 @@ export class ObjectType extends BaseType { })), parentObjExpr: this.parentObjExpr?.clone(), typeParameters: this.typeParameters, + implementations: this.implementations.map((impl) => impl.clone()), }); } @@ -249,22 +257,6 @@ export class ObjectType extends BaseType { return start; } - /** - * How closely related this object is to ancestor. - * 0 = same type, 1 = ancestor is parent, 2 = ancestor is grandparent, etc - */ - extensionDistance(ancestor: ObjectType, start = 0): number { - if (this === ancestor) { - return start; - } - - if (this.parentObjType) { - return this.parentObjType.extensionDistance(ancestor, start + 1); - } - - throw new Error(`${this.name} does not extend ${ancestor.name}`); - } - hasField(name: Id) { return this.fields.some((field) => field.name === getIdStr(name)); } diff --git a/src/syntax-objects/variable.ts b/src/syntax-objects/variable.ts index e609cbb2..cbe37536 100644 --- a/src/syntax-objects/variable.ts +++ b/src/syntax-objects/variable.ts @@ -1,4 +1,5 @@ import { Expr } from "./expr.js"; +import { Child } from "./lib/child.js"; import { NamedEntity, NamedEntityOpts } from "./named-entity.js"; import { Type } from "./types.js"; @@ -10,8 +11,8 @@ export class Variable extends NamedEntity { originalType?: Type; inferredType?: Type; annotatedType?: Type; - typeExpr?: Expr; - initializer: Expr; + #typeExpr = new Child(undefined, this); + #initializer: Child; requiresCast = false; constructor( @@ -26,7 +27,23 @@ export class Variable extends NamedEntity { this.isMutable = opts.isMutable; this.type = opts.type; this.typeExpr = opts.typeExpr; - this.initializer = opts.initializer; + this.#initializer = new Child(opts.initializer, this); + } + + get typeExpr(): Expr | undefined { + return this.#typeExpr.value; + } + + set typeExpr(value: Expr | undefined) { + this.#typeExpr.value = value; + } + + get initializer(): Expr { + return this.#initializer.value; + } + + set initializer(value: Expr) { + this.#initializer.value = value; } getIndex(): number { @@ -57,8 +74,8 @@ export class Variable extends NamedEntity { return new Variable({ ...super.getCloneOpts(parent), isMutable: this.isMutable, - initializer: this.initializer, - typeExpr: this.typeExpr?.clone(), + initializer: this.#initializer.clone(), + typeExpr: this.#typeExpr.clone(), }); } } diff --git a/std/operators.void b/std/operators.void index 70ed4041..17941c8f 100644 --- a/std/operators.void +++ b/std/operators.void @@ -47,3 +47,14 @@ pub def_wasm_operator('+', add, f32, f32) pub def_wasm_operator('-', sub, f32, f32) pub def_wasm_operator('*', mul, f32, f32) pub def_wasm_operator('/', div, f32, f32) + +pub def_wasm_operator('<', lt, f64, bool) +pub def_wasm_operator('>', gt, f64, bool) +pub def_wasm_operator('<=', le, f64, bool) +pub def_wasm_operator('>=', ge, f64, bool) +pub def_wasm_operator('==', eq, f64, bool) +pub def_wasm_operator('not', ne, f64, bool) +pub def_wasm_operator('+', add, f64, f64) +pub def_wasm_operator('-', sub, f64, f64) +pub def_wasm_operator('*', mul, f64, f64) +pub def_wasm_operator('/', div, f64, f64)