Skip to content

ir: decide whether derived facts (usage, subtypes, flattened properties) live in one per-document index #950

Closed

Description

@OmarAlJarrah

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 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

  1. self-assigned this
    on Oct 19, 2026
  2. OmarAlJarrah commented on Oct 20, 2026

    @OmarAlJarrah
    MemberAuthor

    Once #1001 merges, TestRegistryWrites_StayInsideTheFramework is gone. The same rule is the ruleguard rule registryWrites in internal/lintrules/rules.go, which golangci-lint runs. A pass that replaces a type still fails it outside ir and compilers/compile, so the constraint this issue cites is unchanged; only its name moves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

priority:P1Core first-emitter feature on mainstream specstype:featureNew capability or enhancement

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions