diff --git a/README.md b/README.md index 319e32cc..9c2027e7 100644 --- a/README.md +++ b/README.md @@ -240,17 +240,20 @@ for item in iterable ### Match Statements +Match statements are used for type narrowing + ```rust -let x = 3 -match x - 1 => print "One" - 2 => print "Two" - 3 => print "Three" - _ => - // Match statements are exhaustive, they must cover every possible - // case. When not every case is covered, a default handler must be - // provided. - write "A number" +obj Animal +obj Cat extends Animal +obj Dog extends Animal + +let dog = Dog {} + +match(dog) + Dog: print "Woof" + Cat: print "Meow" + else: + print "Blurb" ``` ## Closures diff --git a/reference/control-flow.md b/reference/control-flow.md new file mode 100644 index 00000000..e7a68224 --- /dev/null +++ b/reference/control-flow.md @@ -0,0 +1,46 @@ +# Control Flow + +## Match + +Used to narrow types. Can operate on object to narrow +child objects or on unions to narrow union members + +Signature(s): +``` +fn match(val: T, body: MatchBlock) -> U +fn match(val: T, bind_identifier: Identifier, body: MatchBlock) -> U +``` + +Example: +```void +obj Optional + +obj None extends Optional + +obj Some extends Optional { + value: i32 +} + +fn divide(a: i32, b: i32) -> Optional + if b == 0 + None {} + else: + Some { value: a / b } + +fn main(a: i32, b: i32) -> String + let x = a.divide(b) + match(x) + Some: "The value is ${x}" + None: "Error: divide by zero" + else: "Bleh" +``` + +The second signature of match is useful when the value being matched against +is not already bound to an identifier (i.e. dot pipelines): +```void +fn main(a: i32, b: i32) -> String + a.divide(b) + .match(x) // Here, match binds the result of the previous expression to x + Some: "The value is ${x}" + None: "Error: divide by zero" +``` diff --git a/reference/types/objects.md b/reference/types/objects.md index 5d591fbb..f0edd098 100644 --- a/reference/types/objects.md +++ b/reference/types/objects.md @@ -216,6 +216,32 @@ obj Cat extends Animal { } ``` +## Object Type Narrowing + +```void +obj Optional + +obj None extends Optional + +obj Some extends Optional { + value: i32 +} + +fn divide(a: i32, b: i32) -> Optional + if b == 0 + None { } + else: + Some { value: a / b } + +fn main(a: i32, b: i32) + a.divide(b) + .match(x) + Some: + log "The value is ${x}" + None: + log "Error: divide by zero" +``` + # Traits Traits are first class types that define the behavior of a nominal object. diff --git a/src/__tests__/compiler.test.ts b/src/__tests__/compiler.test.ts index 99135eda..dd600ec3 100644 --- a/src/__tests__/compiler.test.ts +++ b/src/__tests__/compiler.test.ts @@ -20,13 +20,19 @@ describe("E2E Compiler Pipeline", () => { 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); assert(test1, "Test1 exists"); assert(test2, "Test2 exists"); assert(test3, "Test3 exists"); assert(test4, "Test4 exists"); + assert(test5, "Test3 exists"); + assert(test6, "Test4 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); }); }); diff --git a/src/__tests__/fixtures/e2e-file.ts b/src/__tests__/fixtures/e2e-file.ts index 449e1d55..2fc0a517 100644 --- a/src/__tests__/fixtures/e2e-file.ts +++ b/src/__tests__/fixtures/e2e-file.ts @@ -32,6 +32,12 @@ obj Pointy extends Vec { z: i32 } +obj Bitly extends Vec { + x: i32, + y: i32, + z: i32 +} + fn get_x(vec: Vec) vec.x @@ -44,6 +50,12 @@ fn get_member(vec: Point) fn get_member(vec: Pointy) vec.x +fn get_num_from_vec_sub_obj(vec: Vec) + match(vec) + Pointy: get_member(vec) + Point: get_member(vec) + else: -1 + // Should return 13 pub fn test1() let vec = Point { x: 1, y: 2, z: 13 } @@ -59,10 +71,18 @@ pub fn test3() let vec = Vec { x: 1, y: 2 } vec.get_member() - // Should return 52 pub fn test4() let vec = Point { x: 52, y: 2, z: 21 } vec.get_x() +// Test match type guard (Point case), should return 21 +pub fn test5() + 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() + let vec = Bitly { x: 52, y: 2, z: 21 } + get_num_from_vec_sub_obj(vec) `; diff --git a/src/assembler.ts b/src/assembler.ts index 7c241648..5a1dea8d 100644 --- a/src/assembler.ts +++ b/src/assembler.ts @@ -14,16 +14,20 @@ import { binaryenTypeToHeapType, defineStructType, initStruct, + refCast, structGetFieldValue, } from "./lib/binaryen-gc/index.js"; import { HeapTypeRef } 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"; export const assemble = (ast: Expr) => { const mod = new binaryen.Module(); mod.setMemory(1, 150, "buffer"); mod.setFeatures(binaryen.Features.All); - compileExpression({ expr: ast, mod }); + const extensionHelpers = initExtensionHelpers(mod); + compileExpression({ expr: ast, mod, extensionHelpers }); mod.autoDrop(); return mod; }; @@ -31,6 +35,7 @@ export const assemble = (ast: Expr) => { interface CompileExprOpts { expr: T; mod: binaryen.Module; + extensionHelpers: ReturnType; } const compileExpression = (opts: CompileExprOpts): number => { @@ -49,6 +54,7 @@ const compileExpression = (opts: CompileExprOpts): number => { if (expr.isUse()) return mod.nop(); if (expr.isMacro()) return mod.nop(); if (expr.isMacroVariable()) return mod.nop(); + if (expr.isMatch()) return compileMatch({ ...opts, expr }); if (expr.isBool()) { return expr.value ? mod.i32.const(1) : mod.i32.const(0); @@ -73,6 +79,41 @@ const compileBlock = (opts: CompileExprOpts) => { ); }; +const compileMatch = (opts: CompileExprOpts) => { + const { expr } = opts; + + const constructIfChain = (cases: MatchCase[]): number => { + const nextCase = cases.shift(); + if (!nextCase) return opts.mod.unreachable(); + + if (!cases.length) { + return compileExpression({ ...opts, expr: nextCase.expr }); + } + + return opts.mod.if( + opts.mod.call( + "__extends", + [ + opts.mod.i32.const(nextCase.matchType!.syntaxId), + structGetFieldValue({ + mod: opts.mod, + fieldType: opts.extensionHelpers.i32Array, + fieldIndex: 0, + exprRef: compileIdentifier({ ...opts, expr: expr.bindIdentifier }), + }), + ], + binaryen.i32 + ), + compileExpression({ ...opts, expr: nextCase.expr }), + constructIfChain(cases) + ); + }; + + return constructIfChain( + expr.defaultCase ? [...expr.cases, expr.defaultCase] : expr.cases + ); +}; + const compileIdentifier = (opts: CompileExprOpts) => { const { expr, mod } = opts; @@ -82,8 +123,12 @@ const compileIdentifier = (opts: CompileExprOpts) => { } if (entity.isVariable() || entity.isParameter()) { - const type = mapBinaryenType(mod, entity.type!); - return mod.local.get(entity.getIndex(), type); + const type = mapBinaryenType(opts, entity.originalType ?? entity.type!); + const get = mod.local.get(entity.getIndex(), type); + if (entity.requiresCast) { + return refCast(mod, get, mapBinaryenType(opts, entity.type!)); + } + return get; } throw new Error(`Cannot compile identifier ${expr}`); @@ -116,23 +161,23 @@ const compileCall = (opts: CompileExprOpts): number => { return mod.call( expr.fn!.id, args, - mapBinaryenType(mod, expr.fn!.returnType!) + mapBinaryenType(opts, expr.fn!.returnType!) ); }; const compileObjectInit = (opts: CompileExprOpts) => { const { expr, mod } = opts; - const objectType = mapBinaryenType(mod, expr.type!); + const objectType = getExprType(expr) as ObjectType; + const objectBinType = mapBinaryenType(opts, objectType); const obj = expr.argAt(0) as ObjectLiteral; - return initStruct( - mod, - binaryenTypeToHeapType(objectType), - obj.fields.map((field) => + return initStruct(mod, binaryenTypeToHeapType(objectBinType), [ + mod.global.get(`__rtt_${objectType.id}`, opts.extensionHelpers.i32Array), + ...obj.fields.map((field) => compileExpression({ ...opts, expr: field.initializer }) - ) - ); + ), + ]); }; const compileExport = (opts: CompileExprOpts) => { @@ -192,10 +237,10 @@ const compileVariable = (opts: CompileExprOpts): number => { const compileFunction = (opts: CompileExprOpts): number => { const { expr: fn, mod } = opts; - const parameterTypes = getFunctionParameterTypes(mod, fn); - const returnType = mapBinaryenType(mod, fn.getReturnType()); + const parameterTypes = getFunctionParameterTypes(opts, fn); + const returnType = mapBinaryenType(opts, fn.getReturnType()); const body = compileExpression({ ...opts, expr: fn.body! }); - const variableTypes = getFunctionVarTypes(mod, fn); // TODO: Vars should probably be registered with the function type rather than body (for consistency). + const variableTypes = getFunctionVarTypes(opts, fn); // TODO: Vars should probably be registered with the function type rather than body (for consistency). mod.addFunction(fn.id, parameterTypes, returnType, variableTypes, body); @@ -214,14 +259,14 @@ const compileDeclaration = (opts: CompileExprOpts) => { const compileExternFn = (opts: CompileExprOpts & { namespace: string }) => { const { expr: fn, mod, namespace } = opts; - const parameterTypes = getFunctionParameterTypes(mod, fn); + const parameterTypes = getFunctionParameterTypes(opts, fn); mod.addFunctionImport( fn.id, namespace, fn.getNameStr(), parameterTypes, - mapBinaryenType(mod, fn.getReturnType()) + mapBinaryenType(opts, fn.getReturnType()) ); return mod.nop(); @@ -230,15 +275,15 @@ const compileExternFn = (opts: CompileExprOpts & { namespace: string }) => { const compileObjectLiteral = (opts: CompileExprOpts) => { const { expr: obj, mod } = opts; - const literalType = mapBinaryenType(mod, obj.type!); + const objectType = getExprType(obj) as ObjectType; + const literalBinType = mapBinaryenType(opts, objectType); - return initStruct( - mod, - binaryenTypeToHeapType(literalType), - obj.fields.map((field) => + return initStruct(mod, binaryenTypeToHeapType(literalBinType), [ + mod.global.get(`__rtt_${objectType.id}`, opts.extensionHelpers.i32Array), + ...obj.fields.map((field) => compileExpression({ ...opts, expr: field.initializer }) - ) - ); + ), + ]); }; const compileIf = (opts: CompileExprOpts) => { @@ -256,15 +301,17 @@ const compileIf = (opts: CompileExprOpts) => { return mod.if(condition, ifTrue, ifFalse); }; -const getFunctionParameterTypes = (mod: binaryen.Module, fn: Fn) => { - const types = fn.parameters.map((param) => mapBinaryenType(mod, param.type!)); +const getFunctionParameterTypes = (opts: CompileExprOpts, fn: Fn) => { + const types = fn.parameters.map((param) => + mapBinaryenType(opts, param.type!) + ); return binaryen.createType(types); }; -const getFunctionVarTypes = (mod: binaryen.Module, fn: Fn) => - fn.variables.map((v) => mapBinaryenType(mod, v.type!)); +const getFunctionVarTypes = (opts: CompileExprOpts, fn: Fn) => + fn.variables.map((v) => mapBinaryenType(opts, v.type!)); -const mapBinaryenType = (mod: binaryen.Module, type: Type): binaryen.Type => { +const mapBinaryenType = (opts: CompileExprOpts, type: Type): binaryen.Type => { if (isPrimitiveId(type, "bool")) return binaryen.i32; if (isPrimitiveId(type, "i32")) return binaryen.i32; if (isPrimitiveId(type, "f32")) return binaryen.f32; @@ -272,7 +319,7 @@ const mapBinaryenType = (mod: binaryen.Module, type: Type): binaryen.Type => { if (isPrimitiveId(type, "f64")) return binaryen.f64; if (isPrimitiveId(type, "void")) return binaryen.none; if (type.isObjectType()) { - return type.binaryenType ? type.binaryenType : buildObjectType(mod, type); + return type.binaryenType ? type.binaryenType : buildObjectType(opts, type); } throw new Error(`Unsupported type ${type}`); }; @@ -280,20 +327,38 @@ const mapBinaryenType = (mod: binaryen.Module, type: Type): binaryen.Type => { const isPrimitiveId = (type: Type, id: Primitive) => type.isPrimitiveType() && type.name.value === id; +/** TODO: Skip building types for object literals that are part of an initializer of an obj */ const buildObjectType = ( - mod: binaryen.Module, + opts: CompileExprOpts, obj: ObjectType ): HeapTypeRef => { + const mod = opts.mod; + const binaryenType = defineStructType(mod, { name: obj.id, - fields: obj.fields.map((field) => ({ - type: mapBinaryenType(mod, field.type!), - name: field.name, - })), + fields: [ + { + type: opts.extensionHelpers.i32Array, + name: "ancestors", + }, + ...obj.fields.map((field) => ({ + type: mapBinaryenType(opts, field.type!), + name: field.name, + })), + ], supertype: obj.parentObj - ? binaryenTypeToHeapType(mapBinaryenType(mod, obj.parentObj)) + ? binaryenTypeToHeapType(mapBinaryenType(opts, obj.parentObj)) : undefined, }); + + // Set RTT Table (So we don't have to re-calculate it every time) + mod.addGlobal( + `__rtt_${obj.id}`, + opts.extensionHelpers.i32Array, + false, + opts.extensionHelpers.initExtensionArray(obj.getAncestorIds()) + ); + obj.binaryenType = binaryenType; return binaryenType; }; @@ -304,12 +369,12 @@ const compileObjMemberAccess = (opts: CompileExprOpts) => { const member = expr.identifierArgAt(1); const objValue = compileExpression({ ...opts, expr: obj }); const type = getExprType(obj) as ObjectType; - const memberIndex = type.getFieldIndex(member); + const memberIndex = type.getFieldIndex(member) + 1; // +1 to account for the RTT table const field = type.getField(member)!; return structGetFieldValue({ mod, fieldIndex: memberIndex, - fieldType: mapBinaryenType(mod, field.type!), + fieldType: mapBinaryenType(opts, field.type!), exprRef: objValue, }); }; diff --git a/src/assembler/extension-helpers.ts b/src/assembler/extension-helpers.ts new file mode 100644 index 00000000..0b551039 --- /dev/null +++ b/src/assembler/extension-helpers.ts @@ -0,0 +1,80 @@ +import binaryen from "binaryen"; +import { AugmentedBinaryen } from "../lib/binaryen-gc/types.js"; +import { + defineArrayType, + arrayLen, + arrayGet, + arrayNewFixed, + binaryenTypeToHeapType, +} from "../lib/binaryen-gc/index.js"; + +const bin = binaryen as unknown as AugmentedBinaryen; + +export const initExtensionHelpers = (mod: binaryen.Module) => { + const i32Array = defineArrayType(mod, bin.i32, true); + + mod.addFunction( + "__extends", + // Extension Obj Id, Ancestor Ids Array + bin.createType([bin.i32, i32Array]), + bin.i32, + [bin.i32, bin.i32], // Current index, Does Extend + mod.block(null, [ + mod.local.set(2, mod.i32.const(0)), // Current ancestor index + mod.local.set(3, mod.i32.const(0)), // Does extend + mod.block("break", [ + mod.loop( + "loop", + mod.block(null, [ + // Break if we've reached the end of the ancestors + mod.br_if( + "break", + mod.i32.eq( + mod.local.get(2, bin.i32), + arrayLen(mod, mod.local.get(1, i32Array)) + ) + ), + + // Check if we've found the ancestor + mod.if( + mod.i32.eq( + mod.local.get(0, bin.i32), + arrayGet( + mod, + mod.local.get(1, i32Array), + mod.local.get(2, bin.i32), + bin.i32, + false + ) + ), + + // If we have, set doesExtend to true and break + mod.block(null, [ + mod.local.set(3, mod.i32.const(1)), + mod.br("break"), + ]) + ), + + // Increment ancestor index + mod.local.set( + 2, + mod.i32.add(mod.local.get(2, bin.i32), mod.i32.const(1)) + ), + mod.br("loop"), + ]) + ), + ]), + mod.local.get(3, bin.i32), + ]) + ); + + const initExtensionArray = (ancestorIds: number[]) => { + return arrayNewFixed( + mod, + binaryenTypeToHeapType(i32Array), + ancestorIds.map((id) => mod.i32.const(id)) + ); + }; + + return { initExtensionArray, i32Array }; +}; diff --git a/src/lib/binaryen-gc/index.ts b/src/lib/binaryen-gc/index.ts index 310030a7..c990ba48 100644 --- a/src/lib/binaryen-gc/index.ts +++ b/src/lib/binaryen-gc/index.ts @@ -10,7 +10,10 @@ import { const bin = binaryen as unknown as AugmentedBinaryen; -export const defineStructType = (mod: binaryen.Module, struct: Struct) => { +export const defineStructType = ( + mod: binaryen.Module, + struct: Struct +): TypeRef => { const fields = struct.fields; const structIndex = 0; const typeBuilder = bin._TypeBuilderCreate(1); @@ -44,6 +47,42 @@ export const defineStructType = (mod: binaryen.Module, struct: Struct) => { bin._free(fieldPackedTypesPtr); bin._free(fieldMutablesPtr); + const result = typeBuilderBuildAndDispose(typeBuilder); + + annotateStructNames(mod, result, struct); + + return bin._BinaryenTypeFromHeapType(result, false); +}; + +export const defineArrayType = ( + mod: binaryen.Module, + elementType: TypeRef, + mutable = false, + name?: string +): TypeRef => { + const typeBuilder = bin._TypeBuilderCreate(1); + bin._TypeBuilderSetArrayType( + typeBuilder, + 0, + elementType, + bin._BinaryenPackedTypeNotPacked(), + mutable + ); + + const result = typeBuilderBuildAndDispose(typeBuilder); + + if (name) { + bin._BinaryenModuleSetTypeName( + mod.ptr, + result, + bin.stringToUTF8OnStack(name) + ); + } + + return bin._BinaryenTypeFromHeapType(result, false); +}; + +const typeBuilderBuildAndDispose = (typeBuilder: number): HeapTypeRef => { const size = bin._TypeBuilderGetSize(typeBuilder); const out = bin._malloc(Math.max(4 * size, 8)); @@ -55,9 +94,7 @@ export const defineStructType = (mod: binaryen.Module, struct: Struct) => { const result = bin.__i32_load(out); bin._free(out); - annotateStructNames(mod, result, struct); - - return bin._BinaryenTypeFromHeapType(result, false); + return result; }; export const binaryenTypeToHeapType = (type: Type): HeapTypeRef => { @@ -70,6 +107,12 @@ export const refCast = ( type: TypeRef ): ExpressionRef => bin._BinaryenRefCast(mod.ptr, ref, type); +export const refTest = ( + mod: binaryen.Module, + ref: ExpressionRef, + type: TypeRef +): ExpressionRef => bin._BinaryenRefTest(mod.ptr, ref, type); + export const initStruct = ( mod: binaryen.Module, structType: HeapTypeRef, @@ -108,6 +151,75 @@ export const structGetFieldValue = ({ ); }; +export const arrayGet = ( + mod: binaryen.Module, + arrayRef: ExpressionRef, + index: ExpressionRef, + elementType: TypeRef, + signed: boolean +): ExpressionRef => { + return bin._BinaryenArrayGet(mod.ptr, arrayRef, index, elementType, signed); +}; + +export const arraySet = ( + mod: binaryen.Module, + arrayRef: ExpressionRef, + index: ExpressionRef, + value: ExpressionRef +): ExpressionRef => { + return bin._BinaryenArraySet(mod.ptr, arrayRef, index, value); +}; + +export const arrayLen = ( + mod: binaryen.Module, + arrayRef: ExpressionRef +): ExpressionRef => { + return bin._BinaryenArrayLen(mod.ptr, arrayRef); +}; + +export const arrayNew = ( + mod: binaryen.Module, + type: HeapTypeRef, + initialLength: ExpressionRef, + init: ExpressionRef +): ExpressionRef => { + return bin._BinaryenArrayNew(mod.ptr, type, initialLength, init); +}; + +export const arrayNewFixed = ( + mod: binaryen.Module, + type: HeapTypeRef, + values: ExpressionRef[] +): ExpressionRef => { + const valuesPtr = allocU32Array(values); + const result = bin._BinaryenArrayNewFixed( + mod.ptr, + type, + valuesPtr, + values.length + ); + bin._free(valuesPtr); + return result; +}; + +export const arrayCopy = ( + mod: binaryen.Module, + destRef: ExpressionRef, + destIndex: ExpressionRef, + srcRef: ExpressionRef, + srcIndex: ExpressionRef, + length: ExpressionRef +): ExpressionRef => { + return bin._BinaryenArrayCopy( + mod.ptr, + destRef, + destIndex, + srcRef, + srcIndex, + length + ); +}; + /** Returns a pointer to the allocated array */ const allocU32Array = (u32s: number[]): number => { const ptr = bin._malloc(u32s.length << 2); diff --git a/src/lib/binaryen-gc/test.ts b/src/lib/binaryen-gc/test.ts index 5c9309b3..08451f49 100644 --- a/src/lib/binaryen-gc/test.ts +++ b/src/lib/binaryen-gc/test.ts @@ -1,171 +1,112 @@ import binaryen from "binaryen"; import { AugmentedBinaryen } from "./types.js"; import { + arrayGet, + arrayLen, + arrayNew, + arrayNewFixed, + arraySet, binaryenTypeToHeapType, - defineStructType, - initStruct, - refCast, - structGetFieldValue, + defineArrayType, } from "./index.js"; import { run } from "../../run.js"; const bin = binaryen as unknown as AugmentedBinaryen; -// Structural sub-typing experiment export function testGc() { - // Simple module with a function that returns a Vec, and a main function that reads the x value const mod = new binaryen.Module(); mod.setFeatures(binaryen.Features.All); - // type Object = {}; - // type A = { a: i32 }; - // type B = { b: i32 }; - // type Vec = { a: i32; b: i32 }; + const i32Array = defineArrayType(mod, bin.i32, true); - const objectType = defineStructType(mod, { - name: "Object", - fields: [], - }); - - const objectTypeRef = binaryenTypeToHeapType(objectType); - - const aType = defineStructType(mod, { - name: "A", - fields: [{ name: "a", type: bin.i32, mutable: false }], - supertype: objectTypeRef, - }); - - const aTypeRef = binaryenTypeToHeapType(objectType); - - const bType = defineStructType(mod, { - name: "B", - fields: [{ name: "b", type: bin.i32, mutable: false }], - supertype: objectTypeRef, - }); - - const bTypeRef = binaryenTypeToHeapType(bType); - - const vecType = defineStructType(mod, { - name: "Vec", - fields: [ - { name: "a", type: bin.i32, mutable: false }, - { name: "b", type: bin.i32, mutable: false }, - ], - supertype: objectTypeRef, - }); - - const vecTypeRef = binaryenTypeToHeapType(vecType); - - const vec = initStruct(mod, vecTypeRef, [mod.i32.const(1), mod.i32.const(1)]); + const initExtensionArray = (ancestorIds: number[]) => { + return arrayNewFixed( + mod, + binaryenTypeToHeapType(i32Array), + ancestorIds.map((id) => mod.i32.const(id)) + ); + }; mod.addFunction( - "castVec", - bin.createType([bType]), - vecType, - [], - mod.block(null, [refCast(mod, mod.local.get(0, bType), vecType)]) - ); - - mod.addFunction( - "main", - bin.createType([]), + "__extends", + // Extension Obj Id, Ancestor Ids Array + bin.createType([bin.i32, i32Array]), bin.i32, - [bin.anyref], + [bin.i32, bin.i32], // Current index, Does Extend mod.block(null, [ - mod.local.set(0, vec), - structGetFieldValue({ - mod, - fieldIndex: 0, - fieldType: bin.i32, - exprRef: refCast(mod, mod.local.get(0, bin.anyref), bType), - }), + mod.local.set(2, mod.i32.const(0)), // Current ancestor index + mod.local.set(3, mod.i32.const(0)), // Does extend + mod.block("break", [ + mod.loop( + "loop", + mod.block(null, [ + // Break if we've reached the end of the ancestors + mod.br_if( + "break", + mod.i32.eq( + mod.local.get(2, bin.i32), + arrayLen(mod, mod.local.get(1, i32Array)) + ) + ), + + // Check if we've found the ancestor + mod.if( + mod.i32.eq( + mod.local.get(0, bin.i32), + arrayGet( + mod, + mod.local.get(1, i32Array), + mod.local.get(2, bin.i32), + bin.i32, + false + ) + ), + // If we have, set doesExtend to true and break + mod.block(null, [ + mod.local.set(3, mod.i32.const(1)), + mod.br("break"), + ]) + ), + + // Increment ancestor index + mod.local.set( + 2, + mod.i32.add(mod.local.get(2, bin.i32), mod.i32.const(1)) + ), + mod.br("loop"), + ]) + ), + ]), + mod.local.get(3, bin.i32), ]) ); - mod.addFunctionExport("main", "main"); - mod.autoDrop(); - mod.validate(); - - console.log(mod.emitText()); - run(mod); -} - -export function testGcOld() { - // Simple module with a function that returns a Vec, and a main function that reads the x value - const mod = new binaryen.Module(); - mod.setFeatures(binaryen.Features.All); - - const dotType = defineStructType(mod, { - name: "Dot", - fields: [ - { name: "a", type: bin.i32, mutable: false }, - { name: "b", type: bin.i32, mutable: false }, - ], - }); - - const dotTypeRef = binaryenTypeToHeapType(dotType); - - const spotType = defineStructType(mod, { - name: "Spot", - fields: [ - { name: "a", type: bin.i32, mutable: false }, - { name: "b", type: bin.i32, mutable: false }, - { name: "c", type: bin.i32, mutable: false }, - ], - supertype: dotTypeRef, - }); - - const spotTypeRef = binaryenTypeToHeapType(spotType); - - const vecType = defineStructType(mod, { - name: "Vec", - fields: [ - { name: "x", type: bin.i32, mutable: true }, - { name: "y", type: bin.i32, mutable: false }, - { name: "z", type: dotType, mutable: false }, - ], - }); - - const vecTypeRef = binaryenTypeToHeapType(vecType); - - const newStruct = initStruct(mod, vecTypeRef, [ - mod.i32.const(1), - mod.i32.const(2), - initStruct(mod, spotTypeRef, [ - mod.i32.const(1), - mod.i32.const(2), - mod.i32.const(2), - ]), - ]); + mod.addGlobal( + "extensionArray", + i32Array, + false, + initExtensionArray([1, 2, 3]) + ); - // Main function that reads the x value of the Vec mod.addFunction( "main", bin.createType([]), bin.i32, - [vecType], + [i32Array], mod.block(null, [ - mod.local.set(0, newStruct), - structGetFieldValue({ - mod, - fieldIndex: 1, - fieldType: bin.i32, - exprRef: structGetFieldValue({ - mod, - fieldIndex: 2, - fieldType: dotType, - exprRef: mod.local.get(0, vecType), - }), - }), + mod.local.set(0, initExtensionArray([1, 2, 3])), + mod.call( + "__extends", + [mod.i32.const(4), mod.global.get("extensionArray", i32Array)], + bin.i32 + ), ]) ); mod.addFunctionExport("main", "main"); - mod.autoDrop(); - mod.validate(); - console.log(mod.emitText()); + // console.log(mod.emitText()); run(mod); } diff --git a/src/lib/binaryen-gc/types.ts b/src/lib/binaryen-gc/types.ts index 237466cf..fc32727f 100644 --- a/src/lib/binaryen-gc/types.ts +++ b/src/lib/binaryen-gc/types.ts @@ -147,19 +147,6 @@ export type AugmentedBinaryen = typeof binaryen & { value: ExpressionRef ): ExpressionRef; _BinaryenArrayLen(module: ModuleRef, ref: ExpressionRef): ExpressionRef; - _BinaryenArrayGetPtr( - module: ModuleRef, - ref: ExpressionRef, - index: ExpressionRef - ): ExpressionRef; - _BinaryenArraySetPtr( - module: ModuleRef, - ref: ExpressionRef, - index: ExpressionRef, - value: ExpressionRef - ): ExpressionRef; - _BinaryenArrayToStack(module: ModuleRef, ref: ExpressionRef): ExpressionRef; - _BinaryenArrayFromStack(module: ModuleRef, ref: ExpressionRef): ExpressionRef; _BinaryenModuleSetTypeName( module: ModuleRef, type: HeapTypeRef, @@ -171,6 +158,11 @@ export type AugmentedBinaryen = typeof binaryen & { index: number, name: unknown ): void; + _BinaryenRefTest( + module: ModuleRef, + ref: ExpressionRef, + type: TypeRef + ): ExpressionRef; stringToUTF8OnStack(str: string): number; HEAPU32: Uint32Array; }; diff --git a/src/semantics/check-types.ts b/src/semantics/check-types.ts index b35818d2..1ebf2a03 100644 --- a/src/semantics/check-types.ts +++ b/src/semantics/check-types.ts @@ -17,6 +17,7 @@ import { TypeAlias, ObjectLiteral, } from "../syntax-objects/index.js"; +import { Match } from "../syntax-objects/match.js"; import { getExprType } from "./resolution/get-expr-type.js"; import { typesAreEquivalent } from "./resolution/index.js"; @@ -33,6 +34,7 @@ export const checkTypes = (expr: Expr | undefined): Expr => { if (expr.isObjectType()) return checkObjectType(expr); if (expr.isTypeAlias()) return checkTypeAlias(expr); if (expr.isObjectLiteral()) return checkObjectLiteralType(expr); + if (expr.isMatch()) return checkMatch(expr); return expr; }; @@ -311,3 +313,38 @@ const checkObjectLiteralType = (obj: ObjectLiteral) => { obj.fields.forEach((field) => checkTypes(field.initializer)); return obj; }; + +const checkMatch = (match: Match) => { + // (Until unions) + if (!match.defaultCase) { + throw new Error(`Match must have a default case at ${match.location}`); + } + + if (!match.baseType || !match.baseType.isObjectType()) { + throw new Error( + `Unable to determine base type for match at ${match.location}` + ); + } + + for (const mCase of match.cases) { + if (!mCase.matchType) { + throw new Error( + `Unable to determine match type for case at ${mCase.expr.location}` + ); + } + + if (!mCase.matchType.extends(match.baseType)) { + throw new Error( + `Match case type ${mCase.matchType.name} does not extend ${match.baseType.name}` + ); + } + + if (!typesAreEquivalent(mCase.expr.type, match.type)) { + throw new Error( + `All cases must return the same type for now ${mCase.expr.location}` + ); + } + } + + return match; +}; diff --git a/src/semantics/init-entities.ts b/src/semantics/init-entities.ts index 6d1dc7de..3d9079ac 100644 --- a/src/semantics/init-entities.ts +++ b/src/semantics/init-entities.ts @@ -10,7 +10,11 @@ import { TypeAlias, ObjectType, ObjectLiteral, + Identifier, } from "../syntax-objects/index.js"; +import { Match, MatchCase } from "../syntax-objects/match.js"; +import { getExprType } from "./resolution/get-expr-type.js"; +import { resolveTypes } from "./resolution/resolve-types.js"; import { SemanticProcessor } from "./types.js"; export const initEntities: SemanticProcessor = (expr) => { @@ -50,6 +54,10 @@ export const initEntities: SemanticProcessor = (expr) => { return initNominalObjectType(expr); } + if (expr.calls("match")) { + return initMatch(expr); + } + return initCall(expr); }; @@ -252,6 +260,70 @@ const initObjectType = (obj: List) => { }); }; +export const initMatch = (match: List): Match => { + const operand = initEntities(match.exprAt(1)); + const identifierIndex = match.at(2)?.isIdentifier() ? 2 : 1; + const identifier = match.identifierAt(identifierIndex); + const caseExprs = match.sliceAsArray(identifierIndex + 1); + const cases = initMatchCases(caseExprs); + + return new Match({ + ...match.metadata, + operand, + cases: cases.cases, + defaultCase: cases.defaultCase, + bindIdentifier: identifier, + bindVariable: + identifierIndex === 2 // We need a new variable if the second argument is an identifier + ? new Variable({ + name: identifier, + location: identifier.location, + initializer: operand, + isMutable: false, + parent: match, + }) + : undefined, + }); +}; + +const initMatchCases = ( + cases: Expr[] +): { cases: MatchCase[]; defaultCase?: MatchCase } => { + return cases.reduce( + ({ cases, defaultCase }, expr) => { + if (!expr.isList() || !expr.calls(":")) { + throw new Error( + `Match cases must be in the form of : at ${expr.location}` + ); + } + + const isElse = + expr.at(1)?.isIdentifier() && expr.identifierAt(1).is("else"); + + const typeExpr = !isElse ? initEntities(expr.exprAt(1)) : undefined; + + const caseExpr = initEntities(expr.exprAt(2)); + + const scopedCaseExpr = + caseExpr?.isCall() || caseExpr?.isBlock() + ? caseExpr + : new Block({ ...caseExpr.metadata, body: [caseExpr] }); + + const mCase = { matchTypeExpr: typeExpr, expr: scopedCaseExpr }; + + if (isElse) { + return { cases, defaultCase: mCase }; + } + + return { cases: [...cases, mCase], defaultCase }; + }, + { + cases: [] as MatchCase[], + defaultCase: undefined as MatchCase | undefined, + } + ); +}; + const extractObjectFields = (obj: List) => { return obj.sliceAsArray(1).map((v) => { if (!v.isList()) { diff --git a/src/semantics/resolution/get-expr-type.ts b/src/semantics/resolution/get-expr-type.ts index 4a1c3f5b..dff7db52 100644 --- a/src/semantics/resolution/get-expr-type.ts +++ b/src/semantics/resolution/get-expr-type.ts @@ -18,6 +18,7 @@ export const getExprType = (expr?: Expr): Type | undefined => { if (expr.isType()) return expr; if (expr.isBlock()) return expr.type; if (expr.isObjectLiteral()) return expr.type; + if (expr.isMatch()) return expr.type; }; export const getIdentifierType = (id: Identifier): Type | undefined => { diff --git a/src/semantics/resolution/resolve-match.ts b/src/semantics/resolution/resolve-match.ts new file mode 100644 index 00000000..adac98ac --- /dev/null +++ b/src/semantics/resolution/resolve-match.ts @@ -0,0 +1,58 @@ +import { Block } from "../../syntax-objects/block.js"; +import { Call, Parameter, Variable } from "../../syntax-objects/index.js"; +import { Match, MatchCase } from "../../syntax-objects/match.js"; +import { getExprType } from "./get-expr-type.js"; +import { resolveTypes } from "./resolve-types.js"; + +export const resolveMatch = (match: Match): Match => { + match.operand = resolveTypes(match.operand); + match.baseType = getExprType(match.operand); + + const binding = getBinding(match); + resolveCases(binding, match); + match.type = (match.defaultCase?.expr ?? match.cases[0].expr).type; + + return match; +}; + +const resolveCases = (binding: Parameter | Variable, match: Match) => { + match.cases = match.cases.map((c) => resolveCase(binding, c)); + match.defaultCase = match.defaultCase + ? resolveCase(binding, match.defaultCase) + : undefined; +}; + +const resolveCase = ( + binding: Parameter | Variable, + c: MatchCase +): MatchCase => { + const type = getExprType(c.matchTypeExpr); + + const localBinding = binding.clone(); + localBinding.originalType = localBinding.type; + localBinding.type = type; + localBinding.requiresCast = true; + c.expr.registerEntity(localBinding); + + const expr = resolveTypes(c.expr) as Call | Block; + + return { + matchType: type?.isObjectType() ? type : undefined, + expr, + matchTypeExpr: c.matchTypeExpr, + }; +}; + +const getBinding = (match: Match): Parameter | Variable => { + if (match.bindVariable) { + return match.bindVariable; + } + + const binding = match.bindIdentifier.resolve(); + + if (!(binding?.isVariable() || binding?.isParameter())) { + throw new Error(`Match binding must be a variable or parameter`); + } + + return binding; +}; diff --git a/src/semantics/resolution/resolve-types.ts b/src/semantics/resolution/resolve-types.ts index 088c4e28..87afc0d4 100644 --- a/src/semantics/resolution/resolve-types.ts +++ b/src/semantics/resolution/resolve-types.ts @@ -9,6 +9,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 { resolveMatch } from "./resolve-match.js"; import { resolveUse } from "./resolve-use.js"; /** @@ -27,6 +28,7 @@ export const resolveTypes = (expr: Expr | undefined): Expr => { if (expr.isObjectType()) return resolveObjectTypeTypes(expr); if (expr.isTypeAlias()) return resolveTypeAliasTypes(expr); if (expr.isObjectLiteral()) return resolveObjectLiteralTypes(expr); + if (expr.isMatch()) return resolveMatch(expr); return expr; }; diff --git a/src/syntax-objects/block.ts b/src/syntax-objects/block.ts index 3e0299c0..7d9fd13f 100644 --- a/src/syntax-objects/block.ts +++ b/src/syntax-objects/block.ts @@ -10,12 +10,13 @@ export class Block extends ScopedSyntax { constructor( opts: ScopedSyntaxMetadata & { - body: List; + body: List | Expr[]; type?: Type; } ) { super(opts); - this.body = opts.body; + this.body = + opts.body instanceof Array ? new List({ value: opts.body }) : opts.body; this.type = opts.type; } diff --git a/src/syntax-objects/call.ts b/src/syntax-objects/call.ts index ae5e147a..9e367fd5 100644 --- a/src/syntax-objects/call.ts +++ b/src/syntax-objects/call.ts @@ -1,17 +1,20 @@ import { Expr } from "./expr.js"; import { Fn } from "./fn.js"; import { Identifier } from "./identifier.js"; +import { LexicalContext } from "./lexical-context.js"; import { List } from "./list.js"; -import { Syntax, SyntaxMetadata } from "./syntax.js"; +import { ScopedSyntax } from "./scoped-entity.js"; +import { SyntaxMetadata } from "./syntax.js"; import { ObjectType, Type } from "./types.js"; /** Defines a function call */ -export class Call extends Syntax { +export class Call extends ScopedSyntax { readonly syntaxType = "call"; fn?: Fn | ObjectType; fnName: Identifier; args: List; type?: Type; + lexicon: LexicalContext; constructor( opts: SyntaxMetadata & { @@ -19,6 +22,7 @@ export class Call extends Syntax { fn?: Fn; args: List; type?: Type; + lexicon?: LexicalContext; } ) { super(opts); @@ -26,6 +30,7 @@ export class Call extends Syntax { this.fn = opts.fn; this.args = opts.args; this.type = opts.type; + this.lexicon = opts.lexicon ?? new LexicalContext(); opts.args.parent = this; } diff --git a/src/syntax-objects/expr.ts b/src/syntax-objects/expr.ts index ba9ecdb7..ba686fc0 100644 --- a/src/syntax-objects/expr.ts +++ b/src/syntax-objects/expr.ts @@ -19,6 +19,7 @@ import { VoidModule } from "./module.js"; import { Declaration } from "./declaration.js"; import { Use } from "./use.js"; import { ObjectLiteral } from "./object-literal.js"; +import { Match } from "./match.js"; export type Expr = | PrimitiveExpr @@ -35,7 +36,8 @@ export type Expr = | Block | Declaration | Use - | ObjectLiteral; + | ObjectLiteral + | Match; /** * 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 e25972d2..24e914f9 100644 --- a/src/syntax-objects/fn.ts +++ b/src/syntax-objects/fn.ts @@ -68,9 +68,7 @@ export class Fn extends ScopedNamedEntity { } getIndexOfParameter(parameter: Parameter) { - const index = this.parameters.findIndex( - (p) => p.syntaxId === parameter.syntaxId - ); + const index = this.parameters.findIndex((p) => p.id === parameter.id); if (index < 0) { throw new Error(`Parameter ${parameter} not registered with fn ${this}`); } @@ -78,9 +76,7 @@ export class Fn extends ScopedNamedEntity { } getIndexOfVariable(variable: Variable) { - const index = this.variables.findIndex( - (v) => v.syntaxId === variable.syntaxId - ); + const index = this.variables.findIndex((v) => v.id === variable.id); if (index < 0) { const newIndex = this.variables.push(variable) - 1; diff --git a/src/syntax-objects/list.ts b/src/syntax-objects/list.ts index 639c8191..2aea500d 100644 --- a/src/syntax-objects/list.ts +++ b/src/syntax-objects/list.ts @@ -217,6 +217,7 @@ export class List extends Syntax { return this.toArray(); } + /** Clones should return a deep copy of all expressions except for type expressions */ clone(parent?: Expr): List { return new List({ ...super.getCloneOpts(parent), diff --git a/src/syntax-objects/match.ts b/src/syntax-objects/match.ts new file mode 100644 index 00000000..669eb05e --- /dev/null +++ b/src/syntax-objects/match.ts @@ -0,0 +1,83 @@ +import { Block } from "./block.js"; +import { Call } from "./call.js"; +import { Expr } from "./expr.js"; +import { Identifier } from "./identifier.js"; +import { Syntax, SyntaxMetadata } from "./syntax.js"; +import { ObjectType, Type } from "./types.js"; +import { Variable } from "./variable.js"; + +export type MatchCase = { + /** The type to match the base type against */ + matchType?: ObjectType; + matchTypeExpr?: Expr; + expr: Block | Call; +}; + +export type MatchOpts = SyntaxMetadata & { + operand: Expr; + cases: MatchCase[]; + defaultCase?: MatchCase; + type?: Type; + baseType?: Type; + bindVariable?: Variable; + bindIdentifier: Identifier; +}; + +export class Match extends Syntax { + readonly syntaxType = "match"; + /** Match expr return type */ + type?: Type; + /** Type being matched against */ + baseType?: Type; + operand: Expr; + /** A variable to bind the operand to when needed */ + bindVariable?: Variable; + cases: MatchCase[]; + defaultCase?: MatchCase; + bindIdentifier: Identifier; + + constructor(opts: MatchOpts) { + super(opts); + this.operand = opts.operand; + this.cases = opts.cases; + this.defaultCase = opts.defaultCase; + this.type = opts.type; + this.bindVariable = opts.bindVariable; + this.baseType = opts.baseType; + this.bindIdentifier = opts.bindIdentifier; + } + + toJSON(): object { + return ["match", this.operand.toJSON(), ...this.cases, this.defaultCase]; + } + + clone(parent?: Expr): Match { + return new Match({ + ...this.getCloneOpts(parent), + operand: this.operand.clone(), + cases: this.cases.map((c) => ({ ...c, expr: c.expr.clone() })), + defaultCase: this.defaultCase + ? { + ...this.defaultCase, + expr: this.defaultCase.expr.clone(), + } + : undefined, + type: this.type, + bindVariable: this.bindVariable?.clone(), + bindIdentifier: this.bindIdentifier.clone(), + }); + } +} + +/** + * Notes: + * - Matches must be exhaustive. + * When a match type is an object. It must have a default case. + * When it is a union, it must have a case for each type in the union. + * + * - Unions require special handling in the compiler. Each case must + * bind an identifier to the "dereferenced"* value of the union. + * + * *Dereferenced means from the value of the union, not the union itself. The + * value can still be a reference to an object. + */ diff --git a/src/syntax-objects/named-entity.ts b/src/syntax-objects/named-entity.ts index 6f2e4359..1bf166b4 100644 --- a/src/syntax-objects/named-entity.ts +++ b/src/syntax-objects/named-entity.ts @@ -6,11 +6,13 @@ import { Syntax, SyntaxMetadata } from "./syntax.js"; export type NamedEntityOpts = SyntaxMetadata & { name: Id; id?: string; + idNum?: number; isExported?: boolean; }; export abstract class NamedEntity extends Syntax { id: string; + idNum: number; name: Identifier; isExported = false; @@ -19,6 +21,7 @@ export abstract class NamedEntity extends Syntax { this.name = typeof opts.name === "string" ? Identifier.from(opts.name) : opts.name; this.id = opts.id ?? this.genId(); + this.idNum = this.syntaxId; this.isExported = opts.isExported ?? false; } @@ -30,6 +33,7 @@ export abstract class NamedEntity extends Syntax { return { ...super.getCloneOpts(parent), id: this.id, + idNum: this.idNum, name: this.name, isExported: this.isExported, }; diff --git a/src/syntax-objects/parameter.ts b/src/syntax-objects/parameter.ts index 35c8c71d..3327cda1 100644 --- a/src/syntax-objects/parameter.ts +++ b/src/syntax-objects/parameter.ts @@ -7,8 +7,10 @@ export class Parameter extends NamedEntity { readonly syntaxType = "parameter"; /** External label the parameter must be called with e.g. myFunc(label: value) */ label?: Identifier; + originalType?: Type; type?: Type; typeExpr?: Expr; + requiresCast = false; constructor( opts: NamedEntityOpts & { diff --git a/src/syntax-objects/syntax.ts b/src/syntax-objects/syntax.ts index f73d32cd..0f719867 100644 --- a/src/syntax-objects/syntax.ts +++ b/src/syntax-objects/syntax.ts @@ -29,6 +29,7 @@ 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"; export type SyntaxMetadata = { location?: SourceLocation; @@ -117,7 +118,7 @@ export abstract class Syntax { abstract clone(parent?: Expr): Expr; /** Should emit in compliance with core language spec */ - abstract toJSON(): any; + abstract toJSON(): unknown; isScopedEntity(): this is ScopedEntity { return (this as unknown as ScopedEntity).lexicon instanceof LexicalContext; @@ -135,6 +136,10 @@ export abstract class Syntax { return this.syntaxType === "list"; } + isMatch(): this is Match { + return this.syntaxType === "match"; + } + isFloat(): this is Float { return this.syntaxType === "float"; } diff --git a/src/syntax-objects/types.ts b/src/syntax-objects/types.ts index feda5bfe..aeecf7c7 100644 --- a/src/syntax-objects/types.ts +++ b/src/syntax-objects/types.ts @@ -188,7 +188,11 @@ export class ObjectType extends BaseType { toJSON(): TypeJSON { return [ "type", - ["object", ...this.fields.map(({ name, typeExpr }) => [name, typeExpr])], + [ + "object", + this.id, + ...this.fields.map(({ name, typeExpr }) => [name, typeExpr]), + ], ]; } @@ -211,6 +215,14 @@ export class ObjectType extends BaseType { return false; } + getAncestorIds(start: number[] = []): number[] { + start.push(this.idNum); + if (this.parentObj) { + return this.parentObj.getAncestorIds(start); + } + return start; + } + /** * How closely related this object is to ancestor. * 0 = same type, 1 = ancestor is parent, 2 = ancestor is grandparent, etc diff --git a/src/syntax-objects/variable.ts b/src/syntax-objects/variable.ts index cedfab58..6670c7c8 100644 --- a/src/syntax-objects/variable.ts +++ b/src/syntax-objects/variable.ts @@ -6,10 +6,12 @@ export class Variable extends NamedEntity { readonly syntaxType = "variable"; isMutable: boolean; type?: Type; + originalType?: Type; inferredType?: Type; annotatedType?: Type; typeExpr?: Expr; initializer: Expr; + requiresCast = false; constructor( opts: NamedEntityOpts & { @@ -29,7 +31,7 @@ export class Variable extends NamedEntity { getIndex(): number { const index = this.parentFn?.getIndexOfVariable(this) ?? -1; - if (index < -1) { + if (index < 0) { throw new Error(`Variable ${this} is not registered with a function`); } diff --git a/std/macros.void b/std/macros.void index f03c1f06..1e4b8406 100644 --- a/std/macros.void +++ b/std/macros.void @@ -84,22 +84,3 @@ pub macro def_wasm_operator(op, wasm_fn, arg_type, return_type) args: [left, right] ` define_function $op $params return_type($return_type) $body - - - -pub macro match() - log "match" - let value_expr = body.extract(0) - let cases = body.slice(1) - let expand_cases = (cases, index) => - let case = cases.extract(index) - if is_list(case) and not(index + 1 >= cases.length) then: - ` if $(extract case 0) == match_value - then: $(extract case 1) - else: $(&lambda cases (index + 1)) - else: case - - let conditions = expand_cases(cases, 0) - ` - let match_value = $value_expr - $conditions