Repository navigation
Conversation
ToCBOR returns the CBOR (RFC 8949) encoding of any finite TLA+ value as a sequence of integers in 0..255, and CBORSerialize writes the same bytes to a file, so harnesses in Go, Rust, TypeScript and Python can read TLC values without the type loss of JSON. Sets, model values and functions with non-string domains keep their TLA+ meaning. Equal values encode as identical bytes. The encoder decides how a function is written from its domain, not from its Java class, and sorts set elements and keys by their encoded bytes instead of by TLC's intern order. It never normalizes the values it reads. CBORTests writes each golden inline as a sequence of hex literals and requires ToCBOR to return exactly those bytes, for every row of the encoding table and for groups of TLC-equal values held in different Java classes. tests/CBORTests/fixtures.py is a second encoder that shares no code with CBOR.java; run by hand, it checks every golden sequence in CBORTests.tla against its own output. FromCBOR and CBORDeserialize keep their fallback bodies until the decoder lands. tlaplus/tlaplus#1467 [Feature] Signed-off-by: younes-io <git@younes.io>
FromCBOR decodes one CBOR data item from a sequence of integers in 0..255, and CBORDeserialize from a file, so a spec can load a trace or state that a harness wrote. Both use one decoder. It decides the TLC class of a map or a pair list with the same shape rule the encoder uses to pick the form, and builds values already normalized in TLC's order. The decoder is lenient on form and strict on meaning. It accepts set elements, keys and pairs in any order, integers and lengths in any width, and maps with keys that are not strings, because Python, Go and Rust encoders differ on exactly these by default. It rejects duplicates under TLC equality, values TLC cannot represent or compare, and nesting deeper than 512 data items, and names the operator, the file if there is one, and the byte offset. It looks model values up among the model's and never creates one, because TLC reads model values back from its disk queues by index. The tests read every golden back, read inline inputs that TLC never writes and require them to encode again in canonical form, round-trip exhaustive small value spaces, and check that a file CBORSerialize wrote reads back as FromCBOR(ToCBOR(v)). tlaplus/tlaplus#1467 [Feature] Signed-off-by: younes-io <git@younes.io>
CBORTests matches the exact message of every input FromCBOR rejects with AssertError, each input written inline as a sequence of hex literals, so every rejection is shown to be an EvalException that names the byte offset rather than a 2154 stack trace. The cases cover truncation, hostile lengths, trailing bytes, types and tags without a TLA+ meaning, integers outside TLC's range, invalid UTF-8, duplicates and incomparable values under TLC equality, unknown model values, and nesting deeper than 512. The tests also cover arguments that are not byte sequences or strings, a missing file, a decoding error that names the file, and values ToCBOR and CBORSerialize cannot encode, and show that a refused value leaves the existing file as it was. tlaplus/tlaplus#1467 [Tests] Signed-off-by: younes-io <git@younes.io>
The row sits between Bitwise and Combinatorics, where the table's case-insensitive alphabetical order puts it, and links the Java override like the other rows. tlaplus/tlaplus#1467 [Doc] Signed-off-by: younes-io <git@younes.io>
ToCBOR wrote values that FromCBOR refused for their depth. The encoder
now counts data items as the decoder does and refuses a value nested
more than 512 deep, so an integer can lie inside at most 511 sequences
or records, 255 sets, or 170 functions that use tag 33000. The round
trip law in CBOR.tla now excludes the values it never held for: those
with a set or a function domain whose elements TLC cannot compare, such
as {1, "a"}.
The decoder checked each length against the bytes that remain, but each
of 512 nested arrays could claim those bytes again, and a 4 MB file
exhausted a 4 GB heap. A length may now claim only the bytes that the
enclosing arrays and maps do not still need.
The header of CBOR.tla is the format's specification for producers and
readers in other languages, and several statements in it were wrong.
Its two ordering examples held under shortest-first order as well; the
example is now 10, 100, -1. CBOR libraries do not sort the pairs of tag
33000. Go's fxamacker/cbor rejects only map keys that are arrays, not
integers or tagged items. The header now also says that tag 33000 is
not registered with IANA yet, and that a function is an array, a map,
or tag 33000 depending on its domain alone, so a reader must accept all
three forms for the same variable.
No golden compared encodings that first differ at a byte of 0x80 or
more, so an encoder that sorted signed bytes passed every test. Four
goldens now do: two sets of integers and model values, a set of
non-ASCII strings, and a function whose domain mixes a model value and
an integer. CI runs tests/CBORTests/fixtures.py on Ubuntu, so the
golden bytes are checked against the second encoder on every build.
tlaplus/tlaplus#1467
[Bug]
Signed-off-by: younes-io <git@younes.io>
Proposed IANA registration for tag 33000Tags from 32768 up are First Come First Served. IANA asks for the four-line template from RFC 8949 section 9.2 plus a URL that describes the semantics. Before I file it, here is what I would send, so that the number, the data item, and the wording can be agreed in this PR. The description text lives in a gist, https://gist.github.com/younes-io/8bbc63cffd4811f6aa2776c7301eb360, which I will keep in step with this thread; its current text is also below. The module header gets the registry link when the row appears. Update 2026-10-08. The request went to IANA with the template below and both contacts. Update 2026-10-09. IANA registered tag 33000 on 2026-10-08, with the template below as filed: https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml (row 33000, template https://www.iana.org/assignments/cbor-tags/template/33000). The module header and Template Description of semantics A function in TLA+ is a finite table from arguments to values. Its arguments can be integers, strings, booleans, model values, tuples, sets, records, or other functions. CBOR maps allow such keys, but the native dictionaries of several host languages do not. Go cannot use an array as a map key, and a JavaScript object turns every key into a string. Tag 33000 therefore writes a function as an array of pairs, which every language reads. The tagged data item is an array. Each element is an array of exactly two data items, the argument first and then the value at that argument.
The producer of this tag, the Examples. The function The function on the tuples Open points for reviewers
|
|
First reaction: Could the Python testing be adapted and merged into the current Java test suite? |
Yes (shared my email privately). |
build.xml compiled the overrides with source and target 1.8, but IOUtils already calls Files.readString and Files.writeString, which Java 11 added, and the tla2tools.jar that the build downloads is class version 55 and needs Java 11 to run. The build has needed Java 11 since 2021, when IOUtils adopted those calls. The javac task now passes release 11 instead. With release, javac checks every call against the Java 11 class library rather than the library of the JDK that runs the build, so a call to a newer API fails to compile instead of surfacing in review. [Build] Signed-off-by: younes-io <git@younes.io>
tests/CBORTests/fixtures.py checked the golden bytes in CBORTests.tla against a second CBOR encoder, but only CI ran it, only on Ubuntu, and only as a separate workflow step. CBORGoldenTest is a JUnit 4 port of it: the same encoder, the same table of golden values under the same names, and the same checks. Every stated golden must match the bytes this encoder writes, every name must have a value, and every value must be stated by some ASSUME. The test fails once and lists every problem, each with its line in CBORTests.tla and, for a mismatch, the expected bytes as a TLA+ sequence ready to paste. The optional cbor2 printout is gone. The test uses JUnit alone, not tla2tools.jar or CBOR.java, so the two encoders still share no code. The compile target builds tests/java, and the test target runs the test with JUnitCore before TLC, so a golden mismatch fails the build in seconds on every operating system. JUnitCore runs through the plain java task, because Ant's junit task needs ant-junit on the runner. The CI step that ran the Python script is removed, which leaves the workflows as they are upstream. [Tests][Build] Signed-off-by: younes-io <git@younes.io>
The module header and the TAG_FUNCTION comment said the tag was not registered yet. IANA assigned it on 2026-10-08, with the semantics the header describes: https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml [Doc] Signed-off-by: younes-io <git@younes.io>
|
IANA registered tag 33000 today. The row is live at https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml, with the semantics as proposed above and both of us as contacts. Thanks, @lemmy. |
| * decoder rejects for its depth. A tag is a data item: a set takes two levels and the pairs of tag 33000 | ||
| * three. | ||
| */ | ||
| private static final int MAX_DEPTH = 512; |
There was a problem hiding this comment.
A StackOverflowError is a perfectly acceptable failure mode, so there is no need to impose an arbitrary recursion-depth limit. Users can adjust the JVM's thread stack size using -Xss (e.g., -Xss4m) if needed.
There was a problem hiding this comment.
Removed the fixed depth limit and the depth tracking from both the encoder and decoder. Deep values can now reach the JVM stack limit. The docs mention StackOverflowError and adjusting -Xss.
| /** A non-empty set of strings: a map keyed by text. */ | ||
| RECORD, | ||
| /** Anything else: tag 33000 around [x, f[x]] pairs. */ | ||
| PAIRS |
There was a problem hiding this comment.
Consider using established TLA+ terminology, such as sequences/tuples, records, and functions. It's not immediately clear to me what “Pair” refers to or how it relates to these existing concepts.
There was a problem hiding this comment.
Removed the Shape enum, including PAIRS. The code now uses TLC's sequence, record, and function conversions. The remaining mentions of pairs describe the [argument, value] entries in the CBOR encoding, not a separate TLA+ type.
| PAIRS | ||
| } | ||
|
|
||
| private static Shape shape(final Value[] domain) { |
There was a problem hiding this comment.
Consider using TLC's conversion methods on Value. Each returns null when the value isn't of that kind:
v.toTuple() != nullmeans v is a sequence. The domain is exactly 1..n, including the empty case.v.toRcd() != nullmeans v is a record. The domain is a finite set of strings, including the empty case.v.toFcnRcd() != nullmeans v is a function.
There was a problem hiding this comment.
Replaced the custom shape checks with toFcnRcd(), toTuple(), and toRcd(). Tuple conversion comes first so empty functions keep their existing array encoding.
I only try toRcd() when all domain elements are strings. It normalizes before checking their types, which can fail on mixed domains that the encoder otherwise accepts. The finite-domain check also stays before expanding a function.
| /** | ||
| * Writes the one encoding of a TLC value. Values that TLC considers equal produce identical bytes, | ||
| * whatever their Java class and whatever order TLC interned their strings in, so the encoder sorts by | ||
| * encoded bytes and never consults TLC's normalized order. It never calls normalize() either, so it |
There was a problem hiding this comment.
I suggest calling normalize() before writing, as Json does. TLC normalizes values in place everywhere, so avoiding it here doesn't buy much, and it adds subtle code: dodging toFcnRcd(), and removing duplicates from set elements by comparing their bytes. Once a set is normalized, its elements are already in a fixed order with no duplicates. Only record fields still need sorting by their encoded bytes, which RFC 8949, section 4.2.1 requires for CBOR maps.
There was a problem hiding this comment.
I removed the special handling that avoided toFcnRcd() and the claim that encoding never changes TLC values in place.
I kept byte sorting because TLC's order differs from this module's existing encoding order. For example, TLC orders the values as -1, 10, 100, while their encoded bytes sort as 10, 100, -1. Using TLC's order for sets and function arguments would change the output.
I also kept byte-based duplicate removal for sets. Normalizing every set would reject inputs such as {1, "a"}, which ToCBOR currently accepts. FromCBOR still rejects that mixed set, as documented.
| /** | ||
| * Reads one CBOR data item into a TLC value. Liberal about form, strict about meaning: it accepts set | ||
| * elements, map keys and pairs in any order, integers and lengths in any width, and maps with keys that | ||
| * are not strings. Encoders in other languages differ on exactly these by default, and none of them |
There was a problem hiding this comment.
This paragraph should would benefit from using TLA+ terminology. Also, what other languages and why do they matter here?
There was a problem hiding this comment.
Rewrote this using the TLA+ types the decoder returns. The paragraph now states which input forms it accepts and rejects, without the vague reference to other languages.
| * changes the TLA+ value. It rejects every item whose TLA+ meaning is missing, out of TLC's range, or | ||
| * ambiguous, and names the source and the byte offset of the item. | ||
| * | ||
| * <p>It never creates a model value. ModelValue.make at run time leaves ModelValue.mvs stale, and TLC |
There was a problem hiding this comment.
This should either be moved or clarified to state that Decoder handles MVs. It appears to look up and reuse existing MVs, failing if none exists. This is reasonable.
There was a problem hiding this comment.
Clarified this. The decoder handles model values by looking up and reusing existing ones. An unknown name causes an error. Decoding never creates a new model value.
| case 25: | ||
| case 26: | ||
| case 27: | ||
| throw error(start, "a floating-point number has no TLA+ counterpart"); |
There was a problem hiding this comment.
TLA+ does have Reals. The real limitation is TLC's, not the language's: TLC has no value class for non-integer reals.
"TLC cannot represent a floating-point number"
There was a problem hiding this comment.
Changed the message to "TLC cannot represent a floating-point number" and updated the test. The error now describes TLC's limitation, not a limitation of TLA+.
| case 31: | ||
| throw notWellFormed(start); | ||
| default: | ||
| throw error(start, "a simple value has no TLA+ counterpart"); |
There was a problem hiding this comment.
Changed it to "a CBOR simple value has no TLA+ counterpart" and updated the test.
| (* string that is not valid Unicode, a value nested too deep), TLC reports *) | ||
| (* an error. *) | ||
| (***************************************************************************) | ||
| ToCBOR(value) == |
There was a problem hiding this comment.
Should ToCBOR and FromCBOR be declared LOCAL? Apart from tests, under what circumstances would a spec need to call them directly?
There was a problem hiding this comment.
I've kept them public for specs that work with bytes without using files. For example, Len(ToCBOR(message)) can check an encoded message-size limit. FromCBOR(bytes) can interpret a byte sequence already available to the spec.
I don't have an existing non-test consumer to point to yet. These are the intended uses, and I've added examples to the documentation.
| @@ -0,0 +1,112 @@ | |||
| -------------------------------- MODULE CBOR -------------------------------- | |||
| (***************************************************************************) | |||
There was a problem hiding this comment.
This header comment seems overly technical and focused on implementation details. Consider moving it elsewhere or replacing it with a high-level description aimed at users of the CBOR module, who presumably don't need to know the internals of TLA+ serialization and deserialization.
There was a problem hiding this comment.
Shortened the header to explain how to use the operators, with examples and the main limitations. Moved the encoding rules and interoperability details to docs/CBOR.md, with references from the module and README.
Use TLC conversions for function representations while preserving encoded-byte ordering and supported mixed inputs. Remove the fixed nesting limit, clarify decoder errors and model-value handling, and add regression coverage. Keep the in-memory operators public. Shorten the module header and move the encoding contract to docs/CBOR.md. Signed-off-by: younes-io <git@younes.io>
Why
tlaplus/tlaplus#1467 asks for a trace format that harnesses in Rust, Go, TypeScript, and Python can read without losing TLA+ types. JSON cannot tell a set from a sequence, a model value from a string, or a function from a record. This PR adds a
CBORmodule (RFC 8949) withToCBORandFromCBORfor byte sequences andCBORSerializeandCBORDeserializefor files, implemented as TLC overrides in the style ofJson. TLC's-dumpTracecan later be a thin layer on top of it.What changed
modules/CBOR.tladocuments the four public operators,docs/CBOR.mddefines the encoding contract, andtlc2.overrides.CBORimplements the codec and file operations.tests/CBORTests.tlapins the bytes of every value kind, andtests/java/tlc2/overrides/CBORGoldenTest.java, a second encoder with no shared code, checks those bytes fromant test.The encoding
Integers, booleans, and strings are the CBOR types of the same name. A model value is tag 39 (Identifier) around its name. A set is tag 258 (Mathematical finite set) around an array of its elements. A function whose domain is
1..nis an array, which covers sequences, tuples, and the empty function. A function whose domain is a non-empty set of strings is a map, which covers records. Any other function is tag 33000 around an array of[argument, value]pairs.Map keys use the unsigned lexicographic encoded-byte ordering in RFC 8949 section 4.2.1. This module also applies that ordering to set elements and function arguments. That additional contract gives supported TLC-equal values identical bytes regardless of their Java representation or string interning order.
Why a new tag
Tags 39 and 258 were already registered with IANA. Tag 33000 was registered for this module on 2026-10-08. It is needed because a TLA+ function can have integers, model values, tuples, sets, or records as arguments, and a CBOR map with such keys does not survive every decoder. Go's
fxamacker/cborrejects a map whose key is an array, and a JavaScript object has only string keys. An array of pairs avoids requiring function arguments to fit a host language's native map keys. Consumers still need to interpret the tags. Tag 259 does not help, because it marks a map and leaves the key problem in place.Before this registration, no registered tag described a function or map encoded as an array of pairs. The closest are 259 and 275, which tag maps, and 281, which tags Lisp cons cells. 33000 is in the First Come First Served range. A comment below holds the registration template as filed; the registry row is at https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml and
docs/CBOR.mdlinks to it.Scope
StackOverflowError; increasing JVM-Xsscan allow deeper values.ToCBORcan export mixed sets such as{1, "a"}, butFromCBORrejects them because TLC cannot compare their elements.docs/CBOR.mdstates this round-trip restriction.docs/CBOR.md.-dumpTrace cboroption in TLC, and atomic file replacement, whichJsondoes not do either.Tradeoffs
Deciding the form by the value, not by TLC's Java class, is what makes equal values byte-identical. The cost is the shape change above. A typed reader needs a small custom decoder for functions. The alternative, writing by Java class, would make
<<>>and the empty record differ on the wire while TLC says they are equal.Impact on existing code
The new module is registered in
TLCOverrides.javaand included inAllTests.tla. The build now states Java 11 as its floor, which it already needed:IOUtilsuses Java 11 file APIs and the tla2tools.jar it downloads is built for Java 11.ant testruns one JUnit test before TLC.Verification
ant -Dskip.download=true testpasses on the current source using the existing local TLC jar, version2026.10.06.014338, revision94d0c50, and Homebrew Java 27 on macOS. This run does not verify the build's downloaded TLC 1.8.0 jar or the other CI platforms.CBORTests.tlacontains 119 assumptions covering byte-exact encodings, equivalent TLC representations, round trips over exhaustive small value spaces, malformed-input rejection, file operations, and nesting beyond the former fixed limits.CBORGoldenTestpasses all 31 golden cases using a second encoder with no shared implementation code. It runs before TLC inant test.cbor2and Go'sfxamacker/cbor, including a counterexample written from aPOSTCONDITION. Those interoperability checks have not been rerun for the latest revision.