Repository navigation
fix(compilers/openapi): judge union branches against the body - #1406
Conversation
A oneOf or anyOf written beside a body is kept as validation logic when no branch declares shape the body lacks, and degrades otherwise. The verdict differed between spellings of one meaning: a branch repeating the body's type was held to a closed keyword list, so `minProperties`, `$comment` or a repeated `description` beside it degraded the union while the bare branch did not; a property the body composes through allOf was never found; and a bare branch writing only `items`, `prefixItems` or `$dynamicRef` was read as validation logic though each names an element or a referent. Each keyword a branch writes is now judged once, whether the branch writes it alone or beside the body's type. Keywords that document, name or only narrow declare no shape. `type`, `format`, `enum`, `const` and `items` declare none where the body's admitted values, read by the same admission reading allOf refinement uses, hold to them. A branch's property restates one of the body's declarations of it, own or composed through allOf and $refs. Every other keyword declares shape. Two schemas written alike now count as the same only when every $ref in them reads against one base (`Table.Base`). Under another $id, the same spelling names another schema; allOf refinement dropped such a conjunct in silence, and a union branch would have read as a repeat.
every-reader's search named GetAllOf() alone and its probe was the multipart body #998 found, so a change to how another keyword is read had no search to run and no probe that reached it. The search is now the changed keyword's getter or key outside the changed package, and the probe compiles the old and new spelling at every position a schema is written, the multipart body kept as the allOf instance.
Splitting the placement check from the base check left the return for a schema the bindings do not place untested. One condition keeps the guard without a statement no input reaches.
A branch's property is compared with one of the body's declarations at a time, and a properties it nests only as written alike. Both limits now name their tracking issue at the code and in the rule's Status.
$defs holds schemas a branch does not apply rather than naming it, and a schema copied under another base may name another schema rather than always doing so.
The rule searches a keyword's getter and its key, but its Why gave evidence only for the probe, so nothing said why the key half matters: `oneOf` is read by key in three packages where `GetOneOf()` appears in none (#1397). The pathspec now names the excluded package from `compilers/openapi/internal`. #1406 carries the same rule for the same issue. The wording is now the same in both, so they merge cleanly in either order.
A union branch's property was compared with one of the body's declarations at a time, and a `properties` it nests only as written alike, so a property repeating a `type` one declaration writes and an `enum` another writes, or a nested property the body declares with a bound beside it, degraded the union. An `items` was compared through admission's element reading, which keeps one declaration's element and counts a bound in it as saying more. A schema a branch writes for a property or an element is now held to what all of the body's declarations of it admit together, and its own `properties` and `items` to what those declarations declare in turn, judged off one stack within admission's budget. A `$ref` restates where it lands with one of the declarations. The keywords beside a `$ref` conjoin with it at every version, as ref-use-site reads them, so the rule no longer requires a branch and a declaration to agree on writing one.
Inline allOf branches fold what they write for the position when they agree, and agreement compared rendered JSON alone. Under an $id naming another URI, a branch's `$ref` spelled like the position's names another schema, so two such `not`s folded into one entry read at the position and the other constraint was lost. A lone writer under such an $id folded its keyword onto the position the same way. A branch whose site reads its `$ref`s against another base than the position does now folds nothing and joins no agreement: it is kept whole with a report saying why, in a composition and in a refinement.
Nested properties and items are now compared entry by entry, so no row needed the written-alike shortcut: one declaration whose catch-all and readOnly nothing compares, written alike, now holds it.
A field redeclared across allOf conjuncts folds its declarations, and two of them writing an Unmodeled entry alike, or hoisting nodes equal but for where they were declared, were read as one. Under an $id naming another URI, the same `$ref` spelling names another schema, so the second declaration's constraint was dropped with no report and no copy kept. Merger now takes Alike, which the schema package answers from the bindings: an entry or a copy two declarations give is one only where both read their $refs against one base. Otherwise the later one is kept and reported as one given differently, and two hoisted copies overlap as two types declared apart.
Two doc comments said items always declares shape and that a keyword outside the vocabulary test's list declares none whatever it holds; items and properties now restate what the body declares of them.
Every inline allOf branch looked up two bases, each walking the document from its root, so a spine of allOfs nested d levels cost d² in lookups. Only an $id on the branch or a wrapper holding it can give it another base, so a branch under none now looks nothing up. A union verdict read the body's composition and every declaration below it whether or not a branch needed them. That cost time and, worse, budget: a large body could exhaust the read budget on declarations no branch named, turning a union of narrowing branches into one declaring shape. A branch writing only shapeless keywords now leaves the body unread, and the declarations of what a schema nests are read only where it nests something. sameText compares the remembered texts before looking up two bases, which only a pair written alike needs.
A branch was checked against every wrapper writing an $id, which grows with wrappers times branches in a wide allOf. The wrappers holding it are the prefixes of its pointer, at most the flattening depth, so each is looked up in a set instead.
The rule's rewrite for finding every reader of a keyword is carried by the skills change that closes its issue, word for word, so this branch keeps only the compiler change.
A union's branches share one read budget, and each branch restating a property re-read the body's declarations of it. A union of 257 branches each narrowing the body's one property therefore read past the budget and was judged a degraded lowering, while the same union with fewer branches was validation logic: the verdict turned on how many branches there were, not on what they say. What the body declares of a property or element is now a place, which keeps what its declarations admit together and what they declare of what they nest the first time a claim asks, so every branch restating it shares one reading. A property's place is gathered from the body's schemas only when a branch names it, rather than indexing every property the body declares, which also makes a verdict cheaper than before. The budget still bounds the union's reading as a whole, so branches restating many distinct places still share it, as the rule text now says. No real document reaches it: with both admission budgets lifted, 1,840 public OpenAPI 3 documents and the Cloudflare schema compile to the same IR, bar two cyclic allOf fixtures whose walk the budget ends.
Reading each place in the body once stops repeated restatements from spending the read budget, but what each branch writes is still read within the budgets the union's branches share: an enum or const a branch writes counts its members, so two branches each restating a property with an enum of 2,049 members read past the member budget, where either alone does not. The rule text and the reading's comment said only that the body is read once, which read as if the number of branches never mattered. Both now say what the branches spend, and two rows of the budget test hold the pair. Reading a branch's own value set is linear in what it writes, and with both budgets lifted no real document compiles differently, so the shared budget stays.
|
This change also fixes a spelling dependence the restated-branch check has on On openapi: 3.1.0
info: {title: t, version: v}
paths: {}
components:
schemas:
S:
type: object
properties:
a: <body>
b: {type: integer}
oneOf:
- properties:
a: <branch>
required: [a]
- required: [b]
Each cell is the reason on the one
|
A union branch restating the body was still judged by how it spelled two
things. An allOf in a branch, or in a property it writes, always declared
shape, so {allOf: [{required: [a]}]} degraded a union that
{required: [a]} left as validation logic. And a branch's $ref restated a
declaration only when the declaration itself wrote the same $ref, so a
property declared {allOf: [{$ref: X}], description: d}, the usual way to
describe a reference, was not restated by {$ref: X}, nor one declared
through a schema that composes X.
An allOf now declares no shape where each conjunct declares none, judged
as the schema writing it is, and a $ref restates where it lands on any
schema the declarations compose through allOf conjuncts and $refs, which
the walk gathering them already reaches. A $ref conjunct of the branch
itself still declares shape, as a $ref branch does. A claim now carries
the place it restates rather than copies of its declarations, which are
read when a key nests something, so the laziness no longer needs its own
check.
On the golden spec this moves one union, the anyOf requiring at least
one of an application's hostnames or IP subnets, from degraded_lowering
to validation_only: each branch conjoins the property's own $ref with
bounds on it. #1278's NotBesideAllOf row, a branch whose allOf repeats
the body's property, moves to the restating table.
Four places hold two schemas written alike to one base: the text allOf
refinement compares, the inline allOf branches that fold, and the
Unmodeled entries and hoisted copies a redeclared field folds. A base
changes only what a $ref or $dynamicRef names, yet each check fired on
any text under an $id naming another URI. So a branch
{$id: ..., minLength: 1} kept its bound out of Constraints, a branch
writing a $ref only in its properties kept its description from folding,
a refinement whose target sat under another $id kept a branch that
restates it, and two declarations of a field written alike, such as
{type: string, not: {const: q}}, were reported as given differently and
one kept as a conflicting redeclaration. Each lowered as written before
the base checks.
annotation.WritesReference reports whether rendered JSON names a $ref or
$dynamicRef member at any depth, and each site compares bases only where
it does: sameText for the text, a branch for what it folds, an Unmodeled
entry for its value, and two hoisted copies for what they keep under
Unmodeled. sameText still requires one document, as it did before the
base check: a copy in another document is read apart, since its dialect
may differ once a document is read in its own (#839).
A field declared once as a $ref to a schema and once inline as that schema's text is one type where the two are alike: the schema declared elsewhere wins and the inline copy gives way, with nothing kept. Under an $id at the copy, or at the resource holding the schema, a $ref the two spell alike names two schemas, so the copy's constraint was dropped with no report. The base check covered only two inline copies. copies now asks Alike of every pair at least one of which is a copy, and of the sites where the two nodes are written rather than the declarations naming them, since a schema a $ref reaches is read where it is written. A copy keeping a reference read against another base is kept and reported as one declared apart. A node in another document is never read against the model's base, so a source schema at the same pointer cannot stand in for it.
Three readings had no test that fails when they change. A property's element restates what the declarations of that element say together, and keeping only the first declaration of it left every row green; the new row writes the element's type in one declaration and its format in another. bind.Table.Base answers that a base is unknown past the merge bound, and answering it known everywhere left every row green; the test now reads a schema past the bound and one within it. sameText, readsAtPosition and the merge's Alike each take an unknown base for no match, and ignoring that left every row green; a schema merging past the bound now holds each of them to it.
text-decides-under-one-base told a comparison to trust text written
alike only under one base. A base changes only what a $ref or
$dynamicRef names, so text writing neither says the same under any, and
a check held to the rule as written kept {$id: ..., minLength: 1}'s
bound out of Constraints. The rule now holds only text writing a
reference to one base, names annotation.WritesReference as what tells
them apart, and its Probe runs the spec with the $ref deleted.
compare-raw-nodes said #1408 tracks sameText's base check, which this
branch builds and closes, so the parenthetical goes.
The parser-library entry for unmodeled-keywords named the union branch reader in a clause that left unclear whose keys it reads: those a union branch writes, and those of each schema the branch writes for a property or an element.
The Why of `distinction-where-nothing-depends` said every spec testing #1406's `$id` base checks wrote a `$ref`. The tests #1406 added with its fix write none, so the sentence was false of the PR as it merges. Say instead that every spec wrote one while the checks fired on any text under another `$id`.
Deleting what a check depends on shows the check firing where nothing depends on it, but not a check that looks for the dependency anywhere in its input rather than in what it compares. Both forms show a defect at #1406's a4dda68: a type lost the `minLength` of an inline `allOf` branch writing no `$ref`, and the `description` of one whose only `$ref` sat in `properties`, though a description reads against no base. Ask for the second form too, the dependent moved outside what the check compares, and give the rule both symptoms as its evidence.
Three conditionals the branch adds ran across lines, against the styleguide's rule 1.8: copies in typejudge.go, and branchDeclaresShape and shapeless in branchshape.go. Each now names its operand or fits one line. Four table tests the branch adds or extends branched inside their loop, against rule 6.2. The merge test of an Unmodeled entry under another base asserts each row's keys and report count; the rebased-branch test asserts the folded not of every row, empty where none folds; the keyword-vocabulary test drops the type row before the loop and reads the expected reason from the declared list; and the component-copy test lists its four cases as rows, losing a struct field it never read.
A union beside a composition the reading bounds cut short degrades even where each branch restates only what the body itself writes, which the earlier check read without the composition (#1423). Comparing a nested schema's text renders it again at each level, so restating one many levels deep costs about the cube of its depth (#1424). The union-beside-structure rule's status and the two places in branchshape.go that spend the budget and render the text now say so.
bind.Table.Base walks from its document's root on every call, so the readers that ask it at each level of a deep schema pay the square of the depth, bounded by the nesting bound (#1431). Its comment now says so, where a new caller reads it.
A Why could pin a count only to an issue, a merged PR or a revision, but nothing held a universal to the same, so "every spec testing #1406's checks wrote a `$ref`" went false as #1406 moved (#1425). The curating rule now pins any claim about a PR that way, and the claim sweep refuses one resting on an open PR's head. The two #1406 Whys this branch added now name a4dda68, and `claims.md` no longer says what the open #1235 removes. The neighbours review found rules by the act they govern, so a rule probing `$id` base checks was never read against `text-decides-under-one-base`, which governs the same construct (#1426). It now greps for the constructs an edited rule names too, and `posted-findings` holds a suggestion to both. A refusal's findings are held to `SourceTable`, which names no document a `$ref` read, and only `compilers.Detection.Compile` checks that, so a refusal made after the load phase can pass every compiler-level test and fail in the engine (#1432). A new rule, `refusal-findings-in-its-table`, says how to probe it. The bound rule points at it and now covers bounds in lowering, which is where the refusal that showed this sits, and the openapi skill lists it. The curating SKILL.md stays at 500 words through four rewordings that keep their meaning.
The base checks held any text writing a $ref to one base, though an
absolute $ref names the same schema wherever it is read. So a branch
under another $id writing only `not: {$ref: 'https://ex.test/b/n.json'}`
folded nothing, losing its description from the position, and two
declarations writing that $ref alike were reported as conflicting, where
each lowered as written before the base checks.
WritesReference now counts a $ref or $dynamicRef only where its value is
no absolute URI, and a value that is no string still counts. Absolute is
the resolver's own test, moved from bind to fragment.Absolute, so the
check and the resolution cannot part. The rule text in allof-refinement,
allof-inline-residue and allof-redeclared-property, the
text-decides-under-one-base skill rule and the comments say a relative
$ref, and each site gains a row writing an absolute one.
Deleting or moving what a check depends on misses a check that counts the dependent however it is spelled, though one spelling does not depend on the property: an absolute `$ref` names one schema under any base. At #1406's 1855a63, where both earlier forms matched `main`, an inline `allOf` branch under another `$id` whose `not` wrote an absolute `$ref` still went to `Unmodeled` with its `description`; f5db471 matches `main` there too. Ask for that third form, and say text depends on a base only through a relative reference. The heading said nothing depends on what the check compares, where the rule means that what it compares ignores the property; the heading and the pointer in `code-before-claims` now say so. The claim sweep's command also reads a PR's head, as #1425 proposed.
The union-beside-structure rule judged format: password in a branch as declaring no shape, since a redaction format names no form, and an x-sensitive asking the same as an extension, but said neither, while it says format: date declares shape. Point 2 now states it, citing the password rule that makes redaction an annotation, and notes that an SDK reads no entry under either reason, so a secret one branch alone writes is lost to it either way. Two rows of the restating-branch table pin both spellings.
Since an absolute $ref reads alike under any base, a branch is rebased only where what it folds writes a relative one; the comments on allOfComposition.rebased and unfoldedWhy still said any $ref. The shapeless-union test named every position a schema is written, where it compiles five; its comment names those five. The verdict holds at a component, a header, an additionalProperties value, a prefixItems element, a parameter's content, a callback and a webhook too.
Summary
A
oneOforanyOfwritten beside a structural body is kept undervalidation_onlywhen no branch declares shape the body lacks, and is a degraded lowering otherwise (docs/compilers/openapi.md#union-beside-structure). The verdict followed the spelling rather than the meaning:typewas held to a closed list of keywords, sominProperties,$comment,deprecated, anx-key, or adescriptionwritten as the body writes it degraded the union, while the same branch without thetypedid not (openapi: a restated union branch writing a keyword that declares no shape degrades the union #1394).allOfwas never found, so a branch restating it degraded the union (openapi: a branch restating a property the body composes through allOf degrades the union #1395).items,prefixItemsor$dynamicRefwas read as validation logic, though each names an element type or a referent the structural picture then lacks (openapi: a union branch writing only items, prefixItems or $dynamicRef is kept as validation logic #1393).readOnly,writeOnly,default,xml,discriminator,formatorunevaluatedProperties: falsewas read as validation logic too, while the same keyword beside a repeatedtypedeclared shape. Each sets something the IR holds structurally, sovalidation_onlyclaimed a complete picture without it; both spellings now declare shape.propertiesit nests only as written alike, so repeating what two declarations say together, or a nested property the body declares with a bound beside it, degraded the union (openapi: a branch property restating several declarations, or a nested property, degrades the union #1407).allOfin a branch always declared shape, and a branch's$refrestated a declaration only where the declaration wrote the same$ref, so{allOf: [{required: [a]}]}degraded a union{required: [a]}did not, and{$ref: X}did not restate a property declared{allOf: [{$ref: X}], description: d}, the usual way to describe a reference.These come from one mechanism: a bare branch and a restating one were judged by two different predicates, each comparing less than the body says. This change replaces both with one, so a keyword gets the same verdict whether or not the branch also repeats the body.
How a branch is judged
compilers/openapi/internal/schema/branchshape.goreads each key a branch writes:$refbranch declares shape and a boolean one does not.required, the value constraints and collection bounds, and the validation-only keywords ofdocs/ir/boundaries.md#validation-only-constructs, anunevaluatedPropertiesother thanfalseamong them.type,format,enumandconstdeclare none where every value the body admits holds to them. What the body admits is read byadmission, the readerallOfrefinement already uses, over the body's own keywords and itsallOfacross$refs, leaving out the body's own unions.propertiesanditemsdeclare none where each property, and the element, restates the body's declarations of it: in its ownpropertiesoritems, and anywhere itsallOfcomposes (admission.declarations). A schema restates its declarations when it is a boolean, when one of them is written alike, when together they admit nothing, or when each key it writes says nothing they do not say together: a shapeless key, atype,format,enumorconstheld by what they admit together, a$reflanding on a schema they compose throughallOfconjuncts and$refs, and its ownpropertiesanditemsrestating what those declarations declare in turn. Claims are judged off one stack over the branch's own tree, which the load phase bounds with the source (docs/compilers/openapi.md#source-budgets), and what each restates is read within admission's budget, so no recursion is added.allOfdeclares none where each conjunct declares none, judged as the schema writing it is. A$refconjunct of the branch itself declares shape, as a$refbranch does.prefixItems,additionalProperties,patternProperties,unevaluatedProperties: false, a nested union,$dynamicRef,discriminator,readOnly,writeOnly,default,xml, the content vocabulary,$id,$schema,$vocabulary, and keywords of no vocabulary.enum's members included: two branches each restating a property with a 2,049-memberenumread past the member budget where either alone does not, which two rows ofTestOneOf_CoDeclaredUnionReadPastItsBudgetDeclaresShapehold. That reading is linear in what the branches write, so the budget stays shared.This replaces #1278's lexical comparison (
restatesBody,restatesPropertyand their two closed lists), so the schema package no longer importsymland itsinternal/archtestentry drops it. It also drops #1278's rule that a branch's property and the body's declaration agree on writing a$ref, which rested on OpenAPI 3.0 setting aside what stands beside one. This compiler reads those keywords the same way in 3.0 as in 3.1 (docs/compilers/openapi.md#ref-use-site), so they conjoin with the$refhere too.Departures from what the issues drafted, each for a reason given:
minLengthbeside a repeatedtypefrom reading as validation logic, yet expectsminPropertiesthere to read as it. Both are bounds the IR holds on a type, and inside a branch each holds only in that alternative, asrequireddoes, so both narrow, in the branch and in a property it declares alike.readOnly,writeOnlyanddefaultstill declare shape, as its Direction asks, andconstandenumdo unless the body already says them.allOfmerge settles, which is known only once every type has lowered. A branch's property is instead held to what all of the body's declarations admit together, read from the source. A branch repeating the field the merge settles on repeats that too, since the field is one of those declarations or their meet, so this departs from the draft only where the draft would degrade a branch that says nothing new.itemsas narrowing would have turned{type: array, items: …}beside an array body from degraded tovalidation_only. So openapi: a union branch writing only items, prefixItems or $dynamicRef is kept as validation logic #1393 is closed here.itemsis now a nested restatement likeproperties, and a bound in a branch's element narrows.Comparing a schema against one base
Two schemas written alike were treated as the same when they sat in one document. Under an
$idnaming another URI, the same relative$refnames another schema. A base changes only what a relative$refor$dynamicRefnames, so each site below compares bases only where the text it compares writes one (annotation.WritesReference), and text writing none, or only absolute ones, says the same as onmain. Whether a reference is absolute is the resolver's own test,fragment.Absolute, moved there frombindso the two cannot part. Five sites made that comparison:admission.sameText, whichallOfrefinement uses: a property{allOf: [{$ref: names.json}, {type: array, items: {$ref: item.json}}]}under another base kept nothing of its second branch, while the same branch with the$refspelled absolutely was kept. The new restatement would have read such a branch as a repeat too.sameTextnow also requires text writing a$refto read against one base, which the newbind.Table.Basereports. It still requires one document, as onmain: a copy in another document is read apart, since its dialect may differ once each document is read in its own (openapi: a schema in another document is read in the root's dialect #839).allOfbranches fold (voices.go, openapi: inline allOf branches under different $id bases fold a keyword they spell alike #1408): twonots spelling one$refunder two bases folded into one entry read at the position, and the other constraint was lost; a lone writer under another base folded onto the position the same way. A branch whose folding keywords write a$refit reads against another base than the position now folds nothing and joins no agreement. It is kept whole with a report saying why, in a composition and in a refinement. One writing no$refthere,{$id: …, minLength: 1}or a branch whose only$refis in itsproperties, folds as onmain.allOfconjuncts (merge): two declarations writing anUnmodeledentry alike were one entry, and two nodes hoisted equal but for where they were declared were one type, so the second declaration's constraint was dropped with no report and no copy.Mergernow takesAlike, which the schema package answers from the bindings: such an entry, or such a pair of nodes, keeping a$refis one only where both read it against one base, and otherwise the later one is kept and reported as one given differently, two nodes overlapping as two types declared apart.$idat the copy, or at the resource holding the schema, a$refthe two spell alike names two schemas, so the copy's constraint was dropped with no report, onmainas well.copiesnow asksAlikeof every pair at least one of which is a copy, at the sites where the two nodes are written, so a schema in another resource or another document is read where it is written. The declaration sites a type judgment compares now sit in a type of their own (declarationSites), which keepstypeJudgewithin the method cap.The rule text says so in
docs/compilers/openapi.md#allof-refinement,#allof-inline-residueand#allof-redeclared-property.Cost and reach
A verdict reads the body only as far as its branches need. A
$refbranch settles alone, a branch writing only shapeless keywords leaves the body unread, and the declarations of what a schema nests are read only where it nests something; what the body'sallOfadmits, and every schema it composes, are read once a branch writes a key judged against the body, even where the body's own keywords would settle it (#1423). Besides time, this saves budget, since a reading cut short counts as declaring shape: an eager read could turn a union of narrowing branches beside a large body into one declaring shape. What the body declares of a property is gathered from its schemas only when a branch names it, and kept for every branch restating it. Each restating branch used to read it again, so 257 branches narrowing the body's one property read past the budget and degraded a union that, with 250 branches, was validation logic. Rows inTestOneOf_CoDeclaredUnionReadPastItsBudgetDeclaresShaperedden when either read is made eager or a place is read again. No real document reaches the budgets: with both lifted, the 1,840 documents below and the golden spec compile to the same IR as on the branch, bar two cyclicallOffixtures whose walk only the budget ends. The largest union beside a body there with an inline branch the budget reads has 15 branches. An inlineallOfbranch looks up bases only where it, or a wrapper holding it, writes an$id, the one way its base can differ from the position's, and only where what it folds writes a$ref. That shortcut changes no output, so no test pins it: a seam would need a hook through the lowering context, which no reader has, and the numbers below show its effect instead.Instructions retired,
/usr/bin/time -l,mainat e85202b and the branch at a4dda68 alternating, with no other build or test running:main(median, range)GOMAXPROCS=1(too large to run uncollected), five runs a sideGOMAXPROCS=1 GOGC=off, three runs a side for this and belowallOfs of two inline branchesallOfof 1,000 inline branchesallOfof 1,000 wrappers, each writing an$idThe golden spec's two ranges overlap, the collector's timing being most of the spread. Every generated shape grows linearly on both builds. The
$idwrappers there write no$ref, so since 732f682 they look up no base: 1,150 M (1,149–1,152) at 1,000 and 2,197 M (2,196–2,202) at 2,000, against 1,201 M and 2,311 M for a4dda68 run beside them, and 1,135 M and 2,173 M formain: 12 to 15 K instructions a wrapper more thanmain. Every other shape and the golden spec stayed within the spread of a4dda68 run beside them (golden spec 107.0–120.5 B for a4dda68, 107.0–120.2 B for 206ec17, three runs a side); those runs shared the machine with a test run. The union verdict adds about 0.31 M instructions per union over a 40-deep composition, linear in the number of unions, wheremainread nothing because it degraded them all. Before the base shortcut and the lazy reads, the 200-level spine cost 1,761 M and the unions 4,954 M at 1,000; before places were read once and gathered by name, the unions cost 4,126 M.Integration: each of 1,840 OpenAPI 3 documents from public generator test suites, compiled on
mainand the branch, gives the same exit code and the same error set, with no crash; rerun at 1855a63 againstmainat da6c9f1 with the 3.x specs undertestdata/added, 2,072 in all, the IR changes only in that spec and in the two conformance fixtures this branch extends. One IR changes: GitHub'sPATCH /gists/{gist_id}body writesanyOf: [{required: [content]}, {required: [filename]}, {type: object, maxProperties: 0}]beside a nullable object, which moves fromdegraded_loweringtovalidation_only. A model in another document and a declaration an overlay writes merge as onmain, and a union in another document is judged as one in the source.Decisions, not limits
readOnly,writeOnly,default,xmlanddiscriminatordeclare shape, alone or beside a repeatedtype: each sets what the IR says of a node's values or wire form (visibility, a default, XML naming, routing), which a branch could hold only conditionally. Adescriptionor a deprecation is held on a node too, but documents it, so it declares none, and so does a redaction request,format: passwordorx-sensitive, whichdocs/compilers/openapi.md#passwordmakes an annotation; an SDK reads no union entry under either reason, so a secret one branch alone writes is lost to it either way. A shape keyword with no reading here, such asadditionalPropertiesorprefixItems, restates only within a declaration written alike. Both are stated indocs/compilers/openapi.md#union-beside-structure.Stated limits
allOffor a restated property, and 130 for a restatedtype, and from five diamonds, 18 schemas, where the composition is diamond-shaped, since the walks read a schema two paths reach once a path. No union in the golden spec or the reference corpora comes near the bounds. openapi: a union beside a long composition degrades though its branches restate only the body's own keywords #1423 drafts reading the body's own keywords first, which no verdict a complete reading gives can contradict.main, and about the cube of the depth beyond;allOfrefinement of nesteditemsalready costs as much onmain. The nesting bound of openapi: a schema nested thousands of levels deep costs seconds and gigabytes before the depth bound applies #905 caps it at several seconds. openapi: judging a deep schema level by level renders it once a level, costing the cube of its depth #1424 tracks rendering a mapping without re-encoding its members, which fixes both.bind.Table.Basewalks from its document's root on every call, so a reader asking it at each level of a deep schema pays the square of the depth: a spine of 250 levels, each comparing a$refunder a base, costs 2,996 M instructions against 2,718 M onmain. The lowering's bounds end such a spine there, and real documents ask at few levels. openapi: each base lookup walks from the document root, quadratic over a deep spine #1431 tracks answering a base from the nearest$idabove.branchshape.gothat spend the budget and render the text cite openapi: a union beside a long composition degrades though its branches restate only the body's own keywords #1423 and openapi: judging a deep schema level by level renders it once a level, costing the cube of its depth #1424, andTable.Base's comment cites openapi: each base lookup walks from the document root, quadratic over a deep spine #1431.Cost at 5aea659
Against
mainat da6c9f1, instructions retired,/usr/bin/time -l, builds alternating, medians with ranges:GOMAXPROCS=1, five runs a side: 110.1 B (107.5–119.8) onmain, 109.6 B (106.1–111.9) on the branch, inside each other's range; peak memory 4,191 MB and 4,207 MB. GitHub's two specs and Kubernetes', the largest reference specs,GOGC=offtoo, three runs a side: within 0.03%, each range overlapping, peak memory equal.$reforrequired-only branch, and an inlineallOfbranch under no$id, cost nothing measurable; a restatedtype9 K instructions a union; an own, composed,itemsor$ref-landing restatement 40 to 70 K; a property nested three levels about 210 K, +4.0 to 4.2%; a merge or refinement comparison writing a$ref20 to 39 K; and an$idbranch writing one costs 42 to 50 K less than onmain, since it now folds nothing. One union of 2,000 branches, or one branch restating 2,000 properties, costs 30 to 35 K a branch or property.allOfposition two more, an$idwrapper map and a slice; a union restating its body's property about 83 more, and one nested three levels about 220.$idshortcut, an input of 2,000 inlineallOfpositions costs 1.6% more, so the shortcut stays, though no test pins it.Closes #1393
Closes #1394
Closes #1395
Closes #1407
Closes #1408
Test plan
morphic compile --verifyonmainand on the branch:Pair,TupleandDyn(openapi: a union branch writing only items, prefixItems or $dynamicRef is kept as validation logic #1393) move fromvalidation_onlytodegraded_lowering;Bounded,AnnotatedandDescribed(openapi: a restated union branch writing a keyword that declares no shape degrades the union #1394),Render(openapi: a branch restating a property the body composes through allOf degrades the union #1395), andJointandNested(openapi: a branch property restating several declarations, or a nested property, degrades the union #1407) move tovalidation_only, as their bare forms already lowered; openapi: inline allOf branches under different $id bases fold a keyword they spell alike #1408'sSnow keeps its nested branch whole underopenapi:allOf/0/allOf/0and folds only the outernot, asTdoes.type, and a bare and a restating branch over a composed body, at a component, a property, anitems, a parameter, a request body and a response body. Onmaineach restating spelling degrades at all six and each bare one isvalidation_only; on the branch all four arevalidation_onlyat all six, and a branch writingitemsdegrades at all six.TestOneOf_CoDeclaredBranchKeywordIsJudgedAloneAsBesideTheTypecompiles every keyword of the 2020-12 vocabularies, and OpenAPI's own, in a branch alone and beside the body'stype, and requires one verdict for both and the expected one for the keyword.type, at a component, a property, anitems, a parameter, a request body and a response body, over a body with its own properties, one composing them throughallOfand one doing both, in 3.0.3 and 3.1.0. On the branch the two spellings agree in all 1,494 pairs, and every verdict is the keyword's class. Againstmainat da6c9f1, 960 move tovalidation_only, each for a keyword declaring no shape, and 306 todegraded_lowering, each for a bare branch writing one of the keywords the summary names.morphic-harnesspasses all 2,952 that are valid (3.0.3 has no boolean schema), the order-invariance and idempotence oracles included.notor nested property in a restated property, a nullability difference, a widertype, atrueproperty, a branch typed onlynull, atypethe body's$refalready holds, a$refwhose siblings admit nothing, a branch'sallOfrepeating the body's property, and the rows openapi: a restated union branch writing a keyword that declares no shape degrades the union #1394 and openapi: a branch restating a property the body composes through allOf degrades the union #1395 named) move toTestOneOf_CoDeclaredRestatingBranchDeclaresNoShape, renamed where the old name said the branch says more.allOfout of what the body admits, dropping the null-onlytypecase, the boolean, written-alike and admits-nothing shortcuts, the null-keyword and extension cases, either reading ofunevaluatedProperties, movingitems,prefixItems,format,readOnlyordefaultinto the shapeless list, removing the value constraints or the documentation keywords from it, dropping the nestedallOfor the$refstep fromdeclare, gathering no element declarations, comparing with the first declaration only, leaving nestedpropertiesunread oritemstoholds, landing any$ref, giving a nested claim no declarations, reading a property the body never declares as restated, restoringsameText's document-only check, invoices.gotreating every branch as read at the position, letting a rebased branch join the agreement, or reporting it as disagreeing, and in the merge ignoring the base for anUnmodeledentry or for two hoisted copies, or answeringAliketrue for every pair, and reading the body, its declarations or what a property nests eagerly, or never looking up a base under an$idthe branch or a wrapper writes; and reading a place again for each branch restating it, at the top or nested, gathering a property from the first of the body's schemas only, keeping no schema in the set, matching a property written alike against the first declaration only, which the twoPropertyWrittenAs…OfTwoDeclarationsrows now hold, or charging a value set nothing; and readingallOfas shape again, judging a conjunct against the body whole, against nothing, or not at all, letting a branch's own$refconjunct land, landing only on the first schema the declarations compose or on none, judging a boolean conjunct as shape, leaving a$refunjudged, giving the branch itself no declarations, or dropping the boolean shortcut before a property's declarations are read, which theBooleanPropertiesReadNoDeclarationsrow now holds. All 55 were planted again on a4dda68, each reddening at least one test. For 732f682, 28d9ca3 and 206ec17, 27 more were planted on the final code: dropping the$refgate at each site or answering it for every text, droppingsameText's document check, reading a component pair as before, comparing the declarations' sites rather than the nodes',WritesReferencemiscounting a name, a nesting, an error or$dynamicRef, answeringsameText,readsAtPositionorAliketrue for every pair or for an unknown base,Alikereading a site in another document as the source's,Table.Baseanswering an unknown base as known, and keeping one element declaration of two. 26 redden a test. The one that does not reads only the first node'sUnmodeledfor a$ref, which is equivalent wherever two nodes are held alike, since both keep the same entries.union-restated-branches(addingRenderBounded,RenderDescribed,RenderComposed,RenderSizes,RenderJoint,RenderLinksandRenderConjoined) andallof-inline-residue(addingRebased) only gain types: no existing type's IR changes in either golden. The binding canary listsRebased's two$refs underid-base, the reason for every reference the compiler reads against an$idand the parser library refuses.cloudflare/api-schemasopenapi.jsonat 4c8edd11),mainat e85202b against the branch at a4dda68:jq -S 'del(.diagnostics, .sources)'differs in fivereasonvalues and nothing else. Four parameteritemswritten{type: string, anyOf: [{type: string, pattern: …}, …]}under/accounts/{account_id}/gateway/{lists,locations,proxy_endpoints,rules}move fromdegraded_loweringtovalidation_only, with their diagnostic: a pattern beside a repeatedtypenarrows, as the bare pattern already did.alexandria_create_application_request'sanyOf, requiring at least one ofhostnamesandip_subnets, moves the same way: each branch writes its property as anallOfof the declaration's own$refand bounds on it and its element. Each of the 27 inline-branch degradations left writes shape: a property the body does not declare, a catch-all, anallOfcomposing a$ref, a nested union, anenumnarrowing a property, atypethe body contradicts, oritemsbeside an object body. The spec writes no$id.mainand the branch with two declarations or branches written alike under two$idbases, once with the$refspelled absolutely, and once writing no$ref: onmainthe alike spelling loses a constraint the absolute one keeps; on the branch both lower alike, nothing lost, and text writing no$reflowers as onmain. At a4dda68 the last spelling did not:{$id: …, minLength: 1}kept its bound out ofConstraints, a branch whose$refsat in itspropertieslost its description, a refinement whose target sat under another$idkept a branch restating it, and two declarations writing{type: string, not: {const: q}}, or an equal enum or list, were reported as conflicting and the second kept verbatim. 732f682 fixes those, with a test at each site, and 28d9ca3 covers the component site, in one document and across two.testdata/(2,072 in all, 8 writing an$id) compile to the same IR, diagnostics and exit code on a4dda68 and 206ec17.docs/compilers/openapi.md#union-beside-structurealone, without the code, judged 4,500 random unions beside bodies of own, composed and typeless properties, with restating, narrowing and shape-declaring branches,allOfwrappers and$refs: it agrees with the branch on every one. Run againstmainit disagrees on 168 of 1,500, and against the branch withreadOnlyplanted as shapeless on 30, so the comparison sees a wrong rule.$idor writing the union, a body composing another document's schema and an$idbranch in that document, and 3.0.3 and 3.2.0, each lower as the rule says; 3.0.3's resolver honours$idas the base check does.$refunder another$idwas read as base-dependent:{$id: …, description: d, not: {$ref: 'https://ex.test/b/n.json'}}folded nothing and lost its description, and two declarations writing it alike were reported as conflicting, wheremainlowered each as written. f5db471 counts only a relative reference, with a row at each site; the corpus and the golden spec compile alike before and after it.validation_onlyand 95 declaring shape.properties,required,itemsandallOf, odd$ids, a$refthat is no string, a property named$ref, self-referential and cyclic compositions and escaped names, exit as onmainwith no panic;FuzzCompileandFuzzLowerSchema, seeded with these and the grid, ran 2.8 M inputs clean. Relative, fragment, dot-segment and case spellings of the position's own$idfold as the position does. A body whoseallOfnames itself degrades wheremainkept its union as validation logic, the cut-short reading of openapi: a union beside a long composition degrades though its branches restate only the body's own keywords #1423, which records it.validation_onlyas their bare forms are, and openapi: inline allOf branches under different $id bases fold a keyword they spell alike #1408'sSlowers asTdoes. Every example the union-beside-structure rule writes compiles to the verdict it states, and a shapeless union isvalidation_onlyat a component, a header, anadditionalPropertiesvalue, aprefixItemselement, a parameter's content, a callback and a webhook as well as the five positions its test compiles.mainat 50412aa is merged in; the gate on the final head, 9ded526, runs in CI.