diff --git a/README.md b/README.md
index 71063b1..185f97d 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ The Modules
| ---: | ---- | :--: | ---- |
| [`BagsExt.tla`](https://github.com/tlaplus/CommunityModules/blob/master/modules/BagsExt.tla) | Additional operators on bags (e.g. `BagAdd`, `BagRemove`, `FoldBag`, etc.) | [✔](https://github.com/tlaplus/CommunityModules/blob/master/modules/tlc2/overrides/BagsExt.java) | [@muenchnerkindl](https://github.com/muenchnerkindl), [@lemmy](https://github.com/lemmy) |
| [`Bitwise.tla`](https://github.com/tlaplus/CommunityModules/blob/master/modules/Bitwise.tla) | Bitwise And and shift-right. | [✔](https://github.com/tlaplus/CommunityModules/blob/master/modules/tlc2/overrides/Bitwise.java) | [@lemmy](https://github.com/lemmy), [@pfeodrippe](https://github.com/pfeodrippe) |
+| [`CBOR.tla`](https://github.com/tlaplus/CommunityModules/blob/master/modules/CBOR.tla) | CBOR (RFC 8949) encoding and decoding of TLA+ values, in memory or in files, including sets, model values, and functions. [Encoding reference](docs/CBOR.md). | [✔](https://github.com/tlaplus/CommunityModules/blob/master/modules/tlc2/overrides/CBOR.java) | [@younes-io](https://github.com/younes-io) |
| [`Combinatorics.tla`](https://github.com/tlaplus/CommunityModules/blob/master/modules/Combinatorics.tla) | Binomial coefficient (N choose K) and factorial operator | [✔](https://github.com/tlaplus/CommunityModules/blob/master/modules/tlc2/overrides/Combinatorics.java) | [@lemmy](https://github.com/lemmy) |
| [`CSV.tla`](https://github.com/tlaplus/CommunityModules/blob/master/modules/CSV.tla) | Operations on CSV files | [✔](https://github.com/tlaplus/CommunityModules/blob/master/modules/tlc2/overrides/CSV.java) | [@lemmy](https://github.com/lemmy) |
| [`DifferentialEquations.tla`](https://github.com/tlaplus/CommunityModules/blob/master/modules/DifferentialEquations.tla) | see page 178 of [Specifying Systems](https://lamport.azurewebsites.net/tla/book-02-08-08.pdf) | | Leslie Lamport |
diff --git a/build.xml b/build.xml
index ec19905..b86c5cd 100644
--- a/build.xml
+++ b/build.xml
@@ -29,9 +29,11 @@
+
@@ -103,6 +105,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/CBOR.md b/docs/CBOR.md
new file mode 100644
index 0000000..c0a6089
--- /dev/null
+++ b/docs/CBOR.md
@@ -0,0 +1,141 @@
+# CBOR encoding reference
+
+The `CBOR` module encodes TLA+ values as [CBOR, defined by RFC 8949](https://www.rfc-editor.org/rfc/rfc8949.html), and decodes CBOR into TLC values. The rules below define this module's encoding contract.
+
+All four public operators require the CommunityModules Java overrides on TLC's classpath, including `tlc2.overrides.CBOR`. The TLA+ definitions report an error when the overrides are absent. The repository [README](../README.md#how-to-use-it) describes library and classpath setup.
+
+## Operators
+
+| Operator | Result and effects |
+| --- | --- |
+| `ToCBOR(value)` | The encoding of `value` as a sequence of integers in `0..255`, one per byte. No file I/O. |
+| `FromCBOR(bytes)` | The TLC value represented by one CBOR data item. `bytes` is a sequence of integers in `0..255`. No file I/O. |
+| `CBORSerialize(absoluteFilename, value)` | Writes the same bytes as `ToCBOR(value)` and returns `TRUE`. Creates missing parent directories and replaces an existing file. |
+| `CBORDeserialize(absoluteFilename)` | Reads a file and decodes its bytes with the same rules as `FromCBOR`. |
+
+`FromCBOR` also accepts functions that TLC can convert to sequences, such as `[i \in 1..n |-> b[i]]`. The file operators take a filename string. `CBORSerialize` encodes the entire value before it touches the file, so an encoding failure leaves an existing file unchanged. A file I/O failure reports an error.
+
+### In-memory expressions
+
+The public `ToCBOR` operator supports encoded message-size checks without temporary files. The public `FromCBOR` operator interprets byte inputs directly, including byte sequences held in model state.
+
+```tla
+EXTENDS Naturals, Sequences, CBOR
+
+Message == [id |-> 1, payload |-> <>]
+FitsPacket(message, maxBytes) == Len(ToCBOR(message)) <= maxBytes
+Fits == FitsPacket(Message, 1024)
+Decoded == FromCBOR(<<130, 1, 245>>) \* equals <<1, TRUE>>
+RoundTrip == FromCBOR(ToCBOR(Message)) = Message
+```
+
+`Len(ToCBOR(message))` counts CBOR bytes, not characters or transport framing bytes. `Naturals` supplies `<=`, and `Sequences` supplies `Len`.
+
+### File expressions
+
+The file operators exchange values with external programs or load CBOR fixtures.
+
+```tla
+EXTENDS CBOR
+
+Message == [id |-> 1, payload |-> <>]
+WriteMessage == CBORSerialize("/tmp/message.cbor", Message)
+ReadMessage == CBORDeserialize("/tmp/message.cbor")
+```
+
+The read expression requires an existing file. A zero-argument definition such as `ReadMessage` is constant-level, so TLC reads the file once before model checking. These definitions do not establish an execution order between the write and the read.
+
+## Encoding types and tags
+
+For supported values, values that TLC considers equal produce identical bytes, regardless of their internal Java representation or string interning order.
+
+| TLA+ value | CBOR representation |
+| --- | --- |
+| Integer | Integer, major type 0 for nonnegative values or major type 1 for negative values. |
+| `TRUE`, `FALSE` | CBOR `true`, `false`. |
+| String | UTF-8 text string. |
+| Model value | Tag 39 around its name as a text string. |
+| Finite set | Tag 258 around an array of its elements. |
+| Function `f` with `DOMAIN f = 1..n` | Array of `f[1]`, ..., `f[n]`. This includes sequences, tuples, and the empty function. |
+| Function with a nonempty domain of strings, a record | Map from each domain element `x` to `f[x]`. |
+| Other finite function | Tag 33000 around an array of two-element arrays `[x, f[x]]`, one for each domain element `x`. |
+
+The [IANA CBOR tag registry](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml) lists tag 39, "Identifier", tag 258, "Mathematical finite set", and tag 33000, "TLA+ function as an array of [argument, value] pairs". The two-element arrays in tag 33000 are CBOR arrays, not TLA+ record syntax.
+
+### Function representation follows the domain
+
+The encoding depends on the function's domain, not how the function was written in TLA+. For `[x \in S |-> 0]`, the representations are:
+
+| Domain `S` | Representation |
+| --- | --- |
+| `{}` | Empty array. |
+| `1..3` | Array. |
+| `{"a"}` | Map with a text key. |
+| `{1, 3}` | Tag 33000 with argument-value pairs. |
+| A nonempty set of model values | Tag 33000 with argument-value pairs. |
+
+A variable that holds a function can change CBOR representation between states. The empty record encodes as an empty array. A consumer of an arbitrary TLA+ function must account for arrays, maps, and tag 33000. Sequences remain arrays. Other functions that are not records use tag 33000.
+
+### Deterministic ordering and widths
+
+The encoder sorts map keys by unsigned lexicographic order of their complete encoded bytes, as in [RFC 8949, section 4.2.1](https://www.rfc-editor.org/rfc/rfc8949.html#section-4.2.1). It uses the same ordering for set elements and for arguments in tag 33000 arrays. Sequence elements retain their sequence order.
+
+For example, `10` precedes `100`, which precedes `-1`, because their encodings are `0a`, `18 64`, and `20`. This is not numeric order. It is also not the length-first ordering in [RFC 7049, section 3.9](https://www.rfc-editor.org/rfc/rfc7049.html#section-3.9), which puts `-1` before `100`.
+
+Integers, tags, and lengths use their shortest available argument widths. All lengths are definite. The encoder removes duplicate representations of equal set elements, so internal duplicates do not change a set's encoding.
+
+## Interoperability
+
+The encoder emits CBOR maps only for records with string keys. Such maps fit native dictionaries or structures with string keys. Sequences use arrays, and other functions use tag 33000 rather than maps with arbitrary keys.
+
+Arbitrary map keys do not fit every language's native dictionary representation. For example, an array-valued map key can fail when fxamacker/cbor decodes it into a Go map because Go slices cannot be map keys. JavaScript objects convert their keys to strings. The tagged array of pairs preserves function arguments without requiring a native dictionary to support them as keys. A consumer still needs to interpret the module's tags.
+
+Foreign producers need not sort set elements, map keys, or function pairs for these decoders. They must follow this module's ordering and width rules to produce byte-identical output.
+
+A library option called "canonical" is not sufficient evidence of compatibility. Map-key sorting alone does not sort the pairs inside tag 33000. For shortest-form text keys, bytewise and length-first map ordering agree, but those orderings differ for general set elements and function arguments. Python cbor2's canonical set encoding uses length-first ordering, which can differ from this module's ordering. Library behavior can vary with versions and options. The module's rules above are authoritative for byte-identical output.
+
+## Decoder acceptance and rejection
+
+`FromCBOR` and `CBORDeserialize` decode exactly one CBOR data item. They return TLC integers, booleans, strings, sequences, sets, records, functions, or existing model values.
+
+The decoders accept these noncanonical forms:
+
+- Integers, tags, and lengths with argument widths longer than necessary, within the supported value and input limits.
+- Set elements, map keys, and tag 33000 pairs in any order.
+- Maps with non-string keys, when TLC can represent and compare those keys.
+
+Maps and tag 33000 functions are converted according to their domains. A function with domain `1..n` becomes a sequence, a function with a nonempty string domain becomes a record, and another function remains a function. Re-encoding a decoded value uses the encoder's representation and ordering, not necessarily the original bytes.
+
+The decoders reject these inputs:
+
+- Duplicate set elements or duplicate function keys, including map keys. Duplicates are determined by TLC value comparison, not by byte equality. For example, `01` and `18 01` both encode the integer `1`.
+- Set elements or function keys that TLC cannot compare with one another.
+- Integers outside `-2147483648..2147483647`.
+- Floating-point numbers, CBOR byte strings, `null`, `undefined`, and other unsupported simple values.
+- Indefinite-length items, unsupported tags, and malformed CBOR, including truncated input.
+- Tag 39 without a text string, tag 258 without an array, or tag 33000 without an array of two-element arrays.
+- Invalid UTF-8 text.
+- Model value names that are not known to the current TLC model.
+- Any bytes after the first data item.
+
+Decode errors identify a zero-based byte offset. An invalid `FromCBOR` argument, such as a non-sequence or an integer outside `0..255`, produces an argument error instead.
+
+## Round trips and limits
+
+For a value `v` accepted by the encoder, the round-trip equation is:
+
+```tla
+FromCBOR(ToCBOR(v)) = v
+```
+
+The equation requires TLC to be able to compare the elements of each set in `v`, and the domain elements of each function in `v`. TLC cannot compare `1` with `"a"`. `ToCBOR` can encode `{1, "a"}`, but `FromCBOR` rejects those bytes. The same comparability restriction applies to file round trips.
+
+After a successful `CBORSerialize(f, v)`, decoding the unchanged file with `CBORDeserialize(f)` returns `v`, subject to those round-trip requirements. The serialization operator itself returns `TRUE`, not the encoded value.
+
+Sets and function domains must be finite, and TLC must be able to enumerate them. Function arguments and values must themselves be encodable. Operators and other unsupported TLC values cannot be encoded. Integers are limited to TLC's signed 32-bit range.
+
+Strings must contain valid Unicode. The encoder rejects an unpaired UTF-16 surrogate rather than silently replacing it. The decoder rejects malformed UTF-8 rather than inserting a replacement character.
+
+Tag 39 decoding reuses model values already known to TLC, for example values declared in the model's configuration file. It never creates a model value from an input name. Unknown names are errors.
+
+There is no fixed nesting limit. Deep values can exhaust the JVM stack and cause `StackOverflowError` during encoding or decoding. A larger JVM `-Xss` setting can allow deeper values but does not remove that limit.
diff --git a/modules/CBOR.tla b/modules/CBOR.tla
new file mode 100644
index 0000000..049ad1e
--- /dev/null
+++ b/modules/CBOR.tla
@@ -0,0 +1,62 @@
+-------------------------------- MODULE CBOR --------------------------------
+(***************************************************************************)
+(* Encodes TLA+ values as CBOR (RFC 8949) and decodes them back. *)
+(* CBORSerialize and CBORDeserialize exchange values through files. *)
+(* Public ToCBOR and FromCBOR work in memory, without file I/O. *)
+(* ToCBOR returns a sequence of integers in 0..255, one per encoded byte. *)
+(* Use its length to check message sizes, or FromCBOR to interpret bytes. *)
+(* *)
+(* In a module that uses CBOR: *)
+(* EXTENDS Naturals, Sequences, CBOR *)
+(* Message == [id |-> 1, payload |-> <>] *)
+(* Fits == Len(ToCBOR(Message)) <= 1024 *)
+(* Decoded == FromCBOR(<<130, 1, 245>>) \* equals <<1, TRUE>> *)
+(* *)
+(* For files, using the same imports and Message definition: *)
+(* WriteMessage == CBORSerialize("/tmp/message.cbor", Message) *)
+(* ReadMessage == CBORDeserialize("/tmp/message.cbor") *)
+(* *)
+(* Requires CommunityModules.jar or CommunityModules-deps.jar on TLC's *)
+(* classpath. See README.md for setup. *)
+(* Sets and function domains must be finite and enumerable by TLC. *)
+(* Integers must fit TLC's range, and strings must be valid Unicode. *)
+(* Decoding reuses existing model values only. *)
+(* Set elements and function domain elements must be TLC-comparable for *)
+(* round trips. FromCBOR rejects the encoding of {1, "a"}. *)
+(* Unsupported or malformed inputs fail. Deep nesting can exhaust the *)
+(* JVM stack. See docs/CBOR.md in this repository for the full contract. *)
+(***************************************************************************)
+
+LOCAL INSTANCE TLC
+
+(***************************************************************************)
+(* The CBOR encoding of value as a sequence of integers in 0..255, one per *)
+(* byte. If value cannot be encoded (an infinite set, an operator, *)
+(* or a string that is not valid Unicode), TLC reports an error. *)
+(***************************************************************************)
+ToCBOR(value) ==
+ Assert(FALSE, "ToCBOR needs CommunityModules.jar on TLC's classpath.")
+
+(***************************************************************************)
+(* The TLA+ value of the CBOR data item in bytes, a sequence of integers *)
+(* in 0..255. *)
+(***************************************************************************)
+FromCBOR(bytes) ==
+ Assert(FALSE, "FromCBOR needs CommunityModules.jar on TLC's classpath.")
+
+(***************************************************************************)
+(* Writes the bytes of ToCBOR(value) to the file absoluteFilename and *)
+(* equals TRUE. Creates missing parent directories and replaces an *)
+(* existing file. If value cannot be encoded, TLC reports an error and *)
+(* leaves the file untouched. *)
+(***************************************************************************)
+CBORSerialize(absoluteFilename, value) ==
+ Assert(FALSE, "CBORSerialize needs CommunityModules.jar on TLC's classpath.")
+
+(***************************************************************************)
+(* FromCBOR applied to the bytes of the file absoluteFilename. *)
+(***************************************************************************)
+CBORDeserialize(absoluteFilename) ==
+ Assert(FALSE, "CBORDeserialize needs CommunityModules.jar on TLC's classpath.")
+
+=============================================================================
diff --git a/modules/tlc2/overrides/CBOR.java b/modules/tlc2/overrides/CBOR.java
new file mode 100644
index 0000000..0e12c46
--- /dev/null
+++ b/modules/tlc2/overrides/CBOR.java
@@ -0,0 +1,673 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Younes Akhouayri. All rights reserved.
+ *
+ * The MIT License (MIT)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * Contributors:
+ * Younes Akhouayri - initial API and implementation
+ ******************************************************************************/
+package tlc2.overrides;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import tlc2.output.EC;
+import tlc2.tool.EvalException;
+import tlc2.value.Values;
+import tlc2.value.impl.BoolValue;
+import tlc2.value.impl.EnumerableValue;
+import tlc2.value.impl.FcnLambdaValue;
+import tlc2.value.impl.FcnRcdValue;
+import tlc2.value.impl.IntValue;
+import tlc2.value.impl.ModelValue;
+import tlc2.value.impl.RecordValue;
+import tlc2.value.impl.SetEnumValue;
+import tlc2.value.impl.StringValue;
+import tlc2.value.impl.TupleValue;
+import tlc2.value.impl.Value;
+import util.Assert.TLCRuntimeException;
+
+/**
+ * Module overrides for CBOR.tla: encode a finite TLA+ value as CBOR (RFC 8949), as a sequence of bytes or
+ * in a file, and decode it back. CBOR.tla describes the operators. docs/CBOR.md specifies the encoding.
+ *
+ *
There is no intermediate data model. {@link Encoder} walks TLC values and writes bytes, and
+ * {@link Decoder} reads bytes and builds TLC values. The encoder and the decoder's map and tagged-function
+ * paths use {@link #functionValue(FcnRcdValue)} to select a TLC representation. The class depends only on
+ * TLC and the JDK.
+ *
+ *
There is no fixed nesting limit. Deep values may cause a {@link StackOverflowError}.
+ * Increasing -Xss can allow deeper values. The module reports validation and file failures with
+ * {@link EvalException}, rather than Java exceptions that TLC would wrap as error 2154.
+ * A wrong argument type uses the CommunityModules argument codes. Errors about a value or a file use
+ * {@link EC#GENERAL}, whose message TLC prints verbatim. An error that TLC raises itself while the
+ * encoder enumerates a value, such as {x \in Nat : x < 3}, still arrives wrapped.
+ */
+public final class CBOR {
+
+ private CBOR() {
+ }
+
+ // Major types, RFC 8949 section 3.1.
+ private static final int UNSIGNED = 0, NEGATIVE = 1, BYTES = 2, TEXT = 3, ARRAY = 4, MAP = 5, TAG = 6, SIMPLE = 7;
+
+ /** IANA tag 39, "Identifier", around a model value's name. */
+ private static final int TAG_MODEL_VALUE = 39;
+ /** IANA tag 258, "Mathematical finite set", around the array of a set's elements. */
+ private static final int TAG_SET = 258;
+ /**
+ * IANA tag 33000, "TLA+ function as an array of [argument, value] pairs", registered for this module on
+ * 2026-10-08, around the [x, f[x]] pairs of a function that is neither a sequence nor a record.
+ * See docs/CBOR.md for the encoding.
+ */
+ private static final int TAG_FUNCTION = 33000;
+
+ /**
+ * Guards file reads and writes, not encoding, so that a read in one worker never sees half of
+ * another worker's write to the same file.
+ */
+ private static final Object FILES = new Object();
+
+ /** Tuple conversion must precede record conversion, including for empty functions. */
+ private static Value functionValue(final FcnRcdValue f) {
+ final Value tuple = f.toTuple();
+ if (tuple != null) {
+ return tuple;
+ }
+ // toRcd normalizes before checking types, which rejects incomparable domains.
+ for (final Value key : f.getDomainAsValues()) {
+ if (!(key instanceof StringValue)) {
+ return f;
+ }
+ }
+ return f.toRcd();
+ }
+
+ @TLAPlusOperator(identifier = "ToCBOR", module = "CBOR", warn = false)
+ public static TupleValue toCBOR(final Value value) {
+ final byte[] bytes = Encoder.encode(value, "ToCBOR");
+ final Value[] elems = new Value[bytes.length];
+ for (int i = 0; i < elems.length; i++) {
+ elems[i] = IntValue.gen(bytes[i] & 0xff);
+ }
+ return new TupleValue(elems);
+ }
+
+ /** Accepts any value that TLC converts to a sequence, such as [i \in 1..n |-> ...]. */
+ @TLAPlusOperator(identifier = "FromCBOR", module = "CBOR", warn = false)
+ public static Value fromCBOR(final Value bytes) {
+ final Value seq = bytes.toTuple();
+ if (seq == null) {
+ throw notBytes(bytes);
+ }
+ final Value[] elems = ((TupleValue) seq).elems;
+ final byte[] in = new byte[elems.length];
+ for (int i = 0; i < in.length; i++) {
+ final int b = elems[i] instanceof IntValue ? ((IntValue) elems[i]).val : -1;
+ if (b < 0 || b > 255) {
+ throw notBytes(bytes);
+ }
+ in[i] = (byte) b;
+ }
+ return new Decoder(in, "FromCBOR").document();
+ }
+
+ private static EvalException notBytes(final Value v) {
+ return new EvalException(EC.TLC_MODULE_ONE_ARGUMENT_ERROR,
+ new String[] { "FromCBOR", "sequence of integers in 0..255", Values.ppr(v.toString()) });
+ }
+
+ /**
+ * Encodes the whole value before it touches the file, so a value it refuses leaves an existing file
+ * as it was.
+ */
+ @TLAPlusOperator(identifier = "CBORSerialize", module = "CBOR", warn = false)
+ public static BoolValue serialize(final Value absoluteFilename, final Value value) {
+ if (!(absoluteFilename instanceof StringValue)) {
+ throw new EvalException(EC.TLC_MODULE_ARGUMENT_ERROR,
+ new String[] { "first", "CBORSerialize", "string", Values.ppr(absoluteFilename.toString()) });
+ }
+ final String file = ((StringValue) absoluteFilename).val.toString();
+ final byte[] bytes = Encoder.encode(value, "CBORSerialize");
+ synchronized (FILES) {
+ try {
+ final Path path = Paths.get(file);
+ final Path parent = path.toAbsolutePath().getParent();
+ if (parent != null) {
+ Files.createDirectories(parent);
+ }
+ Files.write(path, bytes);
+ } catch (IOException | InvalidPathException e) {
+ throw new EvalException(EC.GENERAL, "CBORSerialize could not write " + file + ": " + e + ".");
+ }
+ }
+ return BoolValue.ValTrue;
+ }
+
+ /**
+ * With the default minLevel, a zero-arity definition such as Log == CBORDeserialize("log.cbor") is
+ * constant-level, so TLC reads the file once, before it starts checking.
+ */
+ @TLAPlusOperator(identifier = "CBORDeserialize", module = "CBOR", warn = false)
+ public static Value deserialize(final Value absoluteFilename) {
+ if (!(absoluteFilename instanceof StringValue)) {
+ throw new EvalException(EC.TLC_MODULE_ONE_ARGUMENT_ERROR,
+ new String[] { "CBORDeserialize", "string", Values.ppr(absoluteFilename.toString()) });
+ }
+ final String file = ((StringValue) absoluteFilename).val.toString();
+ final byte[] bytes;
+ synchronized (FILES) {
+ try {
+ bytes = Files.readAllBytes(Paths.get(file));
+ } catch (NoSuchFileException e) {
+ throw new EvalException(EC.GENERAL,
+ "CBORDeserialize could not read " + file + ": the file does not exist.");
+ } catch (IOException | InvalidPathException e) {
+ throw new EvalException(EC.GENERAL, "CBORDeserialize could not read " + file + ": " + e + ".");
+ }
+ }
+ return new Decoder(bytes, "CBORDeserialize could not read " + file).document();
+ }
+
+ /**
+ * 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 rather than relying on TLC's normalized order. TLC's record and lambda conversions
+ * may normalize their inputs in place.
+ *
+ *
Set elements and the keys of maps and pairs are the sort keys. Each is encoded once into its own
+ * array, sorted, and copied into the parent. Map and pair values stream straight into the output.
+ */
+ private static final class Encoder {
+
+ private final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ private final String operator;
+
+ private Encoder(final String operator) {
+ this.operator = operator;
+ }
+
+ static byte[] encode(final Value v, final String operator) {
+ final Encoder e = new Encoder(operator);
+ e.value(v);
+ return e.out.toByteArray();
+ }
+
+ private void value(final Value v) {
+ if (v instanceof IntValue) {
+ integer(((IntValue) v).val);
+ } else if (v instanceof BoolValue) {
+ out.write(((BoolValue) v).val ? 0xf5 : 0xf4);
+ } else if (v instanceof StringValue) {
+ text(((StringValue) v).val.toString());
+ } else if (v instanceof ModelValue) {
+ head(TAG, TAG_MODEL_VALUE);
+ text(((ModelValue) v).val.toString());
+ } else if (v instanceof TupleValue) {
+ array(((TupleValue) v).elems);
+ } else if (v instanceof RecordValue || v instanceof FcnRcdValue) {
+ function((FcnRcdValue) v.toFcnRcd());
+ } else if (v instanceof FcnLambdaValue) {
+ // The domain, not the function: TLC prints a function's body as its source location.
+ final Value domain = ((FcnLambdaValue) v).getDomain();
+ if (!domain.isFinite()) {
+ throw cannotEncode("a function with the infinite domain", domain);
+ }
+ function((FcnRcdValue) v.toFcnRcd());
+ } else if (v instanceof EnumerableValue) {
+ if (!v.isFinite()) {
+ throw cannotEncode("an infinite set", v);
+ }
+ set(((SetEnumValue) v.toSetEnum()).elems.toArray());
+ } else {
+ throw cannotEncode(v.getKindString(), v);
+ }
+ }
+
+ private void function(final FcnRcdValue f) {
+ final Value form = functionValue(f);
+ if (form instanceof TupleValue) {
+ array(((TupleValue) form).elems);
+ return;
+ }
+ final boolean record = form instanceof RecordValue;
+ final Value[] domain, values;
+ if (record) {
+ final RecordValue r = (RecordValue) form;
+ domain = new Value[r.names.length];
+ for (int i = 0; i < domain.length; i++) {
+ domain[i] = new StringValue(r.names[i]);
+ }
+ values = r.values;
+ } else {
+ domain = f.getDomainAsValues();
+ values = f.values;
+ }
+ final byte[][] keys = encodeEach(domain);
+ final Integer[] order = new Integer[keys.length];
+ for (int i = 0; i < order.length; i++) {
+ order[i] = i;
+ }
+ Arrays.sort(order, (i, j) -> Arrays.compareUnsigned(keys[i], keys[j]));
+ if (record) {
+ head(MAP, keys.length);
+ } else {
+ head(TAG, TAG_FUNCTION);
+ head(ARRAY, keys.length);
+ }
+ for (final int i : order) {
+ if (!record) {
+ head(ARRAY, 2);
+ }
+ write(keys[i]);
+ value(values[i]);
+ }
+ }
+
+ private void array(final Value[] elems) {
+ head(ARRAY, elems.length);
+ for (final Value e : elems) {
+ value(e);
+ }
+ }
+
+ /**
+ * An unnormalized set may hold an element twice, possibly in two Java classes. Equal elements have
+ * equal encodings, so dropping equal neighbours after sorting removes exactly TLC's duplicates.
+ */
+ private void set(final Value[] elems) {
+ final byte[][] items = encodeEach(elems);
+ Arrays.sort(items, Arrays::compareUnsigned);
+ int n = 0;
+ for (final byte[] item : items) {
+ if (n == 0 || !Arrays.equals(item, items[n - 1])) {
+ items[n++] = item;
+ }
+ }
+ head(TAG, TAG_SET);
+ head(ARRAY, n);
+ for (int i = 0; i < n; i++) {
+ write(items[i]);
+ }
+ }
+
+ private byte[][] encodeEach(final Value[] values) {
+ final byte[][] encoded = new byte[values.length][];
+ for (int i = 0; i < values.length; i++) {
+ encoded[i] = encode(values[i], operator);
+ }
+ return encoded;
+ }
+
+ private void integer(final int n) {
+ if (n >= 0) {
+ head(UNSIGNED, n);
+ } else {
+ head(NEGATIVE, -1 - n);
+ }
+ }
+
+ /**
+ * Uses a CharsetEncoder, which reports errors, because String.getBytes turns an unpaired UTF-16
+ * surrogate (SubSeq can cut one out of a string) into '?' without notice.
+ */
+ private void text(final String s) {
+ final ByteBuffer utf8;
+ try {
+ utf8 = StandardCharsets.UTF_8.newEncoder().encode(CharBuffer.wrap(s));
+ } catch (CharacterCodingException e) {
+ throw new EvalException(EC.GENERAL,
+ operator + " cannot encode a string that contains an unpaired UTF-16 surrogate.");
+ }
+ head(TEXT, utf8.remaining());
+ out.write(utf8.array(), utf8.arrayOffset() + utf8.position(), utf8.remaining());
+ }
+
+ /**
+ * The initial byte and the shortest argument. Every argument fits in four bytes: integers are
+ * 32-bit, so -1 - n never exceeds Integer.MAX_VALUE, and lengths are Java array sizes.
+ */
+ private void head(final int major, final int n) {
+ final int ib = major << 5;
+ if (n < 24) {
+ out.write(ib | n);
+ } else if (n <= 0xff) {
+ out.write(ib | 24);
+ out.write(n);
+ } else if (n <= 0xffff) {
+ out.write(ib | 25);
+ out.write(n >>> 8);
+ out.write(n);
+ } else {
+ out.write(ib | 26);
+ out.write(n >>> 24);
+ out.write(n >>> 16);
+ out.write(n >>> 8);
+ out.write(n);
+ }
+ }
+
+ private void write(final byte[] bytes) {
+ out.write(bytes, 0, bytes.length);
+ }
+
+ private EvalException cannotEncode(final String kind, final Value v) {
+ return new EvalException(EC.GENERAL,
+ operator + " cannot encode " + kind + ":\n" + Values.ppr(v.toString()));
+ }
+ }
+
+ /**
+ * Reads one CBOR data item into a TLC integer, boolean, string, sequence, set, record, function,
+ * or existing model value. It accepts non-minimal integer and length encodings, set elements,
+ * map keys, and tag 33000 function arguments in any order, and maps with non-string keys. It rejects duplicate
+ * elements and keys, values TLC cannot represent, and unknown model values, naming the byte offset.
+ *
+ *
Known model values are reused, never created. ModelValue.make at run time leaves ModelValue.mvs
+ * stale, and TLC reads model values back from its disk queues by index into mvs.
+ */
+ private static final class Decoder {
+
+ private final byte[] in;
+ private final String prefix;
+ private int pos;
+ /** The bytes that the unread items of the enclosing arrays and maps need, at one byte an item. */
+ private int owed;
+ private Map modelValues;
+
+ Decoder(final byte[] in, final String prefix) {
+ this.in = in;
+ this.prefix = prefix;
+ }
+
+ Value document() {
+ final Value v = item();
+ if (pos != in.length) {
+ throw error(pos, "there are bytes after the first CBOR data item");
+ }
+ return v;
+ }
+
+ private Value item() {
+ final int start = pos;
+ final int ib = initial();
+ final int major = ib >>> 5;
+ if (major == SIMPLE) {
+ return simple(start, ib & 0x1f);
+ }
+ final long arg = argument(start, major, ib & 0x1f);
+ switch (major) {
+ case UNSIGNED:
+ case NEGATIVE:
+ return integer(start, arg, major == NEGATIVE);
+ case BYTES:
+ throw error(start, "a byte string has no TLA+ counterpart");
+ case TEXT:
+ return new StringValue(text(start, arg));
+ case ARRAY:
+ return new TupleValue(items(count(start, arg, 1)));
+ case MAP:
+ final int n = count(start, arg, 2);
+ final Value[] keys = new Value[n], values = new Value[n];
+ owed += 2 * n;
+ for (int i = 0; i < n; i++) {
+ keys[i] = member();
+ values[i] = member();
+ }
+ return function(start, keys, values);
+ default:
+ return tagged(start, arg);
+ }
+ }
+
+ private Value simple(final int start, final int ai) {
+ switch (ai) {
+ case 20:
+ return BoolValue.ValFalse;
+ case 21:
+ return BoolValue.ValTrue;
+ case 22:
+ throw error(start, "null has no TLA+ counterpart");
+ case 23:
+ throw error(start, "undefined has no TLA+ counterpart");
+ case 25:
+ case 26:
+ case 27:
+ throw error(start, "TLC cannot represent a floating-point number");
+ case 28:
+ case 29:
+ case 30:
+ case 31:
+ throw notWellFormed(start);
+ default:
+ throw error(start, "a CBOR simple value has no TLA+ counterpart");
+ }
+ }
+
+ private Value tagged(final int start, final long tag) {
+ if (tag == TAG_MODEL_VALUE) {
+ final int at = pos;
+ final int ib = initial();
+ if (ib >>> 5 != TEXT) {
+ throw error(start, "tag 39 must enclose a text string");
+ }
+ return modelValue(start, text(at, argument(at, TEXT, ib & 0x1f)));
+ }
+ if (tag == TAG_SET) {
+ return set(start, items(arrayHead(start, "tag 258 must enclose an array")));
+ }
+ if (tag == TAG_FUNCTION) {
+ final String complaint = "tag " + TAG_FUNCTION + " must enclose an array of [key, value] arrays";
+ final int n = arrayHead(start, complaint);
+ final Value[] keys = new Value[n], values = new Value[n];
+ owed += n;
+ for (int i = 0; i < n; i++) {
+ owed--;
+ if (arrayHead(start, complaint) != 2) {
+ throw error(start, complaint);
+ }
+ owed += 2;
+ keys[i] = member();
+ values[i] = member();
+ }
+ return function(start, keys, values);
+ }
+ throw error(start, "tag " + Long.toUnsignedString(tag) + " has no TLA+ counterpart");
+ }
+
+ /** The length of the array that must follow the tag at start. */
+ private int arrayHead(final int start, final String complaint) {
+ final int at = pos;
+ final int ib = initial();
+ if (ib >>> 5 != ARRAY) {
+ throw error(start, complaint);
+ }
+ return count(at, argument(at, ARRAY, ib & 0x1f), 1);
+ }
+
+ private Value function(final int start, final Value[] keys, final Value[] values) {
+ final Integer[] order = distinct(start, keys, "key");
+ final Value[] domain = new Value[keys.length], range = new Value[keys.length];
+ for (int i = 0; i < domain.length; i++) {
+ domain[i] = keys[order[i]];
+ range[i] = values[order[i]];
+ }
+ return functionValue(new FcnRcdValue(domain, range, true));
+ }
+
+ private Value set(final int start, final Value[] elems) {
+ final Integer[] order = distinct(start, elems, "element");
+ final Value[] sorted = new Value[elems.length];
+ for (int i = 0; i < sorted.length; i++) {
+ sorted[i] = elems[order[i]];
+ }
+ return new SetEnumValue(sorted, true);
+ }
+
+ /**
+ * Sorts indices into TLC's order, which lets the caller build normalized values. Equality is TLC's,
+ * not bytewise: 01 and 18 01 are the same element. TLC's compareTo fails on values it cannot
+ * compare (1 and "a"); reporting that here is clearer than a failure at first use in the spec.
+ */
+ private Integer[] distinct(final int start, final Value[] items, final String what) {
+ final Integer[] order = new Integer[items.length];
+ for (int i = 0; i < order.length; i++) {
+ order[i] = i;
+ }
+ Arrays.sort(order, (i, j) -> {
+ try {
+ return items[i].compareTo(items[j]);
+ } catch (TLCRuntimeException e) {
+ throw error(start, "TLC cannot compare the " + what + "s " + ppr(items[Math.min(i, j)]) + " and "
+ + ppr(items[Math.max(i, j)]));
+ }
+ });
+ for (int k = 1; k < order.length; k++) {
+ if (items[order[k - 1]].compareTo(items[order[k]]) == 0) {
+ throw error(start, "the " + what + " " + ppr(items[order[k]]) + " occurs twice");
+ }
+ }
+ return order;
+ }
+
+ /** arg is an unsigned 64-bit number. */
+ private Value integer(final int start, final long arg, final boolean negative) {
+ if (arg < 0 || arg > Integer.MAX_VALUE) {
+ final BigInteger n = new BigInteger(Long.toUnsignedString(arg));
+ throw error(start, "the integer " + (negative ? n.not() : n)
+ + " is outside TLC's range -2147483648..2147483647");
+ }
+ return IntValue.gen(negative ? (int) ~arg : (int) arg);
+ }
+
+ /** A strict decoder: malformed UTF-8 is an error, never U+FFFD. */
+ private String text(final int start, final long length) {
+ final int n = count(start, length, 1);
+ final String s;
+ try {
+ s = StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(in, pos, n)).toString();
+ } catch (CharacterCodingException e) {
+ throw error(start, "a text string is not valid UTF-8");
+ }
+ pos += n;
+ return s;
+ }
+
+ private ModelValue modelValue(final int start, final String name) {
+ if (modelValues == null) {
+ modelValues = new HashMap<>();
+ for (final ModelValue mv : ModelValue.mvs) {
+ modelValues.put(mv.val.toString(), mv);
+ }
+ }
+ final ModelValue mv = modelValues.get(name);
+ if (mv == null) {
+ throw new EvalException(EC.GENERAL,
+ message(start, "the model value " + name + " is not defined in the model")
+ + " Declare it in the .cfg or create it with TLCExt!TLCModelValue(\"" + name + "\").");
+ }
+ return mv;
+ }
+
+ private Value[] items(final int n) {
+ final Value[] items = new Value[n];
+ owed += n;
+ for (int i = 0; i < n; i++) {
+ items[i] = member();
+ }
+ return items;
+ }
+
+ /** The next of the items that an array or a map has added to owed. */
+ private Value member() {
+ owed--;
+ return item();
+ }
+
+ private int initial() {
+ if (pos == in.length) {
+ throw error(pos, "the input ends inside a CBOR data item");
+ }
+ return in[pos++] & 0xff;
+ }
+
+ private long argument(final int start, final int major, final int ai) {
+ if (ai < 24) {
+ return ai;
+ }
+ if (ai == 31 && BYTES <= major && major <= MAP) {
+ throw error(start, "indefinite-length items are not supported");
+ }
+ if (ai > 27) {
+ throw notWellFormed(start);
+ }
+ final int size = 1 << (ai - 24);
+ if (in.length - pos < size) {
+ throw error(start, "the input ends inside a CBOR data item");
+ }
+ long arg = 0;
+ for (int i = 0; i < size; i++) {
+ arg = arg << 8 | in[pos++] & 0xff;
+ }
+ return arg;
+ }
+
+ /**
+ * Checked before anything is allocated. The bytes owed to the enclosing arrays and maps are not
+ * available. Otherwise each nested array could claim the rest of the input, and total allocation
+ * would grow with nesting depth instead of remaining proportional to the input size.
+ */
+ private int count(final int start, final long n, final int minBytesPerItem) {
+ if (n < 0 || n > (in.length - pos - owed) / minBytesPerItem) {
+ throw error(start, "the input ends inside a CBOR data item");
+ }
+ return (int) n;
+ }
+
+ private EvalException notWellFormed(final int start) {
+ return error(start, String.format("the initial byte 0x%02x is not well-formed", in[start] & 0xff));
+ }
+
+ private EvalException error(final int offset, final String problem) {
+ return new EvalException(EC.GENERAL, message(offset, problem));
+ }
+
+ private String message(final int offset, final String problem) {
+ return prefix + ": " + problem + " at byte " + offset + ".";
+ }
+
+ private static String ppr(final Value v) {
+ return Values.ppr(v.toString());
+ }
+ }
+}
diff --git a/modules/tlc2/overrides/TLCOverrides.java b/modules/tlc2/overrides/TLCOverrides.java
index 8938f53..1d1b637 100644
--- a/modules/tlc2/overrides/TLCOverrides.java
+++ b/modules/tlc2/overrides/TLCOverrides.java
@@ -46,7 +46,7 @@ public Class[] get() {
return new Class[] { IOUtils.class, SVG.class, SequencesExt.class, Json.class, Bitwise.class,
FiniteSetsExt.class, Functions.class, CSV.class, Combinatorics.class, BagsExt.class,
DyadicRationals.class, Statistics.class, VectorClocks.class, GraphViz.class, Graphs.class,
- UndirectedGraphs.class };
+ UndirectedGraphs.class, CBOR.class };
} catch (NoClassDefFoundError e) {
// Remove this catch when this Class is moved to `TLC`.
System.out.println("gson dependencies of Json overrides not found, Json module won't work unless "
@@ -54,6 +54,7 @@ public Class[] get() {
}
return new Class[] { IOUtils.class, SVG.class, SequencesExt.class, Bitwise.class, FiniteSetsExt.class,
Functions.class, CSV.class, Combinatorics.class, BagsExt.class, DyadicRationals.class,
- Statistics.class, VectorClocks.class, GraphViz.class, Graphs.class, UndirectedGraphs.class };
+ Statistics.class, VectorClocks.class, GraphViz.class, Graphs.class, UndirectedGraphs.class,
+ CBOR.class };
}
}
diff --git a/tests/AllTests.tla b/tests/AllTests.tla
index 8005e59..dc14bb2 100644
--- a/tests/AllTests.tla
+++ b/tests/AllTests.tla
@@ -8,6 +8,7 @@ EXTENDS RelationTests,
SequencesExtTests,
SVGTests,
JsonTests,
+ CBORTests,
BitwiseTests,
IOUtilsTests,
FiniteSetsExtTests,
diff --git a/tests/CBORTests.tla b/tests/CBORTests.tla
new file mode 100644
index 0000000..6e2600f
--- /dev/null
+++ b/tests/CBORTests.tla
@@ -0,0 +1,423 @@
+----------------------------- MODULE CBORTests -----------------------------
+EXTENDS CBOR, Integers, Sequences, FiniteSets, TLC, TLCExt
+
+(***************************************************************************)
+(* TLC sorts strings and record fields by the order in which it first saw *)
+(* them, not by their text. These strings and field names appear here in *)
+(* the reverse of their CBOR order, before any other use, so TLC's own *)
+(* order disagrees with the CBOR order. An encoder that relies on TLC's *)
+(* order then fails the golden tests "set-strings" and "record". *)
+(***************************************************************************)
+LOCAL CBORInternFirst ==
+ <<"cbor-zz", "cbor-aa", "cbor-a", [cborzz |-> 0, cboraa |-> 0, cbora |-> 0]>>
+
+ASSUME LET T == INSTANCE TLC IN T!PrintT("CBORTests")
+
+ASSUME AssertEq(ToString({"cbor-a", "cbor-zz"}), "{\"cbor-zz\", \"cbor-a\"}")
+ASSUME AssertEq(ToString(DOMAIN [cbora |-> 0, cborzz |-> 0]), "{\"cborzz\", \"cbora\"}")
+
+(***************************************************************************)
+(* A golden test is an ASSUME that calls Golden or SameBytes with a name *)
+(* and the bytes that ToCBOR must return. The JUnit test *)
+(* tests/java/tlc2/overrides/CBORGoldenTest.java, an encoder that shares *)
+(* no code with CBOR.java, checks those bytes against its own encoding of *)
+(* the value it has under that name. *)
+(* *)
+(* All definitions are LOCAL because AllTests extends every test module. *)
+(***************************************************************************)
+
+LOCAL INSTANCE IOUtils
+LOCAL INSTANCE Json
+
+\* The model value that tests/AllTests.cfg defines, without redeclaring the
+\* constant that JsonTests declares.
+LOCAL MV == TLCModelValue("ModelValue")
+
+\* U+00E9, which enters as bytes so that this file stays ASCII.
+LOCAL EAcute == FromCBOR(<<\h62, \hc3, \ha9>>)
+
+LOCAL Golden(name, value, bytes) ==
+ /\ AssertEq(ToCBOR(value), bytes)
+ /\ AssertEq(FromCBOR(bytes), value)
+
+\* TLC-equal values held in different Java classes encode as the same bytes.
+LOCAL SameBytes(name, reps, bytes) == \A i \in DOMAIN reps : AssertEq(ToCBOR(reps[i]), bytes)
+
+\* The bytes of a file as a string, one character per byte. A missing file fails instead of reading as "".
+LOCAL FileText(file) ==
+ LET r == Deserialize(file, [format |-> "TXT", charset |-> "ISO-8859-1"])
+ IN IF r.exitValue = 0 THEN r.stdout ELSE Assert(FALSE, r.stderr)
+
+-----------------------------------------------------------------------------
+
+ASSUME Golden("int", <<0, 23, 24, 255, 256, 65535, 65536, 2147483647,
+ -1, -24, -25, -256, -257, -2147483647 - 1>>,
+ <<\h8e, \h00, \h17, \h18, \h18, \h18, \hff, \h19, \h01, \h00, \h19, \hff, \hff, \h1a, \h00, \h01,
+ \h00, \h00, \h1a, \h7f, \hff, \hff, \hff, \h20, \h37, \h38, \h18, \h38, \hff, \h39, \h01, \h00,
+ \h3a, \h7f, \hff, \hff, \hff>>)
+ASSUME Golden("bool", <>, <<\h82, \hf5, \hf4>>)
+ASSUME Golden("modelvalue", MV, <<\hd8, \h27, \h6a, \h4d, \h6f, \h64, \h65, \h6c, \h56, \h61, \h6c, \h75, \h65>>)
+\* Bytewise order is 10, 100, -1. Length-first order is 10, -1, 100. TLC's order is -1, 10, 100.
+ASSUME Golden("set", {100, -1, 10}, <<\hd9, \h01, \h02, \h83, \h0a, \h18, \h64, \h20>>)
+ASSUME Golden("set-strings", {"cbor-zz", "cbor-a", "cbor-aa"},
+ <<\hd9, \h01, \h02, \h83, \h66, \h63, \h62, \h6f, \h72, \h2d, \h61, \h67, \h63, \h62, \h6f, \h72,
+ \h2d, \h61, \h61, \h67, \h63, \h62, \h6f, \h72, \h2d, \h7a, \h7a>>)
+ASSUME Golden("set-nested", {{}, {1}, {1, 2}, {2}},
+ <<\hd9, \h01, \h02, \h84, \hd9, \h01, \h02, \h80, \hd9, \h01, \h02, \h81, \h01, \hd9, \h01, \h02,
+ \h81, \h02, \hd9, \h01, \h02, \h82, \h01, \h02>>)
+\* In the next three sets, and in "fcn-mixed", the encodings first differ at a byte that is 0x80
+\* or more, which a comparison of signed bytes would order the other way.
+ASSUME Golden("set-wide", {200, 24}, <<\hd9, \h01, \h02, \h82, \h18, \h18, \h18, \hc8>>)
+ASSUME Golden("set-modelvalue", {MV, 1},
+ <<\hd9, \h01, \h02, \h82, \h01, \hd8, \h27, \h6a, \h4d, \h6f, \h64, \h65, \h6c, \h56, \h61, \h6c,
+ \h75, \h65>>)
+ASSUME Golden("set-utf8", {EAcute, "zz"},
+ <<\hd9, \h01, \h02, \h82, \h62, \h7a, \h7a, \h62, \hc3, \ha9>>)
+ASSUME Golden("emptyset", {}, <<\hd9, \h01, \h02, \h80>>)
+ASSUME Golden("interval", {1, 2, 3}, <<\hd9, \h01, \h02, \h83, \h01, \h02, \h03>>)
+ASSUME Golden("seq", <<"a", "b">>, <<\h82, \h61, \h61, \h61, \h62>>)
+ASSUME Golden("empty", <<>>, <<\h80>>)
+ASSUME Golden("record", [cborzz |-> 1, cbora |-> 2, cboraa |-> 3],
+ <<\ha3, \h65, \h63, \h62, \h6f, \h72, \h61, \h02, \h66, \h63, \h62, \h6f, \h72, \h61, \h61, \h03,
+ \h66, \h63, \h62, \h6f, \h72, \h7a, \h7a, \h01>>)
+ASSUME Golden("fcn-int", 0 :> "x" @@ 1 :> "y",
+ <<\hd9, \h80, \he8, \h82, \h82, \h00, \h61, \h78, \h82, \h01, \h61, \h79>>)
+ASSUME Golden("fcn-tuple", [t \in {<<1, 2>>, <<2, 1>>} |-> 3],
+ <<\hd9, \h80, \he8, \h82, \h82, \h82, \h01, \h02, \h03, \h82, \h82, \h02, \h01, \h03>>)
+ASSUME Golden("fcn-set", [s \in SUBSET {1} |-> Cardinality(s)],
+ <<\hd9, \h80, \he8, \h82, \h82, \hd9, \h01, \h02, \h80, \h00, \h82, \hd9, \h01, \h02, \h81, \h01,
+ \h01>>)
+ASSUME Golden("fcn-record", [r \in [a : {1, 2}] |-> r.a],
+ <<\hd9, \h80, \he8, \h82, \h82, \ha1, \h61, \h61, \h01, \h01, \h82, \ha1, \h61, \h61, \h02, \h02>>)
+ASSUME Golden("fcn-modelvalue", [m \in {MV} |-> 0],
+ <<\hd9, \h80, \he8, \h81, \h82, \hd8, \h27, \h6a, \h4d, \h6f, \h64, \h65, \h6c, \h56, \h61, \h6c,
+ \h75, \h65, \h00>>)
+ASSUME Golden("fcn-mixed", [x \in {MV, 1} |-> 0],
+ <<\hd9, \h80, \he8, \h82, \h82, \h01, \h00, \h82, \hd8, \h27, \h6a, \h4d, \h6f, \h64, \h65, \h6c,
+ \h56, \h61, \h6c, \h75, \h65, \h00>>)
+
+LOCAL DumpedTrace ==
+ LET s1 == [x |-> 0, y |-> {}]
+ s2 == [x |-> 1, y |-> {MV}]
+ loc == [beginLine |-> 5, beginColumn |-> 9, endLine |-> 5, endColumn |-> 33, module |-> "T"]
+ IN [counterexample |-> [state |-> {<<1, s1>>, <<2, s2>>},
+ action |-> {<< <<1, s1>>, [name |-> "Next", location |-> loc], <<2, s2>> >>}],
+ vars |-> {"x", "y"}]
+
+ASSUME Golden("trace", DumpedTrace,
+ <<\ha2, \h64, \h76, \h61, \h72, \h73, \hd9, \h01, \h02, \h82, \h61, \h78, \h61, \h79, \h6e, \h63,
+ \h6f, \h75, \h6e, \h74, \h65, \h72, \h65, \h78, \h61, \h6d, \h70, \h6c, \h65, \ha2, \h65, \h73,
+ \h74, \h61, \h74, \h65, \hd9, \h01, \h02, \h82, \h82, \h01, \ha2, \h61, \h78, \h00, \h61, \h79,
+ \hd9, \h01, \h02, \h80, \h82, \h02, \ha2, \h61, \h78, \h01, \h61, \h79, \hd9, \h01, \h02, \h81,
+ \hd8, \h27, \h6a, \h4d, \h6f, \h64, \h65, \h6c, \h56, \h61, \h6c, \h75, \h65, \h66, \h61, \h63,
+ \h74, \h69, \h6f, \h6e, \hd9, \h01, \h02, \h81, \h83, \h82, \h01, \ha2, \h61, \h78, \h00, \h61,
+ \h79, \hd9, \h01, \h02, \h80, \ha2, \h64, \h6e, \h61, \h6d, \h65, \h64, \h4e, \h65, \h78, \h74,
+ \h68, \h6c, \h6f, \h63, \h61, \h74, \h69, \h6f, \h6e, \ha5, \h66, \h6d, \h6f, \h64, \h75, \h6c,
+ \h65, \h61, \h54, \h67, \h65, \h6e, \h64, \h4c, \h69, \h6e, \h65, \h05, \h69, \h62, \h65, \h67,
+ \h69, \h6e, \h4c, \h69, \h6e, \h65, \h05, \h69, \h65, \h6e, \h64, \h43, \h6f, \h6c, \h75, \h6d,
+ \h6e, \h18, \h21, \h6b, \h62, \h65, \h67, \h69, \h6e, \h43, \h6f, \h6c, \h75, \h6d, \h6e, \h09,
+ \h82, \h02, \ha2, \h61, \h78, \h01, \h61, \h79, \hd9, \h01, \h02, \h81, \hd8, \h27, \h6a, \h4d,
+ \h6f, \h64, \h65, \h6c, \h56, \h61, \h6c, \h75, \h65>>)
+
+\* Non-ASCII strings enter as bytes, so this file stays ASCII. The fifth string is U+1F600,
+\* which TLC holds as two UTF-16 code units.
+ASSUME LET bytes == <<\h85, \h60, \h61, \h61, \h62, \hc3, \hbc, \h66, \he6, \h97, \ha5, \he6, \h9c, \hac, \h64, \hf0,
+ \h9f, \h98, \h80>>
+ s == FromCBOR(bytes)
+ IN /\ Golden("string", s, bytes)
+ /\ AssertEq(<>, <<"", "a">>)
+ /\ AssertEq(<>, <<1, 2, 2>>)
+
+\* The bytes of the integer 0 inside n values, one inside the other, that each start with wrap.
+\* A recursive operator cannot build values this deep: TLC's own evaluation overflows the stack.
+LOCAL CBORNested(wrap, n) ==
+ [i \in 1..(n * Len(wrap) + 1) |-> IF i > n * Len(wrap) THEN 0 ELSE wrap[((i - 1) % Len(wrap)) + 1]]
+LOCAL CBORInSeq == <<\h81>>
+LOCAL CBORInRecord == <<\ha1, \h61, \h61>>
+LOCAL CBORInSet == <<\hd9, \h01, \h02, \h81>>
+LOCAL CBORInFcn == <<\hd9, \h80, \he8, \h81, \h82, \h00>>
+
+\* Byte roundtrips beyond the former limits, without recursive value equality.
+ASSUME \A d \in {<>, <>, <>, <>} :
+ LET bytes == CBORNested(d[1], d[2]) IN AssertEq(ToCBOR(FromCBOR(bytes)), bytes)
+
+-----------------------------------------------------------------------------
+
+ASSUME SameBytes("seq", << <<"a", "b">>,
+ [i \in 1..2 |-> IF i = 1 THEN "a" ELSE "b"],
+ 2 :> "b" @@ 1 :> "a",
+ Tail(<<"z", "a", "b">>) >>,
+ <<\h82, \h61, \h61, \h61, \h62>>)
+
+\* JsonDeserialize reads {} as a record without fields.
+ASSUME SameBytes("empty", << <<>>,
+ [x \in {} |-> x],
+ [x \in 1..0 |-> x],
+ [x \in {}, y \in {1} |-> <>],
+ JsonDeserialize("tests/CBORTests/empty-object.json") >>,
+ <<\h80>>)
+
+\* A materialized sequence domain need not arrive in index order.
+ASSUME AssertEq(ToCBOR(3 :> 30 @@ 1 :> 10 @@ 2 :> 20), <<\h83, \h0a, \h14, \h18, \h1e>>)
+
+ASSUME SameBytes("record", << [cbora |-> 2, cboraa |-> 3, cborzz |-> 1],
+ "cbora" :> 2 @@ "cborzz" :> 1 @@ "cboraa" :> 3,
+ [f \in {"cborzz", "cbora", "cboraa"} |->
+ CASE f = "cbora" -> 2 [] f = "cboraa" -> 3 [] OTHER -> 1] >>,
+ <<\ha3, \h65, \h63, \h62, \h6f, \h72, \h61, \h02, \h66, \h63, \h62, \h6f, \h72, \h61, \h61, \h03,
+ \h66, \h63, \h62, \h6f, \h72, \h7a, \h7a, \h01>>)
+
+ASSUME SameBytes("interval", << {3, 1, 2},
+ 1..3,
+ {x \in -5..5 : x > 0 /\ x < 4},
+ {1, 1, 2, 3, 2} >>,
+ <<\hd9, \h01, \h02, \h83, \h01, \h02, \h03>>)
+
+ASSUME SameBytes("set", << {10, 100, 10, -1}, {x \in -1..100 : x \in {-1, 10, 100}} >>,
+ <<\hd9, \h01, \h02, \h83, \h0a, \h18, \h64, \h20>>)
+ASSUME SameBytes("set-nested", << SUBSET {1, 2}, {{1}, {2}} \cup {{}, {1, 2}} >>,
+ <<\hd9, \h01, \h02, \h84, \hd9, \h01, \h02, \h80, \hd9, \h01, \h02, \h81, \h01, \hd9, \h01, \h02,
+ \h81, \h02, \hd9, \h01, \h02, \h82, \h01, \h02>>)
+ASSUME SameBytes("emptyset", << 1..0, {x \in {1} : FALSE}, {1} \ {1} >>,
+ <<\hd9, \h01, \h02, \h80>>)
+ASSUME SameBytes("fcn-int", << [x \in 0..1 |-> IF x = 0 THEN "x" ELSE "y"], 1 :> "y" @@ 0 :> "x" >>,
+ <<\hd9, \h80, \he8, \h82, \h82, \h00, \h61, \h78, \h82, \h01, \h61, \h79>>)
+ASSUME SameBytes("fcn-tuple", << (<<2, 1>> :> 3) @@ (<<1, 2>> :> 3),
+ [t \in ({1, 2} \X {1, 2}) \ {<<1, 1>>, <<2, 2>>} |-> 3] >>,
+ <<\hd9, \h80, \he8, \h82, \h82, \h82, \h01, \h02, \h03, \h82, \h82, \h02, \h01, \h03>>)
+
+-----------------------------------------------------------------------------
+
+\* Encoding does not require TLC to compare heterogeneous elements or arguments.
+ASSUME LET sets == <<{1, "a", 1}, {"a", 1}>>
+ IN \A i \in DOMAIN sets :
+ AssertEq(ToCBOR(sets[i]), <<\hd9, \h01, \h02, \h82, \h01, \h61, \h61>>)
+ASSUME AssertEq(ToCBOR({{1, "a"}, {"a", 1}}),
+ <<\hd9, \h01, \h02, \h81, \hd9, \h01, \h02, \h82, \h01, \h61, \h61>>)
+ASSUME LET functions == <<1 :> 10 @@ MV :> 20, MV :> 20 @@ 1 :> 10>>
+ IN \A i \in DOMAIN functions :
+ AssertEq(ToCBOR(functions[i]),
+ <<\hd9, \h80, \he8, \h82, \h82, \h01, \h0a, \h82, \hd8, \h27, \h6a,
+ \h4d, \h6f, \h64, \h65, \h6c, \h56, \h61, \h6c, \h75, \h65, \h14>>)
+
+\* The values stay aligned with negative arguments when byte order differs from TLC order.
+ASSUME AssertEq(ToCBOR(-1 :> "m" @@ 10 :> "t" @@ 100 :> "h"),
+ <<\hd9, \h80, \he8, \h83, \h82, \h0a, \h61, \h74,
+ \h82, \h18, \h64, \h61, \h68, \h82, \h20, \h61, \h6d>>)
+
+LOCAL RoundTrips(v) == AssertEq(FromCBOR(ToCBOR(v)), v)
+
+\* Re-encoding a decoded value gives the bytes it was decoded from.
+LOCAL Stable(v) == AssertEq(ToCBOR(FromCBOR(ToCBOR(v))), ToCBOR(v))
+
+LOCAL CBORSamples == <<
+ -2147483647 - 1, 2147483647, "",
+ {MV, 1},
+ <<<<>>, {}>>,
+ [x \in {<<>>} |-> 1],
+ [p \in {1, 2} \X {"a", "b"} |-> p[1]],
+ [f \in [{1, 2} -> {TRUE, FALSE}] |-> f[1] /\ f[2]],
+ {[a |-> 1, b |-> {<<1, "x">>}], [a |-> 2, b |-> {}]},
+ <<[c |-> <<>>], {{{}}}, 3 :> [d |-> MV]>>,
+ {TLCModelValue("C_cbor1"), TLCModelValue("C_cbor2")}
+>>
+
+ASSUME \A i \in DOMAIN CBORSamples : RoundTrips(CBORSamples[i]) /\ Stable(CBORSamples[i])
+
+ASSUME \A S \in SUBSET {-2147483647 - 1, -25, -1, 0, 23, 24, 2147483647} : RoundTrips(S)
+ASSUME \A s \in UNION {[1..n -> {"", "a", "cbor-zz"}] : n \in 0..2} : RoundTrips(s)
+ASSUME \A r \in [{"a", "b"} -> BOOLEAN] : RoundTrips(r)
+ASSUME \A f \in [{0, 2} -> {"x", "y"}] : RoundTrips(f)
+ASSUME \A f \in [SUBSET {1, 2} -> {0, 1}] : RoundTrips(f) /\ Stable(f)
+ASSUME \A f \in [{<<1, 2>>, <<2, 1>>} -> BOOLEAN] : RoundTrips(f) /\ Stable(f)
+ASSUME \A f \in [{[a |-> 1], [a |-> 2]} -> {1, 2}] : RoundTrips(f)
+ASSUME \A f \in [{MV, 1} -> {MV, "s"}] : RoundTrips(f) /\ Stable(f)
+ASSUME RoundTrips(SUBSET SUBSET {1, 2}) /\ Stable(SUBSET SUBSET {1, 2})
+ASSUME RoundTrips(DumpedTrace) /\ Stable(DumpedTrace)
+
+\* Comparing function arguments must also compare their nested function domains correctly.
+ASSUME LET f == ((0 :> 10 @@ 2 :> 20) :> 1) @@ ((2 :> 20 @@ 0 :> 11) :> 2)
+ decoded == FromCBOR(ToCBOR(f))
+ IN /\ RoundTrips(f) /\ Stable(f)
+ /\ AssertEq(decoded[2 :> 20 @@ 0 :> 10], 1)
+ /\ AssertEq(decoded[0 :> 11 @@ 2 :> 20], 2)
+
+\* Decoded values are marked normalized, so their order must be TLC's: TLC finds an
+\* element of a normalized set, and an argument of a normalized function with at
+\* least 32 entries, by bisection.
+ASSUME LET s == FromCBOR(<<\hd9, \h01, \h02, \h83, \h0a, \h20, \h18, \h64>>)
+ IN /\ \A x \in {-1, 10, 100} : x \in s
+ /\ 0 \notin s
+ASSUME LET f == FromCBOR(ToCBOR([x \in 0..40 |-> -x])) IN \A x \in 0..40 : f[x] = -x
+ASSUME LET t == FromCBOR(ToCBOR(DumpedTrace))
+ IN /\ Cardinality(t.counterexample.state) = 2
+ /\ \E p \in t.counterexample.state : p[1] = 2 /\ MV \in p[2].y
+ /\ t.vars = {"x", "y"}
+ASSUME LET f == FromCBOR(ToCBOR([t \in {<<1, 2>>, <<2, 1>>} |-> 3]))
+ IN DOMAIN f = {<<1, 2>>, <<2, 1>>} /\ f[<<2, 1>>] = 3
+
+-----------------------------------------------------------------------------
+
+\* FromCBOR reads bytes that TLC never writes, and ToCBOR writes the value it read in the one
+\* canonical form.
+LOCAL Accepts(bytes, value) ==
+ /\ AssertEq(FromCBOR(bytes), value)
+ /\ AssertEq(ToCBOR(FromCBOR(bytes)), ToCBOR(value))
+
+\* Set elements, map keys, and pairs in any order, including the length-first order of
+\* cbor2's canonical=True.
+ASSUME Accepts(<<\hd9, \h01, \h02, \h82, \h02, \h01>>, {1, 2})
+ASSUME Accepts(<<\hd9, \h01, \h02, \h83, \h0a, \h20, \h18, \h64>>, {100, -1, 10})
+ASSUME Accepts(<<\ha2, \h61, \h62, \h01, \h61, \h61, \h02>>, [a |-> 2, b |-> 1])
+ASSUME Accepts(<<\hd9, \h80, \he8, \h82, \h82, \h02, \h01, \h82, \h01, \h00>>, <<0, 1>>)
+\* Integers and lengths in a wider form than needed.
+ASSUME Accepts(<<\h1b, \h00, \h00, \h00, \h00, \h00, \h00, \h00, \h01>>, 1)
+ASSUME Accepts(<<\h98, \h01, \h78, \h01, \h61>>, <<"a">>)
+\* Maps whose keys are not strings, as Python writes a dict.
+ASSUME Accepts(<<\ha2, \h00, \h61, \h78, \h01, \h61, \h79>>, 0 :> "x" @@ 1 :> "y")
+ASSUME Accepts(<<\ha2, \h01, \h61, \h61, \h02, \h61, \h62>>, <<"a", "b">>)
+ASSUME Accepts(<<\ha1, \h82, \h01, \h02, \h03>>, <<1, 2>> :> 3)
+\* Pairs that form a record, and the empty function as an empty map and as no pairs.
+ASSUME Accepts(<<\hd9, \h80, \he8, \h81, \h82, \h61, \h61, \h01>>, [a |-> 1])
+ASSUME Accepts(<<\ha0>>, <<>>)
+ASSUME Accepts(<<\hd9, \h80, \he8, \h80>>, <<>>)
+
+-----------------------------------------------------------------------------
+
+\* CBORSerialize writes the bytes of ToCBOR and creates missing parent directories. The bytes
+\* of "CBOR" read as text: 0x64, the head of a text string of four bytes, is the letter d.
+ASSUME /\ CBORSerialize("build/cbor/a/b/c/text.cbor", "CBOR")
+ /\ AssertEq(ToCBOR("CBOR"), <<\h64, \h43, \h42, \h4f, \h52>>)
+ /\ AssertEq(FileText("build/cbor/a/b/c/text.cbor"), "dCBOR")
+
+\* CBORDeserialize reads a file as FromCBOR reads its bytes.
+ASSUME \A i \in DOMAIN CBORSamples :
+ /\ CBORSerialize("build/cbor/roundtrip.cbor", CBORSamples[i])
+ /\ AssertEq(CBORDeserialize("build/cbor/roundtrip.cbor"), FromCBOR(ToCBOR(CBORSamples[i])))
+
+\* A shorter value replaces a longer one; leftover bytes would be rejected as trailing.
+ASSUME /\ CBORSerialize("build/cbor/overwrite.cbor", 1..100)
+ /\ CBORSerialize("build/cbor/overwrite.cbor", 0)
+ /\ AssertEq(CBORDeserialize("build/cbor/overwrite.cbor"), 0)
+
+-----------------------------------------------------------------------------
+
+\* AssertError needs its message as a literal.
+
+ASSUME AssertError("FromCBOR: the input ends inside a CBOR data item at byte 0.",
+ FromCBOR(<<>>))
+\* An array of two items that holds one.
+ASSUME AssertError("FromCBOR: the input ends inside a CBOR data item at byte 0.",
+ FromCBOR(<<\h82, \h01>>))
+ASSUME AssertError("FromCBOR: the input ends inside a CBOR data item at byte 0.",
+ FromCBOR(<<\h1a, \h00, \h00>>))
+\* An array that claims 2^31 - 1 items is refused before anything is allocated.
+ASSUME AssertError("FromCBOR: the input ends inside a CBOR data item at byte 0.",
+ FromCBOR(<<\h9a, \h7f, \hff, \hff, \hff>>))
+ASSUME AssertError("FromCBOR: there are bytes after the first CBOR data item at byte 1.",
+ FromCBOR(<<\h01, \h01>>))
+\* The inner array claims the three bytes that the outer array still needs for its other items.
+ASSUME AssertError("FromCBOR: the input ends inside a CBOR data item at byte 1.",
+ FromCBOR(<<\h84, \h83, \h00, \h00, \h00>>))
+\* The offset names the innermost offending item.
+ASSUME AssertError("FromCBOR: null has no TLA+ counterpart at byte 2.",
+ FromCBOR(<<\h82, \h01, \hf6>>))
+ASSUME AssertError("FromCBOR: TLC cannot represent a floating-point number at byte 0.",
+ FromCBOR(<<\hf9, \h3c, \h00>>))
+ASSUME AssertError("FromCBOR: null has no TLA+ counterpart at byte 0.",
+ FromCBOR(<<\hf6>>))
+ASSUME AssertError("FromCBOR: undefined has no TLA+ counterpart at byte 0.",
+ FromCBOR(<<\hf7>>))
+ASSUME AssertError("FromCBOR: a CBOR simple value has no TLA+ counterpart at byte 0.",
+ FromCBOR(<<\hf0>>))
+ASSUME AssertError("FromCBOR: a byte string has no TLA+ counterpart at byte 0.",
+ FromCBOR(<<\h41, \h00>>))
+ASSUME AssertError("FromCBOR: indefinite-length items are not supported at byte 0.",
+ FromCBOR(<<\h9f, \h01, \hff>>))
+ASSUME AssertError("FromCBOR: indefinite-length items are not supported at byte 0.",
+ FromCBOR(<<\h7f, \h61, \h61, \hff>>))
+ASSUME AssertError("FromCBOR: the initial byte 0x1c is not well-formed at byte 0.",
+ FromCBOR(<<\h1c>>))
+ASSUME AssertError("FromCBOR: the initial byte 0xff is not well-formed at byte 0.",
+ FromCBOR(<<\hff>>))
+ASSUME AssertError("FromCBOR: tag 1 has no TLA+ counterpart at byte 0.",
+ FromCBOR(<<\hc1, \h00>>))
+\* A bignum.
+ASSUME AssertError("FromCBOR: tag 2 has no TLA+ counterpart at byte 0.",
+ FromCBOR(<<\hc2, \h41, \h01>>))
+ASSUME AssertError("FromCBOR: tag 39 must enclose a text string at byte 0.",
+ FromCBOR(<<\hd8, \h27, \h01>>))
+ASSUME AssertError("FromCBOR: tag 258 must enclose an array at byte 0.",
+ FromCBOR(<<\hd9, \h01, \h02, \ha0>>))
+ASSUME AssertError("FromCBOR: tag 33000 must enclose an array of [key, value] arrays at byte 0.",
+ FromCBOR(<<\hd9, \h80, \he8, \ha0>>))
+ASSUME AssertError("FromCBOR: tag 33000 must enclose an array of [key, value] arrays at byte 0.",
+ FromCBOR(<<\hd9, \h80, \he8, \h81, \h01>>))
+ASSUME AssertError("FromCBOR: the integer 2147483648 is outside TLC's range -2147483648..2147483647 at byte 0.",
+ FromCBOR(<<\h1a, \h80, \h00, \h00, \h00>>))
+ASSUME AssertError("FromCBOR: the integer -2147483649 is outside TLC's range -2147483648..2147483647 at byte 0.",
+ FromCBOR(<<\h3a, \h80, \h00, \h00, \h00>>))
+ASSUME AssertError("FromCBOR: the integer 18446744073709551615 is outside TLC's range -2147483648..2147483647 at byte 0.",
+ FromCBOR(<<\h1b, \hff, \hff, \hff, \hff, \hff, \hff, \hff, \hff>>))
+ASSUME AssertError("FromCBOR: the integer -18446744073709551616 is outside TLC's range -2147483648..2147483647 at byte 0.",
+ FromCBOR(<<\h3b, \hff, \hff, \hff, \hff, \hff, \hff, \hff, \hff>>))
+ASSUME AssertError("FromCBOR: a text string is not valid UTF-8 at byte 0.",
+ FromCBOR(<<\h61, \hff>>))
+\* U+D800 encoded as if it were a character.
+ASSUME AssertError("FromCBOR: a text string is not valid UTF-8 at byte 0.",
+ FromCBOR(<<\h63, \hed, \ha0, \h80>>))
+ASSUME AssertError("FromCBOR: the key \"a\" occurs twice at byte 0.",
+ FromCBOR(<<\ha2, \h61, \h61, \h01, \h61, \h61, \h02>>))
+\* The second 1 is not in shortest form: duplicates are found by TLC equality, not by bytes.
+ASSUME AssertError("FromCBOR: the element 1 occurs twice at byte 0.",
+ FromCBOR(<<\hd9, \h01, \h02, \h82, \h01, \h18, \h01>>))
+\* An empty array and an empty map both denote the empty function.
+ASSUME AssertError("FromCBOR: the element <<>> occurs twice at byte 0.",
+ FromCBOR(<<\hd9, \h01, \h02, \h82, \h80, \ha0>>))
+ASSUME AssertError("FromCBOR: the key 0 occurs twice at byte 0.",
+ FromCBOR(<<\hd9, \h80, \he8, \h82, \h82, \h00, \h01, \h82, \h00, \h02>>))
+ASSUME AssertError("FromCBOR: TLC cannot compare the elements 1 and \"a\" at byte 0.",
+ FromCBOR(<<\hd9, \h01, \h02, \h82, \h01, \h61, \h61>>))
+ASSUME AssertError("FromCBOR: TLC cannot compare the keys 1 and \"a\" at byte 0.",
+ FromCBOR(<<\ha2, \h01, \h00, \h61, \h61, \h00>>))
+ASSUME AssertError("FromCBOR: the model value p99 is not defined in the model at byte 0. Declare it in the .cfg or create it with TLCExt!TLCModelValue(\"p99\").",
+ FromCBOR(<<\hd8, \h27, \h63, \h70, \h39, \h39>>))
+
+ASSUME AssertError("The argument of FromCBOR should be a sequence of integers in 0..255, but instead it is:\n42",
+ FromCBOR(42))
+ASSUME AssertError("The argument of FromCBOR should be a sequence of integers in 0..255, but instead it is:\n<<1, 256>>",
+ FromCBOR(<<1, 256>>))
+ASSUME AssertError("The argument of FromCBOR should be a sequence of integers in 0..255, but instead it is:\n<<-1>>",
+ FromCBOR(<<-1>>))
+ASSUME AssertError("The argument of FromCBOR should be a sequence of integers in 0..255, but instead it is:\n<<\"a\">>",
+ FromCBOR(<<"a">>))
+
+\* The offending value is named even when it is nested.
+ASSUME AssertError("ToCBOR cannot encode a special set constant:\nNat",
+ ToCBOR([a |-> {Nat}]))
+ASSUME AssertError("ToCBOR cannot encode an infinite set:\nSUBSET Nat",
+ ToCBOR(SUBSET Nat))
+ASSUME AssertError("ToCBOR cannot encode a function with the infinite domain:\nNat",
+ ToCBOR([x \in Nat |-> x]))
+\* SubSeq cuts U+1F600 in half, leaving an unpaired surrogate that UTF-8 cannot carry.
+ASSUME AssertError("ToCBOR cannot encode a string that contains an unpaired UTF-16 surrogate.",
+ ToCBOR(SubSeq(FromCBOR(<<\h64, \hf0, \h9f, \h98, \h80>>), 1, 1)))
+
+ASSUME AssertError("CBORDeserialize could not read tests/CBORTests/missing.cbor: the file does not exist.",
+ CBORDeserialize("tests/CBORTests/missing.cbor"))
+\* A decoding error names the file. The JSON text {} starts with 0x7b, the head of a text string
+\* whose length takes the next eight bytes.
+ASSUME AssertError("CBORDeserialize could not read tests/CBORTests/empty-object.json: the input ends inside a CBOR data item at byte 0.",
+ CBORDeserialize("tests/CBORTests/empty-object.json"))
+ASSUME AssertError("The first argument of CBORSerialize should be a string, but instead it is:\n42",
+ CBORSerialize(42, 1))
+ASSUME AssertError("The argument of CBORDeserialize should be a string, but instead it is:\n42",
+ CBORDeserialize(42))
+
+\* A refused value leaves the existing file as it was.
+ASSUME /\ CBORSerialize("build/cbor/refused.cbor", "CBOR")
+ /\ AssertError("CBORSerialize cannot encode a special set constant:\nNat",
+ CBORSerialize("build/cbor/refused.cbor", <<"a", Nat>>))
+ /\ AssertEq(FileText("build/cbor/refused.cbor"), "dCBOR")
+
+=============================================================================
diff --git a/tests/CBORTests/empty-object.json b/tests/CBORTests/empty-object.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/tests/CBORTests/empty-object.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/java/tlc2/overrides/CBORGoldenTest.java b/tests/java/tlc2/overrides/CBORGoldenTest.java
new file mode 100644
index 0000000..8f79285
--- /dev/null
+++ b/tests/java/tlc2/overrides/CBORGoldenTest.java
@@ -0,0 +1,322 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Younes Akhouayri. All rights reserved.
+ *
+ * The MIT License (MIT)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * Contributors:
+ * Younes Akhouayri - initial API and implementation
+ ******************************************************************************/
+package tlc2.overrides;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.junit.Test;
+
+/**
+ * Checks the golden bytes in tests/CBORTests.tla against a second CBOR encoder. {@code ant test} runs it on
+ * every operating system before TLC, and it needs JUnit alone.
+ *
+ *
The encoder below is a second implementation of the encoding table in modules/CBOR.tla that shares no
+ * code with CBOR.java. CBORTests.tla states each golden as an ASSUME that calls Golden or SameBytes with the
+ * golden's name and its bytes as a TLA+ sequence of hex literals ({@code <<\h82, \h61, ...>>}). TLC checks
+ * that ToCBOR writes those bytes; this test checks that they are the bytes this encoder writes for the value
+ * of the same name. It fails on a mismatch, printing the expected sequence, on a name it does not know, and
+ * on a value it has that no ASSUME states.
+ */
+public class CBORGoldenTest {
+
+ private static final int TAG_MODEL_VALUE = 39;
+ private static final int TAG_SET = 258;
+ private static final int TAG_FUNCTION = 33000;
+
+ private static final Comparator BYTEWISE = Arrays::compareUnsigned;
+
+ private static final class Mv {
+ final String name;
+
+ Mv(final String name) {
+ this.name = name;
+ }
+ }
+
+ private static final class Set {
+ final Object[] elems;
+
+ Set(final Object... elems) {
+ this.elems = elems;
+ }
+ }
+
+ /** A function given by its (x, f[x]) pairs. */
+ private static final class Fn {
+ final Object[][] pairs;
+
+ Fn(final Object[]... pairs) {
+ this.pairs = pairs;
+ }
+ }
+
+ private static Object[] pair(final Object x, final Object fx) {
+ return new Object[] { x, fx };
+ }
+
+ private static Map record(final Object... keysAndValues) {
+ final Map r = new LinkedHashMap<>();
+ for (int i = 0; i < keysAndValues.length; i += 2) {
+ r.put((String) keysAndValues[i], keysAndValues[i + 1]);
+ }
+ return r;
+ }
+
+ private static byte[] head(final int major, final long n) {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ if (n < 24) {
+ out.write(major << 5 | (int) n);
+ return out.toByteArray();
+ }
+ final int ai = n <= 0xFFL ? 24 : n <= 0xFFFFL ? 25 : n <= 0xFFFFFFFFL ? 26 : 27;
+ final int width = 1 << ai - 24;
+ out.write(major << 5 | ai);
+ for (int i = width - 1; i >= 0; i--) {
+ out.write((int) (n >>> 8 * i));
+ }
+ return out.toByteArray();
+ }
+
+ private static byte[] keyed(final List