nx_cbor.nx source
↩ module page · 226 lines · 7834 B
1// nx_cbor.nx -- RFC 8949 Concise Binary Object Representation, minimal
2// encoder for the substrate's machine-readable wire format.
3//
4// CBOR is the IETF-standard binary JSON-equivalent (RFC 8949). Same
5// information model as JSON, ~30-50% smaller on the wire, deterministic
6// byte-output, zero schema infrastructure. Used by COSE, CoAP, Dapr,
7// and IPLD; chosen here over Parquet/Arrow because:
8// * spans are small + irregularly-shaped (CBOR strength)
9// * we want sovereign-no-deps (one file, pure NishiLang)
10// * the same logical span can ship to JSONL (human) AND CBOR (machine)
11//
12// Spec: RFC 8949 https://www.rfc-editor.org/rfc/rfc8949.html
13//
14// Major types implemented (subset sufficient for spans):
15// 0 unsigned integer
16// 1 negative integer (encoded as -1-value, unsigned)
17// 4 array of N items
18// 5 map of N pairs
19//
20// Major types deferred (queued):
21// 2 byte string
22// 3 text string (UTF-8)
23// 6 tag
24// 7 floats / simple values
25//
26// genealogy_id: bormann_hoffman_2013_cbor + rfc_8949
27// lineage_id: substrate_native_cbor_v1
28
29// nx_safety_envelope:
30// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
31// sil_target: SIL1
32// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
33// verdict: NOT_YET_EVALUATED
34
35import "nx_syscalls.nx"
36import "nx_tier.nx"
37
38// ===== Major-type bit-pattern constants ============================
39
40const NX_CBOR_MAJ_UINT: nx_int = 0 // 0x00 | length
41const NX_CBOR_MAJ_NINT: nx_int = 1 // 0x20 | length
42const NX_CBOR_MAJ_ARRAY: nx_int = 4 // 0x80 | length
43const NX_CBOR_MAJ_MAP: nx_int = 5 // 0xA0 | length
44
45// Length-marker thresholds (inline 0..23, 1-byte 24, 2-byte 25, 4-byte
46// 26, 8-byte 27).
47const NX_CBOR_LEN_INLINE_MAX: nx_int = 24
48const NX_CBOR_LEN_BYTE_1: nx_int = 24
49const NX_CBOR_LEN_BYTE_2: nx_int = 25
50const NX_CBOR_LEN_BYTE_4: nx_int = 26
51const NX_CBOR_LEN_BYTE_8: nx_int = 27
52
53const NX_CBOR_U8_MAX: nx_int = 256
54const NX_CBOR_U16_MAX: nx_int = 65536
55const NX_CBOR_U32_MAX: nx_int = 4294967296
56
57// Helper: 2^n via repeated multiplication (small n only). Hoisted
58// above nx_cbor_put_length because nxc2's resolver is single-pass.
59func _nx_cbor_pow2(n: nx_int) -> nx_int {
60 var v: nx_int = 1
61 var i: nx_int = 0
62 while i < n {
63 v = v * 2
64 i = i + 1
65 }
66 return v
67}
68
69// ===== Raw byte append (bounds-unchecked; caller sizes buf) ========
70//
71// Per the substrate convention (nx_json_emit), caller pre-allocates
72// the buffer and tracks pos; we never realloc.
73
74func nx_cbor_put_byte(buf: *u8, pos: nx_int, b: nx_int) -> nx_int {
75 buf[pos] = b
76 return pos + 1
77}
78
79// ===== Length-prefix encoder (major type | length-class) ============
80//
81// Emits the header for any major-type value with the given numeric
82// length. Returns updated pos.
83
84func nx_cbor_put_length(buf: *u8, pos: nx_int, major: nx_int, value: nx_int) -> nx_int {
85 let mhi: nx_int = major * 32 // major << 5
86
87 if value < NX_CBOR_LEN_INLINE_MAX {
88 return nx_cbor_put_byte(buf, pos, mhi + value)
89 }
90
91 if value < NX_CBOR_U8_MAX {
92 var p: nx_int = nx_cbor_put_byte(buf, pos, mhi + NX_CBOR_LEN_BYTE_1)
93 p = nx_cbor_put_byte(buf, p, value)
94 return p
95 }
96
97 if value < NX_CBOR_U16_MAX {
98 var p: nx_int = nx_cbor_put_byte(buf, pos, mhi + NX_CBOR_LEN_BYTE_2)
99 p = nx_cbor_put_byte(buf, p, (value / 256) % 256) // hi
100 p = nx_cbor_put_byte(buf, p, value % 256) // lo
101 return p
102 }
103
104 if value < NX_CBOR_U32_MAX {
105 var p: nx_int = nx_cbor_put_byte(buf, pos, mhi + NX_CBOR_LEN_BYTE_4)
106 p = nx_cbor_put_byte(buf, p, (value / 16777216) % 256)
107 p = nx_cbor_put_byte(buf, p, (value / 65536) % 256)
108 p = nx_cbor_put_byte(buf, p, (value / 256) % 256)
109 p = nx_cbor_put_byte(buf, p, value % 256)
110 return p
111 }
112
113 // 8-byte path: write 8 big-endian bytes of value.
114 var p2: nx_int = nx_cbor_put_byte(buf, pos, mhi + NX_CBOR_LEN_BYTE_8)
115 var shift: nx_int = 56
116 var v: nx_int = value
117 while shift >= 0 {
118 let byte_val: nx_int = (v / _nx_cbor_pow2(shift)) % 256
119 p2 = nx_cbor_put_byte(buf, p2, byte_val)
120 shift = shift - 8
121 }
122 return p2
123}
124
125// ===== Unsigned integer ============================================
126
127func nx_cbor_emit_u64(buf: *u8, pos: nx_int, value: nx_int) -> nx_int {
128 if value < 0 { return pos } // caller error; bail. Use emit_i64.
129 return nx_cbor_put_length(buf, pos, NX_CBOR_MAJ_UINT, value)
130}
131
132// ===== Signed integer ==============================================
133//
134// CBOR negative ints encode as major-type-1 with payload = -1 - value.
135// e.g. -1 -> majtype=1, payload=0. Positive ints route to major 0.
136
137func nx_cbor_emit_i64(buf: *u8, pos: nx_int, value: nx_int) -> nx_int {
138 if value >= 0 {
139 return nx_cbor_put_length(buf, pos, NX_CBOR_MAJ_UINT, value)
140 }
141 let payload: nx_int = (0 - value) - 1
142 return nx_cbor_put_length(buf, pos, NX_CBOR_MAJ_NINT, payload)
143}
144
145// ===== Array header ================================================
146
147func nx_cbor_emit_array_header(buf: *u8, pos: nx_int, n_elements: nx_int) -> nx_int {
148 return nx_cbor_put_length(buf, pos, NX_CBOR_MAJ_ARRAY, n_elements)
149}
150
151// ===== Map header ==================================================
152
153func nx_cbor_emit_map_header(buf: *u8, pos: nx_int, n_pairs: nx_int) -> nx_int {
154 return nx_cbor_put_length(buf, pos, NX_CBOR_MAJ_MAP, n_pairs)
155}
156
157// ===== Span emit (high-level) =====================================
158//
159// Encodes one trace span as a CBOR map of 7 named fields. Keys are
160// short u8 codes so the wire stays tiny:
161//
162// 0 trace_id
163// 1 span_id
164// 2 parent_span_id
165// 3 kind
166// 4 start_ms
167// 5 end_ms
168// 6 attrs (array of [k, v] pairs, k+v both i64)
169//
170// Returns updated pos.
171
172const NX_CBOR_SPAN_KEY_TRACE: nx_int = 0
173const NX_CBOR_SPAN_KEY_SPAN: nx_int = 1
174const NX_CBOR_SPAN_KEY_PARENT: nx_int = 2
175const NX_CBOR_SPAN_KEY_KIND: nx_int = 3
176const NX_CBOR_SPAN_KEY_START: nx_int = 4
177const NX_CBOR_SPAN_KEY_END: nx_int = 5
178const NX_CBOR_SPAN_KEY_ATTRS: nx_int = 6
179const NX_CBOR_SPAN_N_PAIRS: nx_int = 7
180
181func nx_cbor_emit_span(buf: *u8, pos: nx_int,
182 trace_id: nx_int, span_id: nx_int,
183 parent: nx_int, kind: nx_int,
184 start_ms: nx_int, end_ms: nx_int,
185 attrs: *i64, n_attrs: nx_int) -> nx_int {
186 var p: nx_int = nx_cbor_emit_map_header(buf, pos, NX_CBOR_SPAN_N_PAIRS)
187
188 p = nx_cbor_emit_u64(buf, p, NX_CBOR_SPAN_KEY_TRACE)
189 p = nx_cbor_emit_i64(buf, p, trace_id)
190
191 p = nx_cbor_emit_u64(buf, p, NX_CBOR_SPAN_KEY_SPAN)
192 p = nx_cbor_emit_i64(buf, p, span_id)
193
194 p = nx_cbor_emit_u64(buf, p, NX_CBOR_SPAN_KEY_PARENT)
195 p = nx_cbor_emit_i64(buf, p, parent)
196
197 p = nx_cbor_emit_u64(buf, p, NX_CBOR_SPAN_KEY_KIND)
198 p = nx_cbor_emit_i64(buf, p, kind)
199
200 p = nx_cbor_emit_u64(buf, p, NX_CBOR_SPAN_KEY_START)
201 p = nx_cbor_emit_i64(buf, p, start_ms)
202
203 p = nx_cbor_emit_u64(buf, p, NX_CBOR_SPAN_KEY_END)
204 p = nx_cbor_emit_i64(buf, p, end_ms)
205
206 p = nx_cbor_emit_u64(buf, p, NX_CBOR_SPAN_KEY_ATTRS)
207 p = nx_cbor_emit_array_header(buf, p, n_attrs)
208 var i: nx_int = 0
209 while i < n_attrs {
210 p = nx_cbor_emit_array_header(buf, p, 2)
211 p = nx_cbor_emit_i64(buf, p, attrs[2 * i])
212 p = nx_cbor_emit_i64(buf, p, attrs[2 * i + 1])
213 i = i + 1
214 }
215
216 return p
217}
218
219// ===== Flush to fd =================================================
220//
221// Helper to write the encoded bytes to a file descriptor. Returns
222// the syscall's byte count (>= 0) or negative errno.
223
224func nx_cbor_flush_fd(fd: nx_int, buf: *u8, n: nx_int) -> nx_int {
225 return sys_write(fd, buf, n)
226}