Skip to content

feat(ir)!: say what unions, literals and catch-alls admit - #1361

Open
OmarAlJarrah wants to merge 10 commits into
fix/ir-flattened-secretfrom
feat/ir-type-node-facts
Open

OmarAlJarrah wants to merge 10 commits into
fix/ir-flattened-secretfrom
feat/ir-type-node-facts

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Summary

Four facts a source states about a type node had no field in the IR. In two of them the IR said two things at once.

  • Open unions (ir: a union can't be marked open #664). Union gains Closed, as Enum has it. A JSON Schema oneOf or anyOf admits only what matches a listed branch, so every union the OpenAPI compiler lowers is closed. A Smithy union, a protobuf oneof and a GraphQL output union are open: a consumer must represent a variant it does not know. docs/ir/unions.md#open-unions states the default for each format, and the planned lowerings follow it. The capability matrix gains an open-unions row. The OpenAPI compiler gains a promotion target, union.open, read by presence as enum.open is. It has no default key, because no spelling has become common. testdata/conformance/openapi/matrix.md therefore records why no corpus spec can witness the row, and names the test that holds the promotion instead.
  • Input-only unions (spike: evaluate first-class IR representation for input-only unions and non-finite numeric values #25, case 1). Union gains InputOnly, as Model has it. A GraphQL @oneOf input object is the source: an emitter projecting separate input and output types reads the flag instead of a format-named marker.
  • Non-finite values (spike: evaluate first-class IR representation for input-only unions and non-finite numeric values #25, case 2). ValueKind gains inf, neginf and nan, each with no payload. A protobuf [default = inf] was a construct the IR could not hold, and every target with an IEEE float has a constant for each value. They are kinds rather than a flag on number, so a consumer switching on Kind meets them as kinds to handle, never as a number with an empty Num. BigVal stays a finite JSON number, and irverify's payload rules hold the new kinds with no rule of their own.
  • Literal value types (ir: a Literal carries no value type or format #668). Literal gains ValueType, read as enumValueType reads an enum's: the type and format the value fits, else the type, else the value's own kind. {type: integer, const: 80.0} is integer and {type: number, const: 80} is number; they were one Value. A date const is date. A type or format the value fits no longer draws an info report or an Unmodeled entry. One it does not fit still draws both, and so does a type set naming several types. irverify holds a ValueType to the declared kinds (ir/unknown-prim-kind at the field).
  • A closed rest beside patterns (ir: a closed object with patternProperties also carries a catch-all typed as the top type #987). AdditionalProps.Value becomes *TypeRef. patternProperties beside additionalProperties: false used to type every key no pattern matches as the top type, while Additional said no such key was allowed. That catch-all now holds the patterns and no value. docs/ir/models.md#catch-alls states the three shapes. irverify reports the two that would say two things: a catch-all typing no key (ir/catch-all-empty), and a closed model's catch-all with a value (ir/catch-all-closed).

On the golden spec (spec @319dc756), against this PR's base, the only change is the two new members on its 974 unions. The diagnostics are identical. The spec writes no const and no patternProperties: its 195 literals are null or list members of an enum lowered as a union. That is the rule-3 ranking these issues carry.

Closes #664
Closes #668
Closes #987
Closes #25

Breaking

IRVersion moves to 0.31.0, the generation after main's 0.30.0 (#1327). The branch's earlier commits name 0.28.0 and 0.29.0, from before #1325, #1333 and #1327 landed; its merges of main renumbered it. A consumer pinned to 0.30.0 refuses the document for its unknown members, closed and inputOnly on every union and valueType on a literal. One that decodes AdditionalProps.value as required fails on a closed pattern object, where the member is absent. In Go, AdditionalProps.Value is a *TypeRef. An open PR claiming the same version moves its own constant on if it merges after this one.

Stack

Base: #1355, on #1354, #1351 and the emitter-design stack. main (with #1229) is merged down the whole stack, so this diff is this PR's alone.

Test plan

  • go test on the touched packages: ir, ir/irverify, ir/irfacts, compilers/openapi/internal/{schema,lowering,merge}, compilers/openapi/e2e, internal/conformance, cmd/morphic, cmd/morphic-census. The goldens, the shape pin and the census were regenerated with -update.
  • New tests: schema.TestLiteral_ValueTypeReadsTheTypeAndFormatItsValueFits (13 rows: fit, misfit, untyped, several types, composite, null); schema.TestObject_CatchAllBesideAnotherFactStaysModel gains the two pattern rows; irverify.TestCheckCatchAlls_ReportsACatchAllSayingTwoThings; irverify.TestVerify_ALiteralsUndeclaredValueTypeIsAViolation; lowering.TestPromoterUnionOpenness_OpensOnAMappedKey. The maps conformance case gains ClosedPatternMap.
  • Probes. The issue's three schemas compiled: integer, number, date, with no diagnostic. A misfit type: integer, const: x keeps type and reports it, and type: object beside an object const keeps nothing. The golden spec was compiled at the base and at this head, and the type and diagnostic differential is as stated above.
  • Push checklist: subject within 72 characters with the number, no artifact hits, no conflict with main or the base, skills_lint clean.
  • After main gained fix!: type the local time formats and keep time's offset #1325, the whole stack was merged forward and every touched package rerun: ir/..., compilers/..., cmd/..., engine/..., pass/..., internal/....
  • CI.

Four type-node facts a source states had no field, and two of them made
the IR say two things at once.

- Union gains Closed and InputOnly. A JSON Schema oneOf or anyOf is
  closed, while a Smithy union, a protobuf oneof and a GraphQL output
  union are open; a GraphQL @OneOf input object is input-only, as an
  input object is. Every union the OpenAPI compiler lowers is closed,
  and the new promotion target union.open opens one under a mapping
  that names a key. No default key names it.
- Literal gains ValueType, read as an enum's is. 80 declared an integer
  and 80 declared a number were one Value, and a date const was a
  string, with the type or format kept under Unmodeled with an info
  report. Both now reach the field where the value fits them.
- AdditionalProps.Value becomes optional. additionalProperties: false
  beside patternProperties typed the keys no pattern matches as the top
  type while Additional said none was allowed; such a catch-all now
  holds the patterns and no value. irverify reports a catch-all typing
  no key (ir/catch-all-empty) and a closed model's catch-all with a
  value (ir/catch-all-closed).
- ValueKind gains inf, neginf and nan, so a protobuf default of inf is
  kept rather than dropped. BigVal stays a finite JSON number.

BREAKING CHANGE: IRVersion moves to 0.28.0. A consumer pinned to 0.27.0
refuses the new document for its unknown members. AdditionalProps.Value
is now a *TypeRef in Go, and a decoder reading it as required fails on
a closed pattern object's catch-all.
TestRegistryChecks_ListsEveryRegistryCheck holds the hand list to every
function shaped like a registry check, and checkCatchAlls is one.
A zero Union now writes closed and inputOnly, which the zero-shape
contract lists. The comments on checkPrimKinds and the elected-lowering
census test had grown past the 100-word cap.
…at/ir-type-node-facts

# Conflicts:
#	docs/ir/version-history.md
#	ir/irversion_test.go
OmarAlJarrah added a commit that referenced this pull request Nov 2, 2026
#1325 took 0.28.0 on main and #1361 moved to 0.29.0, so this branch's
generation is 0.30.0: the constant, its generations row with the shape
recomputed, and its version-history entry.
The exhaustive linter holds every switch over ir.ValueKind to the whole
enum. The literal type check in the census and the scalar renderer test
now name inf, neginf and nan, and the census answers that no declared type
holds a kind ir does not declare, which keeps the type keyword verbatim
rather than claiming a home for it.
…at/ir-type-node-facts

# Conflicts:
#	docs/ir/version-history.md
#	ir/document.go
#	ir/irversion_test.go
The planted exhaustive finding is meant to name ir.ValueCtor alone; the
switch now lists inf, neginf and nan, the kinds ir.ValueKind gained.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant