Skip to content

Commit abdeee7

Browse files
committed
feat: support first class function via builtin function
1 parent 1126ef1 commit abdeee7

File tree

9 files changed

+4488
-55
lines changed

9 files changed

+4488
-55
lines changed

src/builtins.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ import {
4343
NodeKind,
4444
LiteralExpression,
4545
ArrayLiteralExpression,
46-
IdentifierExpression
46+
IdentifierExpression,
47+
FunctionExpression
4748
} from "./ast";
4849

4950
import {
@@ -194,6 +195,8 @@ export namespace BuiltinNames {
194195
export const instantiate = "~lib/builtins/instantiate";
195196
export const idof = "~lib/builtins/idof";
196197

198+
export const experimental_first_class_function = "~lib/builtins/experimental_first_class_function";
199+
197200
export const i8 = "~lib/builtins/i8";
198201
export const i16 = "~lib/builtins/i16";
199202
export const i32 = "~lib/builtins/i32";
@@ -3609,6 +3612,26 @@ function builtin_unchecked(ctx: BuiltinFunctionContext): ExpressionRef {
36093612
}
36103613
builtinFunctions.set(BuiltinNames.unchecked, builtin_unchecked);
36113614

3615+
// experimental_first_class_function(expr: *) -> *
3616+
function builtin_experimental_first_class_function(ctx: BuiltinFunctionContext): ExpressionRef {
3617+
let compiler = ctx.compiler;
3618+
let module = compiler.module;
3619+
if (
3620+
checkTypeAbsent(ctx) |
3621+
checkArgsRequired(ctx, 1)
3622+
) return module.unreachable();
3623+
let operand = ctx.operands[0];
3624+
if (operand.kind != NodeKind.Function) {
3625+
let prototype = ctx.prototype;
3626+
prototype.program.error(DiagnosticCode._0_expected, operand.range, "FunctionExpression");
3627+
return module.unreachable();
3628+
}
3629+
let functionExpression = <FunctionExpression>operand;
3630+
let expr = compiler.compileFirstClassFunction(functionExpression, ctx.contextualType, Constraints.None);
3631+
return expr;
3632+
}
3633+
builtinFunctions.set(BuiltinNames.experimental_first_class_function, builtin_experimental_first_class_function);
3634+
36123635
// call_indirect<T?>(index: u32, ...args: *[]) -> T
36133636
function builtin_call_indirect(ctx: BuiltinFunctionContext): ExpressionRef {
36143637
let compiler = ctx.compiler;

src/compiler.ts

Lines changed: 109 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2054,20 +2054,23 @@ export class Compiler extends DiagnosticEmitter {
20542054

20552055
// === Table ====================================================================================
20562056

2057+
private registerFunctionInTable(instance: Function): u32 {
2058+
// Add to the function table
2059+
let functionTable = this.functionTable;
2060+
let tableBase = this.options.tableBase;
2061+
if (!tableBase) tableBase = 1; // leave first elem blank
2062+
let index = tableBase + functionTable.length;
2063+
functionTable.push(instance);
2064+
return index;
2065+
}
2066+
20572067
/** Ensures that a runtime counterpart of the specified function exists and returns its address. */
20582068
ensureRuntimeFunction(instance: Function): i64 {
20592069
assert(instance.is(CommonFlags.Compiled) && !instance.is(CommonFlags.Stub));
20602070
let program = this.program;
20612071
let memorySegment = instance.memorySegment;
20622072
if (!memorySegment) {
2063-
2064-
// Add to the function table
2065-
let functionTable = this.functionTable;
2066-
let tableBase = this.options.tableBase;
2067-
if (!tableBase) tableBase = 1; // leave first elem blank
2068-
let index = tableBase + functionTable.length;
2069-
functionTable.push(instance);
2070-
2073+
let index = this.registerFunctionInTable(instance);
20712074
// Create runtime function
20722075
let rtInstance = assert(this.resolver.resolveClass(program.functionPrototype, [ instance.type ]));
20732076
let buf = rtInstance.createBuffer();
@@ -7026,12 +7029,96 @@ export class Compiler extends DiagnosticEmitter {
70267029
return module.unreachable();
70277030
}
70287031

7032+
compileFirstClassFunction(
7033+
expression: FunctionExpression,
7034+
contextualType: Type,
7035+
constraints: Constraints
7036+
): ExpressionRef {
7037+
let module = this.module;
7038+
let instance = this.doCompileFunctionExpression(expression, contextualType, constraints);
7039+
let currentType = this.currentType;
7040+
if (!instance) return module.unreachable();
7041+
let rtInstance = assert(this.resolver.resolveClass(this.program.functionPrototype, [ instance.type ]));
7042+
const functionIndexInTable = this.registerFunctionInTable(instance);
7043+
let ctor = this.ensureConstructor(rtInstance, expression);
7044+
let indexMember = assert(rtInstance.getMember("_index"));
7045+
assert(indexMember.kind == ElementKind.PropertyPrototype);
7046+
let indexProperty = assert((<PropertyPrototype>indexMember).instance);
7047+
let indexSetter = assert(indexProperty.setterInstance);
7048+
let tmp = this.currentFlow.getTempLocal(Type.i32);
7049+
let expr = module.block(null, [
7050+
this.makeCallDirect(
7051+
indexSetter,
7052+
[
7053+
module.local_tee(
7054+
tmp.index,
7055+
this.makeCallDirect(ctor, [module.i32(0)], expression, /*immediatelyDropped*/ false),
7056+
/*isManaged*/ true, // TODO(maybe can be false to optimize performance)
7057+
TypeRef.I32
7058+
),
7059+
module.i32(functionIndexInTable),
7060+
],
7061+
expression
7062+
),
7063+
module.local_get(tmp.index, TypeRef.I32 )
7064+
], TypeRef.I32);
7065+
this.currentType = currentType;
7066+
return expr;
7067+
}
7068+
70297069
private compileFunctionExpression(
70307070
expression: FunctionExpression,
70317071
contextualType: Type,
70327072
constraints: Constraints
70337073
): ExpressionRef {
7034-
let declaration = expression.declaration.clone(); // generic contexts can have multiple
7074+
let declaration = expression.declaration;
7075+
let isNamed = declaration.name.text.length > 0;
7076+
let module = this.module;
7077+
let flow = this.currentFlow;
7078+
let isSemanticallyAnonymous = !isNamed || contextualType != Type.void;
7079+
7080+
let instance = this.doCompileFunctionExpression(expression, contextualType, constraints);
7081+
if (!instance) return module.unreachable();
7082+
let offset = this.ensureRuntimeFunction(instance); // reports
7083+
let expr = this.options.isWasm64
7084+
? module.i64(i64_low(offset), i64_high(offset))
7085+
: module.i32(i64_low(offset));
7086+
7087+
// add a constant local referring to the function if applicable
7088+
if (!isSemanticallyAnonymous) {
7089+
let fname = instance.name;
7090+
let existingLocal = flow.getScopedLocal(fname);
7091+
if (existingLocal) {
7092+
if (!existingLocal.declaration.range.source.isNative) {
7093+
this.errorRelated(
7094+
DiagnosticCode.Duplicate_identifier_0,
7095+
declaration.name.range,
7096+
existingLocal.declaration.name.range,
7097+
fname
7098+
);
7099+
} else { // scoped locals are shared temps that don't track declarations
7100+
this.error(
7101+
DiagnosticCode.Duplicate_identifier_0,
7102+
declaration.name.range, fname
7103+
);
7104+
}
7105+
} else {
7106+
let ftype = instance.type;
7107+
let local = flow.addScopedLocal(instance.name, ftype);
7108+
flow.setLocalFlag(local.index, LocalFlags.Constant | LocalFlags.Initialized);
7109+
expr = module.local_tee(local.index, expr, ftype.isManaged);
7110+
}
7111+
}
7112+
7113+
return expr;
7114+
}
7115+
7116+
private doCompileFunctionExpression(
7117+
expression: FunctionExpression,
7118+
contextualType: Type,
7119+
constraints: Constraints
7120+
): Function | null {
7121+
let declaration = expression.declaration;
70357122
assert(!declaration.typeParameters); // function expression cannot be generic
70367123
let flow = this.currentFlow;
70377124
let sourceFunction = flow.sourceFunction;
@@ -7047,7 +7134,6 @@ export class Compiler extends DiagnosticEmitter {
70477134
);
70487135
let instance: Function | null;
70497136
let contextualTypeArguments = cloneMap(flow.contextualTypeArguments);
7050-
let module = this.module;
70517137

70527138
// compile according to context. this differs from a normal function in that omitted parameter
70537139
// and return types can be inferred and omitted arguments can be replaced with dummies.
@@ -7065,7 +7151,7 @@ export class Compiler extends DiagnosticEmitter {
70657151
DiagnosticCode.Expected_0_arguments_but_got_1,
70667152
expression.range, numParameters.toString(), numPresentParameters.toString()
70677153
);
7068-
return module.unreachable();
7154+
return null;
70697155
}
70707156

70717157
// check non-omitted parameter types
@@ -7077,13 +7163,13 @@ export class Compiler extends DiagnosticEmitter {
70777163
sourceFunction.parent,
70787164
contextualTypeArguments
70797165
);
7080-
if (!resolvedType) return module.unreachable();
7166+
if (!resolvedType) return null;
70817167
if (!parameterTypes[i].isStrictlyAssignableTo(resolvedType)) {
70827168
this.error(
70837169
DiagnosticCode.Type_0_is_not_assignable_to_type_1,
70847170
parameterNode.range, parameterTypes[i].toString(), resolvedType.toString()
70857171
);
7086-
return module.unreachable();
7172+
return null;
70877173
}
70887174
}
70897175
// any unused parameters are inherited but ignored
@@ -7097,7 +7183,7 @@ export class Compiler extends DiagnosticEmitter {
70977183
sourceFunction.parent,
70987184
contextualTypeArguments
70997185
);
7100-
if (!resolvedType) return module.unreachable();
7186+
if (!resolvedType) return null;
71017187
if (
71027188
returnType == Type.void
71037189
? resolvedType != Type.void
@@ -7107,7 +7193,7 @@ export class Compiler extends DiagnosticEmitter {
71077193
DiagnosticCode.Type_0_is_not_assignable_to_type_1,
71087194
signatureNode.returnType.range, resolvedType.toString(), returnType.toString()
71097195
);
7110-
return module.unreachable();
7196+
return null;
71117197
}
71127198
}
71137199

@@ -7120,20 +7206,20 @@ export class Compiler extends DiagnosticEmitter {
71207206
DiagnosticCode._this_cannot_be_referenced_in_current_location,
71217207
thisTypeNode.range
71227208
);
7123-
return module.unreachable();
7209+
return null;
71247210
}
71257211
let resolvedType = this.resolver.resolveType(
71267212
thisTypeNode,
71277213
sourceFunction.parent,
71287214
contextualTypeArguments
71297215
);
7130-
if (!resolvedType) return module.unreachable();
7216+
if (!resolvedType) return null;
71317217
if (!thisType.isStrictlyAssignableTo(resolvedType)) {
71327218
this.error(
71337219
DiagnosticCode.Type_0_is_not_assignable_to_type_1,
71347220
thisTypeNode.range, thisType.toString(), resolvedType.toString()
71357221
);
7136-
return module.unreachable();
7222+
return null;
71377223
}
71387224
}
71397225

@@ -7148,52 +7234,21 @@ export class Compiler extends DiagnosticEmitter {
71487234
instance.flow.outer = flow;
71497235
let worked = this.compileFunction(instance);
71507236
this.currentType = contextualSignature.type;
7151-
if (!worked) return module.unreachable();
7237+
if (!worked) return null;
71527238

71537239
// otherwise compile like a normal function
71547240
} else {
71557241
instance = this.resolver.resolveFunction(prototype, null, contextualTypeArguments);
7156-
if (!instance) return this.module.unreachable();
7242+
if (!instance) return null;
71577243
instance.flow.outer = flow;
71587244
let worked = this.compileFunction(instance);
71597245
this.currentType = instance.signature.type;
7160-
if (!worked) return module.unreachable();
7246+
if (!worked) return null;
71617247
}
7162-
7163-
let offset = this.ensureRuntimeFunction(instance); // reports
7164-
let expr = this.options.isWasm64
7165-
? module.i64(i64_low(offset), i64_high(offset))
7166-
: module.i32(i64_low(offset));
7167-
7168-
// add a constant local referring to the function if applicable
7169-
if (!isSemanticallyAnonymous) {
7170-
let fname = instance.name;
7171-
let existingLocal = flow.getScopedLocal(fname);
7172-
if (existingLocal) {
7173-
if (!existingLocal.declaration.range.source.isNative) {
7174-
this.errorRelated(
7175-
DiagnosticCode.Duplicate_identifier_0,
7176-
declaration.name.range,
7177-
existingLocal.declaration.name.range,
7178-
fname
7179-
);
7180-
} else { // scoped locals are shared temps that don't track declarations
7181-
this.error(
7182-
DiagnosticCode.Duplicate_identifier_0,
7183-
declaration.name.range, fname
7184-
);
7185-
}
7186-
} else {
7187-
let ftype = instance.type;
7188-
let local = flow.addScopedLocal(instance.name, ftype);
7189-
flow.setLocalFlag(local.index, LocalFlags.Constant | LocalFlags.Initialized);
7190-
expr = module.local_tee(local.index, expr, ftype.isManaged);
7191-
}
7192-
}
7193-
7194-
return expr;
7248+
return instance;
71957249
}
71967250

7251+
71977252
/** Makes sure the enclosing source file of the specified expression has been compiled. */
71987253
private maybeCompileEnclosingSource(expression: Expression): void {
71997254
let internalPath = expression.range.source.internalPath;

src/diagnostics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ export abstract class DiagnosticEmitter {
454454
arg1: string | null = null,
455455
arg2: string | null = null
456456
): void {
457+
console.trace()
457458
this.emitDiagnostic(code, DiagnosticCategory.Error, range, null, arg0, arg1, arg2);
458459
}
459460

std/assembly/builtins.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ export declare function assert<T>(isTrueish: T, message?: string): T;
202202
@unsafe @builtin
203203
export declare function unchecked<T>(expr: T): T;
204204

205+
// @ts-ignore: decorator
206+
@unsafe @builtin
207+
export declare function experimental_first_class_function<T>(expr: T): T;
208+
205209
// @ts-ignore: decorator
206210
@unsafe @builtin
207211
export declare function call_indirect<T>(index: u32, ...args: auto[]): T;

std/assembly/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ declare function decodeURI(str: string): string;
281281
/** Decodes a Uniform Resource Identifier (URI) component previously created by encodeURIComponent. */
282282
declare function decodeURIComponent(str: string): string;
283283

284+
declare function experimental_first_class_function<T>(value: T): T & Function;
285+
284286
/** Atomic operations. */
285287
declare namespace atomic {
286288
/** Atomically loads an integer value from memory and returns it. */

0 commit comments

Comments
 (0)