Skip to content

javascript: call-graph engine for the JavaScript front end (on reshape) - #472

Merged
swapnilpaliwal-sd merged 22 commits into
mainfrom
javascript-engine-reshape
Sep 14, 2026
Merged

swapnilpaliwal-sd merged 22 commits into
mainfrom
javascript-engine-reshape

Conversation

@swapnilpaliwal-sd

@swapnilpaliwal-sd swapnilpaliwal-sd commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #577.

Tracking #456. Implements #457, #458, #459, #460, #461, #462, #463, #464, #467, the probe batch #480, #481, #482, #484, #485, #486, #488, #489, #490, #491, and the engine halves of #479, #483, #487 on top of the parser fix #494; #465 lists what is left.

Draft on purpose: nothing here merges until the engine is code-complete and the held-out evaluation is settled. The branch is the integration point; this PR carries the running evaluation.

What this is

A call-graph engine for the JavaScript front end, under graph/javascript/, with its oracle, scorer, evaluation harness and regression suite under graph/test/javascript/.

JavaScript declares no types, so the engine is a may-analysis over values, not a type lookup: every expression, binding, parameter, property and return carries the set of values it may hold, a function, a class, an instance, a prototype, a module surface, an object literal, an array, or the platform, and a call site resolves through the callee's value. An untyped receiver produces nothing; the site is a declared unknown with its reason attached. graph/javascript/README.md has the layer order and the decisions; PARSER-DEFECTS.md the ten IR gaps the rules met, six of them since fixed in the parser and their workarounds retired.

How it is measured

checker.getResolvedSignature from a ts.Program built with allowJs + checkJs over the project's own files, per site. Sites the compiler types as any (about half) enter no rate. Scoring is per site: EXACT / SOUND_SUPERSET / WRONG / MISSED over compiler-decided client→client sites; platform targets, implicit constructors, JSDoc-type targets and targets outside the IR are bucketed apart.

Results (production code, per-site, compiler-decided client→client)

Development corpus, 15 projects, 9,867 decided sites:

exact 0.985
recall (exact + sound superset) 0.998
precision 1.000 (WRONG = 0)
sound supersets 127
conservation loss 0 on every project
solve time ≤ 8 s per project

Held-out corpus, 4 projects, 9,649 decided sites, validation only, no rule was derived from it:

project decided sites exact recall precision solve
A 1,840 0.997 0.999 1.000 6 s
B 2,150 0.965 0.977 1.000 16 s
C 2,258 0.809 0.993 0.997 4 s
D 3,401 0.976 0.989 1.000 8 s
aggregate 9,649 0.938 0.989 0.999

The generalisation gap is 4.7 points of exact and 0.9 of recall, and it is almost entirely WIDTH on project C: 417 of its sites are sound supersets, nearly all 2-wide (the self-dispatch fan over a deep component hierarchy). WRONG is 7 sites in 9,649, and 0 in 9,867 on the development corpus.

