Skip to content

Commit 8a14f44

Browse files
typescript: a callback handed to a library higher-order function is reached from the site that hands it over, not from every caller of the same signature
`xs.map((x) => ...)` stored the arrow in the parameter of a bodiless `map` declaration, so it became a value candidate of one function type shared by every `.map` call: impact through one map callback reached every caller of `.map` in the repository, and a walk down from any caller reached every map callback. Where no project body runs at the call site (a .d.ts member, an ambient declaration, an unresolved callee), the argument no longer flows into the parameter. The site itself gets a `callback_registered` edge to the function it hands over, the tier the JavaScript engine already uses for the same shape. A project higher-order function with a body keeps its dispatch through the parameter. Golden case 78 covers inline, named, local-held and ambient hand-overs, with a project HOF as control. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
1 parent 9d626cc commit 8a14f44

50 files changed

Lines changed: 385 additions & 35 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎graph/bundle/SCHEMA.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h
548548
| `implicit_constructor` | javascript | `new C()` / `super()` where no constructor exists up the chain: the synthesized default runs. A correct end; callee is NULL. |
549549
| `dynamic_terminal` | javascript | `obj[expr]()`, `eval`, `import()`: no static target by construction; callee is NULL. |
550550
| `fan_capped` | javascript, java, csharp | More targets than --dispatch-cap: the set was refused rather than emitted. JavaScript: callee is NULL. Java and C#: callee is the declared base method the fan would have started from; dispatch-capped-sites.csv carries the refused count. |
551-
| `callback_registered` | javascript | The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on('x', h)`, `setTimeout(f)`), which may invoke it. Not the site's own callee; a reachability edge, labelled so it is never read as a resolved call. |
551+
| `callback_registered` | javascript, typescript | The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on('x', h)`, `setTimeout(f)`), which may invoke it. Not the site's own callee; a reachability edge, labelled so it is never read as a resolved call. |
552552
| `event_dispatch` | javascript | `x.emit('name')` reaching a handler registered by `x.on('name', h)` on a value x may hold — name-sensitive for literal names, every handler on that value for a computed one. |
553553
| `intrinsic_terminal` | typescript | The site is a JSX intrinsic element or a dynamic `import()` — a runtime intrinsic, not a function the graph can name. |
554554

