Skip to content

Commit cce65b3

Browse files
Merge pull request #1694 from AxiomCodeAI/loop/ja-is695
javascript: memoize/once wrappers keep their fallback, and a declared closure answers per site (#695)
2 parents 1dc12ab + b6c628c commit cce65b3

7 files changed

Lines changed: 377 additions & 7 deletions

File tree

‎graph/javascript/engine/resolution/value-flow.dl‎

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,12 @@ installed_function(val, m) :- expr_kind(_, "ASSIGNMENT", _, val), expr_child(_,
371371
// filtering by nesting is needed. Plus `@returns {T}`.
372372
return_value(m, k, i) :- expr_root(_, "RETURN", e), expr_owner(_, m, _, e), !expr_kind(_, "THIS", _, e),
373373
!wrapped_result_return(e), expr_value(e, k, i).
374+
// A wrapper closure's carrying return keeps every value that does NOT come from the
375+
// parameter's call: `fn() || fallback`, `override || (cached = make())`, a variable
376+
// that starts as a default (below). Not an `undefined` placeholder (`let result =
377+
// undefined`): that is the cache before its fill, never what a call returns.
378+
return_value(m, k, i) :- expr_root(_, "RETURN", e), expr_owner(_, m, _, e), wrapped_result_return(e),
379+
carry_rest(e, x), expr_value(x, k, i), (k != "ambient" ; i != "undefined").
374380
return_value(m, "inst", t) :- method_return_type(_, _, tr, m), type_ref_type(tr, t).
375381

376382
// ── a WRAPPER's closure returns what its argument returns, per call site (#695) ──
@@ -394,6 +400,9 @@ return_value(m, "inst", t) :- method_return_type(_, _, tr, m), type_ref_type(tr,
394400
wrapper_closure(h, c) :- expr_root(_, "RETURN", e), expr_owner(_, h, _, e), expr_introduces(_, c, e).
395401
wrapper_closure(h, c) :- expr_root(_, "RETURN", e), expr_owner(_, h, _, e), expr_binding(_, v, e),
396402
var_init(_, _, init, v), expr_introduces(_, c, init).
403+
// ... or names a function declared inside the wrapper: `function inner() { ... } return inner`.
404+
wrapper_closure(h, c) :- expr_root(_, "RETURN", e), expr_owner(_, h, _, e), expr_binding(_, v, e),
405+
function_binding_method(v, c), method_enclosing(c, h).
397406
param_call_kind("FUNCTION_CALL").
398407
param_call_kind("FUNCTION_CALL_CALL").
399408
param_call_kind("FUNCTION_CALL_APPLY").
@@ -413,7 +422,7 @@ var_carries_param_call(v, p) :- expr_kind(_, "ASSIGNMENT", _, a), expr_operator(
413422
// the wrapper's parameter at that position.
414423
wrapper_forwards(h, c, pos) :- wrapper_closure(h, c), param_decl(_, _, ppos, h, p), !param_is_rest(_, p),
415424
pos = to_number(ppos), expr_root(_, "RETURN", e), expr_owner(_, c, _, e), carries_param_call(e, p).
416-
wrapped_result_return(e) :- wrapper_closure(h, c), param_decl(_, _, _, h, p),
425+
wrapped_result_return(e) :- wrapper_closure(h, c), param_decl(_, _, _, h, p), !param_is_rest(_, p),
417426
expr_root(_, "RETURN", e), expr_owner(_, c, _, e), carries_param_call(e, p).
418427
// ── the wrapper WRITES that parameter before the closure calls it (#1706) ──
419428
// `fn = () => x`, `if (!fn) fn = dflt`, `fn = fn || dflt`, `fn ??= dflt` in the
@@ -456,16 +465,53 @@ block_kind_branches("TRY").
456465
block_kind_branches("CATCH").
457466
wrapper_param_replaced(h, pos) :- wrapper_param_write(h, pos, _, a), !param_write_keeps_arg(a).
458467
wrapper_param_written(h, pos, o) :- wrapper_param_write(h, pos, p, a), param_write_leaf(a, o), !expr_param(_, p, o).
468+
// Only the parameter's call is answered per site. carry_part walks the carrying
469+
// return down the same operators as carries_param_call; carry_rest is each operand
470+
// on the way that carries no parameter call (a fallback, an override, a variable's
471+
// default initializer), which keeps its ordinary value.
472+
carry_part(e, e) :- wrapped_result_return(e).
473+
carry_part(e, c) :- carry_part(e, x), carries_param_call(x, _), carry_step(x, c).
474+
carry_step(x, c) :- expr_kind(_, "CONDITIONAL", _, x), expr_child(_, x, r, _, c), r != "CONDITION".
475+
carry_step(x, c) :- expr_kind(_, "BINARY", _, x), expr_operator(_, op, x), binary_op_passes_value(op), expr_child(_, x, _, _, c).
476+
carry_step(x, c) :- expr_kind(_, "ASSIGNMENT", _, x), expr_child(_, x, "ASSIGNMENT_VALUE", _, c).
477+
carry_step(x, c) :- expr_kind(_, "SEQUENCE", _, x), sequence_last(x, c).
478+
carry_step(x, c) :- expr_binding(_, v, x), var_init(_, _, c, v).
479+
carry_step(x, c) :- expr_binding(_, v, x), expr_kind(_, "ASSIGNMENT", _, a), expr_operator(_, op, a), assignment_op_writes_value(op),
480+
expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt), expr_binding(_, v, tgt), expr_child(_, a, "ASSIGNMENT_VALUE", _, c).
481+
carry_rest(e, x) :- carry_part(e, x), !carries_param_call(x, _).
482+
// What the carrying return holds read context-insensitively: every caller's argument
483+
// merged (and nothing once the wrapper is over the fan cap). Only the fallbacks below
484+
// use it, where no one site's argument says what the parameter is.
485+
carried_value(c, k, i) :- expr_root(_, "RETURN", e), expr_owner(_, c, _, e), wrapped_result_return(e), expr_value(e, k, i).
459486
// The site's own value, and a call through it.
460487
expr_value(s, "wrap", s) :- expr_kind(_, "CALL", _, s), expr_resolves_to_method(s, h), wrapper_forwards(h, _, _).
461488
expr_value(ce, k, i) :- expr_kind(_, "CALL", _, ce), callee_value(ce, "wrap", s),
489+
!call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, ce, _, _), wrap_call_value(s, k, i).
490+
// wrap_site(Site, Wrapper, Position); wrap_site_arg when the site passes that position
491+
// itself (no spread at or before it), so its argument alone is the parameter.
492+
wrap_site(s, h, pos) :- expr_kind(_, "CALL", _, s), expr_resolves_to_method(s, h), wrapper_forwards(h, _, pos).
493+
wrap_site_spread(s, pos) :- wrapper_forwards(_, _, pos), call_arg(s, p2, a), p2 <= pos, expr_kind(_, "SPREAD", _, a).
494+
wrap_site_arg(s, pos, arg) :- wrap_site(s, h, pos), call_arg(s, pos, arg), !wrap_site_spread(s, pos), !wrapper_param_replaced(h, pos).
495+
// The argument is a function: what it returns. The argument is itself a wrapped
496+
// getter (`memoize(memoize(f))`, `once(getAlpha)`): what a call through that site returns.
497+
wrap_call_value(s, k, i) :- wrap_site_arg(s, _, arg), expr_value(arg, "func", g), return_value(g, k, i).
498+
wrap_call_value(s, k, i) :- wrap_site_arg(s, _, arg), expr_value(arg, "wrap", s2), wrap_call_value(s2, k, i).
499+
// No argument at that position: the parameter's default, `lazy(make = makeDefault)`.
500+
wrap_call_value(s, k, i) :- wrap_site(s, h, pos), !call_arg(s, pos, _), !wrap_site_spread(s, pos), !wrapper_param_replaced(h, pos),
501+
param_decl(_, _, ppos, h, p), to_number(ppos) = pos, param_default_text(_, txt, h, p),
502+
expr_root(_, "PARAMETER_DEFAULT", d), expr_owner(_, h, _, d), expr_text(_, txt, d),
503+
expr_value(d, "func", g), return_value(g, k, i).
504+
// A spread argument: the site cannot say which function lands in the parameter.
505+
wrap_call_value(s, k, i) :- wrap_site_spread(s, pos), wrap_site(s, h, pos), !wrapper_param_replaced(h, pos), wrapper_forwards(h, c, pos), carried_value(c, k, i).
506+
// What the wrapper itself writes into the parameter (#1706) is called at every site, whatever it passes.
507+
wrap_call_value(s, k, i) :- wrap_site(s, h, pos), wrapper_param_written(h, pos, o), expr_value(o, "func", g), return_value(g, k, i).
508+
// The closure called inside the wrapper itself (`const c = () => fn(); c(); return c`)
509+
// is not reached through a site: there the parameter is this invocation's, read merged.
510+
expr_value(ce, k, i) :- expr_kind(_, "CALL", _, ce), expr_resolves_to_method(ce, c), wrapper_forwards(h, c, _),
462511
!call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, ce, _, _),
463-
expr_resolves_to_method(s, h), wrapper_forwards(h, _, pos), !wrapper_param_replaced(h, pos), call_arg(s, pos, arg),
464-
expr_value(arg, "func", g), return_value(g, k, i).
465-
expr_value(ce, k, i) :- expr_kind(_, "CALL", _, ce), callee_value(ce, "wrap", s),
466-
!call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, ce, _, _),
467-
expr_resolves_to_method(s, h), wrapper_forwards(h, _, pos), wrapper_param_written(h, pos, o),
468-
expr_value(o, "func", g), return_value(g, k, i).
512+
expr_owner(_, m, _, ce), within_wrapper(m, h), carried_value(c, k, i).
513+
within_wrapper(h, h) :- wrapper_forwards(h, _, _).
514+
within_wrapper(m, h) :- method_enclosing(m, e), within_wrapper(e, h).
469515
// `return this` is POLYMORPHIC: a fluent `add()` declared on Node and called on an
470516
// AbstractBlock returns the AbstractBlock. So a `this`-return is not a value of the
471517
// method — it is the RECEIVER of each call, substituted at the site (below). A