The probe batch (#480–#491)

Ten synthetic probe programs, each written to break one part of the value model, were scored against both oracles before and after: class field initializers, getters as values, every heritage form, re-exports, promises, generators, JSDoc generics and casts, property descriptors, five callee spellings, a dozen platform models. After: every probe WRONG = 0; i-fields and the qualified-heritage probe 1.000; the rest 0.83–0.98 exact with every remaining miss attributed to a parser gap. The parser then closed those gaps (#494: binding paths, every re-export's import row, the linked extends expression, anonymous default targets) and the engine halves followed, a path walker for destructuring, type_super through the extends expression's value, the default-export recovery joins retired. Every site the probes listed now resolves except a setter's parameter (no SETTER_INVOCATION site exists) and a concatenated key. The two rounds moved the held-out corpus by +0.3 exact / +0.4 recall without a rule derived from it.

Project B is the one that did not terminate before #467: the fan cap took it from >40 minutes to 9 seconds with development rates unchanged to three decimals. A second, smaller merge point surfaced after it, a write through a @param {Object}-typed value landed on the ONE identity every such parameter shares, and is closed the same way (the platform's properties are not tracked).

What is deliberately not resolved

Sites the compiler types as any, 7,319 on the development corpus, 18,272 held-out, enter no rate. Platform targets (LIB_AMBIENT_OK 6,040 / 5,561) are classified from the value reached, not the syntax; the LIB_MISSED residue (562 / 547) is almost entirely string/number/promise methods on the result of a project call, which nothing here types (#465). A fan wider than --dispatch-cap is refused and declared (fan_capped), as in the other rule sets.

A second oracle: execution

graph/test/javascript/torture/ is a hand-written program exercising every invocation form the language has (classes, prototypes, mixins, closures, callbacks, events, promises, timers, registries, Proxy, Reflect, tagged templates, the module graph). instrument.mjs rewrites every function body to record (caller → callee) at entry with the caller carried in an AsyncLocalStorage, so a .then continuation, a timer callback or an event handler attributes to the function that registered it. Every executed edge is then checked against the engine's graph:

executed edges 167 (7 module loads matched to import edges)
found 164, recall 0.98
missing 3, each named in known-missing.txt: an inner destructured default (PD-JS-5), two Proxy traps (the computed-key registries resolve since #490)
static-only (may-edges the entry script did not take) 37, all callback_registered / event_dispatch / self-dispatch supersets, none a fabrication

What the runtime oracle found and this PR fixed: self dispatch (this.area() in a base method runs the subclass override, this now fans over constructed subclasses, RTA-bounded), prototype mixins via Object.assign, rest parameters as arrays, obj['name'] with a literal key, Reflect.apply, emitter method chaining, emit arguments reaching handler parameters, sort comparators. Callbacks and events are edges now: every function literal passed as an argument has an incoming callback_registered edge (1,486 dev / 2,842 held-out, from 10% before), and emit('x') reaches its on('x', h) handlers (event_dispatch, 388 / 483 rows).

The compiler and execution disagree on purpose in one place, for this.area() in a base method tsc names the base declaration while execution names the override, so self dispatch costs 0.4 points of compiler-exact and is a sound superset there.

A real application with its real dependencies

graph/test/javascript/realapp/ is an the web framework service (routes, a param handler, middleware, an error handler, an EventEmitter-backed store, async handlers, the utility library, debug) with a pinned node_modules. All 68 packages are parsed in place and staged as --library; the app is exercised end to end under the tracer with node_modules instrumented too, so the edges FROM the web framework INTO the app's handlers are recorded.

compiler, per site (node_modules present) 45 / 45 client→client exact
executed edges scored 102 (client→client, client→lib, lib→client; 458 lib→lib out of scope)
found 100, recall 0.98
missing 2, named: the utility library's runtime-built API, a tracer artifact (the lazy Object.defineProperty getter resolves since #489)
solve 13 s

What it took, all in resolution/frameworks.dl and value-flow: merge-descriptors and setprototypeof as assign-/inherit-like models (<subject>() gets its members by reflection, Router() its prototype); Object.create as an allocation; string literals as values (proto[method] = … over the methods package's array); a require() binding holding module.exports; the export-table overwrite flag read the right way round (it marks the LIVE module.exports = row, the engine had been dropping exactly that row); library parameters untracked (a utility library's parameters had merged every caller into 31,533 values per export); and an the web framework handler model, req/res/next as values in the staged the web framework IR, stated as facts and checked here against what ran.

The honest caveat: framework indirection is MODELLED, not inferred. The web framework is covered; another framework needs its own facts, which is how every practical JavaScript call-graph tool works.

Regression suite

Nineteen hand-written cases, nine by construction, ten converted from the probe programs, with edges and compiler adjudication pinned; the four sites still missing (an inner destructured default, a setter's parameter, a concatenated key) are each named in a known-missing file so they flip to a failure the day the IR carries them; --bless cannot bless away a MISSED or WRONG line. graph/test/javascript/run-tests.sh --oracle: 19/19.

…in-repo parser, a JavaScript bundle adapter and vocabulary, bin/axiom-graph --language javascript

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@swapnilpaliwal-sd swapnilpaliwal-sd added engine Resolution / call-graph engine rules enhancement New feature or request javascript JavaScript labels Sep 14, 2026
@swapnilpaliwal-sd swapnilpaliwal-sd self-assigned this Sep 14, 2026
swapnilpaliwal-sd and others added 13 commits September 13, 2026 19:13
… to graph/test/javascript, the command is bin/axiomcode-graph
… handed to a call and a handler reached from emit are in the graph; case 09

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… alias from the caller's cwd, which failed every solve started outside the repository

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… over constructed subclasses, prototype mixins, rest arrays, literal keys, Reflect.apply, emitter chaining, handler arguments, sort comparators

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Object.create, express handlers), the overwrite flag read the right way round, a CommonJS require binding holds module.exports, library parameters untracked; a real express application scored against execution with node_modules instrumented

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ODE_MODULES_DEPTH); the scorer identifies staged library methods so client->library edges score like client->client

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ion callees, numeric/template/computed keys, new-returning-object, property descriptors and the OBJECT_DEFINE_PROPERTY export, promises, generators, JSDoc @this/casts/Map/Set/Promise generics, platform models, export * as ns, the anonymous default class, dotted heritage text, getters as values; ten probe cases join the suite

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* README: the front page a reader expects — tagline, quick start first, what you get, how it works, accuracy, layout, development

The substance is unchanged (every measured number is kept); the order and shape follow what
established projects do: what it is in one line, how to run it in three, then depth. Two
open-source project names and two library names are dropped from the public text.

* README: what it does and why coding agents need it, in plain language; the three trust properties; languages with their maturity
…ds Sup` takes the class the callers passed; scope_enclosing_method walks the block chain to the owning function

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… badge; language support with logos, parser/engine columns and maturity (#493)
…gets, dotted superclasses, every re-export's import, destructuring paths (#494)

Fixes #484, #479, #487, #459; the parser half of #483. Parser suite 39/39, corpus 41/41, TypeScript 46/46, Python 21/21, gen_decls --check OK, scrub 0 hits, engine tsc clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…heritage link, missing extends expression, getters), the parser-blocked residue under 'What is not built'

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 14, 2026
#484/#487 (#494), bin/axiomcode, the README; js_variable is 27 columns (bindingPath, isRestBinding after the hash)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
swapnilpaliwal-sd and others added 6 commits September 13, 2026 23:43
…ges move to a Status section (they cannot render while the repository is private) (#496)
…ava, C#); Status section follows Language support
…s walked from a value (pattern variables, pattern-parameter references, a destructured require's dotted importedName), type_super through the linked extends expression, Object.entries as pairs, the default-export recovery joins retired; five known-missing lists emptied

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engine Resolution / call-graph engine rules enhancement New feature or request javascript JavaScript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

javascript: merge the engine into main

1 participant