Repository navigation
ir: decide whether derived facts (usage, subtypes, flattened properties) live in one per-document index #950
Description
Problem
The IR has four facts it derives from the declared ones, and each has a different home, or none:
| Fact | Where it lives at c387e25c |
What happens |
|---|---|---|
usage (Input/Output/Error/Multipart) |
TypeCommon.Usage, "computed by a pass" (ir/typedef.go) |
No pass computes it. Every type of every document carries 0. emitter-design §3.2 also gives TypePlan its own Usage, a second home. |
| subtypes | ir.Supertypes / ir.IsSubtype, walked afresh per call |
pass.Validate asks once per discriminator route, so its work is N(N+1) on a valid spec (#850). |
| flattened properties | "computed, never stored" (ir-design §4.3) | No function exists (#812); ir.ExposedProps walks per call. |
| strongly connected components | nowhere | The plan needs them for recursive types (#718). |
The usage pass the design names cannot be written as designed. A pass that replaces a type writes the registry, and TestRegistryWrites_StayInsideTheFramework forbids that outside compilers/compile and ir.
ir-design §16 open question 4 leans toward storing usage "since emitters all need it and it's expensive to derive". That premise was measured on the golden spec, the Cloudflare API schema (cloudflare/api-schemas openapi.json at 911ad78), compiled with go run ./cmd/morphic compile openapi.json -o golden.ir.json at c387e25c. Deriving usage by reachability over ir.TypeEdges, from every operation's parameters, request, responses and errors, visits 46,990 types with 152,746 allocations in about 35 ms. The compile takes about 7 s, so the derivation costs about 0.5% of it. It reaches 41,109 of the 41,592 types: 14,163 input, 22,854 output, 9,933 error and 215 multipart.
Direction
Two options. Option A is the proposal.
- A. One index per document.
irgains one index per document (for exampleir.Facts(doc)), built once and lazily per fact. It answersIsSubtypefrom memoized closures (ir: IsSubtype walks the hierarchy afresh per call, so its callers go quadratic #850's first shape), usage by reachability, flattened properties by ir: provide FlattenedProperties with defined order and override rules #812's rules, and the strongly connected components (docs/emitter-design: recursive types and very large model sets need plan-level facts #718).pass.Validateand the plan both read it.TypeCommon.Usageleaves the IR at the next version bump, unless an external consumer needs usage in the JSON; then a final pass stamps it from the index. - B. A usage pass stores the field. This needs a sanctioned way for passes to write types (pass: give transform passes a sanctioned way to replace and remove types #966), a pass that cannot be disabled, and a re-run after every transform that changes reachability (filter, version-slice, overlay). Its advantage is that the fact stays in the encoded IR for non-Go consumers.
A has no stale data, no pass ordering and no stored duplicate, and it removes #850's quadratic by construction. B keeps usage in the JSON. Either way, ir-design §16 question 4 is settled with the measurement above.
Done when one implementation answers each fact, IsSubtype's callers stop re-walking, and the plan reads usage from one place.
Out of scope
The plan layer itself, and #812's ordering and override rules.
Related: #812, #850, #718, #67, #700, #715
Priority: P1, rule 5 (direct impact: the emitter-plan contract reads usage, which the IR never carries). jq '[.types[] | select(.usage)] | length' golden.ir.json → 0 of 41,592 (spec @911ad78, morphic @c387e25c).
Activity
OmarAlJarrah commentedon Oct 20, 2026
Once #1001 merges,
TestRegistryWrites_StayInsideTheFrameworkis gone. The same rule is the ruleguard ruleregistryWritesininternal/lintrules/rules.go, which golangci-lint runs. A pass that replaces a type still fails it outsideirandcompilers/compile, so the constraint this issue cites is unchanged; only its name moves.