@@ -569,7 +569,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h
569569
- **python** — A `boundary_lib` edge may point at a builtin (callee_provenance builtin, callee_label `builtin:NAME`) or at an unstaged import path (callee_provenance external) — neither has a methods row.
570570
- **java** — A `boundary_lib` edge with callee_provenance external names a method of an ancestor type no staged IR declares (callee_label `external:<type>.<name>`, no methods row). A site whose receiver is declared as such a type is multi_inferred even with one client override: the platform method itself, and the platform's own subclasses, are the other possible targets. Stage the library to replace the label with the real method.
571571
- **python** — The reason a site is ambiguous_unknown is exported per site in ext_call_site_unresolved (site, caller, reason, detail).
572-
- **all** — THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list.
572+
- **all** — THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript, callback_registered in TypeScript. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list.
573573
- **java** — A multi_inferred fan is CHA-wide: it is every override the hierarchy admits, bounded only by the dispatch cap. type_instantiated is computed and exported but NOT read by any rule, so the fan is not narrowed to types the program constructs. Narrow it yourself by joining dispatch_candidates to type_instantiated — see the dispatch_envelope_of query. A receiver is ALSO typed by what flows into it (a local's initializer, the arguments callers pass to a parameter, the receivers callers invoke a method on for its `this`), and each flow-in type resolves its member directly, outside the fan: that is why a `fan_capped` site still carries edges, and why they are the types the program was seen to hand over, not the whole hierarchy.
574574
- **typescript** — A multi_inferred fan is CHA-wide, as in Java: type_instantiated is computed and exported but NOT read by any rule. The fan also has sources that are not virtual dispatch at all — an overload set or a union-typed receiver produces one too.
575575
- **python** — A multi_inferred fan IS narrowed by the instantiation set: type_instantiated_reachable (the constructed classes and their bases) bounds dispatch in resolution/dispatch.dl. Python is the only front end where that narrowing is applied, so a fan here is tighter than the same shape would be in Java or TypeScript.

‎graph/bundle/schema.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ export const VOCAB: readonly VocabSpec[] = [
510510
{ table: 'call_edges', column: 'tier', value: 'implicit_constructor', languages: S, meaning: '`new C()` / `super()` where no constructor exists up the chain: the synthesized default runs. A correct end; callee is NULL.' },
511511
{ table: 'call_edges', column: 'tier', value: 'dynamic_terminal', languages: S, meaning: '`obj[expr]()`, `eval`, `import()`: no static target by construction; callee is NULL.' },
512512
{ table: 'call_edges', column: 'tier', value: 'fan_capped', languages: ['javascript', 'java', 'csharp'], meaning: 'More targets than --dispatch-cap: the set was refused rather than emitted. JavaScript: callee is NULL. Java and C#: callee is the declared base method the fan would have started from; dispatch-capped-sites.csv carries the refused count.' },
513-
{ table: 'call_edges', column: 'tier', value: 'callback_registered', languages: S, meaning: 'The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on(\'x\', h)`, `setTimeout(f)`), which may invoke it. Not the site\'s own callee; a reachability edge, labelled so it is never read as a resolved call.' },
513+
{ table: 'call_edges', column: 'tier', value: 'callback_registered', languages: ['javascript', 'typescript'], meaning: 'The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on(\'x\', h)`, `setTimeout(f)`), which may invoke it. Not the site\'s own callee; a reachability edge, labelled so it is never read as a resolved call.' },
514514
{ table: 'call_edges', column: 'tier', value: 'event_dispatch', languages: S, meaning: '`x.emit(\'name\')` reaching a handler registered by `x.on(\'name\', h)` on a value x may hold — name-sensitive for literal names, every handler on that value for a computed one.' },
515515
{ table: 'call_edges', column: 'tier', value: 'intrinsic_terminal', languages: T, meaning: 'The site is a JSX intrinsic element or a dynamic `import()` — a runtime intrinsic, not a function the graph can name.' },
516516

@@ -747,7 +747,7 @@ export const NOTES: readonly NoteSpec[] = [
747747
{ language: 'python', table: 'entry_points', note: 'EMPTY. The Python rule set does not derive entry points; entry_reachable is therefore empty too.' },
748748
{ language: 'python', table: 'overrides', note: 'EMPTY — this table is Java-shaped. The Python dispatch envelope is in dispatch_candidates with basis `mro`, and `value` for a function assigned onto an instance\'s member; the raw linearisation is in ext_mro_position.' },
749749
{ language: 'python', table: 'type_instantiated', note: 'Every row has how = `new`: the rule set records that some client call constructs the class, not which form.' },
750-
{ language: 'all', table: 'call_edges', note: 'THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list.' },
750+
{ language: 'all', table: 'call_edges', note: 'THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript, callback_registered in TypeScript. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list.' },
751751
{ language: 'java', table: 'call_edges', note: 'A multi_inferred fan is CHA-wide: it is every override the hierarchy admits, bounded only by the dispatch cap. type_instantiated is computed and exported but NOT read by any rule, so the fan is not narrowed to types the program constructs. Narrow it yourself by joining dispatch_candidates to type_instantiated — see the dispatch_envelope_of query. A receiver is ALSO typed by what flows into it (a local\'s initializer, the arguments callers pass to a parameter, the receivers callers invoke a method on for its `this`), and each flow-in type resolves its member directly, outside the fan: that is why a `fan_capped` site still carries edges, and why they are the types the program was seen to hand over, not the whole hierarchy.' },
752752
{ language: 'typescript', table: 'call_edges', note: 'A multi_inferred fan is CHA-wide, as in Java: type_instantiated is computed and exported but NOT read by any rule. The fan also has sources that are not virtual dispatch at all — an overload set or a union-typed receiver produces one too.' },
753753
{ language: 'python', table: 'call_edges', note: 'A multi_inferred fan IS narrowed by the instantiation set: type_instantiated_reachable (the constructed classes and their bases) bounds dispatch in resolution/dispatch.dl. Python is the only front end where that narrowing is applied, so a fan here is tighter than the same shape would be in Java or TypeScript.' },
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// A FUNCTION HANDED TO A HIGHER-ORDER FUNCTION WITH NO BODY. `map`, `forEach` and `onReady` are declarations
2+
// only, so the site that hands the callback over is what reaches it: each function below reaches its own
3+
// callback and nothing another function handed to the same `map`.
4+
import { RATE } from './rate'
5+
6+
const seen: number[] = []
7+
function record(n: number): void {
8+
seen[0] = n
9+
}
10+
11+
export function scaled(xs: number[]): number[] {
12+
return xs.map((x) => x * RATE)
13+
}
14+
15+
export function doubled(xs: number[]): number[] {
16+
return xs.map((x) => x * 2)
17+
}
18+
19+
function double(n: number): number {
20+
return n * 2
21+
}
22+
23+
// a named function handed over: still reached, from the site that names it
24+
export function named(xs: number[]): number[] {
25+
return xs.map(double)
26+
}
27+
28+
// …and one held by a local first
29+
export function viaLocal(xs: number[]): void {
30+
const log = (n: number) => record(n)
31+
xs.forEach(log)
32+
}
33+
34+
// an ambient declaration with no body anywhere: a host API that calls back
35+
declare function onReady(cb: () => void): void
36+
37+
export function atStartup(): void {
38+
onReady(() => record(RATE))
39+
}
40+
41+
// CONTROL: a project higher-order function has a body that calls its parameter, so the callback is
42+
// still a dispatch candidate of the parameter's function type, reached through `each`'s own call.
43+
export function each(xs: number[], fn: (n: number) => void): void {
44+
for (const x of xs) fn(x)
45+
}
46+
47+
export function useEach(xs: number[]): void {
48+
each(xs, (n) => record(n + RATE))
49+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// The slice of the standard library this case hands callbacks to. Declared here, as client code, so the
2+
// client-only pass sees `map` and `forEach` as what they are to the project: signatures with no body.
3+
interface Array<T> {
4+
map<U>(fn: (v: T) => U): U[];
5+
forEach(fn: (v: T) => void): void;
6+
}
7+
interface Object {}
8+
interface Boolean {}
9+
interface Number {}
10+
interface String {}
11+
interface Function {}
12+
interface CallableFunction extends Function {}
13+
interface NewableFunction extends Function {}
14+
interface IArguments {}
15+
interface RegExp {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const RATE = 3
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "ESNext",
5+
"moduleResolution": "bundler",
6+
"strict": true,
7+
"noEmit": true
8+
}
9+
}

‎graph/test/typescript/expected/08-function-values-and-callbacks.edges‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
ambiguous_unknown FUNCTION_CALL values#useLibrary() @L40 -> -
22
ambiguous_unknown METHOD_CALL values#drive() @L29 -> -
3+
callback_registered FUNCTION_CALL values#useLibrary() @L40 -> values#local(?)
34
known_edge CONSTRUCTOR_CALL values#drive() @L27 -> Pipeline#<new>()
45
known_edge FUNCTION_CALL values#apply(Transform,string) @L12 -> values#<arrow@4>(string)
56
known_edge FUNCTION_CALL values#drive() @L30 -> values#<arrow@4>(string)

‎graph/test/typescript/expected/08-function-values-and-callbacks.lib.edges‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ ambiguous_unknown METHOD_CALL values#drive() @L29 -> -
22
boundary_lib FUNCTION_CALL values#useLibrary() @L40 -> hooks#<arrow@1>(string)
33
boundary_lib FUNCTION_CALL values#useLibrary() @L40 -> hooks#identity(?)
44
boundary_lib FUNCTION_CALL values#useLibrary() @L40 -> hooks#withHook(Hook,string)
5+
callback_registered FUNCTION_CALL values#useLibrary() @L40 -> values#local(?)
56
known_edge CONSTRUCTOR_CALL values#drive() @L27 -> Pipeline#<new>()
67
known_edge FUNCTION_CALL values#apply(Transform,string) @L12 -> values#<arrow@4>(string)
78
known_edge FUNCTION_CALL values#drive() @L30 -> values#<arrow@4>(string)

‎graph/test/typescript/expected/17-constrained-generics.edges‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ ambiguous_unknown FUNCTION_CALL consumer#widestLast() @L35 -> -
77
ambiguous_unknown METHOD_CALL consumer#ownConstraintNamed(T) @L46 -> -
88
ambiguous_unknown METHOD_CALL curried-local#pluck(unknown,unknown) @L33 -> -
99
ambiguous_unknown METHOD_CALL curried-local#widest(unknown) @L49 -> -
10+
callback_registered METHOD_CALL curried-local#pluck(unknown,unknown) @L33 -> curried-local#<arrow@33>()@L33
1011
known_edge FUNCTION_CALL consumer#localDataFirst() @L56 -> curried-local#pluck(readonly T[],'name')
1112
known_edge FUNCTION_CALL consumer#localDataLast() @L59 -> curried-local#pluck('name')
1213
known_edge FUNCTION_CALL consumer#localKeyed(Row) @L62 -> curried-local#at(T,T)

‎graph/test/typescript/expected/17-constrained-generics.lib.edges‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ boundary_lib FUNCTION_CALL consumer#keyedFirst(Row) @L23 -> curried#at(T,T)
77
boundary_lib FUNCTION_CALL consumer#keyedLast() @L27 -> curried#at(T)
88
boundary_lib FUNCTION_CALL consumer#widestFirst() @L31 -> curried#widest(readonly T[])
99
boundary_lib FUNCTION_CALL consumer#widestLast() @L35 -> curried#widest()
10+
callback_registered METHOD_CALL curried-local#pluck(unknown,unknown) @L33 -> curried-local#<arrow@33>()@L33
1011
known_edge FUNCTION_CALL consumer#localDataFirst() @L56 -> curried-local#pluck(readonly T[],'name')
1112
known_edge FUNCTION_CALL consumer#localDataLast() @L59 -> curried-local#pluck('name')
1213
known_edge FUNCTION_CALL consumer#localKeyed(Row) @L62 -> curried-local#at(T,T)

0 commit comments

Comments
 (0)