Repository navigation
Conversation
| // Escaped function values and partial applications share the native call convention. | ||
| :noOpt | ||
| :checkIR | ||
| :expect [42, 42, 42] | ||
| fun triple(first: Int)(second: Int)(third: Int): Int = first + second + third | ||
| let partial = triple(1) | ||
| let firstClass = triple | ||
| [triple(1)(2)(39), partial(2)(39), firstClass(1)(2)(39)] | ||
| //│ = [42, 42, 42] | ||
| //│ firstClass = class anonymous | ||
| //│ partial = fun |
There was a problem hiding this comment.
There is no :ssjs flag here (please have it later) so I put the generated JS code below:
let triple, partial, firstClass, tmp, callPrefix, tmp1, callPrefix1, callPrefix2, tmp2, Function$1, function1, callee, callee1, callee2, callee3, callee4, first1, second1, third, inlinedVal1;
triple = function triple(first2) {
runtime.checkArgs("triple", 1, arguments.length);
return (second2) => {
return (third1) => {
let tmp3;
tmp3 = first2 + second2;
return tmp3 + third1
}
}
};
partial = runtime.checkCall(triple(1));
globalThis.Object.freeze(class Function$ extends globalThis.Function {
static {
Function$1 = this
}
constructor() {
super();
}
get call$__checkNotMethod() { runtime.deboundMethod("call", "Function$"); }
call(thisArg, first2) {
runtime.checkArgs("call", 2, arguments.length);
return runtime.checkCall(triple(first2))
}
toString() { return runtime.render(this); }
static [definitionMetadata] = ["class", "Function$"];
});
function1 = globalThis.Object.freeze(new Function$1());
firstClass = function1;
first1 = 1;
second1 = 2;
third = 39;
inlinedLbl1: {
let tmp3;
tmp3 = first1 + second1;
inlinedVal1 = tmp3 + third;
break inlinedLbl1;
}
tmp = inlinedVal1;
callee = partial;
callPrefix = runtime.safeCall(callee.call(undefined, 2));
callee1 = callPrefix;
tmp1 = runtime.safeCall(callee1.call(undefined, 39));
callee2 = firstClass;
callPrefix1 = runtime.safeCall(callee2.call(undefined, 1));
callee3 = callPrefix1;
callPrefix2 = runtime.safeCall(callee3.call(undefined, 2));
callee4 = callPrefix2;
tmp2 = runtime.safeCall(callee4.call(undefined, 39));
block$res4 = globalThis.Object.freeze([ tmp, tmp1, tmp2 ]);The problem is: now we do not generate a class for each lambda function. Instead, we reuse the call method that is builtin in JS Function class. e.g., partial = runtime.checkCall(triple(1));. It is fine only in this transformer, but fails when we pass the generated code to the partial evaluator, since we cannot handle a lambda there. We still need to wrap those first-class-used lambda functions with a class, so that they can be partially evaluated. Meanwhile, I think we still want to keep applications like triple(1)(2)(39) straightforward, i.e., we do not want to introduce any class instance when evaluating it.
| // Recursive calls retain all their argument lists too. | ||
| :checkIR | ||
| :expect 42 | ||
| fun sum(n: Int)(acc: Int): Int = | ||
| if n is 0 then acc else sum(n - 1)(acc + n) | ||
| sum(6)(21) | ||
| //│ = 42 |
There was a problem hiding this comment.
These examples (including the add(40)(2) above) are a bit too simple, so we always get inlined computations for calls like sum(6)(21), with or without :noOpt. The following one could be better for demonstrating that a curried call is correctly transformed:
:noOpt
:ssjs
fun fib(x)(y)(n) =
if n is
0 then x
1 then y
else fib(y)(x + y)(n - 1)
fib(0)(1)(10)
let f = fib(0)(1)
f(10)
//│ —————————————| JS (sanitized) |—————————————————————————————————————————————————————————————————————
//│ let fib, f, callee5, fib$tailrec, x, y, n, inlinedVal2;
//│ fib$tailrec = function fib$tailrec(x1, y1, n1) {
//│ runtime.checkArgs("fib$tailrec", 3, arguments.length);
//│ loopLabel: while (true) {
//│ let tmp3, tmp4;
//│ switch (n1) {
//│ case 0:
//│ return x1;
//│ case 1:
//│ return y1;
//│ }
//│ tmp3 = x1 + y1;
//│ tmp4 = n1 - 1;
//│ x1 = y1;
//│ y1 = tmp3;
//│ n1 = tmp4;
//│ continue loopLabel;
//│ }
//│ };
//│ fib = function fib(x1) {
//│ runtime.checkArgs("fib", 1, arguments.length);
//│ return (y1) => {
//│ return (n1) => {
//│ return runtime.checkCall(fib$tailrec(x1, y1, n1))
//│ }
//│ }
//│ };
//│ x = 0;
//│ y = 1;
//│ n = 10;
//│ inlinedLbl2: {
//│ inlinedVal2 = runtime.checkCall(fib$tailrec(x, y, n));
//│ break inlinedLbl2;
//│ }
//│ f = runtime.checkCall(fib(0)(1));
//│ callee5 = f;
//│ block$res5 = runtime.safeCall(callee5.call(undefined, 10));
//│ —————————————————| Output |—————————————————————————————————————————————————————————————————————————
//│ = 55
//│ f = fun| // Incoming curried JS values receive undefined at every bare application. | ||
| :noOpt | ||
| :expect [42, 42] | ||
| val native: Int -> Int -> Int = globalThis.eval("(function (x) { 'use strict'; if (this !== undefined) throw new Error('outer receiver'); return function (y) { if (this !== undefined) throw new Error('inner receiver'); return x + y; }; })") |
There was a problem hiding this comment.
I also wonder if now we can handle an imported module (with or without :ftc) member correctly, with the new symbol resolution.
Btw, this example should be fine if we do not transform it into a class instance. The partial evaluator should treat globalThis.eval dynamically, so we don't need to consider how to specialize the function it returns.
| sym.irFunDefn.map(_.params).orElse: | ||
| sym.defn.map(_.params.map(LoweredParamList.callingConvention)) | ||
|
|
||
| private def function(p: Path): Opt[(TermSymbol, LoweredParamList)] = |
There was a problem hiding this comment.
The function name function is probably too simple to reflect the things done inside.
[GPT-6 Astra]
With
:ftc, curried calls such asadd(40)(2)could returnundefined, and calls through function values assumed that every value had been transformed. RewriteFirstClassFunctionTransformeraround resolved definition symbols and a shared JavaScript calling convention.Calls through function values now use
.call(undefined, ...)for bare calls and preserve the receiver for member calls. Generated closures accept the explicit receiver argument, capture method values and receivers when read, and use fresh forwarding parameters. External declarations retain their optional-argument and result-normalization conventions.Keep curried definitions and saturated multi-calls intact for later optimization. Escaping partial applications return native functions compatible with the same call convention. Reflection retains multiple parameter lists. The interop rewrite and currying fix are separate commits.
Add new-resolution diff tests covering incoming JS functions, mixed function values, receiver capture, optional/rest arguments, partial applications, recursion, and generators. The original regression now checks both the preserved multi-call and its optimized result. Updated golden outputs are included.
Validation:
hkmc2AllTests/testpassed, including compilation prerequisites and all diff tests; Metals compilation passed.