Skip to content

Commit 91e439c

Browse files
feat: store exact bytes in fixed arrays
1 parent deac134 commit 91e439c

62 files changed

Lines changed: 2503 additions & 807 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cddl-matrix/roadmap.toml

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ entries = [
149149
"matrix.heterogeneous-array.occurrence-bounds",
150150
"matrix.open-array.optional-adjacency",
151151
"matrix.heterogeneous-array.atomic-bounds-handover",
152-
"matrix.fixed-bytes.static-representation",
153152
"matrix.fixed-array.static-representation",
154153
"shared-carrier-delivery-requirements",
155154
"matrix.inline-group-occurrence-carrier",
@@ -685,29 +684,6 @@ verification_md = '''Probe h'cafe', b64'yv4=', h'CAFE', and a raw UTF-8 control
685684
due_action_md = '''Perform the ordered closeout actions only after fresh evidence satisfies the external predicate and the verification succeeds; otherwise retain the closeout and its accommodations.
686685
'''
687686

688-
[[record]]
689-
id = "matrix.fixed-bytes.static-representation"
690-
title = "Fixed-byte representation optimization"
691-
692-
[record.payload]
693-
kind = "work"
694-
body_md = '''### Fixed-byte representation optimization
695-
696-
`bytes .size 32` can map to `[u8; 32]`: the representation itself carries the constraint. The
697-
`uint .size 1` → `u8` mapping is the shipped precedent, independently of the `TryFrom` door, which
698-
stays as the ergonomic entry point.
699-
700-
'''
701-
work_state = "ready"
702-
work_intent = "optimize"
703-
work_kind = "optimization"
704-
risk = "wrong_public_api"
705-
acceptance_md = '''Map an exact bytes .size N value to [u8; N], retain TryFrom as the ergonomic entry point, and cover Rust, wasm, JSON, component, cross-crate, matrix, fixed-member, and recombination surfaces without changing fixed-array campaign identity.
706-
'''
707-
priority_band = "normal"
708-
priority_rationale_md = '''The exact byte-array representation carries its length invariant statically and has the shipped uint .size 1 to u8 precedent, while remaining an independent optimization.
709-
'''
710-
711687
[[record]]
712688
id = "matrix.fixed-bytes.validator-corroboration"
713689
title = "When a rust `cddl` release fixes fixed-byte CBOR validation (README gap #17)"
@@ -2595,11 +2571,6 @@ source = "matrix.extern-execution-multifile"
25952571
kind = "parent_of"
25962572
target = "matrix.multifile.extern-shape-coverage"
25972573

2598-
[[relation]]
2599-
source = "matrix.fixed-array.static-representation"
2600-
kind = "complements"
2601-
target = "matrix.fixed-bytes.static-representation"
2602-
26032574
[[relation]]
26042575
source = "matrix.fixed-map-key-kinds"
26052576
kind = "related"
@@ -2610,11 +2581,6 @@ source = "matrix.fixed-map-key-kinds"
26102581
kind = "related"
26112582
target = "matrix.nint-capability"
26122583

2613-
[[relation]]
2614-
source = "matrix.heterogeneous-array.occurrence-bounds"
2615-
kind = "parent_of"
2616-
target = "matrix.fixed-bytes.static-representation"
2617-
26182584
[[relation]]
26192585
source = "matrix.heterogeneous-array.occurrence-bounds"
26202586
kind = "parent_of"

docs/docs/command_line_flags.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ cddl-codegen --input=example --output=export --binary-wrappers true
210210
:::info `--no-synthesized-rust-collection-aliases`
211211
Suppresses emission of Rust `pub type` aliases for generator-**synthesized** collection wrappers — currently a table rule's auto-named keys-list, e.g. `pub type FooList = Vec<Foo>;` minted for `tbl = { * foo => uint }`. Off by default.
212212

213-
Rule-declared aliases are never suppressed, even when structurally transparent: an explicitly authored `foo_list = [* foo]` or `signature = bytes .size 32` is a human-written name and always stays. The suppression is emission-only — generated code references collections structurally (`Vec<Foo>`), never through the alias, so no field type or serialization changes, and the crate still compiles. Wasm-side wrappers and aliases are untouched.
213+
Rule-declared aliases are never suppressed, even when structurally transparent: an explicitly authored `foo_list = [* foo]` or `payload = bytes` is a human-written name and always stays. The suppression is emission-only — generated code references collections structurally (`Vec<Foo>`), never through the alias, so no field type or serialization changes, and the crate still compiles. Wasm-side wrappers and aliases are untouched. An exact byte rule such as `signature = bytes .size 32` is not this case: it is a checked wrapper whose native inner carrier is `[u8; 32]`.
214214

215215
This is a flag rather than a default change because it removes public API (the crate's own synthesized aliases, not just extern-element ones) that existing consumers may depend on.
216216

docs/docs/component_differences.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ There are **no wrapper classes** on this face. An array field is `list<t>`, a ma
6464
gaps second; the borrow-parameter rule above covers the ownership half, and WIT's type system has
6565
neither of the gaps (nested lists, lists of handles and nested `option`s are all expressible).
6666

67+
An exact `bytes .size N` remains `list<u8>` in WIT even though native storage is `[u8; N]`.
68+
Direct constructors retain their loose list door; an optional or nested list/table position converts
69+
each list at the consuming boundary and returns the native `RangeCheck` through its `result` on a
70+
wrong length. Getters make the reverse array-to-list projection.
71+
6772
The consequence is that **constraints move from the type to the door**. A `[+ t]` non-empty,
6873
bounded, or `@duplicates reject` **set** all despecialize to a plain `list<…>`, so the invariant
6974
is re-checked where the list is *consumed*: the constructor or setter returns

docs/docs/current_capacities.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,12 @@ sidebar_position: 3
104104
> group `A`. Rename either symbol to avoid the collision. The scoped-symbol provenance grid needed
105105
> to remove this limitation is tracked in the matrix roadmap.
106106
107-
* Length bounds - `foo = bytes .size (0..32)`
107+
* Length bounds - `foo = bytes .size (0..32)`. An **exact** byte window (`bytes .size N`,
108+
including zero) is stored as `[u8; N]`; non-exact byte windows remain `Vec<u8>`. Constructors
109+
deliberately accept a `Vec<u8>` and return `RangeCheck` on a wrong length before atomically
110+
converting it to the static carrier. JSON follows the same invariant, including exact schema
111+
cardinality. This is specific to byte strings: exact homogeneous CDDL array occurrences still
112+
use their existing collection carriers.
108113
* Type-enforced homogeneous collections - bare `[* T]` maps to `Vec<T>` and `[+ T]` / `1* T` to `NonEmptyVec<T>`; every other supported array occurrence window maps to `BoundedVec<T, MIN, MAX>`, except `@duplicates reject`, which maps to `BoundedOrderedSet<T, MIN, MAX>` and enforces both order-preserving uniqueness and bounds at one door. A unique-key table maps `{ * K => V }` to a loose map, `{ + K => V }` to `NonEmptyMap<K, V>`, and every other window (including omitted exact-once) to `BoundedMap<K, V, MIN, MAX>`. A preserve table uses `PairMap` / `NonEmptyPairMap` for those loose forms and `BoundedPairMap<K, V, MIN, MAX>` for every other window; duplicate keys count separately and retain entry order. The bounded carriers route loose API, CBOR, JSON, wasm, and component input through their `TryFrom` door and expose no mutable carrier. See [Output format](output_format#non-empty-containers).
109114
* Integer width via `.size` - `u = uint .size 1` maps to `u8`, `uint .size 8` to `u64`, etc. `.size` on a signed `int` is **rejected gracefully**: per the spec semantics clarified by the RFC author ([cbor-wg/cddl#32](https://github.com/cbor-wg/cddl/issues/32)), `int = uint / nint` and a control distributes over the choice with `.size` undefined (never-matching) on `nint`, so `int .size N` means exactly the `uint .size N` window `0...(256**N)` — a signed `i{8N}` reading would mis-enforce it in both directions, and the aligned unsigned reading is spelled `uint .size N` directly. If you mean an N-byte signed integer, use the explicit range (e.g. `-9223372036854775808..9223372036854775807` maps to `i64`), which is fully supported.
110115
* Value ranges - integer (`foo = -10..3`, `bar = int .le 10`) and float (`f = 0.5..10.5`, `g = float64 .lt 10.5`) windows are both enforced (float checks are NaN-safe; see the range note below for the boundaries)

docs/docs/examples.mdx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ pub struct Limitations {
269269
pub u_64: u64,
270270
pub i_8: i8,
271271
pub i_64: i64,
272-
pub hash32: Vec<u8>,
272+
pub hash32: [u8; 32],
273273
pub bounded: String,
274274
}
275275

@@ -283,8 +283,24 @@ impl Limitations {
283283
i_64: i64,
284284
hash32: Vec<u8>,
285285
bounded: String,
286-
) -> Self {
287-
Self {
286+
) -> Result<Self, DeserializeError> {
287+
let hash32: [u8; 32] =
288+
hash32
289+
.try_into()
290+
.map_err(|bytes: Vec<u8>| DeserializeFailure::RangeCheck {
291+
found: bytes.len() as i128,
292+
min: Some(32),
293+
max: Some(32),
294+
})?;
295+
if bounded.len() < 10 || bounded.len() > 20 {
296+
return Err(DeserializeFailure::RangeCheck {
297+
found: bounded.len() as i128,
298+
min: Some(10),
299+
max: Some(20),
300+
}
301+
.into());
302+
}
303+
Ok(Self {
288304
u_8,
289305
u_16,
290306
u_32,
@@ -293,7 +309,7 @@ impl Limitations {
293309
i_64,
294310
hash32,
295311
bounded,
296-
}
312+
})
297313
}
298314
}
299315
```
@@ -1150,4 +1166,3 @@ impl TypeChoice {
11501166

11511167

11521168

1153-

docs/docs/wasm_differences.mdx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@ rather than bare vectors.
128128

129129
Secondarily, the wrapper also covers element types `wasm_bindgen` can't put in a bare `Vec` at all:
130130
`Vec<bool>` (no `VectorIntoWasmAbi` for bool) and doubly-nested types like `Vec<Vec<T>>` (which
131-
includes any array of byte strings, since `bytes` is already `Vec<u8>`).
131+
includes any array of byte strings, since a non-exact `bytes` value is already `Vec<u8>`).
132+
An exact `bytes .size N` is `[u8; N]` in Rust but still crosses wasm as a `Vec<u8>`: getters copy
133+
the array into that list, while a direct constructor may keep the loose list door and collection
134+
`add`/`insert` paths return `JsError` if their stored-carrier handover finds a wrong length.
132135

133136
Text arrays do **not** get a list wrapper: `wasm_bindgen` exposes `Vec<String>` directly (a JS
134137
string array, in both parameter and return position), and strings are **copied** at the boundary,

0 commit comments

Comments
 (0)