CultLib/tests/vectors/union-parity
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-11-12 04:01:36 +00:00
..
.gitattributes Union parity vectors: committed bytes written by the C# reference, outcomes as data, README 2026-10-11 00:28:20 +02:00
arm-arity.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
arm-key-above-int32.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
arm-key-below-int32.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
arm-key-float32.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
arm-key-float64-negzero.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
arm-key-int32-max.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
arm-key-int32-min.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
arm-key-wide-coding.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
bounds-outcomes.msgpack C# reference: one payload nesting bound (256) on read and write, nil arm slots refused, bounds vectors 2026-10-11 08:33:15 +02:00
bytes-after-deep-tail.msgpack Bytes after the value: a payload is one MessagePack value (C#) (#125) 2026-11-12 04:01:36 +00:00
bytes-after-never-used.msgpack Bytes after the value: a payload is one MessagePack value (C#) (#125) 2026-11-12 04:01:36 +00:00
bytes-after-nil.msgpack Bytes after the value: a payload is one MessagePack value (C#) (#125) 2026-11-12 04:01:36 +00:00
bytes-after-outcomes.msgpack Bytes after the value: a payload is one MessagePack value (C#) (#125) 2026-11-12 04:01:36 +00:00
bytes-after-second-document.msgpack Bytes after the value: a payload is one MessagePack value (C#) (#125) 2026-11-12 04:01:36 +00:00
depth-at-limit.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
depth-past-limit.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
depth-skipped-at-limit.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
depth-skipped-past-limit.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
extra-slot-arm.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
extra-slot-consent.cc Slot tolerance: a record with unread slots is held read-only, not shed (#124) 2026-11-12 02:45:08 +00:00
extra-slot-list-element.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
extra-slot-nested-record.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
extra-slot-record.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
extra-slot-store.cc Slot tolerance: a record with unread slots is held read-only, not shed (#124) 2026-11-12 02:45:08 +00:00
inner-union-outer-key.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
nil-arm-slots.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
outcomes.msgpack Union parity vectors: committed bytes written by the C# reference, outcomes as data, README 2026-10-11 00:28:20 +02:00
README.md Bytes after the value: a payload is one MessagePack value (C#) (#125) 2026-11-12 04:01:36 +00:00
slot-outcomes.msgpack Slot tolerance: a record with unread slots is held read-only, not shed (#124) 2026-11-12 02:45:08 +00:00
union-surface.cc Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
union-surface.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
unit-arm-ceiling.msgpack Union parity vectors: unit-arm-denied, -floor and -ceiling, written by the C# reference 2026-10-14 20:33:01 +02:00
unit-arm-denied.msgpack Union parity vectors: unit-arm-denied, -floor and -ceiling, written by the C# reference 2026-10-14 20:33:01 +02:00
unit-arm-floor.msgpack Union parity vectors: unit-arm-denied, -floor and -ceiling, written by the C# reference 2026-10-14 20:33:01 +02:00
unknown-arm-field.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00
unknown-arm-list.msgpack Regenerate union-parity unknown-arm-list.msgpack with its Pending id 2026-10-14 19:47:56 +02:00
unknown-arm-nested.msgpack Regenerate the leaf and union golden vectors with element ids 2026-10-14 19:43:28 +02:00

union-parity vectors

Written only by the C# reference, tests/GameCult.Caching.Tests/UnionParityVectorTests.cs, with CULTCACHE_WRITE_VECTORS=1. Every other run reads these files and compares them byte for byte; a changed vector is a reviewed commit. Other runtimes decode them and are held to the same outcomes.

Document tests.union_surface (schema tests.union_surface.v1), a positional array of [Id, Version, Root, Outcomes, Part, Crafted, Missing]. A union value is nil or [armKey, [armSlots...]].

Union Arm keys Arm slots
Node 0 Box, 1 Text, 2 Use, 5 Image, 6 Wrap (3 and 4 retired) Box [Id, Children: Node list]; Text [Id, Content]; Use [Id, Composition, Slots: Node list]; Image [Id, Uri, nil (retired 2), Alt]; Wrap [Inner: Node]
Outcome 0 Pending, 1 Completed, 2 Denied, -2147483648 Floor, 2147483647 Ceiling Pending [Message, Id]; Completed [SourceVersion, Id]; Denied, Floor and Ceiling [Id]: the arms of a list carry an element id
Item 0 Gem, 1 Tool, 2 Part Gem [Name]; Tool [Name, Uses]; Part [Name, Count]
CraftedItem 0 Tool, 1 Part the same arms as Item under its own keys; a key only Item declares is unknown to it

Composition is a record, not an arm: [Name, Body: Node].

Files

  • union-surface.msgpack: the payload. Value: Id surface, Version 3; Root is a Box root holding Text t1, a Use u1 and Image i2; the Use's Composition comp holds Box inner (Text deep, Image img), and its Slots are Text s1, nil, Box b2 (empty); Outcomes are Pending, Completed, Denied; Part is Tool hammer; Crafted (a CraftedItem field) is Part gear; Missing is nil. A union inside a record inside a union, an id-only arm, a list of unions with a nil element, an inner union keyed apart from its outer. Written by C#.
  • union-surface.cc: a single-file store written by the C# store holding that one record under key surface. Its payload equals union-surface.msgpack; rewriting the loaded store is byte-identical. The writer pins the store's clock, so the record's stored-at is 2026-01-01T00:00:00.0000000+00:00 and the whole file reproduces byte for byte.
  • unit-arm-denied.msgpack, unit-arm-floor.msgpack, unit-arm-ceiling.msgpack: a member-less arm of a union no list reaches (so its arms carry no element id), alone, written by C#: [2, []], [-2147483648, []], [2147483647, []]. These pin the unit-arm encoding; the id-only arms above are not unit arms.
  • outcomes.msgpack: the expected outcome of every file below, as data. An array of entries [file, outcome, union, key, detail]: outcome is refused or read-only; union is the union (or record) named; key is the arm key or nil; detail is as in the tables below.

Refusals (outcome refused)

A refusal names the union and the arm key and never echoes the payload. Each payload is union-surface.msgpack with one part replaced.

File Replaced part detail Must name
unknown-arm-field.msgpack Root is an arm with the retired key 3 unknown-arm Node, key 3
unknown-arm-list.msgpack an Outcomes element has key 7 unknown-arm Outcome, key 7
unknown-arm-nested.msgpack key 9 three Boxes deep in Children unknown-arm Node, key 9
inner-union-outer-key.msgpack the Crafted field carries key 2, declared only by Item unknown-arm CraftedItem, key 2
arm-arity.msgpack Root is written [1], an arm with no slots array arity a refusal of the arm as malformed (C#: MessagePack's "Invalid Union data")

Extra trailing slot (outcome read-only)

Rulings cultlib-gaps:ruling:extra-trailing-slots and cultlib-gaps:ruling:slot-mark-consent: a payload carrying a slot past its reader's declared slots, in any object at any depth, is read, held read-only, and every write or removal of it is refused before anything is staged. The first four payloads are union-surface.msgpack with the string "extra-slot" appended as one more slot of one object; a reader decodes the value of union-surface.msgpack from each. A unit arm with an extra slot is pinned only as a store record, unit-arm in extra-slot-store.cc (below): outcomes.msgpack keeps the nine rows the leaf walkers count.

An object's declared slots are the slots its type's own formatter writes for one instance (for these fixtures, the highest key plus one); a reader derives them from the layout it writes, never from a list of public members. Only a type the runtime writes by its own positional [Key] layout is checked. Exempt, as in the C# reference (cultlib-gaps:ruling:leaf-acceptance-follows-reference): a type the runtime formats some other way (a user codec or formatter, a hand-written serializer, a resolver ahead of the runtime's own that supplies a layout of its own, a collection type written as its items; a resolver that hands out the runtime's own positional layout exempts nothing), and a type no instance of which the runtime can write for measuring (cultlib-gaps:ruling:slot-unmeasurable-type). Neither is ever marked. No vector carries an exempt type: each runtime pins its own exemption with its own fixture. Only an array longer than that marks: a short array (missing trailing slots) keeps the defaults and is writable, and a non-nil value in a retired slot below the highest key (Image's slot 2) does not mark. A union adds no depth: an arm's array is counted against the arm's own slots.

File detail Object with the extra slot Depth Mark: type, slots carried, slots declared
extra-slot-record.msgpack record the document (slot 7) 0 UnionSurface, 8, 7
extra-slot-nested-record.msgpack nested-record Composition comp nested Composition, 3, 2
extra-slot-arm.msgpack arm the Root Box arm nested Box, 3, 2
extra-slot-list-element.msgpack list-element Text s1, an element of Use.Slots nested Text, 3, 2
record unit-arm of extra-slot-store.cc unit-arm Denied2 in [2, ["extra-slot"]] (union UnitOutcome, key 2), slot 1 of the document 1 Denied2, 1, 0
depth-skipped-at-limit.msgpack (bounds file) depth the document (slot 7, a value of nested arrays) 0 UnionSurface, 8, 7

Each payload marks exactly one object. Four rows of outcomes.msgpack are read-only (record, nested-record, arm, list-element); the fifth read-only payload is in bounds-outcomes.msgpack, and the unit arm's outcome is its row in slot-outcomes.msgpack. The payload nesting check runs before the mark: depth-skipped-past-limit.msgpack is refused, never marked.

What a runtime asserts for a read-only payload

Load it as a store record under the reader's own schema id (the C# reference puts each UnionSurface payload in a tests.union_surface store; the unit arm is already a store record, unit-arm of extra-slot-store.cc). Then:

  1. the value is read past the extra slot;
  2. the record reports the mark as in the table: the first type read with extra slots, the slots it carried and the slots the reader declares, and the count of objects that carried extra slots (1 here);
  3. the record is not writable (C#: CultCache.IsWritable(key) false), and a key no store holds is writable;
  4. a replacing write, a variant write and a removal of the key, and a flatten of a variant whose base chain holds the marked record, are refused before anything is staged, with the runtime's schema conflict refusal carrying the mark; its message names the type and both counts and never contains extra-slot, the slot's value;
  5. a write to another key of the same store succeeds.

Consent: a mark is explained only when it is at depth 0 and the schema comparison of the record's stored id against the reader reported ignored extra slots covering it: the record is stored under an id the reader lists compatible, that id's catalog entry has more members, and the mark's slot count is at most the highest reported slot plus one. Declaring that id consents to an explained mark only. Every mark in the table above is unexplained (stored under the reader's own id), so each record stays read-only whatever is declared.

Slot stores (slot-outcomes.msgpack)

Two single-file stores, each record written as a value and its payload then replaced, the store's clock pinned to 2026-01-01T00:00:00Z as union-surface.cc's:

  • extra-slot-store.cc, read with tests.union_surface (schema tests.union_surface.v1) and tests.slot_unit (schema tests.slot_unit.v1, a document [Name, Outcome: UnitOutcome]). marked is extra-slot-nested-record.msgpack; clean is union-surface.msgpack; unit-arm is the tests.slot_unit document ["unit-arm", [2, ["extra-slot"]]], whose Denied2 arm sits at depth 1 (the document is depth 0; a union adds none); marked-variant is a variant of marked overriding only Id (slot 0) to "marked-variant".
  • extra-slot-consent.cc: every record is stored under tests.union_surface.wide, whose catalog entry is tests.union_surface's seven members plus slot 7 (Wider). Read it with a tests.union_surface type of schema tests.union_surface.v2 (the reference names it SurfaceDeclaring) that lists tests.union_surface.wide compatible, so the comparison reports slot 7 ignored. consented is union-surface.msgpack plus one root slot (slot 7); over-consent plus two (slots 7 and 8, past the highest reported); nested is extra-slot-nested-record.msgpack plus one root slot; clean is union-surface.msgpack.

slot-outcomes.msgpack is a second outcomes file, as bounds-outcomes.msgpack is, so readers of outcomes.msgpack stay green. It is an array of rows [store, key, writable, explained, type, slots, declared, occurrences, flattenRefusedBy], one per record of both stores: writable is what IsWritable answers; flattenRefusedBy is nil unless the record is a variant whose flatten is refused, and then it is the key of the base-chain record whose own refusal refuses it (the refusal names that key; the store is unchanged). For an unmarked record explained through occurrences are nil, otherwise they are the mark: whether it is explained, the first type read with extra slots, the slots it carried, the slots the reader declares, and how many objects carried extra slots. A depth-0 type is the reader's document type (the reference's names: SurfaceDeclaring for the consent store). For each row, open a copy of the store, assert the mark and IsWritable, flatten the key when flattenRefusedBy is set and assert the refusal, then remove the key: refused (with the mark, the file unchanged) exactly where writable is false. In the consent store a write of consented lands and a write of over-consent is refused.

The carried-verbatim check: open extra-slot-store.cc, assert marked read-only and clean writable, write clean and flush: marked's payload bytes, schema id and stored-at, and every catalog entry's bytes, equal the originals.

Bounds (bounds-outcomes.msgpack)

A second outcomes file, so runtimes that read only outcomes.msgpack stay green. Its first entry is ["limit", 256], the payload nesting limit; every other entry is [file, outcome, union, key, detail] with outcome accepted, refused or read-only. For accepted, detail names the file the value's re-encoding must equal. Each payload is union-surface.msgpack with one part replaced.

Arm keys are Int32 whatever their MessagePack width; a float is never a key.

File Replaced part outcome, detail Must
arm-key-int32-min.msgpack a fourth Outcomes element Floor, key d2 80000000 accepted, itself re-encode byte-identical
arm-key-int32-max.msgpack a fourth element Ceiling, key ce 7fffffff accepted, itself re-encode byte-identical
arm-key-wide-coding.msgpack Ceiling's key as uint64 cf 000000007fffffff accepted, arm-key-int32-max.msgpack re-encode to the canonical coding
arm-key-above-int32.msgpack a unit arm with key ce 80000000 refused, key-out-of-range name Outcome, say outside Int32
arm-key-below-int32.msgpack a unit arm with key d3 ffffffff7fffffff refused, key-out-of-range name Outcome, say outside Int32
arm-key-float32.msgpack Completed with key float32 ca 3f800000 refused, key-not-integer name Outcome, say non-integer
arm-key-float64-negzero.msgpack Pending with key float64 cb 8000000000000000 refused, key-not-integer name Outcome, say non-integer
nil-arm-slots.msgpack a fourth element [1, nil] refused, nil-slots name Outcome and key 1; never read as a null element

Payload nesting: every array and map counts, read or skipped, and the outermost array is depth 1. Depth 256 is accepted and 257 refused, on read and on write, naming the type, the depth reached and the limit, never the payload. depth-at-limit and depth-past-limit are chains of Wrap arms ending in a leaf (a Box with no children, or a Text whose content is a canary string); depth-skipped-* is the base document with one extra trailing slot of plain nested arrays around a canary string, which no formatter reads.

File Depth outcome, detail Must
depth-at-limit.msgpack 256 accepted, itself decode, re-encode byte-identical; the constructed value writes it
depth-past-limit.msgpack 257 refused, depth refuse on read; refuse to write the constructed value and return nothing
depth-skipped-at-limit.msgpack 256 read-only, depth read the value of union-surface.msgpack
depth-skipped-past-limit.msgpack 257 refused, depth refuse on read though the typed depth is under 10

A walk of the payload must not recurse: 100000 nested arrays in an extra slot are refused on a 256 KiB stack.

Bytes after the value (bytes-after-outcomes.msgpack)

A record's payload is exactly one MessagePack value. Each file is union-surface.msgpack followed by a tail the first value does not own; the C# reference writes them and every other runtime is held to the same refusal.

File Tail after the value outcome, detail
bytes-after-nil.msgpack c0 (1 byte) refused, bytes-after
bytes-after-never-used.msgpack c1 c1 (2 bytes, the never-used code) refused, bytes-after
bytes-after-deep-tail.msgpack 300 nested fixarray(1) around a canary string (324 bytes), nested past the limit refused, bytes-after
bytes-after-second-document.msgpack a second copy of union-surface.msgpack refused, bytes-after

bytes-after-outcomes.msgpack is an array of [file, "refused", "UnionSurface", nil, "bytes-after"] rows, the row shape of bounds-outcomes.msgpack but in its own file, so the bounds walkers (which assert 12 rows) are unchanged.

Must: refuse on read, naming the type and the count of bytes after the first value (1, 2, the tail length) in a clause that says "bytes after its first MessagePack value"; never echo the canary; never report the depth refusal ("the limit is") for any of them, because the tail is not walked; and a single-file store holding bytes-after-nil.msgpack as a record payload refuses on open.

What a runtime walks: only the first value of the payload, to find where it ends (a leaf counts as complete at its own length, a container when its last child completes). It compares that end with the payload length; a payload that ends early is not bytes after the value and stays the decoder's to refuse. It never walks the tail, so the tail's own shape (a stray c1, 300 levels) is never judged.

A CDN manifest read as a record follows the same rule; its vectors are contracts/cultmesh/content-acceptance-vectors.json (trailing_* are refusals).