‎graph/javascript/souffle/decls_all.dl‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,15 @@
333333
.decl block_kind_branches(c0:symbol)
334334
.decl wrapper_param_replaced(c0:symbol, c1:number)
335335
.decl wrapper_param_written(c0:symbol, c1:number, c2:symbol)
336+
.decl carry_part(c0:symbol, c1:symbol)
337+
.decl carry_step(c0:symbol, c1:symbol)
338+
.decl carry_rest(c0:symbol, c1:symbol)
339+
.decl carried_value(c0:symbol, c1:symbol, c2:symbol)
340+
.decl wrap_site(c0:symbol, c1:symbol, c2:number)
341+
.decl wrap_site_spread(c0:symbol, c1:number)
342+
.decl wrap_site_arg(c0:symbol, c1:number, c2:symbol)
343+
.decl wrap_call_value(c0:symbol, c1:symbol, c2:symbol)
344+
.decl within_wrapper(c0:symbol, c1:symbol)
336345
.decl prop_value(c0:symbol, c1:symbol, c2:symbol, c3:symbol, c4:symbol)
337346
.decl member_write(c0:symbol, c1:symbol, c2:symbol, c3:symbol, c4:symbol)
338347
.decl prototype_object(c0:symbol, c1:symbol)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// The closure is a function DECLARED inside the wrapper and returned by name,
2+
// with enough callers to put the wrapper over the parameter fan cap.
3+
const memoize = require("./memoize");
4+
const AlphaPlugin = require("./alpha");
5+
const BetaPlugin = require("./beta");
6+
7+
function onceDecl(fn) {
8+
let r;
9+
function inner() { if (!r) r = fn(); return r; }
10+
return inner;
11+
}
12+
// CONTROL: a declared closure that calls its parameter but returns something else.
13+
class Mark { markOnly() { return 4; } }
14+
function tapDecl(g) {
15+
function inner() { g(); return new Mark(); }
16+
return inner;
17+
}
18+
// CONTROL: a function declared OUTSIDE the wrapper, returned by name.
19+
function shared() { return new Mark(); }
20+
function pickShared(fn) { fn(); return shared; }
21+
22+
const declAlpha = onceDecl(() => new AlphaPlugin());
23+
const declBeta = onceDecl(() => new BetaPlugin());
24+
const getAlpha = memoize(() => new AlphaPlugin());
25+
const declOfGetter = onceDecl(getAlpha);
26+
const tappedDecl = tapDecl(() => new AlphaPlugin());
27+
const picked = pickShared(() => new BetaPlugin());
28+
29+
function declared() {
30+
declAlpha().run();
31+
declBeta().run();
32+
declOfGetter().run();
33+
tappedDecl().markOnly();
34+
picked().markOnly();
35+
}
36+
37+
const d01 = onceDecl(() => 1); const d02 = onceDecl(() => 2); const d03 = onceDecl(() => 3);
38+
const d04 = onceDecl(() => 4); const d05 = onceDecl(() => 5); const d06 = onceDecl(() => 6);
39+
const d07 = onceDecl(() => 7); const d08 = onceDecl(() => 8); const d09 = onceDecl(() => 9);
40+
const d10 = onceDecl(() => 10); const d11 = onceDecl(() => 11); const d12 = onceDecl(() => 12);
41+
const d13 = onceDecl(() => 13); const d14 = onceDecl(() => 14); const d15 = onceDecl(() => 15);
42+
const d16 = onceDecl(() => 16); const d17 = onceDecl(() => 17); const d18 = onceDecl(() => 18);
43+
const d19 = onceDecl(() => 19); const d20 = onceDecl(() => 20); const d21 = onceDecl(() => 21);
44+
const d22 = onceDecl(() => 22); const d23 = onceDecl(() => 23); const d24 = onceDecl(() => 24);
45+
const d25 = onceDecl(() => 25); const d26 = onceDecl(() => 26); const d27 = onceDecl(() => 27);
46+
47+
module.exports = { declared, d01, d02, d03, d04, d05, d06, d07, d08, d09, d10, d11, d12, d13,
48+
d14, d15, d16, d17, d18, d19, d20, d21, d22, d23, d24, d25, d26, d27 };
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Wrapper shapes whose closure returns MORE than the parameter's call, or whose
2+
// site does not name the parameter itself. Each call must keep what it returned
3+
// before the per-site answer: the fallback, the override, the default.
4+
const memoize = require("./memoize");
5+
const AlphaPlugin = require("./alpha");
6+
const BetaPlugin = require("./beta");
7+
8+
class Fallback { fallbackOnly() { return 0; } }
9+
class Mock { mockOnly() { return 1; } }
10+
class Defaulted { defaultOnly() { return 2; } }
11+
12+
// `fn() || fallback`: what the argument returns, or the fallback.
13+
const fallback = new Fallback();
14+
function withFallback(fn) {
15+
return () => fn() || fallback;
16+
}
17+
// An override that wins over the built value.
18+
let override = null;
19+
function setOverride(o) { override = o; }
20+
function lazyOr(make) {
21+
let cached;
22+
return () => override || (cached = cached || make());
23+
}
24+
// A result variable that starts as a default object.
25+
const defaults = { defaultsOnly() { return 3; } };
26+
function memoDefault(fn) {
27+
let result = defaults;
28+
let done = false;
29+
return () => { if (!done) { done = true; result = fn() || result; } return result; };
30+
}
31+
// A default parameter, a spread argument, a closure called inside its own wrapper.
32+
const makeDefault = () => new Defaulted();
33+
function lazy(make = makeDefault) { return () => make(); }
34+
function eager(fn) { const c = () => fn(); c().run(); return c; }
35+
36+
const getFb = withFallback(() => new AlphaPlugin());
37+
setOverride(new Mock());
38+
const getOr = lazyOr(() => new BetaPlugin());
39+
const getD = memoDefault(() => new AlphaPlugin());
40+
const getDefault = lazy();
41+
const args = [() => new BetaPlugin()];
42+
const getSpread = lazy(...args);
43+
const warm = eager(() => new AlphaPlugin());
44+
// A wrapped getter wrapped again: the inner site's argument decides.
45+
const getNested = memoize(memoize(() => new BetaPlugin()));
46+
47+
function shapes() {
48+
getFb().run();
49+
getFb().fallbackOnly();
50+
getOr().run();
51+
getOr().mockOnly();
52+
getD().run();
53+
getD().defaultsOnly();
54+
getDefault().defaultOnly();
55+
getSpread().run();
56+
warm().run();
57+
getNested().run();
58+
}
59+
60+
module.exports = { shapes };
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> -
2+
unresolved declared.js:9:34 FUNCTION_CALL fn callee_untyped
3+
unresolved declared.js:9:34 FUNCTION_CALL fn param_fan_capped
24
unresolved memoize.js:7:14 FUNCTION_CALL fn callee_untyped
35
unresolved memoize.js:7:14 FUNCTION_CALL fn param_fan_capped
6+
unresolved shapes.js:55:3 METHOD_CALL run member_absent
7+
value_callee declared.js:9:34 fn parameter
48
value_callee memoize.js:7:14 fn parameter

0 commit comments

Comments
 (0)