From 4adf9ecd0f1c4682627620a3c850f15df861b7c6 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Mon, 27 Apr 2026 14:55:38 -0400 Subject: [PATCH 1/6] feat: add initial support for multi-value returns --- src/ast.ts | 9 +- src/compiler.ts | 257 ++++++++++++++++++++++---- src/extra/ast.ts | 1 + src/module.ts | 68 +++---- src/parser.ts | 9 +- src/program.ts | 16 +- src/resolver.ts | 139 ++++++++++++-- src/types.ts | 80 +++++++- test.sh | 30 +++ test.ts | 23 +++ test.wasm | Bin 0 -> 217 bytes test.wat | 62 +++++++ tests/compiler/tuple-bindings.json | 10 + tests/compiler/tuple-bindings.ts | 3 + tests/compiler/tuple-errors.json | 13 +- tests/compiler/tuple-errors.ts | 17 ++ tests/compiler/tuple-type.debug.wat | 39 ++++ tests/compiler/tuple-type.json | 2 - tests/compiler/tuple-type.release.wat | 20 ++ tests/compiler/tuple-type.ts | 16 +- tests/parser/tuple.ts.fixture.ts | 8 +- 21 files changed, 711 insertions(+), 111 deletions(-) create mode 100755 test.sh create mode 100644 test.ts create mode 100644 test.wasm create mode 100644 test.wat create mode 100644 tests/compiler/tuple-bindings.json create mode 100644 tests/compiler/tuple-bindings.ts create mode 100644 tests/compiler/tuple-type.debug.wat create mode 100644 tests/compiler/tuple-type.release.wat diff --git a/src/ast.ts b/src/ast.ts index da649de549..04582da82f 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -166,9 +166,10 @@ export abstract class Node { elements: TypeNode[], elementNames: (IdentifierExpression | null)[] | null, isNullable: bool, - range: Range + range: Range, + isReadonly: bool = false ): TupleTypeNode { - return new TupleTypeNode(elements, elementNames, isNullable, range); + return new TupleTypeNode(elements, elementNames, isNullable, range, isReadonly); } static createOmittedType( @@ -954,7 +955,9 @@ export class TupleTypeNode extends TypeNode { /** Whether nullable or not. */ isNullable: bool, /** Source range. */ - range: Range + range: Range, + /** Whether this is a readonly tuple. */ + public isReadonly: bool = false ) { super(NodeKind.TupleType, isNullable, range); } diff --git a/src/compiler.ts b/src/compiler.ts index 7e4c3c9c40..cebb9c2fa0 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -442,6 +442,8 @@ export class Compiler extends DiagnosticEmitter { currentParent: Element | null = null; /** Current type in compilation. */ currentType: Type = Type.void; + /** Current multi-value result types in compilation. */ + currentReturnTypes: Type[] | null = null; /** Start function statements. */ currentBody: ExpressionRef[]; /** Counting memory offset. */ @@ -526,6 +528,37 @@ export class Compiler extends DiagnosticEmitter { this.shadowStack = new ShadowStackPass(this); } + private typesEqual(left: Type[] | null, right: Type[] | null): bool { + if (left) { + if (!right) return false; + let numTypes = left.length; + if (numTypes != right.length) return false; + for (let i = 0; i < numTypes; ++i) { + if (!left[i].equals(right[i])) return false; + } + return true; + } + return right == null; + } + + private typesContain(types: Type[], type: Type): bool { + for (let i = 0, k = types.length; i < k; ++i) { + if (types[i] == type) return true; + } + return false; + } + + private returnTypesToString(types: Type[]): string { + let sb = new Array(); + sb.push("readonly ["); + for (let i = 0, k = types.length; i < k; ++i) { + if (i) sb.push(", "); + sb.push(types[i].toString()); + } + sb.push("]"); + return sb.join(""); + } + /** Performs compilation of the underlying {@link Program} to a {@link Module}. */ compile(): Module { let options = this.options; @@ -958,6 +991,19 @@ export class Compiler extends DiagnosticEmitter { // utilize varargs stub to fill in omitted arguments functionInstance = this.ensureVarargsStub(functionInstance); this.runtimeFeatures |= RuntimeFeatures.setArgumentsLength; + signature = functionInstance.signature; + } + if ( + this.options.bindingsHint && + signature.returnTypes && + signature.returnTypes.length > 1 + ) { + this.error( + DiagnosticCode.Not_implemented_0, + functionInstance.identifierNode.range, + "Exported multi-value functions are not yet supported with --bindings" + ); + return; } this.compileFunction(functionInstance); if (functionInstance.is(CommonFlags.Compiled)) { @@ -973,7 +1019,7 @@ export class Compiler extends DiagnosticEmitter { let thisType = signature.thisType; if ( thisType && lowerRequiresExportRuntime(thisType) || - liftRequiresExportRuntime(signature.returnType) + !signature.returnTypes && liftRequiresExportRuntime(signature.returnType) ) { this.desiresExportRuntime = true; } else { @@ -986,7 +1032,17 @@ export class Compiler extends DiagnosticEmitter { } } } - if (functionInstance.signature.returnType.kind == TypeKind.Func) this.module.setClosedWorld(false); + let returnTypes = functionInstance.signature.returnTypes; + if (returnTypes) { + for (let i = 0, k = returnTypes.length; i < k; ++i) { + if (returnTypes[i].kind == TypeKind.Func) { + this.module.setClosedWorld(false); + break; + } + } + } else if (functionInstance.signature.returnType.kind == TypeKind.Func) { + this.module.setClosedWorld(false); + } } return; } @@ -1610,6 +1666,7 @@ export class Compiler extends DiagnosticEmitter { pendingElements.add(instance); let previousType = this.currentType; + let previousReturnTypes = this.currentReturnTypes; let module = this.module; let signature = instance.signature; let bodyNode = instance.prototype.bodyNode; @@ -1666,7 +1723,7 @@ export class Compiler extends DiagnosticEmitter { signature.paramRefs, signature.resultRefs, typesToRefs(instance.getNonParameterLocalTypes()), - module.flatten(stmts, instance.signature.returnType.toRef()) + module.flatten(stmts, instance.signature.resultRefs) ); // imported function @@ -1685,7 +1742,7 @@ export class Compiler extends DiagnosticEmitter { let thisType = signature.thisType; if ( thisType && liftRequiresExportRuntime(thisType) || - lowerRequiresExportRuntime(signature.returnType) + !signature.returnTypes && lowerRequiresExportRuntime(signature.returnType) ) { this.desiresExportRuntime = true; } else { @@ -1735,10 +1792,14 @@ export class Compiler extends DiagnosticEmitter { if (instance.is(CommonFlags.Ambient) || instance.is(CommonFlags.Export)) { // Verify and print warn if signature has v128 type for imported or exported functions let hasVectorValueOperands = signature.hasVectorValueOperands; - if (hasVectorValueOperands) { + let returnTypes = signature.returnTypes; + let hasVectorValueReturn = returnTypes + ? this.typesContain(returnTypes, Type.v128) + : signature.returnType == Type.v128; + if (hasVectorValueOperands || hasVectorValueReturn) { let range: Range; let fnTypeNode = instance.prototype.functionTypeNode; - if (signature.returnType == Type.v128) { + if (hasVectorValueReturn) { range = fnTypeNode.returnType.range; } else { let firstIndex = signature.getVectorValueOperandIndices()[0]; @@ -1753,6 +1814,7 @@ export class Compiler extends DiagnosticEmitter { instance.finalize(module, funcRef); this.currentType = previousType; + this.currentReturnTypes = previousReturnTypes; pendingElements.delete(instance); return true; } @@ -1766,7 +1828,9 @@ export class Compiler extends DiagnosticEmitter { ): bool { let module = this.module; let bodyNode = assert(instance.prototype.bodyNode); - let returnType = instance.signature.returnType; + let signature = instance.signature; + let returnType = signature.returnType; + let returnTypes = signature.returnTypes; let flow = this.currentFlow; let thisLocal = instance.signature.thisType ? assert(flow.lookupLocal(CommonNames.this_)) @@ -1786,16 +1850,23 @@ export class Compiler extends DiagnosticEmitter { // none of the following can be an arrow function assert(!instance.isAny(CommonFlags.Constructor | CommonFlags.Get | CommonFlags.Set)); - let expr = this.compileExpression((bodyNode).expression, returnType, Constraints.ConvImplicit); - if (!flow.canOverflow(expr, returnType)) flow.set(FlowFlags.ReturnsWrapped); - if (flow.isNonnull(expr, returnType)) flow.set(FlowFlags.ReturnsNonNull); + let expression = (bodyNode).expression; + let expr = returnTypes + ? this.compileMultiValueReturnExpression(expression, returnTypes, Constraints.ConvImplicit) + : this.compileExpression(expression, returnType, Constraints.ConvImplicit); + if (!returnTypes) { + if (!flow.canOverflow(expr, returnType)) flow.set(FlowFlags.ReturnsWrapped); + if (flow.isNonnull(expr, returnType)) flow.set(FlowFlags.ReturnsNonNull); + } if (!stmts) stmts = [ expr ]; else stmts.push(expr); if (!flow.is(FlowFlags.Terminates)) { - if (!flow.canOverflow(expr, returnType)) flow.set(FlowFlags.ReturnsWrapped); - if (flow.isNonnull(expr, returnType)) flow.set(FlowFlags.ReturnsNonNull); + if (!returnTypes) { + if (!flow.canOverflow(expr, returnType)) flow.set(FlowFlags.ReturnsWrapped); + if (flow.isNonnull(expr, returnType)) flow.set(FlowFlags.ReturnsNonNull); + } flow.set(FlowFlags.Returns | FlowFlags.Terminates); } } @@ -1860,7 +1931,7 @@ export class Compiler extends DiagnosticEmitter { } // if this is a normal function, make sure that all branches terminate - } else if (returnType != Type.void && !flow.is(FlowFlags.Terminates)) { + } else if ((returnTypes || returnType != Type.void) && !flow.is(FlowFlags.Terminates)) { this.error( DiagnosticCode.A_function_whose_declared_type_is_not_void_must_return_a_value, instance.prototype.functionTypeNode.returnType.range @@ -2813,6 +2884,83 @@ export class Compiler extends DiagnosticEmitter { } } + private compileMultiValueReturnExpression( + expression: Expression, + returnTypes: Type[], + constraints: Constraints + ): ExpressionRef { + if (expression.kind == NodeKind.Literal && (expression).literalKind == LiteralKind.Array) { + return this.compileMultiValueArrayLiteral( + expression, + returnTypes, + constraints + ); + } + + let expr = this.compileExpression(expression, Type.auto); + if (!this.typesEqual(this.currentReturnTypes, returnTypes)) { + this.error( + DiagnosticCode.Type_0_is_not_assignable_to_type_1, + expression.range, + this.currentReturnTypes + ? this.returnTypesToString(this.currentReturnTypes) + : this.currentType.toString(), + this.returnTypesToString(returnTypes) + ); + this.currentType = Type.void; + this.currentReturnTypes = returnTypes; + return this.module.unreachable(); + } + this.currentType = Type.void; + this.currentReturnTypes = returnTypes; + return expr; + } + + private compileMultiValueArrayLiteral( + expression: ArrayLiteralExpression, + returnTypes: Type[], + constraints: Constraints + ): ExpressionRef { + let module = this.module; + let expressions = expression.elementExpressions; + let length = expressions.length; + let numTypes = returnTypes.length; + if (length != numTypes) { + this.error( + DiagnosticCode.Type_0_is_not_assignable_to_type_1, + expression.range, + `array literal of length ${length}`, + this.returnTypesToString(returnTypes) + ); + this.currentType = Type.void; + this.currentReturnTypes = returnTypes; + return module.unreachable(); + } + let values = new Array(length); + let elementConstraints = Constraints.ConvImplicit; + if (constraints & Constraints.MustWrap) elementConstraints |= Constraints.MustWrap; + for (let i = 0; i < length; ++i) { + let elementExpression = expressions[i]; + if (elementExpression.kind == NodeKind.Omitted) { + this.error( + DiagnosticCode.Type_expected, + elementExpression.range + ); + this.currentType = Type.void; + this.currentReturnTypes = returnTypes; + return module.unreachable(); + } + unchecked(values[i] = this.compileExpression( + elementExpression, + returnTypes[i], + elementConstraints + )); + } + this.currentType = Type.void; + this.currentReturnTypes = returnTypes; + return module.tuple_make(values); + } + private compileReturnStatement( statement: ReturnStatement ): ExpressionRef { @@ -2820,22 +2968,29 @@ export class Compiler extends DiagnosticEmitter { let expr: ExpressionRef = 0; let flow = this.currentFlow; let returnType = flow.returnType; + let returnTypes = flow.sourceFunction.signature.returnTypes; let valueExpression = statement.value; if (valueExpression) { let constraints = Constraints.ConvImplicit; if (flow.sourceFunction.is(CommonFlags.ModuleExport)) constraints |= Constraints.MustWrap; - expr = this.compileExpression(valueExpression, returnType, constraints); - if (!flow.canOverflow(expr, returnType)) flow.set(FlowFlags.ReturnsWrapped); - if (flow.isNonnull(expr, returnType)) flow.set(FlowFlags.ReturnsNonNull); + if (returnTypes) { + expr = this.compileMultiValueReturnExpression(valueExpression, returnTypes, constraints); + } else { + expr = this.compileExpression(valueExpression, returnType, constraints); + if (!flow.canOverflow(expr, returnType)) flow.set(FlowFlags.ReturnsWrapped); + if (flow.isNonnull(expr, returnType)) flow.set(FlowFlags.ReturnsNonNull); + } if (flow.sourceFunction.is(CommonFlags.Constructor) && valueExpression.kind != NodeKind.This) { flow.set(FlowFlags.MayReturnNonThis); } - } else if (returnType != Type.void) { + } else if (returnTypes || returnType != Type.void) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, - statement.range, "void", returnType.toString() + statement.range, + "void", + returnTypes ? this.returnTypesToString(returnTypes) : returnType.toString() ); this.currentType = returnType; return module.unreachable(); @@ -2848,7 +3003,7 @@ export class Compiler extends DiagnosticEmitter { if (flow.isInline) { let inlineReturnLabel = assert(flow.inlineReturnLabel); return expr - ? this.currentType == Type.void + ? this.currentType == Type.void && !this.currentReturnTypes ? module.block(null, [ expr, module.br(inlineReturnLabel) ]) : module.br(inlineReturnLabel, 0, expr) : module.br(inlineReturnLabel); @@ -2856,7 +3011,7 @@ export class Compiler extends DiagnosticEmitter { // Otherwise emit a normal return return expr - ? this.currentType == Type.void + ? this.currentType == Type.void && !this.currentReturnTypes ? module.block(null, [ expr, module.return() ]) : module.return(expr) : module.return(); @@ -3438,6 +3593,7 @@ export class Compiler extends DiagnosticEmitter { expression = (expression).expression; } this.currentType = contextualType; + this.currentReturnTypes = null; if (contextualType == Type.void) constraints |= Constraints.WillDrop; let expr: ExpressionRef; switch (expression.kind) { @@ -3525,6 +3681,23 @@ export class Compiler extends DiagnosticEmitter { } // ensure conversion and wrapping in case the respective function doesn't on its own let currentType = this.currentType; + let currentReturnTypes = this.currentReturnTypes; + if (currentReturnTypes) { + if (contextualType == Type.void && (constraints & Constraints.WillDrop)) { + expr = this.module.drop(expr); + this.currentReturnTypes = null; + } else if (contextualType != Type.auto) { + this.error( + DiagnosticCode.Type_0_is_not_assignable_to_type_1, + expression.range, + this.returnTypesToString(currentReturnTypes), + contextualType.toString() + ); + expr = this.module.unreachable(); + this.currentReturnTypes = null; + } + currentType = this.currentType = Type.void; + } let wrap = (constraints & Constraints.MustWrap) != 0; if (currentType != contextualType.nonNullableType) { // allow assigning non-nullable to nullable if (constraints & Constraints.ConvExplicit) { @@ -6518,11 +6691,13 @@ export class Compiler extends DiagnosticEmitter { // Free any new scoped locals and reset to the original flow let returnType = flow.returnType; + let returnTypes = flow.sourceFunction.signature.returnTypes; this.currentFlow = previousFlow; // Create an outer block that we can break to when returning a value out of order this.currentType = returnType; - return module.block(flow.inlineReturnLabel, body, returnType.toRef()); + this.currentReturnTypes = returnTypes; + return module.block(flow.inlineReturnLabel, body, instance.signature.resultRefs); } /** Makes sure that the arguments length helper global is present. */ @@ -6661,7 +6836,7 @@ export class Compiler extends DiagnosticEmitter { stub.signature.paramRefs, stub.signature.resultRefs, typesToRefs(stub.getNonParameterLocalTypes()), - module.flatten(stmts, returnType.toRef()) + module.flatten(stmts, stub.signature.resultRefs) ); stub.set(CommonFlags.Compiled); stub.finalize(module, funcRef); @@ -6754,13 +6929,14 @@ export class Compiler extends DiagnosticEmitter { let calledName = needsVarargsStub ? this.ensureVarargsStub(overrideInstance).internalName : overrideInstance.internalName; - let returnTypeRef = overrideSignature.returnType.toRef(); + let returnTypeRef = overrideSignature.resultRefs; + let returnTypes = overrideSignature.returnTypes; let stmts = new Array(); if (needsVarargsStub) { // Safe to prepend since paramExprs are local.get's stmts.push(module.global_set(this.ensureArgumentsLength(), module.i32(numParameters))); } - if (returnType == Type.void) { + if (!returnTypes && returnType == Type.void) { stmts.push( module.call(calledName, paramExprs, returnTypeRef) ); @@ -6809,7 +6985,7 @@ export class Compiler extends DiagnosticEmitter { for (let i = 0, k = parameterTypes.length; i < k; ++i) { paramExprs[1 + i] = module.local_get(1 + i, parameterTypes[i].toRef()); } - body = module.call(instance.internalName, paramExprs, returnType.toRef()); + body = module.call(instance.internalName, paramExprs, stub.signature.resultRefs); // Otherwise trap } else { @@ -6827,7 +7003,7 @@ export class Compiler extends DiagnosticEmitter { module.block(null, [ builder.render(tempIndex), body - ], returnType.toRef()) + ], stub.signature.resultRefs) ); stub.set(CommonFlags.Compiled); } @@ -6971,7 +7147,8 @@ export class Compiler extends DiagnosticEmitter { instance = this.ensureVarargsStub(instance); if (!this.compileFunction(instance)) return module.unreachable(); instance.flow.flags = original.flow.flags; - let returnTypeRef = returnType.toRef(); + let returnTypes = instance.signature.returnTypes; + let resultRefs = instance.signature.resultRefs; // We know the last operand is optional and omitted, so inject setting // ~argumentsLength into that operand, which is always safe. let lastOperand = operands[maxOperands - 1]; @@ -6982,12 +7159,14 @@ export class Compiler extends DiagnosticEmitter { lastOperand ], lastOperandType.toRef()); this.operandsTostack(instance.signature, operands); - let expr = module.call(instance.internalName, operands, returnTypeRef); - if (returnType != Type.void && immediatelyDropped) { + let expr = module.call(instance.internalName, operands, resultRefs); + if ((returnTypes || returnType != Type.void) && immediatelyDropped) { expr = module.drop(expr); this.currentType = Type.void; + this.currentReturnTypes = null; } else { this.currentType = returnType; + this.currentReturnTypes = returnTypes; } return expr; } @@ -6999,8 +7178,9 @@ export class Compiler extends DiagnosticEmitter { } if (operands) this.operandsTostack(instance.signature, operands); - let expr = module.call(instance.internalName, operands, returnType.toRef()); + let expr = module.call(instance.internalName, operands, instance.signature.resultRefs); this.currentType = returnType; + this.currentReturnTypes = instance.signature.returnTypes; return expr; } @@ -7109,6 +7289,7 @@ export class Compiler extends DiagnosticEmitter { signature.resultRefs ); this.currentType = returnType; + this.currentReturnTypes = signature.returnTypes; return expr; } @@ -10054,8 +10235,20 @@ export class Compiler extends DiagnosticEmitter { supported = false; } } - if (!this.program.checkTypeSupported(signature.returnType, reportNode.returnType)) { - supported = false; + let returnTypes = signature.returnTypes; + if (returnTypes) { + if (!this.program.checkFeatureEnabled(Feature.MultiValue, reportNode.returnType)) { + supported = false; + } + for (let i = 0, k = returnTypes.length; i < k; ++i) { + if (!this.program.checkTypeSupported(returnTypes[i], reportNode.returnType)) { + supported = false; + } + } + } else { + if (!this.program.checkTypeSupported(signature.returnType, reportNode.returnType)) { + supported = false; + } } return supported; } diff --git a/src/extra/ast.ts b/src/extra/ast.ts index 699b2e0c71..07a07f02cc 100644 --- a/src/extra/ast.ts +++ b/src/extra/ast.ts @@ -461,6 +461,7 @@ export class ASTBuilder { visitTupleTypeNode(node: TupleTypeNode): void { let sb = this.sb; + if (node.isReadonly) sb.push("readonly "); sb.push("["); let elements = node.elements; let elementNames = node.elementNames; diff --git a/src/module.ts b/src/module.ts index 2719f60c50..f0c8a3427f 100644 --- a/src/module.ts +++ b/src/module.ts @@ -3839,6 +3839,23 @@ function determinePackedType(type: Type): PackedType { return PackedType.NotPacked; } +/** Prepares a type, preserving nullable reference types. */ +function prepareTypeWithNullability( + builder: binaryen.TypeBuilderRef, + seen: Map, + type: Type +): TypeRef { + return type.is(TypeFlags.Nullable) + ? binaryen._TypeBuilderGetTempRefType( + builder, + binaryen._BinaryenTypeGetHeapType( + prepareType(builder, seen, type.nonNullableType) + ), + true + ) + : prepareType(builder, seen, type); +} + /** Recursively prepares the given GC type, potentially returning a temporary type. */ function prepareType(builder: binaryen.TypeBuilderRef, seen: Map, type: Type): TypeRef { // Obtain basic type if applicable @@ -3899,19 +3916,7 @@ function prepareType(builder: binaryen.TypeBuilderRef, seen: Map(); let parameterTypes = signatureReference.parameterTypes; for (let i = 0, k = parameterTypes.length; i < k; ++i) { - let paramType = parameterTypes[i]; - if (paramType.is(TypeFlags.Nullable)) { - paramTypes.push( - binaryen._TypeBuilderGetTempRefType( - builder, - binaryen._BinaryenTypeGetHeapType( - prepareType(builder, seen, paramType.nonNullableType) - ), - true - ) - ); - } else { - paramTypes.push(prepareType(builder, seen, paramType)); + paramTypes.push(prepareTypeWithNullability(builder, seen, parameterTypes[i])); + } + let returnTypes = signatureReference.returnTypes; + if (returnTypes) { + for (let i = 0, k = returnTypes.length; i < k; ++i) { + resultTypes.push(prepareTypeWithNullability(builder, seen, returnTypes[i])); } + } else if (signatureReference.returnType == Type.void) { + resultTypes.push(TypeRef.None); + } else { + resultTypes.push(prepareTypeWithNullability(builder, seen, signatureReference.returnType)); } - let returnType = signatureReference.returnType; - resultTypes.push( - returnType == Type.void - ? TypeRef.None - : returnType.is(TypeFlags.Nullable) - ? binaryen._TypeBuilderGetTempRefType( - builder, - binaryen._BinaryenTypeGetHeapType( - prepareType(builder, seen, returnType.nonNullableType) - ), - true - ) - : prepareType(builder, seen, returnType) - ); let tempParamType: TypeRef; if (paramTypes.length > 1) { let cArrPT = allocPtrArray(paramTypes); diff --git a/src/parser.ts b/src/parser.ts index 157351cd3a..011ccb53da 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -572,6 +572,7 @@ export class Parser extends DiagnosticEmitter { } else if (token == Token.Readonly) { let innerType = this.parseType(tn, acceptParenthesized, suppressErrors); if (!innerType) return null; + if (innerType.kind == NodeKind.TupleType) (innerType).isReadonly = true; type = innerType; type.range.start = startPos; @@ -609,7 +610,13 @@ export class Parser extends DiagnosticEmitter { return null; } } - type = Node.createTupleType(elements, hasElementNames ? elementNames : null, false, tn.range(startPos, tn.pos)); + type = Node.createTupleType( + elements, + hasElementNames ? elementNames : null, + false, + tn.range(startPos, tn.pos), + false + ); // 'void' } else if (token == Token.Void) { diff --git a/src/program.ts b/src/program.ts index f991cc1226..23cc7b5d58 100644 --- a/src/program.ts +++ b/src/program.ts @@ -2018,9 +2018,19 @@ export class Program extends DiagnosticEmitter { return false; } } - let returnType = signatureReference.returnType; - if (!this.checkTypeSupported(returnType, reportNode)) { - return false; + let returnTypes = signatureReference.returnTypes; + if (returnTypes) { + if (!this.checkFeatureEnabled(Feature.MultiValue, reportNode)) return false; + for (let i = 0, k = returnTypes.length; i < k; ++i) { + if (!this.checkTypeSupported(returnTypes[i], reportNode)) { + return false; + } + } + } else { + let returnType = signatureReference.returnType; + if (!this.checkTypeSupported(returnType, reportNode)) { + return false; + } } } } diff --git a/src/resolver.ts b/src/resolver.ts index 9576a4768f..3ea51b697b 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -435,6 +435,7 @@ export class Resolver extends DiagnosticEmitter { } let returnTypeNode = node.returnType; let returnType: Type | null; + let returnTypes: Type[] | null = null; if (isTypeOmitted(returnTypeNode)) { if (reportMode == ReportMode.Report) { this.error( @@ -444,17 +445,104 @@ export class Resolver extends DiagnosticEmitter { } returnType = Type.void; } else { - returnType = this.resolveType( - returnTypeNode, + if (returnTypeNode.kind == NodeKind.TupleType && (returnTypeNode).isReadonly) { + returnType = Type.void; + returnTypes = this.resolveMultiValueReturnTypes( + returnTypeNode, + flow, + ctxElement, + ctxTypes, + reportMode + ); + if (!returnTypes) return null; + } else { + returnType = this.resolveType( + returnTypeNode, + flow, + ctxElement, + ctxTypes, + reportMode + ); + } + if (!returnType) return null; + } + let signature = Signature.create( + this.program, + parameterTypes, + returnType, + thisType, + requiredParameters, + hasRest, + returnTypes + ); + return node.isNullable ? signature.type.asNullable() : signature.type; + } + + /** Resolves a readonly tuple return annotation to multiple return types. */ + private resolveMultiValueReturnTypes( + /** The readonly tuple return type to resolve. */ + node: TupleTypeNode, + /** The flow */ + flow: Flow | null, + /** Contextual element. */ + ctxElement: Element, + /** Contextual types, i.e. `T`. */ + ctxTypes: Map | null = null, + /** How to proceed with eventual diagnostics. */ + reportMode: ReportMode = ReportMode.Report + ): Type[] | null { + if (node.isNullable) { + if (reportMode == ReportMode.Report) { + this.error( + DiagnosticCode.Not_implemented_0, + node.range, "Nullable multi-value returns" + ); + } + return null; + } + let elements = node.elements; + let numElements = elements.length; + if (numElements < 2) { + if (reportMode == ReportMode.Report) { + this.error( + DiagnosticCode.Not_implemented_0, + node.range, "Multi-value returns with fewer than two values" + ); + } + return null; + } + let types = new Array(numElements); + for (let i = 0; i < numElements; ++i) { + let elementNode = elements[i]; + if (elementNode.kind == NodeKind.TupleType) { + if (reportMode == ReportMode.Report) { + this.error( + DiagnosticCode.Not_implemented_0, + elementNode.range, "Nested tuple types" + ); + } + return null; + } + let elementType = this.resolveType( + elementNode, flow, ctxElement, ctxTypes, reportMode ); - if (!returnType) return null; + if (!elementType) return null; + if (elementType == Type.void) { + if (reportMode == ReportMode.Report) { + this.error( + DiagnosticCode.Type_0_is_illegal_in_this_context, + elementNode.range, elementType.toString() + ); + } + return null; + } + types[i] = elementType; } - let signature = Signature.create(this.program, parameterTypes, returnType, thisType, requiredParameters, hasRest); - return node.isNullable ? signature.type.asNullable() : signature.type; + return types; } /** Resolves a {@link TupleTypeNode}. */ @@ -2892,6 +2980,7 @@ export class Resolver extends DiagnosticEmitter { // resolve return type let returnType: Type; + let returnTypes: Type[] | null = null; if (prototype.is(CommonFlags.Set)) { returnType = Type.void; // not annotated } else if (prototype.is(CommonFlags.Constructor)) { @@ -2907,18 +2996,38 @@ export class Resolver extends DiagnosticEmitter { } return null; } - let type = this.resolveType( - typeNode, - null, - prototype.parent, // relative to function - ctxTypes, - reportMode - ); - if (!type) return null; - returnType = type; + if (typeNode.kind == NodeKind.TupleType && (typeNode).isReadonly) { + returnType = Type.void; + returnTypes = this.resolveMultiValueReturnTypes( + typeNode, + null, + prototype.parent, + ctxTypes, + reportMode + ); + if (!returnTypes) return null; + } else { + let type = this.resolveType( + typeNode, + null, + prototype.parent, // relative to function + ctxTypes, + reportMode + ); + if (!type) return null; + returnType = type; + } } - let signature = Signature.create(this.program, parameterTypes, returnType, thisType, requiredParameters, hasRest); + let signature = Signature.create( + this.program, + parameterTypes, + returnType, + thisType, + requiredParameters, + hasRest, + returnTypes + ); let nameInclTypeParameters = prototype.name; if (instanceKey.length) nameInclTypeParameters += `<${instanceKey}>`; diff --git a/src/types.ts b/src/types.ts index 6a07f6fbc8..d0dd906bcf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -946,6 +946,8 @@ export class Signature { requiredParameters: i32 = parameterTypes ? parameterTypes.length : 0, /** Whether the last parameter is a rest parameter. */ hasRest: bool = false, + /** Return types, if this signature returns multiple values. */ + returnTypes: Type[] | null = null, ): Signature { // get the usize type, and the type of the signature let usizeType = program.options.usizeType; @@ -960,7 +962,17 @@ export class Signature { let nextId = program.nextSignatureId; // construct the signature and calculate it's unique key - let signature = new Signature(program, parameterTypes, returnType, thisType, requiredParameters, hasRest, nextId, type); + let signature = new Signature( + program, + parameterTypes, + returnType, + thisType, + requiredParameters, + hasRest, + nextId, + type, + returnTypes + ); let uniqueKey = signature.toString(); // check if it exists, and return it @@ -995,6 +1007,8 @@ export class Signature { public readonly id: u32, /** Respective function type. */ public readonly type: Type, + /** Return types, if this signature returns multiple values. */ + public readonly returnTypes: Type[] | null = null, ) {} get paramRefs(): TypeRef { @@ -1016,6 +1030,8 @@ export class Signature { } get resultRefs(): TypeRef { + let returnTypes = this.returnTypes; + if (returnTypes) return createType(typesToRefs(returnTypes)); return this.returnType.toRef(); } @@ -1035,7 +1051,7 @@ export class Signature { if (this.hasRest != other.hasRest) return false; // check return type - if (!this.returnType.equals(other.returnType)) return false; + if (!signatureReturnTypesEqual(this, other)) return false; // check parameter types let selfParameterTypes = this.parameterTypes; @@ -1069,11 +1085,7 @@ export class Signature { if (this.hasRest != target.hasRest) return false; // TODO // check return type (covariant) - let thisReturnType = this.returnType; - let targetReturnType = target.returnType; - if (!(thisReturnType == targetReturnType || thisReturnType.isAssignableTo(targetReturnType))) { - return false; - } + if (!signatureReturnTypesAssignableTo(this, target)) return false; // check parameter types (invariant) let thisParameterTypes = this.parameterTypes; let targetParameterTypes = target.parameterTypes; @@ -1176,7 +1188,17 @@ export class Signature { } } sb.push(validWat ? "%29=>" : ") => "); - sb.push(this.returnType.toString(validWat)); + let returnTypes = this.returnTypes; + if (returnTypes) { + sb.push("readonly ["); + for (let i = 0, k = returnTypes.length; i < k; ++i) { + if (i) sb.push(validWat ? "," : ", "); + sb.push(returnTypes[i].toString(validWat)); + } + sb.push("]"); + } else { + sb.push(this.returnType.toString(validWat)); + } return sb.join(""); } @@ -1194,7 +1216,47 @@ export class Signature { this.returnType, this.thisType, requiredParameters, - hasRest + hasRest, + this.returnTypes ); } } + +function signatureReturnTypesEqual(left: Signature, right: Signature): bool { + let leftReturnTypes = left.returnTypes; + let rightReturnTypes = right.returnTypes; + if (leftReturnTypes) { + return rightReturnTypes != null && typesEqual(leftReturnTypes, rightReturnTypes); + } + return !rightReturnTypes && left.returnType.equals(right.returnType); +} + +function signatureReturnTypesAssignableTo(left: Signature, right: Signature): bool { + let leftReturnTypes = left.returnTypes; + let rightReturnTypes = right.returnTypes; + if (leftReturnTypes) { + return rightReturnTypes != null && typesAssignableTo(leftReturnTypes, rightReturnTypes); + } + if (rightReturnTypes) return false; + let leftReturnType = left.returnType; + let rightReturnType = right.returnType; + return leftReturnType == rightReturnType || leftReturnType.isAssignableTo(rightReturnType); +} + +function typesEqual(left: Type[], right: Type[]): bool { + let numTypes = left.length; + if (numTypes != right.length) return false; + for (let i = 0; i < numTypes; ++i) { + if (!left[i].equals(right[i])) return false; + } + return true; +} + +function typesAssignableTo(left: Type[], right: Type[]): bool { + let numTypes = left.length; + if (numTypes != right.length) return false; + for (let i = 0; i < numTypes; ++i) { + if (!left[i].isAssignableTo(right[i])) return false; + } + return true; +} diff --git a/test.sh b/test.sh new file mode 100755 index 0000000000..8e5c018411 --- /dev/null +++ b/test.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ASC="$ROOT_DIR/bin/asc.js" +TS_FILE="$ROOT_DIR/test.ts" +WASM_FILE="$ROOT_DIR/test.wasm" +WAT_FILE="$ROOT_DIR/test.wat" + +echo "Compiling $TS_FILE -> $WASM_FILE and $WAT_FILE" +node --enable-source-maps --no-warnings "$ASC" "$TS_FILE" \ + --enable multi-value \ + --outFile "$WASM_FILE" \ + --textFile "$WAT_FILE" + +echo +echo "Running exports with wasmtime" +run() { + local fn="$1" + shift + echo "\$ wasmtime run --invoke $fn $WASM_FILE $*" + wasmtime run --invoke "$fn" "$WASM_FILE" "$@" + echo +} + +run pairI32 3 7 +run foo 10 20 +run pairI64 5 9 +run mixI32I64 11 22 +run forward 1 2 diff --git a/test.ts b/test.ts new file mode 100644 index 0000000000..0a99fbe418 --- /dev/null +++ b/test.ts @@ -0,0 +1,23 @@ +export function pairI32(a: i32, b: i32): readonly [i32, i32] { + return [a, b]; +} + +export function swap(a: i32, b: i32): readonly [i32, i32] { + return [b, a]; +} + +export function foo(a: i32, b: i32): readonly [i32, i32] { + return swap(a, b); +} + +export function pairI64(a: i64, b: i64): readonly [i64, i64] { + return [a, b]; +} + +export function mixI32I64(a: i32, b: i64): readonly [i32, i64] { + return [a + 1, b + 2]; +} + +export function forward(a: i32, b: i32): readonly [i32, i32] { + return pairI32(a + 10, b + 20); +} diff --git a/test.wasm b/test.wasm new file mode 100644 index 0000000000000000000000000000000000000000..8c37ae1ddc10f033107b15226af6a10e93cfc780 GIT binary patch literal 217 zcmXAf!3u*g7=!b-+D^;RgY4`xY-9KC=37|U1cjpL!3~DpagV)Ug+THpftx%5pp&dc z2&`Hx3yW!CNv+mZAc|=qqFC}<6*9mcOd$LF3d4CdZM7a;J64x75KXfiK(gt3h`qjD zArs%ec&|7|y6MN+jSs}iKYc&G1rkNDWRO~=QKF>Guv3Z5Bpc^@vn@w%EOjN_#V0e_ LN-2{qx}5)iSpp, Array]): [i32] { +function func5(x: readonly [Array, Array]): readonly [i32] { return [x[1].length]; } function func6(x: [i32, i32] | null): [i32, i32] | null { return x; } -function func7(x: [[Array], [string]]): [[Array], [string]] { +function func7(x: readonly [[Array], [string]]): readonly [[Array], [string]] { return x; } function func8(x: [left: i32, right: i32]): [first: i32, second: i32] { @@ -61,7 +61,7 @@ function func10(x: [string | null, i32] | null): [string | null, i32] | null { type type0 = []; type type1 = [i32, i32]; type type2 = [i32, [i32, i32]]; -type type3 = [i32, string]; +type type3 = readonly [i32, string]; type type4 = [i32, i32] | null; type type5 = [[i32, i32], [i32, i32]]; type type6 = [Array, Array, T]; From 7cd582df616ef38f454ec85aa889721ba9b9a18e Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Mon, 27 Apr 2026 15:04:38 -0400 Subject: [PATCH 2/6] chore: remove helper since it's somewhat out of scope --- src/module.ts | 68 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/src/module.ts b/src/module.ts index f0c8a3427f..c951d8b5d5 100644 --- a/src/module.ts +++ b/src/module.ts @@ -3839,23 +3839,6 @@ function determinePackedType(type: Type): PackedType { return PackedType.NotPacked; } -/** Prepares a type, preserving nullable reference types. */ -function prepareTypeWithNullability( - builder: binaryen.TypeBuilderRef, - seen: Map, - type: Type -): TypeRef { - return type.is(TypeFlags.Nullable) - ? binaryen._TypeBuilderGetTempRefType( - builder, - binaryen._BinaryenTypeGetHeapType( - prepareType(builder, seen, type.nonNullableType) - ), - true - ) - : prepareType(builder, seen, type); -} - /** Recursively prepares the given GC type, potentially returning a temporary type. */ function prepareType(builder: binaryen.TypeBuilderRef, seen: Map, type: Type): TypeRef { // Obtain basic type if applicable @@ -3916,7 +3899,17 @@ function prepareType(builder: binaryen.TypeBuilderRef, seen: Map(); let parameterTypes = signatureReference.parameterTypes; for (let i = 0, k = parameterTypes.length; i < k; ++i) { - paramTypes.push(prepareTypeWithNullability(builder, seen, parameterTypes[i])); + let paramType = parameterTypes[i]; + paramTypes.push( + paramType.is(TypeFlags.Nullable) + ? binaryen._TypeBuilderGetTempRefType( + builder, + binaryen._BinaryenTypeGetHeapType( + prepareType(builder, seen, paramType.nonNullableType) + ), + true + ) + : prepareType(builder, seen, paramType) + ); } let returnTypes = signatureReference.returnTypes; if (returnTypes) { for (let i = 0, k = returnTypes.length; i < k; ++i) { - resultTypes.push(prepareTypeWithNullability(builder, seen, returnTypes[i])); + let returnType = returnTypes[i]; + resultTypes.push( + returnType.is(TypeFlags.Nullable) + ? binaryen._TypeBuilderGetTempRefType( + builder, + binaryen._BinaryenTypeGetHeapType( + prepareType(builder, seen, returnType.nonNullableType) + ), + true + ) + : prepareType(builder, seen, returnType) + ); } } else if (signatureReference.returnType == Type.void) { resultTypes.push(TypeRef.None); } else { - resultTypes.push(prepareTypeWithNullability(builder, seen, signatureReference.returnType)); + let returnType = signatureReference.returnType; + resultTypes.push( + returnType.is(TypeFlags.Nullable) + ? binaryen._TypeBuilderGetTempRefType( + builder, + binaryen._BinaryenTypeGetHeapType( + prepareType(builder, seen, returnType.nonNullableType) + ), + true + ) + : prepareType(builder, seen, returnType) + ); } let tempParamType: TypeRef; if (paramTypes.length > 1) { From d9fba971b82935198cbb40394b6a878eff8066c4 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Mon, 27 Apr 2026 15:14:48 -0400 Subject: [PATCH 3/6] chore: clean up --- src/module.ts | 24 ++++++++++++----------- src/parser.ts | 8 +------- src/resolver.ts | 52 ++++++------------------------------------------- src/types.ts | 12 +----------- 4 files changed, 21 insertions(+), 75 deletions(-) diff --git a/src/module.ts b/src/module.ts index c951d8b5d5..86d2daa80e 100644 --- a/src/module.ts +++ b/src/module.ts @@ -3899,17 +3899,19 @@ function prepareType(builder: binaryen.TypeBuilderRef, seen: MapreturnTypeNode).isReadonly) { returnType = Type.void; - returnTypes = this.resolveMultiValueReturnTypes( - returnTypeNode, - flow, - ctxElement, - ctxTypes, - reportMode - ); + returnTypes = this.resolveMultiValueReturnTypes(returnTypeNode, flow, ctxElement, ctxTypes, reportMode); if (!returnTypes) return null; } else { - returnType = this.resolveType( - returnTypeNode, - flow, - ctxElement, - ctxTypes, - reportMode - ); + returnType = this.resolveType(returnTypeNode, flow, ctxElement, ctxTypes, reportMode); } if (!returnType) return null; } - let signature = Signature.create( - this.program, - parameterTypes, - returnType, - thisType, - requiredParameters, - hasRest, - returnTypes - ); + let signature = Signature.create(this.program, parameterTypes, returnType, thisType, requiredParameters, hasRest, returnTypes); return node.isNullable ? signature.type.asNullable() : signature.type; } @@ -2998,36 +2978,16 @@ export class Resolver extends DiagnosticEmitter { } if (typeNode.kind == NodeKind.TupleType && (typeNode).isReadonly) { returnType = Type.void; - returnTypes = this.resolveMultiValueReturnTypes( - typeNode, - null, - prototype.parent, - ctxTypes, - reportMode - ); + returnTypes = this.resolveMultiValueReturnTypes(typeNode, null, prototype.parent, ctxTypes, reportMode); if (!returnTypes) return null; } else { - let type = this.resolveType( - typeNode, - null, - prototype.parent, // relative to function - ctxTypes, - reportMode - ); + let type = this.resolveType(typeNode, null, prototype.parent, ctxTypes, reportMode); if (!type) return null; returnType = type; } } - let signature = Signature.create( - this.program, - parameterTypes, - returnType, - thisType, - requiredParameters, - hasRest, - returnTypes - ); + let signature = Signature.create(this.program, parameterTypes, returnType, thisType, requiredParameters, hasRest, returnTypes); let nameInclTypeParameters = prototype.name; if (instanceKey.length) nameInclTypeParameters += `<${instanceKey}>`; diff --git a/src/types.ts b/src/types.ts index d0dd906bcf..3531f1be15 100644 --- a/src/types.ts +++ b/src/types.ts @@ -962,17 +962,7 @@ export class Signature { let nextId = program.nextSignatureId; // construct the signature and calculate it's unique key - let signature = new Signature( - program, - parameterTypes, - returnType, - thisType, - requiredParameters, - hasRest, - nextId, - type, - returnTypes - ); + let signature = new Signature(program, parameterTypes, returnType, thisType, requiredParameters, hasRest, nextId, type, returnTypes ); let uniqueKey = signature.toString(); // check if it exists, and return it From 6d6c0ea113db029825cb570ade053840d04536af Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Mon, 27 Apr 2026 15:22:27 -0400 Subject: [PATCH 4/6] chore: check for nullability when accessing returnTypes --- src/compiler.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/compiler.ts b/src/compiler.ts index cebb9c2fa0..433d87197b 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -993,10 +993,11 @@ export class Compiler extends DiagnosticEmitter { this.runtimeFeatures |= RuntimeFeatures.setArgumentsLength; signature = functionInstance.signature; } + let signatureReturnTypes = signature.returnTypes; if ( this.options.bindingsHint && - signature.returnTypes && - signature.returnTypes.length > 1 + signatureReturnTypes != null && + signatureReturnTypes.length > 1 ) { this.error( DiagnosticCode.Not_implemented_0, @@ -2899,11 +2900,12 @@ export class Compiler extends DiagnosticEmitter { let expr = this.compileExpression(expression, Type.auto); if (!this.typesEqual(this.currentReturnTypes, returnTypes)) { + let currentReturnTypes = this.currentReturnTypes; this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, expression.range, - this.currentReturnTypes - ? this.returnTypesToString(this.currentReturnTypes) + currentReturnTypes + ? this.returnTypesToString(currentReturnTypes) : this.currentType.toString(), this.returnTypesToString(returnTypes) ); From 49745ad69919832ee8f29a8d35572df5d3bf2b77 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Mon, 27 Apr 2026 15:22:46 -0400 Subject: [PATCH 5/6] chore: remove extraneous code --- src/compiler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/compiler.ts b/src/compiler.ts index 433d87197b..db56484311 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -6725,7 +6725,6 @@ export class Compiler extends DiagnosticEmitter { let originalSignature = original.signature; let originalParameterTypes = originalSignature.parameterTypes; let originalParameterDeclarations = original.prototype.functionTypeNode.parameters; - let returnType = originalSignature.returnType; let isInstance = original.is(CommonFlags.Instance); // arguments excl. `this`, operands incl. `this` From 07d17fdbffa5f509c4119705818702316eac289c Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Mon, 27 Apr 2026 16:53:38 -0400 Subject: [PATCH 6/6] tests: add more --- tests/compiler/tuple-type.debug.wat | 61 ++++++++++++++++++++++++++- tests/compiler/tuple-type.release.wat | 45 +++++++++++++++++++- tests/compiler/tuple-type.ts | 10 +++++ 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/tests/compiler/tuple-type.debug.wat b/tests/compiler/tuple-type.debug.wat index 48b79d667c..a008ae5944 100644 --- a/tests/compiler/tuple-type.debug.wat +++ b/tests/compiler/tuple-type.debug.wat @@ -1,6 +1,13 @@ (module (type $0 (func (param i32 i64) (result i32 i64))) - (type $1 (func (param f32 f64) (result f32 f64))) + (type $1 (func (param i32 i32 i64) (result i32 i64))) + (type $2 (func (result i32 i64))) + (type $3 (func (param f32 f64) (result f32 f64))) + (type $4 (func (param i32 i64))) + (type $5 (func (param i32 i32 i32 i32))) + (type $6 (func)) + (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32))) + (global $~argumentsLength (mut i32) (i32.const 0)) (global $~lib/memory/__data_end i32 (i32.const 8)) (global $~lib/memory/__stack_pointer (mut i32) (i32.const 32776)) (global $~lib/memory/__heap_base i32 (i32.const 32776)) @@ -11,7 +18,9 @@ (export "floats" (func $tuple-type/floats)) (export "forward" (func $tuple-type/forward)) (export "named" (func $tuple-type/named)) + (export "dropCall" (func $tuple-type/dropCall)) (export "memory" (memory $0)) + (export "indirect" (func $export:tuple-type/indirect)) (func $tuple-type/pair (param $a i32) (param $b i64) (result i32 i64) local.get $a local.get $b @@ -36,4 +45,54 @@ tuple.make 2 return ) + (func $tuple-type/indirect (param $fn i32) (param $a i32) (param $b i64) (result i32 i64) + local.get $a + local.get $b + i32.const 2 + global.set $~argumentsLength + local.get $fn + i32.load + call_indirect (type $0) + return + ) + (func $tuple-type/dropCall (param $a i32) (param $b i64) + local.get $a + local.get $b + call $tuple-type/pair + tuple.drop 2 + ) + (func $~stack_check + global.get $~lib/memory/__stack_pointer + global.get $~lib/memory/__data_end + i32.lt_s + if + i32.const 32800 + i32.const 32848 + i32.const 1 + i32.const 1 + call $~lib/builtins/abort + unreachable + end + ) + (func $export:tuple-type/indirect (param $0 i32) (param $1 i32) (param $2 i64) (result i32 i64) + (local $3 (tuple i32 i64)) + global.get $~lib/memory/__stack_pointer + i32.const 4 + i32.sub + global.set $~lib/memory/__stack_pointer + call $~stack_check + global.get $~lib/memory/__stack_pointer + local.get $0 + i32.store + local.get $0 + local.get $1 + local.get $2 + call $tuple-type/indirect + local.set $3 + global.get $~lib/memory/__stack_pointer + i32.const 4 + i32.add + global.set $~lib/memory/__stack_pointer + local.get $3 + ) ) diff --git a/tests/compiler/tuple-type.release.wat b/tests/compiler/tuple-type.release.wat index f812a0baf9..fe3b484ede 100644 --- a/tests/compiler/tuple-type.release.wat +++ b/tests/compiler/tuple-type.release.wat @@ -1,12 +1,21 @@ (module (type $0 (func (param i32 i64) (result i32 i64))) - (type $1 (func (param f32 f64) (result f32 f64))) + (type $1 (func (result i32 i64))) + (type $2 (func (param f32 f64) (result f32 f64))) + (type $3 (func (param i32 i64))) + (type $4 (func (param i32 i32 i32 i32))) + (type $5 (func (param i32 i32 i64) (result i32 i64))) + (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32))) + (global $~lib/memory/__stack_pointer (mut i32) (i32.const 33792)) (memory $0 0) + (table $0 1 1 funcref) (export "pair" (func $tuple-type/pair)) (export "floats" (func $tuple-type/floats)) (export "forward" (func $tuple-type/pair)) (export "named" (func $tuple-type/pair)) + (export "dropCall" (func $tuple-type/dropCall)) (export "memory" (memory $0)) + (export "indirect" (func $export:tuple-type/indirect)) (func $tuple-type/pair (param $0 i32) (param $1 i64) (result i32 i64) local.get $0 local.get $1 @@ -17,4 +26,38 @@ local.get $1 tuple.make 2 ) + (func $tuple-type/dropCall (param $0 i32) (param $1 i64) + ) + (func $export:tuple-type/indirect (param $0 i32) (param $1 i32) (param $2 i64) (result i32 i64) + (local $3 (tuple i32 i64)) + global.get $~lib/memory/__stack_pointer + i32.const 4 + i32.sub + global.set $~lib/memory/__stack_pointer + global.get $~lib/memory/__stack_pointer + i32.const 1024 + i32.lt_s + if + i32.const 33824 + i32.const 33872 + i32.const 1 + i32.const 1 + call $~lib/builtins/abort + unreachable + end + global.get $~lib/memory/__stack_pointer + local.get $0 + i32.store + local.get $1 + local.get $2 + local.get $0 + i32.load + call_indirect (type $0) + local.set $3 + global.get $~lib/memory/__stack_pointer + i32.const 4 + i32.add + global.set $~lib/memory/__stack_pointer + local.get $3 + ) ) diff --git a/tests/compiler/tuple-type.ts b/tests/compiler/tuple-type.ts index b9bc7338d9..3ce639d782 100644 --- a/tests/compiler/tuple-type.ts +++ b/tests/compiler/tuple-type.ts @@ -13,3 +13,13 @@ export function forward(a: i32, b: i64): readonly [i32, i64] { export function named(a: i32, b: i64): readonly [lo: i32, hi: i64] { return [a, b]; } + +type PairFn = (a: i32, b: i64) => readonly [i32, i64]; + +export function indirect(fn: PairFn, a: i32, b: i64): readonly [i32, i64] { + return fn(a, b); +} + +export function dropCall(a: i32, b: i64): void { + pair(a, b); +}