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..db56484311 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,20 @@ export class Compiler extends DiagnosticEmitter { // utilize varargs stub to fill in omitted arguments functionInstance = this.ensureVarargsStub(functionInstance); this.runtimeFeatures |= RuntimeFeatures.setArgumentsLength; + signature = functionInstance.signature; + } + let signatureReturnTypes = signature.returnTypes; + if ( + this.options.bindingsHint && + signatureReturnTypes != null && + signatureReturnTypes.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 +1020,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 +1033,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 +1667,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 +1724,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 +1743,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 +1793,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 +1815,7 @@ export class Compiler extends DiagnosticEmitter { instance.finalize(module, funcRef); this.currentType = previousType; + this.currentReturnTypes = previousReturnTypes; pendingElements.delete(instance); return true; } @@ -1766,7 +1829,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 +1851,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 +1932,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 +2885,84 @@ 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)) { + let currentReturnTypes = this.currentReturnTypes; + this.error( + DiagnosticCode.Type_0_is_not_assignable_to_type_1, + expression.range, + currentReturnTypes + ? this.returnTypesToString(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 +2970,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 +3005,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 +3013,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 +3595,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 +3683,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 +6693,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. */ @@ -6548,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` @@ -6661,7 +6837,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 +6930,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 +6986,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 +7004,7 @@ export class Compiler extends DiagnosticEmitter { module.block(null, [ builder.render(tempIndex), body - ], returnType.toRef()) + ], stub.signature.resultRefs) ); stub.set(CommonFlags.Compiled); } @@ -6971,7 +7148,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 +7160,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 +7179,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 +7290,7 @@ export class Compiler extends DiagnosticEmitter { signature.resultRefs ); this.currentType = returnType; + this.currentReturnTypes = signature.returnTypes; return expr; } @@ -10054,8 +10236,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..86d2daa80e 100644 --- a/src/module.ts +++ b/src/module.ts @@ -3954,25 +3954,40 @@ function prepareType(builder: binaryen.TypeBuilderRef, seen: Map 1) { let cArrPT = allocPtrArray(paramTypes); diff --git a/src/parser.ts b/src/parser.ts index 157351cd3a..71ed4c5f71 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,7 @@ 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..ce75bc6479 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,84 @@ 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 +2960,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 +2976,18 @@ 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, 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..3531f1be15 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,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); + 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 +997,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 +1020,8 @@ export class Signature { } get resultRefs(): TypeRef { + let returnTypes = this.returnTypes; + if (returnTypes) return createType(typesToRefs(returnTypes)); return this.returnType.toRef(); } @@ -1035,7 +1041,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 +1075,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 +1178,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 +1206,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 0000000000..8c37ae1ddc Binary files /dev/null and b/test.wasm differ diff --git a/test.wat b/test.wat new file mode 100644 index 0000000000..e87cd35d00 --- /dev/null +++ b/test.wat @@ -0,0 +1,62 @@ +(module + (type $0 (func (param i32 i32) (result i32 i32))) + (type $1 (func (param i64 i64) (result i64 i64))) + (type $2 (func (param i32 i64) (result i32 i64))) + (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)) + (memory $0 0) + (table $0 1 1 funcref) + (elem $0 (i32.const 1)) + (export "pairI32" (func $test/pairI32)) + (export "swap" (func $test/swap)) + (export "foo" (func $test/foo)) + (export "pairI64" (func $test/pairI64)) + (export "mixI32I64" (func $test/mixI32I64)) + (export "forward" (func $test/forward)) + (export "memory" (memory $0)) + (func $test/pairI32 (param $0 i32) (param $1 i32) (result i32 i32) + local.get $0 + local.get $1 + tuple.make 2 + return + ) + (func $test/swap (param $0 i32) (param $1 i32) (result i32 i32) + local.get $1 + local.get $0 + tuple.make 2 + return + ) + (func $test/foo (param $0 i32) (param $1 i32) (result i32 i32) + local.get $0 + local.get $1 + call $test/swap + return + ) + (func $test/pairI64 (param $0 i64) (param $1 i64) (result i64 i64) + local.get $0 + local.get $1 + tuple.make 2 + return + ) + (func $test/mixI32I64 (param $0 i32) (param $1 i64) (result i32 i64) + local.get $0 + i32.const 1 + i32.add + local.get $1 + i64.const 2 + i64.add + tuple.make 2 + return + ) + (func $test/forward (param $0 i32) (param $1 i32) (result i32 i32) + local.get $0 + i32.const 10 + i32.add + local.get $1 + i32.const 20 + i32.add + call $test/pairI32 + return + ) +) diff --git a/tests/compiler/tuple-bindings.json b/tests/compiler/tuple-bindings.json new file mode 100644 index 0000000000..396f4679fc --- /dev/null +++ b/tests/compiler/tuple-bindings.json @@ -0,0 +1,10 @@ +{ + "asc_flags": [ + "--enable", "multi-value", + "--bindings", "raw" + ], + "stderr": [ + "AS100: Not implemented: Exported multi-value functions are not yet supported with --bindings", + "in tuple-bindings.ts(1,17)" + ] +} diff --git a/tests/compiler/tuple-bindings.ts b/tests/compiler/tuple-bindings.ts new file mode 100644 index 0000000000..55e08c7d62 --- /dev/null +++ b/tests/compiler/tuple-bindings.ts @@ -0,0 +1,3 @@ +export function tupleBindingsUnsupported(a: i32, b: i64): readonly [i32, i64] { + return [a, b]; +} diff --git a/tests/compiler/tuple-errors.json b/tests/compiler/tuple-errors.json index bf732842af..9c0ac6840e 100644 --- a/tests/compiler/tuple-errors.json +++ b/tests/compiler/tuple-errors.json @@ -32,6 +32,17 @@ "in tuple-errors.ts(53,35)", "in tuple-errors.ts(56,35)", "in tuple-errors.ts(60,39)", - "in tuple-errors.ts(63,39)" + "in tuple-errors.ts(63,39)", + "in tuple-errors.ts(67,13)", + "AS100: Not implemented: Tuple types", + "in tuple-errors.ts(69,52)", + "AS100: Not implemented: Multi-value returns with fewer than two values", + "in tuple-errors.ts(71,46)", + "AS100: Not implemented: Multi-value returns with fewer than two values", + "in tuple-errors.ts(74,46)", + "AS100: Not implemented: Nested tuple types", + "in tuple-errors.ts(77,58)", + "AS100: Not implemented: Nullable multi-value returns", + "in tuple-errors.ts(80,45)" ] } diff --git a/tests/compiler/tuple-errors.ts b/tests/compiler/tuple-errors.ts index 2bab1e6bb2..4e5d5eaedc 100644 --- a/tests/compiler/tuple-errors.ts +++ b/tests/compiler/tuple-errors.ts @@ -63,3 +63,20 @@ export function TupleTypeMismatch1(x: [i32, f32]): [f32, i32] { export function TupleTypeMismatch2(x: [f64, f32]): [f32, f64] { return x; } + +export type ReadonlyTupleAliasUnimplemented = readonly [i32, i32]; + +export function ReadonlyTupleParamUnimplemented(x: readonly [i32, i32]): void {} + +export function MultiValueReturnTooShort1(): readonly [] { + return []; +} +export function MultiValueReturnTooShort2(): readonly [i32] { + return [0]; +} +export function MultiValueReturnNested(): readonly [i32, readonly [i32, i32]] { + return [0, [1, 2]]; +} +export function MultiValueReturnNullable(): readonly [i32, i32] | null { + return [0, 1]; +} diff --git a/tests/compiler/tuple-type.debug.wat b/tests/compiler/tuple-type.debug.wat new file mode 100644 index 0000000000..a008ae5944 --- /dev/null +++ b/tests/compiler/tuple-type.debug.wat @@ -0,0 +1,98 @@ +(module + (type $0 (func (param i32 i64) (result i32 i64))) + (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)) + (memory $0 0) + (table $0 1 1 funcref) + (elem $0 (i32.const 1)) + (export "pair" (func $tuple-type/pair)) + (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 + tuple.make 2 + return + ) + (func $tuple-type/floats (param $a f32) (param $b f64) (result f32 f64) + local.get $a + local.get $b + tuple.make 2 + return + ) + (func $tuple-type/forward (param $a i32) (param $b i64) (result i32 i64) + local.get $a + local.get $b + call $tuple-type/pair + return + ) + (func $tuple-type/named (param $a i32) (param $b i64) (result i32 i64) + local.get $a + local.get $b + 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.json b/tests/compiler/tuple-type.json index 8a83de2d20..37e0cad9f0 100644 --- a/tests/compiler/tuple-type.json +++ b/tests/compiler/tuple-type.json @@ -1,7 +1,5 @@ { "asc_flags": [ "--enable", "multi-value" - ], - "stderr": [ ] } diff --git a/tests/compiler/tuple-type.release.wat b/tests/compiler/tuple-type.release.wat new file mode 100644 index 0000000000..fe3b484ede --- /dev/null +++ b/tests/compiler/tuple-type.release.wat @@ -0,0 +1,63 @@ +(module + (type $0 (func (param i32 i64) (result i32 i64))) + (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 + tuple.make 2 + ) + (func $tuple-type/floats (param $0 f32) (param $1 f64) (result f32 f64) + local.get $0 + 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 8cf62aeb86..3ce639d782 100644 --- a/tests/compiler/tuple-type.ts +++ b/tests/compiler/tuple-type.ts @@ -1 +1,25 @@ -// nothing yet just getting the parser working first +export function pair(a: i32, b: i64): readonly [i32, i64] { + return [a, b]; +} + +export function floats(a: f32, b: f64): readonly [f32, f64] { + return [a, b]; +} + +export function forward(a: i32, b: i64): readonly [i32, i64] { + return pair(a, b); +} + +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); +} diff --git a/tests/parser/tuple.ts.fixture.ts b/tests/parser/tuple.ts.fixture.ts index bf461f898e..cc74b92ee7 100644 --- a/tests/parser/tuple.ts.fixture.ts +++ b/tests/parser/tuple.ts.fixture.ts @@ -37,16 +37,16 @@ function func2(x: [i32, i32]): [i32, i32] { function func3(x: [i32, [i32, i32]], y: i32): [i32, [i32, i32]] { return x; } -function func4(x: [i32, string]): [void] { +function func4(x: readonly [i32, string]): [void] { return [void(0)]; } -function func5(x: [Array, 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];