Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compilers/openapi/internal/operation/streaming.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func streamCandidates(c lowering.Ctx, payload *ir.Payload) []streamCandidate {
case content.Item != nil:
out = append(out, streamCandidate{events: *content.Item, declared: true})
case c.MediaTypeStreams(content.MediaType):
// For a frame format the schema under the media type describes one
// For a frame format the schema under the media type is read as one
// frame, not the whole body — the opposite of how an ordinary content
// is read — so it is the element type as well as the content type.
out = append(out, streamCandidate{events: content.Type})
Expand Down
8 changes: 4 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ between stages. The IR itself is specified in [`ir-design.md`](./ir-design.md).
## 1. Design principles

1. **The IR is the ABI.** Compilers and emitters never see each other. A compiler's only output
is an IR document plus diagnostics; a emitter's only input is an IR document plus its own
is an IR document plus diagnostics; an emitter's only input is an IR document plus its own
options. Everything either side needs must round-trip through the IR (learned from oagen's
layering and Kiota's refiner contract — see `prior-art.md`).
2. **Lossless by default, lowered late.** The IR is designed against the *union* of all source
Expand Down Expand Up @@ -371,7 +371,7 @@ Designed, not yet built:
marker) so automated overlay-generation loops stay auditable. Two related hooks live
deliberately elsewhere: *source-document* patching (e.g. the OpenAPI Overlay spec) is a
compiler option applied before lowering — some fixes must land before naming/hoisting
heuristics consume the broken shape — and *per-target-language* naming/compat overlays are a
heuristics consume the broken shape — and *per-target-language* naming/compat overlays are an
emitter input keyed by IR ID, so one IR document drives different compat baselines per
language.

Expand Down Expand Up @@ -430,14 +430,14 @@ though both are post-milestone scope:
### 2.4 Runtime/SDK policy is a separate input

Retry, timeout, telemetry, error-class taxonomy, user-agent — the *behavioral* configuration of
generated SDKs — is a emitter input alongside the IR, not part of it. The IR describes the API;
generated SDKs — is an emitter input alongside the IR, not part of it. The IR describes the API;
policy describes the SDK. (oagen embeds both in one root; we keep the trees separate so the same
IR document can drive SDKs, docs, and mock servers without dragging SDK opinions along.)

The canonical policy-input vocabulary, taken from oagen's production `SdkBehavior` (the best
real-world enumeration available): **retry** (retryable status codes, max attempts, full backoff
strategy with jitter), **timeouts** (defaults + env override), **error taxonomy** (status →
logical exception-kind map, client/server catch-alls, doc-URL template), **telemetry** (request-
logical exception-kind and fault, doc-URL template), **telemetry** (request-
ID and client-telemetry headers), **logging** (a closed lifecycle-event list), **user-agent**
construction (identifier template, app-info enrichment), **idempotency injection** (header name,
auto-generate rules), **pagination pacing** (auto-page delay), and **request guards** (option
Expand Down
2 changes: 1 addition & 1 deletion docs/compilers/openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -2340,7 +2340,7 @@ A request body or success response with a Content whose media type the streaming
`application/jsonl`, `application/x-ndjson` and `text/event-stream` by default, matched on the
normalized key ([`media-types`](#media-types)) at every minor — lowers to `Operation.RequestStream`
and `ResponseStream` for the directions that have one, each with `Events` the Content's own `Type`
(for a frame format the schema describes one frame, not the body), `Streaming` the `client`,
(for a frame format the schema is read as one frame, not the body), `Streaming` the `client`,
`server` or `bidi` summary of them, and `Provenance.Inferred` the marker `streaming-media-type` on
the operation, joined by `,` to any other marker, since below 3.2 a media type is a guess about
framing and not a declaration. A 3.2 `itemSchema` ([`item-schema`](#item-schema)) is a declaration:
Expand Down
984 changes: 750 additions & 234 deletions docs/emitter-design.md

Large diffs are not rendered by default.

43 changes: 25 additions & 18 deletions docs/ir-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ type Naming struct {
policy, and reserved-word escaping. An emitter may read `Source` to recover the letter case of the
words `Canonical` fixes (`iOS` in `iOSVersion`), segmenting it with this section's grammar rather
than one of its own, and never to find where a word ends (emitter-design §4.12). Anonymous
(hoisted) types have empty `Source` and a `Hint`; whether a emitter inlines them or names them is
(hoisted) types have empty `Source` and a `Hint`; whether an emitter inlines them or names them is
its choice.

**`Hint` is the same word sequence in the same grammar.** It is not a presentation affordance
Expand Down Expand Up @@ -629,7 +629,7 @@ type TypeRef struct {
```

`Target` is never empty. A position that admits no type — a model with no base, a stream with no
event union — holds a `nil *TypeRef` (§4.1), so a `TypeRef` naming nothing is one a lowering left
element type — holds a `nil *TypeRef` (§4.1), so a `TypeRef` naming nothing is one a lowering left
unfilled, and `irverify` reports it (`ir/type-ref-no-target`).

Nullability lives on the reference, not the target type, because the same type is nullable in one
Expand Down Expand Up @@ -1821,7 +1821,7 @@ type ResponseConditions struct {
type ErrorCase struct { // a response too: Name/Conditions/Payload/Headers as on Response
Name Naming // for formats with named errors; Hint elsewhere (the status key, neutralized)
Conditions ResponseConditions
Payload *Payload // nil = no body; the error-flagged models are its contents' types
Payload *Payload // nil = no body; its contents' types may be of any kind
Headers []Header // error metadata fields (Retry-After, rate-limit family)
Fault string // "" | "client" | "server" — protocol-neutral fault classification
// (Smithy @error; OpenAPI 4XX/5XX is its HTTP lowering). Drives
Expand All @@ -1843,9 +1843,9 @@ Design notes:
per-protocol view.
- **All responses and all content types survive** (oagen's "primary response" collapse is the
counterexample). The plan layer picks a primary for SDK ergonomics; the IR doesn't.
- **Errors reference error models**, which `irfacts.Index.Usage` marks `UsageError` (§4.9), with
status conditions and range collapsing exactly as Kiota's error mappings (`404`, `4XX`,
catch-all).
- **An error's body is of any kind**, a model or a union, a list or a primitive, and
`irfacts.Index.Usage` marks what it reaches `UsageError` (§4.9), with status conditions and range
collapsing exactly as Kiota's error mappings (`404`, `4XX`, catch-all).
- **A status's class is the protocol's; a catch-all's is no one's.** An HTTP status code states its
own class (RFC 9110 §15): a status below 400 lowers to a `Response`, and one from 400 to an
`ErrorCase` whose `Fault` is `client` for 4xx and `server` for 5xx, since that is what the code
Expand Down Expand Up @@ -1918,11 +1918,12 @@ type LongRunning struct {
type StreamingMode string // "none" | "client" | "server" | "bidi"

type StreamDetail struct {
Events *TypeRef // stream element type when it differs from the payload content type —
// for event streams this is a WireTagged Union of event models; within an
// event model, Property.EventHeader marks frame-header members and
Events *TypeRef // stream element type, of any kind; nil where the contents that stream
// name different elements, which stay on the payload. For a format with
// event streams it is a WireTagged Union of event models: within an event
// model, Property.EventHeader marks frame-header members and
// Property.EventPayload marks a raw-payload member (Smithy
// @eventHeader/@eventPayload); per-event content types and terminal
// @eventHeader/@eventPayload), and per-event content types and terminal
// events live on Variant.Event
Initial *TypeRef // initial-request / initial-response message preceding the stream
// (Smithy event stream initial messages); nil = none
Expand All @@ -1932,10 +1933,16 @@ type StreamDetail struct {
```

Streaming mode is on the operation core because it is protocol-independent (gRPC streams, SSE,
WebSocket, Smithy event streams all project onto it); the wire mechanism (SSE vs chunked vs
WS frames vs length-prefixed events) lives in the binding. Smithy **waiters**
(`smithy.waiters#waitable`) are *not* folded into `LongRunning` — acceptor lists and JMESPath
matchers are preserved verbatim in `Operation.Unmodeled` (§15).
WebSocket, Smithy event streams all project onto it). `Streaming` summarizes an operation, and a
stream belongs to one content of its payload, so an OpenAPI operation can offer `application/json`
beside `application/x-ndjson`, and the plan elects among them (emitter-design §3.1). The wire
mechanism (SSE vs JSON Lines vs WS frames vs length-prefixed events) has no field of its own: over
HTTP the streaming content's `MediaType` states it, another binding frames its own messages, and
the plan derives which (emitter-design §3.1). `Content.Item` declares what one item is; a compiler
that reads a stream from a media type alone names the content's `Type` as `Events`, a reading
`Provenance.Inferred` marks. An element need not be a union, and `Variant.Event` applies only where
it is one. Smithy **waiters** (`smithy.waiters#waitable`) are *not* folded into `LongRunning` —
acceptor lists and JMESPath matchers are preserved verbatim in `Operation.Unmodeled` (§15).

---

Expand Down Expand Up @@ -2152,7 +2159,7 @@ member's `Provenance` at its defining occurrence; the occurrence list goes to
`Unmodeled["graphql:extends"]` when SDL round-trip fidelity is wanted.

Arbitrary client-composed selection sets are out of scope by design: Morphic generates SDK
surface, and the full type graph — including per-field arguments — is retained so a emitter can
surface, and the full type graph — including per-field arguments — is retained so an emitter can
still offer query builders.

### 8.5 Erlang/OTP
Expand Down Expand Up @@ -2882,9 +2889,9 @@ the IR and preserve nothing.
`ir.Decimal` without materializing it (§6).
2. **Dedup aliasing mechanics** — alias table on Document vs ID rewriting with a redirect map;
leaning alias table (IDs immutable, always).
3. **Content negotiation in the plan layer** — the IR keeps all media types; the default
primary-selection policy (json > form > multipart > binary?) belongs to emitters but should be
shared as a standard plan helper.
3. **Content negotiation in the plan layer** — settled: the IR keeps all media types, and one
shared plan helper elects among them for a success, a failure and a stream alike, by a ranking
the plan's policy holds (emitter-design §3.1).
4. **Whether `UsageFlags` is stored or always recomputed** — settled: derived by `ir/irfacts`,
not stored (§4.9). On the golden spec (`cloudflare/api-schemas` at `843eae2f`) deriving usage
for every type, the subtype index and the type graph included, takes about 50 ms of a compile
Expand Down
6 changes: 3 additions & 3 deletions docs/prior-art.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ and the differ/compat/verify subsystems added since the original survey).
| Inline-schema naming logic duplicated across ≥6 call sites that must stay byte-identical — worse, *inferred discriminator mappings are name strings* that must reproduce the hoisting pass's naming byte-for-byte across files or they dangle | One hoisting pass, keyed by source-pointer identity; `Discriminator.Mapping` points at IDs, severing the naming coupling |
| Reference nodes bake in the *target's* kind (`enum` vs `model` ref), so no reference can be built without global knowledge of all targets — the root cause of the two-pass module-global state | One `TypeRef{Target TypeID}`; kind lives on the registry entry |
| `allOf` eagerly flattened to a field list — the inheritance relationship is lost | Composition preserved un-lowered (base + mixin provenance kept) |
| oneOf merged as *optional fields* at top-level-schema and request-body positions — even when explicitly discriminated, for argument-spreading ergonomics (variant `required` discarded); only property-level unions survive as union nodes | Unions always survive as union nodes; ergonomic flattening is a emitter plan decision |
| oneOf merged as *optional fields* at top-level-schema and request-body positions — even when explicitly discriminated, for argument-spreading ergonomics (variant `required` discarded); only property-level unions survive as union nodes | Unions always survive as union nodes; ergonomic flattening is an emitter plan decision |
| Pagination/envelope detection via hardcoded name lists (with a fabricated `after` cursor param when none exists); single-resource envelope unwrapping applied *destructively* — the wrapper key vanishes from the IR with no recorded path | Heuristics are injectable compiler policy, never IR semantics; unwrap decisions are recorded as `PropPath`s, never applied to the stored shape |
| Discriminator *inference* is structural (const-property across variants — sound), but its output is welded to derived names (see above); name lists appear as tie-break preference order | Structural inference marked `Inferred`; output keyed by ID |
| Failures are `console.warn`; unresolvable model refs left dangling with a warning; unrecognized schema shapes degrade to `unknown` — silently when anonymous | Typed diagnostics with severity + provenance, collected on the IR document |
| Single "primary response" privileged (all 2xx kept, but inline-model extraction and pagination classification follow whichever iterates last); media-type multiplicity collapsed by fixed priority | All responses and all content types are first-class |
| Mutable module-global parser state (non-reentrant; concurrent parses race) | Compilers are pure `(source, options) → (IR, diagnostics)` |
| Collision-cascade operation renaming: same-named ops renamed in place by appending path context — adding one endpoint can rename *existing* SDK methods | Names are presentation; identity is the ID; naming collisions are a emitter policy concern |
| Collision-cascade operation renaming: same-named ops renamed in place by appending path context — adding one endpoint can rename *existing* SDK methods | Names are presentation; identity is the ID; naming collisions are an emitter policy concern |
| Name sanitization accretes hardcoded domain word-lists (acronym sets, `Json`-fork collapse passes doing registry surgery via string rewriting) | Acronym/cleanup policy is injectable per-compiler; the registry is never edited by string rewrite |
| Catch-all `additionalProperties` smuggled as a magic-named synthetic field (collides with a real property of that name); `patternProperties` truncated to the first pattern | `AdditionalProps{Value, Key, Patterns}` is its own channel |
| `$ref`-site vs ref-target annotation merging patched ad-hoc for parameters only; model fields typed by `$ref` still lose target defaults | One documented precedence rule (use-site overrides target), applied uniformly in the compiler |
Expand Down Expand Up @@ -132,7 +132,7 @@ refiners (in-place tree mutations) → per-language writers`. The CodeDOM is a *
| Kiota behavior | Morphic decision |
|---|---|
| Children keyed by *name* in each block → elaborate collision-reconciliation logic | ID-keyed containers; name uniqueness is a validation, not an identity mechanism |
| Unions flattened early into `MemberN`-named wrapper conventions; unresolvable shapes fall back to opaque `UntypedNode` | Unions keep variant identity, tag mode, and open/closed semantics until a emitter lowers them |
| Unions flattened early into `MemberN`-named wrapper conventions; unresolvable shapes fall back to opaque `UntypedNode` | Unions keep variant identity, tag mode, and open/closed semantics until an emitter lowers them |
| `allOf` supports only one true base; extra entries silently intersection-merged | Base + explicit mixin list, merge deferred |
| Enums are string-backed only; no numeric values, no open/closed distinction | Enums carry a value type, member values, and an explicit open/closed bit |
| Collections are flags on the type reference (`CollectionKind`), maps are not a distinct type | List/Map/Tuple are proper type nodes |
Expand Down
8 changes: 4 additions & 4 deletions docs/reference-learnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Every template-based repo computes imports by **mutation or text-grep**, and eve
import-bug class: openapi-generator's `addImport` mutates `model.imports` during lowering (dead +
missing imports recurring); fastapi greps the rendered string with `IDENTIFIER_PATTERN` to drop
unused imports; openapi-python-client threads mutable import sets via `object.__setattr__`.
**CONFIRMS** that import computation is a emitter concern, and **challenges** any accumulator design.
**CONFIRMS** that import computation is an emitter concern, and **challenges** any accumulator design.
**Recommendation:** Morphic's flat ID-keyed registry (invariant #3) makes the import set a *pure
reachability walk* over `TypeRef` targets per output file — compute it as a derived function in
refine, and **track symbol references structurally** as you emit each reference (oagen/TypeSpec do
Expand Down Expand Up @@ -138,7 +138,7 @@ wrapper). Streaming stays on the operation core (`StreamDetail`/`Variant.Event`)
streaming concept is the counterexample. *Superseded in part:* `pathParamsInOptions` is not a plan
predicate — how a target passes its inputs varies by language, so emitter-design §3.6 makes the shape
each target's (§4.13) — and `isArrayResponse` is not a shape of its own: an unpaginated array is a
`value` of `ReturnShape`, decoded as any type is, since the typed targets decode a list whole
`value` of `BodyShape`, decoded as any type is, since the typed targets decode a list whole
(emitter-design §3.1).

## A6. Naming / casing / reserved words: neutral in IR, rendered in refine — STRONGLY CONFIRMS #3/#4
Expand Down Expand Up @@ -243,7 +243,7 @@ lacks (the strongest validation the oagen audit produced).
cautionary middle ground. **Ship Morphic's per-kind-struct + generated switch-completeness test in
milestone 1** — ogen's panics are exactly the runtime failure the test prevents. Also ship
`walkTypeRef`/`mapTypeRef` generic-traversal helpers (oagen leans on them constantly).
- **Recursion is a emitter problem, solved at emit.** Morphic's ID-referenced registry makes the IR
- **Recursion is an emitter problem, solved at emit.** Morphic's ID-referenced registry makes the IR
cycle-free (invariant #3) — vindicated against ogen's whole recursion module (`reflect.DeepEqual`
cycle finder + hard errors on required recursive fields), datamodel's SCC cross-module relocation,
and openapi-python's two-phase fixpoint. But the Go **emitter still must break emitted-output cycles
Expand Down Expand Up @@ -295,7 +295,7 @@ discriminator-participant → `Base`; other $refs → `Mixins`; inline → `Prop
**never merge**; keep `FlattenedProperties()` computed, never stored (openapi-generator's
`allVars`-beside-`vars` is the storage smell). TypeSpec independently validates the split with distinct
`sourceModels[].usage: "is"|"spread"|"intersection"` provenance — **consider a `MixinKind`/provenance
tag on each `Mixins` entry** so a emitter can tell structural-copy (`is`/spread) from compose-by-
tag on each `Mixins` entry** so an emitter can tell structural-copy (`is`/spread) from compose-by-
reference. Harvest the merge algorithms as **emitter-refiner** material (below). Any convenience
simplification lives in `pass/` (composable, order-explicit, default OFF, provenance-recording), never
a default compiler mutation.
Expand Down
Loading
Loading