From 216697584cef89c38455d4609823cf79aaaf00d0 Mon Sep 17 00:00:00 2001 From: Drew Youngwerth Date: Sat, 7 Sep 2024 01:37:32 -0700 Subject: [PATCH] [Voi-99] Very basic generic function support (#35) * BnrCall gc support * Fix parsing bug: commas are not operators. * Potential basic generic type resolution * Semi working * Fix typechecking bug * Detect duplicate var defs * Use type args to find a good fn match * Forgot to add these changes to last commit * IT WORKS! * Add unit test for generics * Cleanup --- src/__tests__/compiler.test.ts | 20 ++++- src/__tests__/fixtures/e2e-file.ts | 24 +++++- src/assembler.ts | 70 +++++++++++++---- src/lib/binaryen-gc/index.ts | 8 ++ src/lib/grammar.ts | 7 +- src/parser/lexer.ts | 6 ++ src/semantics/check-types.ts | 13 +++- src/semantics/init-entities.ts | 60 +++++++++++++-- src/semantics/resolution/get-call-fn.ts | 75 +++++++++++++++---- .../resolution/resolve-call-types.ts | 11 +-- src/semantics/resolution/resolve-fn-type.ts | 66 ++++++++++++++-- src/semantics/resolution/resolve-match.ts | 4 + src/semantics/resolution/resolve-types.ts | 15 +++- .../resolution/types-are-equivalent.ts | 4 + src/syntax-objects/block.ts | 6 +- src/syntax-objects/call.ts | 18 +++-- src/syntax-objects/declaration.ts | 3 +- src/syntax-objects/fn.ts | 43 +++++++++-- src/syntax-objects/macro-lambda.ts | 4 +- src/syntax-objects/macro-variable.ts | 2 +- src/syntax-objects/module.ts | 3 +- src/syntax-objects/named-entity.ts | 2 +- src/syntax-objects/object-literal.ts | 5 +- src/syntax-objects/parameter.ts | 5 +- src/syntax-objects/scoped-entity.ts | 1 - src/syntax-objects/syntax.ts | 5 ++ src/syntax-objects/types.ts | 38 +++++++--- src/syntax-objects/variable.ts | 2 +- std/array.void | 22 ++++++ std/index.void | 1 + std/macros.void | 3 +- 31 files changed, 450 insertions(+), 96 deletions(-) create mode 100644 std/array.void diff --git a/src/__tests__/compiler.test.ts b/src/__tests__/compiler.test.ts index 01647d86..4e2945e1 100644 --- a/src/__tests__/compiler.test.ts +++ b/src/__tests__/compiler.test.ts @@ -1,6 +1,7 @@ import { e2eVoidText, gcVoidText, + genericsText, goodTypeInferenceText, tcoText, } from "./fixtures/e2e-file.js"; @@ -36,18 +37,21 @@ describe("E2E Compiler Pipeline", () => { const test4 = getWasmFn("test4", instance); const test5 = getWasmFn("test5", instance); const test6 = getWasmFn("test6", instance); + const test7 = getWasmFn("test7", instance); assert(test1, "Test1 exists"); assert(test2, "Test2 exists"); assert(test3, "Test3 exists"); assert(test4, "Test4 exists"); - assert(test5, "Test3 exists"); - assert(test6, "Test4 exists"); + assert(test5, "Test5 exists"); + assert(test6, "Test6 exists"); + assert(test7, "Test7 exists"); 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); - t.expect(test5(), "test 5 returns correct value").toEqual(21); - t.expect(test6(), "test 6 returns correct value").toEqual(-1); + 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); }); test("Compiler can do tco", async (t) => { @@ -55,4 +59,12 @@ describe("E2E Compiler Pipeline", () => { await compile(tcoText); t.expect(spy).toHaveBeenCalledTimes(1); }); + + test("Generic fn compilation", async (t) => { + const mod = await compile(genericsText); + const instance = getWasmInstance(mod); + const main = getWasmFn("main", instance); + assert(main, "Main exists"); + t.expect(main(), "main 1 returns correct value").toEqual(143); + }); }); diff --git a/src/__tests__/fixtures/e2e-file.ts b/src/__tests__/fixtures/e2e-file.ts index 619b511c..5077ed7c 100644 --- a/src/__tests__/fixtures/e2e-file.ts +++ b/src/__tests__/fixtures/e2e-file.ts @@ -76,13 +76,18 @@ pub fn test4() let vec = Point { x: 52, y: 2, z: 21 } vec.get_x() -// Test match type guard (Point case), should return 21 +// Test match type guard (Pointy case), should return 52 pub fn test5() + let vec = Pointy { x: 52, y: 2, z: 21 } + get_num_from_vec_sub_obj(vec) + +// Test match type guard (Point case), should return 21 +pub fn test6() let vec = Point { x: 52, y: 2, z: 21 } get_num_from_vec_sub_obj(vec) // Test match type guard (else case), should return -1 -pub fn test6() +pub fn test7() let vec = Bitly { x: 52, y: 2, z: 21 } get_num_from_vec_sub_obj(vec) `; @@ -114,3 +119,18 @@ fn fib_alias(n: i32, a: i64, b: i64) -> i64 pub fn main() -> i64 fib(10, 0i64, 1i64) `; + +export const genericsText = ` +use std::all + +type DSArrayI32 = DSArray + +pub fn main() + let arr2 = ds_array_init(10) + arr2.set(0, 1.5) + arr2.get(0) + + let arr: DSArrayI32 = ds_array_init(10) + arr.set(9, 143) + arr.get(9) +`; diff --git a/src/assembler.ts b/src/assembler.ts index 98caa6b4..935f9361 100644 --- a/src/assembler.ts +++ b/src/assembler.ts @@ -4,7 +4,12 @@ import { Expr } from "./syntax-objects/expr.js"; import { Fn } from "./syntax-objects/fn.js"; import { Identifier } from "./syntax-objects/identifier.js"; import { Int } from "./syntax-objects/int.js"; -import { Type, Primitive, ObjectType } from "./syntax-objects/types.js"; +import { + Type, + Primitive, + ObjectType, + DSArrayType, +} from "./syntax-objects/types.js"; import { Variable } from "./syntax-objects/variable.js"; import { Block } from "./syntax-objects/block.js"; import { Declaration } from "./syntax-objects/declaration.js"; @@ -17,7 +22,8 @@ import { refCast, structGetFieldValue, } from "./lib/binaryen-gc/index.js"; -import { HeapTypeRef } from "./lib/binaryen-gc/types.js"; +import * as gc from "./lib/binaryen-gc/index.js"; +import { TypeRef } from "./lib/binaryen-gc/types.js"; import { getExprType } from "./semantics/resolution/get-expr-type.js"; import { Match, MatchCase } from "./syntax-objects/match.js"; import { initExtensionHelpers } from "./assembler/extension-helpers.js"; @@ -264,14 +270,30 @@ const compileAssign = (opts: CompileExprOpts): number => { const compileBnrCall = (opts: CompileExprOpts): number => { const { expr } = opts; const funcId = expr.labeledArgAt(0) as Identifier; - const argTypes = expr.labeledArgAt(1) as Call; - const namespace = argTypes.identifierArgAt(0).value; - const args = expr.labeledArgAt(3) as Call; - const func = (opts.mod as any)[namespace][funcId.value]; + const namespace = (expr.labeledArgAt(1) as Identifier).value; + const args = expr.labeledArgAt(2) as Call; + + const func = + namespace === "gc" + ? (...args: unknown[]) => (gc as any)[funcId.value](opts.mod, ...args) + : (opts.mod as any)[namespace][funcId.value]; + return func( - ...(args.argArrayMap((expr: Expr) => - compileExpression({ ...opts, expr }) - ) ?? []) + ...(args.argArrayMap((expr: Expr) => { + if (expr?.isCall() && expr.calls("BnrType")) { + const type = getExprType(expr.typeArgs?.at(0)); + if (!type) return opts.mod.nop(); + return mapBinaryenType(opts, type); + } + + if (expr?.isCall() && expr.calls("BnrConst")) { + const arg = expr.argAt(0); + if (!arg) return opts.mod.nop(); + if ("value" in arg) return arg.value; + } + + return compileExpression({ ...opts, expr }); + }) ?? []) ); }; @@ -287,6 +309,17 @@ const compileVariable = (opts: CompileExprOpts): number => { const compileFunction = (opts: CompileExprOpts): number => { const { expr: fn, mod } = opts; + if (fn.genericInstances) { + fn.genericInstances.forEach((instance) => + compileFunction({ ...opts, expr: instance }) + ); + return mod.nop(); + } + + if (fn.typeParameters) { + return mod.nop(); + } + const parameterTypes = getFunctionParameterTypes(opts, fn); const returnType = mapBinaryenType(opts, fn.getReturnType()); @@ -378,20 +411,25 @@ const mapBinaryenType = (opts: CompileExprOpts, type: Type): binaryen.Type => { if (isPrimitiveId(type, "i64")) return binaryen.i64; if (isPrimitiveId(type, "f64")) return binaryen.f64; if (isPrimitiveId(type, "void")) return binaryen.none; - if (type.isObjectType()) { - return type.binaryenType ? type.binaryenType : buildObjectType(opts, type); - } + if (type.isObjectType()) return buildObjectType(opts, type); + if (type.isDSArrayType()) return buildDSArrayType(opts, type); throw new Error(`Unsupported type ${type}`); }; const isPrimitiveId = (type: Type, id: Primitive) => type.isPrimitiveType() && type.name.value === id; +const buildDSArrayType = (opts: CompileExprOpts, type: DSArrayType) => { + if (type.binaryenType) return type.binaryenType; + const mod = opts.mod; + const elemType = mapBinaryenType(opts, type.elemType!); + type.binaryenType = gc.defineArrayType(mod, elemType, true, type.id); + return type.binaryenType; +}; + /** TODO: Skip building types for object literals that are part of an initializer of an obj */ -const buildObjectType = ( - opts: CompileExprOpts, - obj: ObjectType -): HeapTypeRef => { +const buildObjectType = (opts: CompileExprOpts, obj: ObjectType): TypeRef => { + if (obj.binaryenType) return obj.binaryenType; const mod = opts.mod; const binaryenType = defineStructType(mod, { diff --git a/src/lib/binaryen-gc/index.ts b/src/lib/binaryen-gc/index.ts index c990ba48..7149b51d 100644 --- a/src/lib/binaryen-gc/index.ts +++ b/src/lib/binaryen-gc/index.ts @@ -101,6 +101,14 @@ export const binaryenTypeToHeapType = (type: Type): HeapTypeRef => { return bin._BinaryenTypeGetHeapType(type); }; +// So we can use the from compileBnrCall +export const modBinaryenTypeToHeapType = ( + _mod: binaryen.Module, + type: Type +): HeapTypeRef => { + return bin._BinaryenTypeGetHeapType(type); +}; + export const refCast = ( mod: binaryen.Module, ref: ExpressionRef, diff --git a/src/lib/grammar.ts b/src/lib/grammar.ts index a8ebb5ff..aca8f619 100644 --- a/src/lib/grammar.ts +++ b/src/lib/grammar.ts @@ -2,7 +2,11 @@ import { Expr } from "../syntax-objects/expr.js"; import { Identifier } from "../syntax-objects/identifier.js"; export const isTerminator = (char: string) => - isWhitespace(char) || isBracket(char) || isQuote(char) || isOpChar(char); + isWhitespace(char) || + isBracket(char) || + isQuote(char) || + isOpChar(char) || + char === ","; export const isQuote = newTest(["'", '"', "`"]); @@ -19,7 +23,6 @@ export const isOpChar = newTest([ ":", "?", ".", - ",", ";", "<", ">", diff --git a/src/parser/lexer.ts b/src/parser/lexer.ts index 388a3682..48fa377e 100644 --- a/src/parser/lexer.ts +++ b/src/parser/lexer.ts @@ -26,6 +26,11 @@ export const lexer = (chars: CharStream): Token => { break; } + if (!token.hasChars && char === ",") { + token.addChar(chars.consumeChar()); + break; + } + if (!token.hasChars && isOpChar(char)) { consumeOperator(chars, token); break; @@ -62,6 +67,7 @@ export const lexer = (chars: CharStream): Token => { const consumeOperator = (chars: CharStream, token: Token) => { while (isOpChar(chars.next)) { + if (token.value === ">" && chars.next === ">") break; // Ugly hack to support generics, means >> is not a valid operator token.addChar(chars.consumeChar()); } }; diff --git a/src/semantics/check-types.ts b/src/semantics/check-types.ts index c1f2df4d..99b07983 100644 --- a/src/semantics/check-types.ts +++ b/src/semantics/check-types.ts @@ -51,7 +51,6 @@ const checkCallTypes = (call: Call): Call | ObjectLiteral => { if (call.calls("=")) return checkAssign(call); 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}`); @@ -63,6 +62,8 @@ const checkCallTypes = (call: Call): Call | ObjectLiteral => { ); } + call.args = call.args.map(checkTypes); + return call; }; @@ -188,6 +189,16 @@ const checkUse = (use: Use) => { }; const checkFnTypes = (fn: Fn): Fn => { + if (fn.genericInstances) { + fn.genericInstances.forEach(checkFnTypes); + return fn; + } + + // If the function has type parameters and not genericInstances, it isn't in use and wont be compiled. + if (fn.typeParameters) { + return fn; + } + checkParameters(fn.parameters); checkTypes(fn.body); diff --git a/src/semantics/init-entities.ts b/src/semantics/init-entities.ts index 150f408c..d96fef37 100644 --- a/src/semantics/init-entities.ts +++ b/src/semantics/init-entities.ts @@ -10,6 +10,7 @@ import { TypeAlias, ObjectType, ObjectLiteral, + DSArrayType, } from "../syntax-objects/index.js"; import { Match, MatchCase } from "../syntax-objects/match.js"; import { SemanticProcessor } from "./types.js"; @@ -66,16 +67,27 @@ const initBlock = (block: List): Block => { const initFn = (expr: List): Fn => { const name = expr.identifierAt(1); - const parameters = expr - .listAt(2) - .sliceAsArray(1) + const parameterList = expr.listAt(2); + + const typeParameters = + parameterList.at(1)?.isList() && parameterList.listAt(1).calls("generics") + ? parameterList + .listAt(1) + .sliceAsArray(1) + .flatMap((p) => (p.isIdentifier() ? p : [])) + : undefined; + + const parameters = parameterList + .sliceAsArray(typeParameters ? 2 : 1) .flatMap((p) => listToParameter(p as List)); + const returnTypeExpr = getReturnTypeExprForFn(expr, 3); const fn = new Fn({ name, returnTypeExpr: returnTypeExpr, parameters, + typeParameters, ...expr.metadata, }); @@ -104,6 +116,11 @@ const listToParameter = ( }); } + if (list.identifierAt(0).is("generics")) { + return []; + } + + // I think this is for labeled args... if (list.identifierAt(0).is("object")) { return list.sliceAsArray(1).flatMap((e) => listToParameter(e as List)); } @@ -151,6 +168,12 @@ const initVar = (varDef: List): Variable => { throw new Error("Invalid variable definition, invalid identifier"); } + if (name.resolve()) { + throw new Error( + `Variable name already in use: ${name} at ${name.location}` + ); + } + const initializer = varDef.at(2); if (!initializer) { @@ -212,8 +235,16 @@ const initCall = (call: List) => { throw new Error("Invalid fn call"); } - const args = call.slice(1).map(initEntities); - return new Call({ ...call.metadata, fnName, args }); + const typeArgs = + call.at(1)?.isList() && call.listAt(1).calls("generics") + ? call + .listAt(1) + .slice(1) + .map((expr) => initTypeExprEntities(expr)!) + : undefined; + + const args = call.slice(typeArgs ? 2 : 1).map(initEntities); + return new Call({ ...call.metadata, fnName, args, typeArgs }); }; const initTypeExprEntities = (type?: Expr): Expr | undefined => { @@ -236,9 +267,28 @@ const initTypeExprEntities = (type?: Expr): Expr | undefined => { return initObjectType(type); } + if (type.calls("DSArray")) { + return initDSArray(type); + } + throw new Error("Invalid type entity"); }; +const initDSArray = (type: List) => { + const generics = type.listAt(1); + const elemTypeExpr = initTypeExprEntities(generics.at(1)); + + if (!elemTypeExpr) { + throw new Error("Invalid DSArray type"); + } + + return new DSArrayType({ + ...type.metadata, + elemTypeExpr, + name: type.syntaxId.toString(), + }); +}; + const initNominalObjectType = (obj: List) => { const hasExtension = obj.optionalIdentifierAt(2)?.is("extends"); return new ObjectType({ diff --git a/src/semantics/resolution/get-call-fn.ts b/src/semantics/resolution/get-call-fn.ts index 32d79082..0b976cac 100644 --- a/src/semantics/resolution/get-call-fn.ts +++ b/src/semantics/resolution/get-call-fn.ts @@ -1,4 +1,4 @@ -import { Call, Expr, Fn } from "../../syntax-objects/index.js"; +import { Call, Expr, Fn, Parameter } from "../../syntax-objects/index.js"; import { getExprType } from "./get-expr-type.js"; import { typesAreEquivalent } from "./types-are-equivalent.js"; import { resolveFnTypes } from "./resolve-fn-type.js"; @@ -6,19 +6,8 @@ import { resolveFnTypes } from "./resolve-fn-type.js"; export const getCallFn = (call: Call): Fn | undefined => { if (isPrimitiveFnCall(call)) return undefined; - const candidates = call.resolveFns(call.fnName).filter((candidate) => { - resolveFnTypes(candidate); - const params = candidate.parameters; - return params.every((p, index) => { - const arg = call.argAt(index); - if (!arg) return false; - const argType = getExprType(arg); - if (!argType) return false; - const argLabel = getExprLabel(arg); - const labelsMatch = p.label === argLabel; - return typesAreEquivalent(argType, p.type!) && labelsMatch; - }); - }); + const unfilteredCandidates = call.resolveFns(call.fnName); + const candidates = filterCandidates(call, unfilteredCandidates); if (!candidates.length) { return undefined; @@ -28,6 +17,64 @@ export const getCallFn = (call: Call): Fn | undefined => { return findBestFnMatch(candidates, call); }; +const filterCandidates = (call: Call, candidates: Fn[]): Fn[] => + candidates.flatMap((candidate) => { + if (candidate.typeParameters) { + return filterCandidateWithGenerics(call, candidate); + } + + resolveFnTypes(candidate); + const params = candidate.parameters; + const paramsMatch = params.every((p, i) => parametersMatch(p, i, call)); + const typeArgsMatch = + call.typeArgs && candidate.appliedTypeArgs + ? candidate.appliedTypeArgs.every((t, i) => { + const argType = getExprType(call.typeArgs?.at(i)); + const appliedType = getExprType(t); + return typesAreEquivalent(argType, appliedType); + }) + : true; + const match = paramsMatch && typeArgsMatch; + return match ? candidate : []; + }); + +const filterCandidateWithGenerics = (call: Call, candidate: Fn): Fn[] => { + // Resolve generics + if (!candidate.genericInstances) resolveFnTypes(candidate, call); + + // Fn not compatible with call + if (!candidate.genericInstances?.length) return []; + + // First attempt + const genericsInstances = filterCandidates(call, candidate.genericInstances); + + // If we have instances, return them + if (genericsInstances.length) return genericsInstances; + + // If no instances, attempt to resolve generics with this call, as a compatible instance + // is still possible + const beforeLen = candidate.genericInstances.length; + resolveFnTypes(candidate, call); + const afterLen = candidate.genericInstances.length; + + if (beforeLen === afterLen) { + // No new instances were created, so this call is not compatible + return []; + } + + return filterCandidates(call, candidate.genericInstances); +}; + +const parametersMatch = (p: Parameter, index: number, call: Call) => { + const arg = call.argAt(index); + if (!arg) return false; + const argType = getExprType(arg); + if (!argType) return false; + const argLabel = getExprLabel(arg); + const labelsMatch = p.label === argLabel; + return typesAreEquivalent(argType, p.type!) && labelsMatch; +}; + const findBestFnMatch = (candidates: Fn[], call: Call): Fn => { let winner: Fn | undefined = undefined; let tied = false; diff --git a/src/semantics/resolution/resolve-call-types.ts b/src/semantics/resolution/resolve-call-types.ts index d3537b41..70c3dac3 100644 --- a/src/semantics/resolution/resolve-call-types.ts +++ b/src/semantics/resolution/resolve-call-types.ts @@ -8,7 +8,6 @@ import { resolveTypes } from "./resolve-types.js"; export const resolveCallTypes = (call: Call): Call => { if (call.calls("export")) return resolveExport(call); if (call.calls("if")) return resolveIf(call); - if (call.calls("binaryen")) return resolveBinaryenCall(call); if (call.calls(":")) return checkLabeledArg(call); call.args = call.args.map(resolveTypes); @@ -21,6 +20,10 @@ export const resolveCallTypes = (call: Call): Call => { return resolveObjectInit(call, type); } + if (call.typeArgs) { + call.typeArgs = call.typeArgs.map(resolveTypes); + } + call.fn = getCallFn(call); call.type = call.fn?.returnType; return call; @@ -93,9 +96,3 @@ export const resolveIf = (call: Call) => { call.type = thenType; return call; }; - -export const resolveBinaryenCall = (call: Call) => { - const returnTypeCall = call.callArgAt(2); - call.type = getExprType(returnTypeCall.argAt(1)); - return call; -}; diff --git a/src/semantics/resolution/resolve-fn-type.ts b/src/semantics/resolution/resolve-fn-type.ts index 136d157c..d17292c8 100644 --- a/src/semantics/resolution/resolve-fn-type.ts +++ b/src/semantics/resolution/resolve-fn-type.ts @@ -1,16 +1,36 @@ +import { Call } from "../../syntax-objects/call.js"; import { Fn } from "../../syntax-objects/fn.js"; +import { List } from "../../syntax-objects/list.js"; import { Parameter } from "../../syntax-objects/parameter.js"; +import { TypeAlias } from "../../syntax-objects/types.js"; import { getExprType } from "./get-expr-type.js"; import { resolveTypes } from "./resolve-types.js"; -export const resolveFnTypes = (fn: Fn): Fn => { +export type ResolveFnTypesOpts = { + typeArgs?: List; + args?: List; +}; + +/** Pass call to potentially resolve generics */ +export const resolveFnTypes = (fn: Fn, call?: Call): Fn => { if (fn.resolved) { // Already resolved return fn; } + if (fn.typeParameters && call) { + // May want to check if there is already a resolved instance with matching type args here + // currently get-call-fn.ts does this, but it may be better to do it here + return attemptToResolveFnWithGenerics(fn, call); + } + + if (fn.typeParameters && !call) { + return fn; + } + resolveParameters(fn.parameters); if (fn.returnTypeExpr) { + fn.returnTypeExpr = resolveTypes(fn.returnTypeExpr); fn.annotatedReturnType = getExprType(fn.returnTypeExpr); fn.returnType = fn.annotatedReturnType; } @@ -33,11 +53,47 @@ const resolveParameters = (params: Parameter[]) => { throw new Error(`Unable to determine type for ${p}`); } + p.typeExpr = resolveTypes(p.typeExpr); const type = getExprType(p.typeExpr); - if (!type) { - throw new Error(`Unable to resolve type for ${p}`); - } - p.type = type; }); }; + +const attemptToResolveFnWithGenerics = (fn: Fn, call: Call): Fn => { + if (call.typeArgs) { + return resolveGenericsWithTypeArgs(fn, call.typeArgs); + } + + // TODO try type inference with args + return fn; +}; + +const resolveGenericsWithTypeArgs = (fn: Fn, args: List): Fn => { + const typeParameters = fn.typeParameters!; + + if (args.length !== typeParameters.length) { + return fn; + } + + const newFn = fn.clone(); + newFn.id = fn.id + `#${fn.genericInstances?.length ?? 0}`; + newFn.typeParameters = undefined; + newFn.appliedTypeArgs = []; + + /** Register resolved type entities for each type param */ + typeParameters.forEach((typeParam, index) => { + const typeArg = args.exprAt(index); + const identifier = typeParam.clone(); + const type = new TypeAlias({ + name: identifier, + typeExpr: typeArg, + }); + type.type = getExprType(typeArg); + newFn.appliedTypeArgs?.push(type); + newFn.registerEntity(type); + }); + + const resolvedFn = resolveFnTypes(newFn); + fn.registerGenericInstance(resolvedFn); + return fn; +}; diff --git a/src/semantics/resolution/resolve-match.ts b/src/semantics/resolution/resolve-match.ts index adac98ac..75eed2f3 100644 --- a/src/semantics/resolution/resolve-match.ts +++ b/src/semantics/resolution/resolve-match.ts @@ -32,6 +32,10 @@ const resolveCase = ( localBinding.originalType = localBinding.type; localBinding.type = type; localBinding.requiresCast = true; + + // NOTE: This binding is temporary and will be overwritten in the next case. + // We may need to introduce an wrapping block and register it to the blocks scope + // to avoid this. c.expr.registerEntity(localBinding); const expr = resolveTypes(c.expr) as Call | Block; diff --git a/src/semantics/resolution/resolve-types.ts b/src/semantics/resolution/resolve-types.ts index 8b0f0116..d0058e13 100644 --- a/src/semantics/resolution/resolve-types.ts +++ b/src/semantics/resolution/resolve-types.ts @@ -5,6 +5,7 @@ import { List } from "../../syntax-objects/list.js"; import { VoidModule } from "../../syntax-objects/module.js"; import { ObjectLiteral } from "../../syntax-objects/object-literal.js"; import { + DSArrayType, ObjectType, TypeAlias, voidBaseObject, @@ -30,6 +31,7 @@ export const resolveTypes = (expr: Expr | undefined): Expr => { if (expr.isList()) return resolveListTypes(expr); if (expr.isUse()) return resolveUse(expr); if (expr.isObjectType()) return resolveObjectTypeTypes(expr); + if (expr.isDSArrayType()) return resolveDSArrayTypeTypes(expr); if (expr.isTypeAlias()) return resolveTypeAliasTypes(expr); if (expr.isObjectLiteral()) return resolveObjectLiteralTypes(expr); if (expr.isMatch()) return resolveMatch(expr); @@ -37,7 +39,7 @@ export const resolveTypes = (expr: Expr | undefined): Expr => { }; const resolveBlockTypes = (block: Block): Block => { - block.body = block.body.map(resolveTypes); + block.applyMap(resolveTypes); block.type = getExprType(block.body.at(-1)); return block; }; @@ -68,10 +70,17 @@ const resolveListTypes = (list: List) => { return list.map(resolveTypes); }; +const resolveDSArrayTypeTypes = (arr: DSArrayType): DSArrayType => { + arr.elemTypeExpr = resolveTypes(arr.elemTypeExpr); + arr.elemType = getExprType(arr.elemTypeExpr); + arr.id = `${arr.id}#${arr.elemType?.id}`; + return arr; +}; + const resolveObjectTypeTypes = (obj: ObjectType): ObjectType => { obj.fields.forEach((field) => { - const type = getExprType(field.typeExpr); - field.type = type; + field.typeExpr = resolveTypes(field.typeExpr); + field.type = getExprType(field.typeExpr); }); if (obj.parentObjExpr) { diff --git a/src/semantics/resolution/types-are-equivalent.ts b/src/semantics/resolution/types-are-equivalent.ts index ef4f1c87..04bd25ac 100644 --- a/src/semantics/resolution/types-are-equivalent.ts +++ b/src/semantics/resolution/types-are-equivalent.ts @@ -21,5 +21,9 @@ export const typesAreEquivalent = ( ); } + if (a.isDSArrayType() && b.isDSArrayType()) { + return typesAreEquivalent(a.elemType, b.elemType); + } + return false; }; diff --git a/src/syntax-objects/block.ts b/src/syntax-objects/block.ts index 7d9fd13f..2978abf4 100644 --- a/src/syntax-objects/block.ts +++ b/src/syntax-objects/block.ts @@ -41,8 +41,11 @@ export class Block extends ScopedSyntax { return this; } + /** Sets the parent on each element immediately before the mapping of the next */ applyMap(fn: (expr: Expr, index: number, array: Expr[]) => Expr) { - this.body = this.body.map(fn); + const body = this.body; + this.body = new List({ ...this.body.metadata, value: [] }); + body.each((expr, index, array) => this.body.push(fn(expr, index, array))); return this; } @@ -59,7 +62,6 @@ export class Block extends ScopedSyntax { return new Block({ ...this.getCloneOpts(parent), body: this.body.clone(), - type: this.type, }); } } diff --git a/src/syntax-objects/call.ts b/src/syntax-objects/call.ts index 49a095d4..c4947de6 100644 --- a/src/syntax-objects/call.ts +++ b/src/syntax-objects/call.ts @@ -3,18 +3,17 @@ import { Fn } from "./fn.js"; import { Identifier } from "./identifier.js"; import { LexicalContext } from "./lexical-context.js"; import { List } from "./list.js"; -import { ScopedSyntax } from "./scoped-entity.js"; -import { SyntaxMetadata } from "./syntax.js"; +import { Syntax, SyntaxMetadata } from "./syntax.js"; import { ObjectType, Type } from "./types.js"; /** Defines a function call */ -export class Call extends ScopedSyntax { +export class Call extends Syntax { readonly syntaxType = "call"; fn?: Fn | ObjectType; fnName: Identifier; args: List; + typeArgs?: List; _type?: Type; - lexicon: LexicalContext; constructor( opts: SyntaxMetadata & { @@ -23,15 +22,17 @@ export class Call extends ScopedSyntax { args: List; type?: Type; lexicon?: LexicalContext; + typeArgs?: List; } ) { super(opts); this.fnName = opts.fnName; this.fn = opts.fn; this.args = opts.args; + this.args.parent = this; + this.typeArgs = opts.typeArgs; + if (this.typeArgs) this.typeArgs.parent = this; this._type = opts.type; - this.lexicon = opts.lexicon ?? new LexicalContext(); - opts.args.parent = this; } set type(type: Type | undefined) { @@ -120,8 +121,9 @@ export class Call extends ScopedSyntax { clone(parent?: Expr) { return new Call({ ...this.getCloneOpts(parent), - fnName: this.fnName, - args: this.args, + fnName: this.fnName.clone(), + args: this.args.clone(), + typeArgs: this.typeArgs?.clone(), }); } } diff --git a/src/syntax-objects/declaration.ts b/src/syntax-objects/declaration.ts index 7d04b136..d865c5c0 100644 --- a/src/syntax-objects/declaration.ts +++ b/src/syntax-objects/declaration.ts @@ -17,6 +17,7 @@ export class Declaration extends Syntax { super(opts); this.namespace = opts.namespace; this.fns = opts.fns ?? []; + this.fns.forEach((fn) => (fn.parent = this)); } toJSON() { @@ -27,7 +28,7 @@ export class Declaration extends Syntax { return new Declaration({ ...this.getCloneOpts(parent), namespace: this.namespace, - fns: this.fns, + fns: this.fns.map((fn) => fn.clone()), }); } } diff --git a/src/syntax-objects/fn.ts b/src/syntax-objects/fn.ts index 1f30f8bf..3b2cf221 100644 --- a/src/syntax-objects/fn.ts +++ b/src/syntax-objects/fn.ts @@ -1,4 +1,5 @@ import type { Expr } from "./expr.js"; +import { Identifier } from "./identifier.js"; import { ScopedNamedEntity, ScopedNamedEntityOpts } from "./named-entity.js"; import { Parameter } from "./parameter.js"; import { FnType, Type } from "./types.js"; @@ -8,8 +9,12 @@ export class Fn extends ScopedNamedEntity { readonly syntaxType = "fn"; variables: Variable[] = []; _parameters: Parameter[] = []; + typeParameters?: Identifier[]; + appliedTypeArgs?: Type[] = []; + /** When a function has generics, resolved versions of the functions go here */ + genericInstances?: Fn[] = []; returnType?: Type; - returnTypeExpr?: Expr; + _returnTypeExpr?: Expr; inferredReturnType?: Type; annotatedReturnType?: Type; resolved?: boolean; @@ -21,6 +26,8 @@ export class Fn extends ScopedNamedEntity { returnTypeExpr?: Expr; variables?: Variable[]; parameters: Parameter[]; + typeParameters?: Identifier[]; + genericInstances?: Fn[]; body?: Expr; } ) { @@ -28,6 +35,8 @@ export class Fn extends ScopedNamedEntity { this.returnType = opts.returnType; this.parameters = opts.parameters ?? []; this.variables = opts.variables ?? []; + this.typeParameters = opts.typeParameters; + this.genericInstances = opts.genericInstances; this.returnTypeExpr = opts.returnTypeExpr; this.body = opts.body; } @@ -56,6 +65,27 @@ export class Fn extends ScopedNamedEntity { }); } + get returnTypeExpr() { + return this._returnTypeExpr; + } + + set returnTypeExpr(returnTypeExpr: Expr | undefined) { + if (returnTypeExpr) { + returnTypeExpr.parent = this; + } + + this._returnTypeExpr = returnTypeExpr; + } + + // Register a version of this function with resolved generics + registerGenericInstance(fn: Fn) { + if (!this.genericInstances) { + this.genericInstances = []; + } + + this.genericInstances.push(fn); + } + getNameStr(): string { return this.name.value; } @@ -111,12 +141,14 @@ export class Fn extends ScopedNamedEntity { } clone(parent?: Expr | undefined): Fn { + // Don't clone generic instances return new Fn({ ...super.getCloneOpts(parent), - variables: this.variables, - parameters: this.parameters, - returnType: this.returnType, - body: this.body, + variables: this.variables.map((v) => v.clone()), + parameters: this.parameters.map((p) => p.clone()), + returnTypeExpr: this.returnTypeExpr?.clone(), + body: this.body?.clone(), + typeParameters: this.typeParameters?.map((tp) => tp.clone()), }); } @@ -125,6 +157,7 @@ export class Fn extends ScopedNamedEntity { "fn", this.id, ["parameters", ...this.parameters], + ["type-parameters", ...(this.typeParameters ?? [])], ["return-type", this.returnType], this.body, ]; diff --git a/src/syntax-objects/macro-lambda.ts b/src/syntax-objects/macro-lambda.ts index 13b5633b..a75ed48d 100644 --- a/src/syntax-objects/macro-lambda.ts +++ b/src/syntax-objects/macro-lambda.ts @@ -27,8 +27,8 @@ export class MacroLambda extends ScopedSyntax { clone(parent?: Expr | undefined): MacroLambda { return new MacroLambda({ ...super.getCloneOpts(parent), - parameters: this.parameters, - body: this.body, + parameters: this.parameters.map((p) => p.clone()), + body: this.body.clone(), }); } diff --git a/src/syntax-objects/macro-variable.ts b/src/syntax-objects/macro-variable.ts index 6a013d22..d5a5e965 100644 --- a/src/syntax-objects/macro-variable.ts +++ b/src/syntax-objects/macro-variable.ts @@ -35,7 +35,7 @@ export class MacroVariable extends NamedEntity { ...super.getCloneOpts(parent), location: this.location, isMutable: this.isMutable, - value: this.value, + value: this.value?.clone(), }); } } diff --git a/src/syntax-objects/module.ts b/src/syntax-objects/module.ts index fe83165b..bbf84a1e 100644 --- a/src/syntax-objects/module.ts +++ b/src/syntax-objects/module.ts @@ -51,6 +51,7 @@ export class VoidModule extends ScopedNamedEntity { ...super.getCloneOpts(), value: this.value.map(fn), phase: this.phase, + isIndex: this.isIndex, }); } @@ -68,7 +69,7 @@ export class VoidModule extends ScopedNamedEntity { clone(parent?: Expr | undefined): VoidModule { return new VoidModule({ ...super.getCloneOpts(parent), - value: this.value, + value: this.value.map((expr) => expr.clone()), phase: this.phase, }); } diff --git a/src/syntax-objects/named-entity.ts b/src/syntax-objects/named-entity.ts index 1bf166b4..c2c7a714 100644 --- a/src/syntax-objects/named-entity.ts +++ b/src/syntax-objects/named-entity.ts @@ -60,7 +60,7 @@ export abstract class ScopedNamedEntity extends NamedEntity { getCloneOpts(parent?: Expr): ScopedNamedEntityOpts { return { ...super.getCloneOpts(parent), - lexicon: this.lexicon, + // lexicon: this.lexicon, }; } } diff --git a/src/syntax-objects/object-literal.ts b/src/syntax-objects/object-literal.ts index d947d508..6ae7808d 100644 --- a/src/syntax-objects/object-literal.ts +++ b/src/syntax-objects/object-literal.ts @@ -15,7 +15,10 @@ export class ObjectLiteral extends Syntax { clone(parent?: Expr): ObjectLiteral { return new ObjectLiteral({ ...super.getCloneOpts(parent), - fields: this.fields, + fields: this.fields.map(({ name, initializer }) => ({ + name, + initializer: initializer.clone(), + })), }); } diff --git a/src/syntax-objects/parameter.ts b/src/syntax-objects/parameter.ts index 3327cda1..3c00a543 100644 --- a/src/syntax-objects/parameter.ts +++ b/src/syntax-objects/parameter.ts @@ -23,6 +23,9 @@ export class Parameter extends NamedEntity { this.label = opts.label; this.type = opts.type; this.typeExpr = opts.typeExpr; + if (this.typeExpr) { + this.typeExpr.parent = this; + } } getIndex(): number { @@ -40,8 +43,8 @@ export class Parameter extends NamedEntity { clone(parent?: Expr | undefined): Parameter { return new Parameter({ ...super.getCloneOpts(parent), - type: this.type, label: this.label, + typeExpr: this.typeExpr?.clone(), }); } diff --git a/src/syntax-objects/scoped-entity.ts b/src/syntax-objects/scoped-entity.ts index 1c5842e7..33d3a073 100644 --- a/src/syntax-objects/scoped-entity.ts +++ b/src/syntax-objects/scoped-entity.ts @@ -21,7 +21,6 @@ export abstract class ScopedSyntax extends Syntax { getCloneOpts(parent?: Expr | undefined): ScopedSyntaxMetadata { return { ...super.getCloneOpts(parent), - lexicon: this.lexicon, }; } } diff --git a/src/syntax-objects/syntax.ts b/src/syntax-objects/syntax.ts index 436ef531..7e677120 100644 --- a/src/syntax-objects/syntax.ts +++ b/src/syntax-objects/syntax.ts @@ -22,6 +22,7 @@ import type { ObjectType, Type, TypeAlias, + DSArrayType, } from "./types.js"; import type { Variable } from "./variable.js"; import type { Whitespace } from "./whitespace.js"; @@ -160,6 +161,10 @@ export abstract class Syntax { return this.isType() && this.kindOfType === "object"; } + isDSArrayType(): this is DSArrayType { + return this.isType() && this.kindOfType === "ds-array"; + } + isPrimitiveType(): this is PrimitiveType { return this.isType() && this.kindOfType === "primitive"; } diff --git a/src/syntax-objects/types.ts b/src/syntax-objects/types.ts index aeecf7c7..11e3f8f5 100644 --- a/src/syntax-objects/types.ts +++ b/src/syntax-objects/types.ts @@ -10,7 +10,7 @@ export type Type = | IntersectionType | ObjectType | TupleType - | ArrayType + | DSArrayType | FnType | TypeAlias; @@ -177,6 +177,9 @@ export class ObjectType extends BaseType { ) { super(opts); this.fields = opts.value; + this.fields.forEach((field) => { + field.typeExpr.parent = this; + }); this.parentObj = opts.parentObj; this.parentObjExpr = opts.parentObjExpr; } @@ -199,7 +202,13 @@ export class ObjectType extends BaseType { clone(parent?: Expr): ObjectType { return new ObjectType({ ...super.getCloneOpts(parent), - value: this.fields, + value: this.fields.map((field) => ({ + ...field, + typeExpr: field.typeExpr.clone(), + type: field.type?.clone(), + })), + parentObj: this.parentObj, + parentObjExpr: this.parentObj?.clone(), }); } @@ -252,22 +261,31 @@ export class ObjectType extends BaseType { } } -export class ArrayType extends BaseType { - readonly kindOfType = "array"; +/** Dynamically Sized Array (The raw gc array type) */ +export class DSArrayType extends BaseType { + readonly kindOfType = "ds-array"; readonly size = Infinity; - value: Type; + elemTypeExpr: Expr; + elemType?: Type; + /** Type used for locals, globals, function return type */ + binaryenType?: number; - constructor(opts: NamedEntityOpts & { value: Type }) { + constructor(opts: NamedEntityOpts & { elemTypeExpr: Expr; elemType?: Type }) { super(opts); - this.value = opts.value; + this.elemTypeExpr = opts.elemTypeExpr; + this.elemTypeExpr.parent = this; + this.elemType = opts.elemType; } - clone(parent?: Expr): ArrayType { - return new ArrayType({ ...super.getCloneOpts(parent), value: this.value }); + clone(parent?: Expr): DSArrayType { + return new DSArrayType({ + ...super.getCloneOpts(parent), + elemTypeExpr: this.elemTypeExpr.clone(), + }); } toJSON(): TypeJSON { - return ["type", ["array", this.value]]; + return ["type", ["DSArray", this.elemType]]; } } diff --git a/src/syntax-objects/variable.ts b/src/syntax-objects/variable.ts index 1669e2c5..e609cbb2 100644 --- a/src/syntax-objects/variable.ts +++ b/src/syntax-objects/variable.ts @@ -58,7 +58,7 @@ export class Variable extends NamedEntity { ...super.getCloneOpts(parent), isMutable: this.isMutable, initializer: this.initializer, - type: this.type, + typeExpr: this.typeExpr?.clone(), }); } } diff --git a/std/array.void b/std/array.void new file mode 100644 index 00000000..c7457d85 --- /dev/null +++ b/std/array.void @@ -0,0 +1,22 @@ +macro binaryen_gc_call(func, args) + ` binaryen func: $func namespace: gc args: $args + +macro bin_type_to_heap_type(type) + // binaryen_gc_call(modBinaryenTypeToHeapType, ` [BnrType<($type)>]) + ` binaryen + func: modBinaryenTypeToHeapType + namespace: gc + args: [BnrType<($type)>] + +pub fn ds_array_init(size: i32) -> DSArray + binaryen_gc_call(arrayNew, [bin_type_to_heap_type(DSArray), size]) + +pub fn get(arr: DSArray, index: i32) -> T + binaryen_gc_call( + arrayGet, + [arr, index, BnrType, BnrConst(false)] + ) + +pub fn set(arr: DSArray, index: i32, value: T) -> DSArray + binaryen_gc_call(arraySet, [arr, index, value]) + arr diff --git a/std/index.void b/std/index.void index 476c03e7..c627a7a7 100644 --- a/std/index.void +++ b/std/index.void @@ -3,3 +3,4 @@ pub use macros::all pub use operators::all pub use utils::all pub use strings::all +pub use array::all diff --git a/std/macros.void b/std/macros.void index 7e592edd..90e008d3 100644 --- a/std/macros.void +++ b/std/macros.void @@ -85,8 +85,7 @@ pub macro def_wasm_operator(op, wasm_fn, arg_type, return_type) let params = `(parameters, left: $arg_type, right: $arg_type) let body = ` binaryen func: $wasm_fn - arg_types: [$arg_type, $arg_type] - return_type: $return_type + namespace: $arg_type args: [left, right] ` define_function $op $params return_type($return_type) $body