Skip to content

Commit 038cf8b

Browse files
committed
feat: add initial tail-call support behind feature flag
1 parent bf7f959 commit 038cf8b

7 files changed

Lines changed: 4042 additions & 25 deletions

File tree

src/compiler.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ import {
223223
lowerRequiresExportRuntime
224224
} from "./bindings/js";
225225

226+
import * as binaryen from "./glue/binaryen";
227+
226228
/** Features enabled by default. */
227229
export const defaultFeatures = Feature.MutableGlobals
228230
| Feature.SignExtension
@@ -375,7 +377,9 @@ export const enum Constraints {
375377
/** Indicates that static data is preferred. */
376378
PreferStatic = 1 << 4,
377379
/** Indicates that the value will become `this` of a property access or instance call. */
378-
IsThis = 1 << 5
380+
IsThis = 1 << 5,
381+
/** Indicates that the expression is compiled for an immediate return position. */
382+
WillReturn = 1 << 6
379383
}
380384

381385
/** Runtime features to be activated by the compiler. */
@@ -2825,6 +2829,7 @@ export class Compiler extends DiagnosticEmitter {
28252829
if (valueExpression) {
28262830
let constraints = Constraints.ConvImplicit;
28272831
if (flow.sourceFunction.is(CommonFlags.ModuleExport)) constraints |= Constraints.MustWrap;
2832+
if (!flow.isInline && this.options.hasFeature(Feature.TailCalls)) constraints |= Constraints.WillReturn;
28282833

28292834
expr = this.compileExpression(valueExpression, returnType, constraints);
28302835
if (!flow.canOverflow(expr, returnType)) flow.set(FlowFlags.ReturnsWrapped);
@@ -2854,6 +2859,13 @@ export class Compiler extends DiagnosticEmitter {
28542859
: module.br(inlineReturnLabel);
28552860
}
28562861

2862+
if (
2863+
expr &&
2864+
this.options.hasFeature(Feature.TailCalls) &&
2865+
getExpressionId(expr) == ExpressionId.Call &&
2866+
binaryen._BinaryenCallIsReturn(expr)
2867+
) return expr;
2868+
28572869
// Otherwise emit a normal return
28582870
return expr
28592871
? this.currentType == Type.void
@@ -6145,6 +6157,8 @@ export class Compiler extends DiagnosticEmitter {
61456157
Constraints.ConvImplicit | Constraints.IsThis
61466158
);
61476159
}
6160+
if (!this.canTailReturn(constraints, contextualType, functionInstance.signature.returnType))
6161+
constraints &= ~Constraints.WillReturn;
61486162
return this.compileCallDirect(
61496163
functionInstance,
61506164
expression.args,
@@ -6184,6 +6198,27 @@ export class Compiler extends DiagnosticEmitter {
61846198
return module.unreachable();
61856199
}
61866200

6201+
private canTailReturn(constraints: Constraints, contextType: Type, returnType: Type): bool {
6202+
if ((constraints & Constraints.WillReturn) == 0) return false;
6203+
// Tail calls are only valid as long as no result conversions are needed.
6204+
// `compileExpression` skips conversion for:
6205+
// 1. `currentType == nonnull<contextType>`, and
6206+
// 2. nullable reference identity (`T | null` -> `T | null`) where conversion is a no-op.
6207+
let sameWithoutNullable = returnType.equals(contextType.nonNullableType);
6208+
let sameNullableRef = returnType.isReference && returnType.equals(contextType);
6209+
if (!sameWithoutNullable && !sameNullableRef) return false;
6210+
if ((constraints & Constraints.MustWrap) == 0) return true;
6211+
switch (returnType.kind) {
6212+
case TypeKind.I8:
6213+
case TypeKind.I16:
6214+
case TypeKind.U8:
6215+
case TypeKind.U16:
6216+
return false;
6217+
default:
6218+
return true;
6219+
}
6220+
}
6221+
61876222
/** Compiles the given arguments like a call expression according to the specified context. */
61886223
private compileCallExpressionLike(
61896224
/** Called expression. */
@@ -6439,7 +6474,13 @@ export class Compiler extends DiagnosticEmitter {
64396474
operands[index] = paramExpr;
64406475
}
64416476
assert(index == numArgumentsInclThis);
6442-
return this.makeCallDirect(instance, operands, reportNode, (constraints & Constraints.WillDrop) != 0);
6477+
return this.makeCallDirect(
6478+
instance,
6479+
operands,
6480+
reportNode,
6481+
(constraints & Constraints.WillDrop) != 0,
6482+
(constraints & Constraints.WillReturn) != 0
6483+
);
64436484
}
64446485

64456486
makeCallInline(
@@ -6883,7 +6924,8 @@ export class Compiler extends DiagnosticEmitter {
68836924
instance: Function,
68846925
operands: ExpressionRef[] | null,
68856926
reportNode: Node,
6886-
immediatelyDropped: bool = false
6927+
immediatelyDropped: bool = false,
6928+
isReturn: bool = false
68876929
): ExpressionRef {
68886930
if (instance.hasDecorator(DecoratorFlags.Inline)) {
68896931
if (!instance.is(CommonFlags.Overridden)) {
@@ -6982,8 +7024,10 @@ export class Compiler extends DiagnosticEmitter {
69827024
lastOperand
69837025
], lastOperandType.toRef());
69847026
this.operandsTostack(instance.signature, operands);
6985-
let expr = module.call(instance.internalName, operands, returnTypeRef);
6986-
if (returnType != Type.void && immediatelyDropped) {
7027+
let expr = isReturn
7028+
? module.return_call(instance.internalName, operands, returnTypeRef)
7029+
: module.call(instance.internalName, operands, returnTypeRef);
7030+
if (!isReturn && returnType != Type.void && immediatelyDropped) {
69877031
expr = module.drop(expr);
69887032
this.currentType = Type.void;
69897033
} else {
@@ -6999,7 +7043,9 @@ export class Compiler extends DiagnosticEmitter {
69997043
}
70007044

70017045
if (operands) this.operandsTostack(instance.signature, operands);
7002-
let expr = module.call(instance.internalName, operands, returnType.toRef());
7046+
let expr = isReturn
7047+
? module.return_call(instance.internalName, operands, returnType.toRef())
7048+
: module.call(instance.internalName, operands, returnType.toRef());
70037049
this.currentType = returnType;
70047050
return expr;
70057051
}

src/module.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,35 +1842,20 @@ export class Module {
18421842
index: ExpressionRef,
18431843
operands: ExpressionRef[] | null,
18441844
params: TypeRef,
1845-
results: TypeRef,
1846-
isReturn: bool = false
1845+
results: TypeRef
18471846
): ExpressionRef {
18481847
let cStr = this.allocStringCached(tableName != null
18491848
? tableName
18501849
: CommonNames.DefaultTable
18511850
);
18521851
let cArr = allocPtrArray(operands);
1853-
let ret = isReturn
1854-
? binaryen._BinaryenReturnCallIndirect(
1855-
this.ref, cStr, index, cArr, operands ? operands.length : 0, params, results
1856-
)
1857-
: binaryen._BinaryenCallIndirect(
1858-
this.ref, cStr, index, cArr, operands ? operands.length : 0, params, results
1859-
);
1852+
let ret = binaryen._BinaryenCallIndirect(
1853+
this.ref, cStr, index, cArr, operands ? operands.length : 0, params, results
1854+
);
18601855
binaryen._free(cArr);
18611856
return ret;
18621857
}
18631858

1864-
return_call_indirect(
1865-
tableName: string | null,
1866-
index: ExpressionRef,
1867-
operands: ExpressionRef[] | null,
1868-
params: TypeRef,
1869-
results: TypeRef
1870-
): ExpressionRef {
1871-
return this.call_indirect(tableName, index, operands, params, results, true);
1872-
}
1873-
18741859
unreachable(): ExpressionRef {
18751860
return binaryen._BinaryenUnreachable(this.ref);
18761861
}

0 commit comments

Comments
 (0)