You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit bc067d4
Browse filesBrowse the repository at this point in the historyBrowse files
Merge main into reshape: the dispatch envelope (#474) lands under graph/
Rename detection carried every edited file from src/ to graph/ on its own. One conflict,
in the bundle test: reshape renamed the bundle's CSV output directory graph/ -> csv/
(the repository now HAS a top-level graph/), and #474 added dispatch_candidates to the
same core-table loop. Both, not either.
The new files — 37 .envelope goldens and test/tools/envelope_report.py — needed placing
by hand: git can infer where an EDITED file went when its directory was renamed, but not
where a file created on the other branch belongs.
Verified on the merged tree: bundle-test.sh passes (3 languages, one schema across them,
canonical queries run, schema doc current), the Python literal gate passes, all 37 goldens
are under graph/test/*/expected, and the three rule files carry method_dispatch_candidate.
Copy file name to clipboardExpand all lines: graph/bundle/SCHEMA.md
+51-15Lines changed: 51 additions & 15 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -26,12 +26,13 @@ Identifiers are the parser's hashes and are opaque; join them to `methods` / `ty
26
26
1. This is a call graph of one codebase, derived by a type-directed Datalog engine. Start with `SELECT value FROM run WHERE key='language'` — every language-specific fact below is keyed on it.
27
27
2. The graph is `call_edges`: one row per (call site, possible target). Rows join to `methods` (names, files, lines) on `caller_id` / `callee_method_id`, and to `call_sites` on `call_site_id` for where the call is written. Identifiers are opaque hashes — never parse them, always join.
28
28
3. Trust is explicit. `tier` says what kind of claim a row is: `known_edge` (one resolved target), `multi_inferred` (a sound set — every row of the set is a real possibility), `boundary_lib` (leaves the client; not expanded further), `ambiguous_*` (a declared unknown: callee is NULL). Pick the tiers your question tolerates and filter on them; never treat an `ambiguous_*` row as an edge.
29
-
4. Before answering "nothing calls X" or "X cannot reach Y", check `unresolved_sites` for the methods on the path: a caller listed there has a call the engine could not resolve, so the answer is a lower bound and should say so.
30
-
5. Library targets (`callee_provenance = lib`) are named in `methods` with `provenance = lib` but their bodies were not analysed; a Python `builtin`/`external` target has no methods row and lives in `callee_label`.
31
-
6.`schema_vocab` lists every value a column can hold FOR THIS LANGUAGE with its meaning — filter on `language = (SELECT value FROM run WHERE key='language')`. `schema_notes` lists the caveats for this language (empty tables, what an id may point at). Read both before interpreting `kind`, `tier` or an empty table.
32
-
7.`schema_queries` holds tested SQL for the common questions (callers, callees, blast radius, entry reachability, the method at a file:line, the blind spots of a method). Bind the named parameters and run.
33
-
8. Tables named `ext_<relation>` are the language's raw engine relations with positional columns c0…cN; `schema_tables` carries each one's description lifted from its rule. Use them only when a core table does not hold what you need.
34
-
9. When you report a result, carry the tier and the unresolved count with it. A consumer who cannot see the confidence of an edge cannot use it.
29
+
4.`call_edges` is what the engine CONCLUDED; `dispatch_candidates` is what the hierarchy ADMITTED. Read the second when you need an upper bound rather than a best answer — a candidate whose owner is absent from `type_instantiated` is admitted by the hierarchy but never constructed in this run, which is how you narrow it yourself. `basis` separates a declared relationship from a shape match.
30
+
5. Before answering "nothing calls X" or "X cannot reach Y", check `unresolved_sites` for the methods on the path: a caller listed there has a call the engine could not resolve, so the answer is a lower bound and should say so.
31
+
6. Library targets (`callee_provenance = lib`) are named in `methods` with `provenance = lib` but their bodies were not analysed; a Python `builtin`/`external` target has no methods row and lives in `callee_label`.
32
+
7.`schema_vocab` lists every value a column can hold FOR THIS LANGUAGE with its meaning — filter on `language = (SELECT value FROM run WHERE key='language')`. `schema_notes` lists the caveats for this language (empty tables, what an id may point at). Read both before interpreting `kind`, `tier` or an empty table.
33
+
8.`schema_queries` holds tested SQL for the common questions (callers, callees, blast radius, entry reachability, the method at a file:line, the blind spots of a method, the dispatch envelope of a method). Bind the named parameters and run.
34
+
9. Tables named `ext_<relation>` are the language's raw engine relations with positional columns c0…cN; `schema_tables` carries each one's description lifted from its rule. Use them only when a core table does not hold what you need.
35
+
10. When you report a result, carry the tier and the unresolved count with it. A consumer who cannot see the confidence of an edge cannot use it.
35
36
36
37
## Canonical queries (`schema_queries`)
37
38
@@ -62,15 +63,16 @@ WHERE caller.qualified_name = :qualified_name
62
63
ORDER BYs.start_line, target
63
64
```
64
65
65
-
**`blast_radius`** — If this method changes, which methods are transitively affected, up to :depth hops, through sound edges only (known_edge and multi_inferred)? _(:qualified_name, :depth)_
66
+
**`blast_radius`** — If this method changes, which methods are transitively affected, up to :depth hops, through RESOLVED client edges only (a declared unknown is not traversed, and the count of them is returned alongside)? _(:qualified_name, :depth)_
66
67
67
68
```sql
68
69
WITH RECURSIVE up(id, depth) AS (
69
70
SELECT id, 0FROM methods WHERE qualified_name = :qualified_name
70
71
UNION
71
72
SELECTe.caller_id, up.depth+1
72
73
FROM call_edges e JOIN up ONe.callee_method_id=up.id
SELECTMIN(up.depth) AS depth, m.qualified_name, m.file_path, m.start_line,
76
78
(SELECTcount(*) FROM unresolved_sites u WHEREu.caller_id=m.id) AS unresolved_calls_inside
@@ -108,6 +110,18 @@ WHERE m.qualified_name = :qualified_name
108
110
ORDER BYs.start_line
109
111
```
110
112
113
+
**`dispatch_envelope_of`** — What else might actually run at a call that resolves to this method — the set the graph narrowed from, and whether each candidate is a declaration or a shape match? _(:qualified_name)_
EXISTS (SELECT1FROM type_instantiated i WHEREi.type_id=cand.owner_type_id) AS owner_instantiated
118
+
FROM dispatch_candidates d
119
+
JOIN methods base ONbase.id=d.base_method_id
120
+
JOIN methods cand ONcand.id=d.candidate_method_id
121
+
WHEREbase.qualified_name= :qualified_name
122
+
ORDER BYd.basis, cand.qualified_name
123
+
```
124
+
111
125
**`subtypes_of`** — Which types extend or implement this type (transitively)? _(:qualified_name)_
112
126
113
127
```sql
@@ -342,7 +356,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h
342
356
| value | languages | meaning |
343
357
|---|---|---|
344
358
|`known_edge`| all | Exactly one target resolved. The strongest claim. |
345
-
|`multi_inferred`| all | A sound SET of possible targets (virtual dispatch over instantiated subtypes); each member is one row. The set over-approximates; no member is a guess. |
359
+
|`multi_inferred`| all | A sound SET of possible targets; each member is one row. The set over-approximates — every member is a real possibility, but not every member runs. HOW WIDE the set is differs by language: see the per-language notes on this table for whether the fan is narrowed by the instantiation set. |
346
360
|`boundary_lib`| all | The target is outside the client (library, builtin, or unstaged external). The chain is not expanded past it here. |
347
361
|`ambiguous_unknown`| all | Declared blind spot: the engine could not resolve the site (unresolved receiver, missing type, reflection…). callee is NULL. Never dropped. |
348
362
|`ambiguous_anon`| java | Known structural gap: an anonymous-class creation has no candidate rule yet. callee is NULL. |
@@ -402,6 +416,10 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h
402
416
403
417
-**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.
404
418
-**python** — The reason a site is ambiguous_unknown is exported per site in ext_call_site_unresolved (site, caller, reason, detail).
419
+
-**all** — THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge, multi_inferred, boundary_lib, and in TypeScript ALSO ambient_terminal and intrinsic_terminal. BLIND SPOT (callee is NULL): ambiguous_unknown, and in Java ALSO ambiguous_anon. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in TypeScript and nowhere else — derive the set from this note or from unresolved_sites, never from a hardcoded list.
420
+
-**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.
421
+
-**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.
422
+
-**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.
405
423
-**all** — The raw relation has a seventh column, ToExpr, that is always `-` (reserved). It is dropped here.
406
424
-**all** — An unresolved site (tier ambiguous_*) has NULL callee_method_id, callee_label and callee_provenance. The raw relation writes `-` in those slots.
407
425
@@ -414,9 +432,27 @@ Transitive supertype closure: (type, ancestor) for every ancestor reachable thro
414
432
| 0 |`type_id` 🔑 | TEXT || FK → types.id. |
415
433
| 1 |`ancestor_type_id` 🔑 | TEXT || FK → types.id. |
416
434
435
+
### `dispatch_candidates`
436
+
437
+
THE DISPATCH ENVELOPE: (base method, method that may run instead) for every call that statically resolves to the base. This is the set `call_edges` narrowed FROM — the difference between "these are the targets" and "these are the targets, out of these possibilities". Populated in every language; `basis` says what admitted the pair, because the three front ends admit by different means.
438
+
439
+
| # | column | type | null | meaning |
440
+
|---|---|---|---|---|
441
+
| 0 |`base_method_id` 🔑 | TEXT || FK → methods.id — the method a call resolves to statically. |
442
+
| 1 |`candidate_method_id` 🔑 | TEXT || FK → methods.id — a method that may run instead at such a call. |
443
+
| 2 |`basis` 🔑 | TEXT || What admitted the pair — see vocabulary. Filter on it to trust only declarations. |
444
+
445
+
**`dispatch_candidates.basis` values**
446
+
447
+
| value | languages | meaning |
448
+
|---|---|---|
449
+
|`nominal`| java, typescript | A written extends/implements reaches the candidate's owner from the base's owner. The strongest evidence there is: the author declared the relationship. |
450
+
|`structural`| typescript | No declaration; the candidate's owner satisfies the base's owner by SHAPE. Emitted only for supertypes with no nominal implementor at all, so it never competes with a declared answer — but it is a heuristic, and a consumer that wants declarations only filters it out. |
451
+
|`mro`| python | The subtype's C3 linearisation picks the candidate for that attribute name. Not merely "the subtype declares this name" — a name a sibling base wins is attributed to that sibling. |
452
+
417
453
### `overrides`
418
454
419
-
Virtual-dispatch pairs: (base method, overriding method) wherever a call to the base may run the override. Java only today — see notes for what the other front ends offer instead.
455
+
Virtual-dispatch pairs: (base method, overriding method) wherever a call to the base may run the override. Java only, and kept for compatibility — it is exactly `dispatch_candidates` filtered to `basis = nominal`. Prefer `dispatch_candidates`, which is populated in every language.
420
456
421
457
| # | column | type | null | meaning |
422
458
|---|---|---|---|---|
@@ -425,8 +461,8 @@ Virtual-dispatch pairs: (base method, overriding method) wherever a call to the
425
461
426
462
**Notes**
427
463
428
-
-**typescript** — EMPTY. TypeScript dispatch is captured directly as multi_inferred edges; the structural and nominal implementor sets are in ext_implementors, ext_structural_implementor and ext_type_satisfies.
429
-
-**python** — EMPTY. Python method lookup is by MRO, exported positionally in ext_mro_position (type, ancestor, …, position).
464
+
-**typescript** — EMPTY — this table is Java-shaped. The TypeScript dispatch envelope is in dispatch_candidates, with basis `nominal` or `structural`.
465
+
-**python** — EMPTY — this table is Java-shaped. The Python dispatch envelope is in dispatch_candidates with basis `mro`; the raw linearisation is in ext_mro_position.
430
466
431
467
### `entry_points`
432
468
@@ -475,7 +511,7 @@ The blind spots, attributed to the code that contains them: (caller, site) for e
475
511
476
512
### `type_instantiated`
477
513
478
-
Types the client actually creates an instance of — the rapid-type-analysis set that bounds virtual dispatch. (A subtype nothing instantiates cannot receive a dispatched call.)
514
+
Types this run creates an instance of — the rapid-type-analysis set that bounds virtual dispatch. (A subtype nothing instantiates cannot receive a dispatched call.) Deliberately an over-approximation: narrowing it on evidence the run does not have would lose real edges. Populated in every language.
479
515
480
516
| # | column | type | null | meaning |
481
517
|---|---|---|---|---|
@@ -486,13 +522,13 @@ Types the client actually creates an instance of — the rapid-type-analysis set
486
522
487
523
| value | languages | meaning |
488
524
|---|---|---|
489
-
|`new`|java, python | A constructor call in the client. |
|`anonymous`| java | An anonymous class exists only by being instantiated. |
491
527
|`enum_constant`| java | An enum's constants are its instances. |
492
528
493
529
**Notes**
494
530
495
-
-**typescript** — EMPTY. The TypeScript rule set does not export an instantiation set.
531
+
-**typescript** — Every row has how = `new`. Not restricted to client provenance: a type the library constructs is still a type that exists at run time, and dropping it would narrow the envelope unsoundly.
496
532
-**python** — Every row has how = `new`: the rule set records that some client call constructs the class, not which form.
0 commit comments