Skip to main content

brink_format/inkb/
write.rs

1//! Encoding (write) half of the `.inkb` binary format.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5
6use crate::codec::{
7    crc32, write_def_id, write_i32, write_str, write_u8, write_u16, write_u32, write_u64,
8};
9use crate::definition::{
10    AddressDef, AddressPath, AliasEntry, CallAtom, CapabilityParam, ContainerDef, DirectEffects,
11    EffectRowEntry, ExternalFnDef, FrameShapeDef, GlobalVarDef, LineEntry, ListDef, ListItemDef,
12    ScopeLineTable, StructShapeDef,
13};
14use crate::id::DefinitionId;
15use crate::line::{LineContent, LinePart, PluralCategory, SelectKey};
16use crate::story::StoryData;
17use crate::value::{ListValue, MapKey, ProjSegment, Value, ValueType};
18
19use super::{
20    CAP_PARAM_ANY, CAT_FEW, CAT_MANY, CAT_ONE, CAT_OTHER, CAT_TWO, CAT_ZERO, HANDLE_PARAM_NONE,
21    HEADER_PREAMBLE, KEY_CARDINAL, KEY_EXACT, KEY_KEYWORD, KEY_ORDINAL, LINE_PLAIN, LINE_TEMPLATE,
22    MAGIC, PART_LITERAL, PART_SELECT, PART_SLOT, PART_SPAN, PROJ_SEG_INDEX, PROJ_SEG_KEY,
23    SECTION_COUNT, SECTION_ENTRY_SIZE, SectionKind, VAL_ARRAY, VAL_BOOL, VAL_CLOSURE,
24    VAL_DIVERT_TARGET, VAL_FLOAT, VAL_FN_REF, VAL_FRAGMENT_REF, VAL_HANDLE, VAL_INT, VAL_LIST,
25    VAL_MAP, VAL_MAT2, VAL_MAT3, VAL_MAT4, VAL_NULL, VAL_OPTION, VAL_PROJECTION, VAL_QUAT,
26    VAL_RANGE, VAL_RECORD, VAL_STRING, VAL_VAR_POINTER, VAL_VEC2, VAL_VEC3, VAL_VEC4, VAL_WEIGHTED,
27    VERSION,
28};
29
30// ── Tier 1: Full story write ────────────────────────────────────────────────
31
32/// Encode a [`StoryData`] into the `.inkb` binary format with sectioned header.
33#[expect(clippy::cast_possible_truncation)]
34pub fn write_inkb(story: &StoryData, buf: &mut Vec<u8>) {
35    let base = buf.len();
36
37    // The `Visibility` section (M-2b, tag `0x0E`) is **optional**: emitted
38    // only when the story has `#@private` definitions. All-public stories —
39    // the entire pre-modules world — omit it, so their offset table stays
40    // at `SECTION_COUNT` entries (which now includes the mandatory M-3
41    // `AliasTable` section, always present — possibly empty — from v5
42    // onward; see `SectionKind::AliasTable`).
43    let has_visibility = !story.private_defs.is_empty();
44    // The `FrameShapes` section (FS-3, tag `0x10`) is likewise **optional**:
45    // emitted only when the story carries `await` frame shapes. Behind the
46    // E052 fence no `await` compiles, so this is always empty today and every
47    // existing story omits it (byte-identical, no `VERSION` bump).
48    let has_frame_shapes = !story.frame_shapes.is_empty();
49    let section_count =
50        SECTION_COUNT as usize + usize::from(has_visibility) + usize::from(has_frame_shapes);
51    let header_size = HEADER_PREAMBLE + section_count * SECTION_ENTRY_SIZE;
52
53    // Write placeholder header (zeros) — we'll patch it after writing sections.
54    buf.resize(base + header_size, 0);
55
56    // Track (kind, offset) pairs as we write each section, in canonical
57    // tag order. The offset table is self-describing (count + per-entry tag),
58    // so a conditionally-omitted section is fully readable.
59    let mut sections: Vec<(SectionKind, u32)> = Vec::with_capacity(section_count);
60
61    macro_rules! section {
62        ($kind:expr, $write:expr) => {{
63            let offset = (buf.len() - base) as u32;
64            $write;
65            sections.push(($kind, offset));
66        }};
67    }
68
69    section!(
70        SectionKind::NameTable,
71        write_section_name_table(&story.name_table, buf)
72    );
73    section!(
74        SectionKind::Variables,
75        write_section_variables(&story.variables, buf)
76    );
77    section!(
78        SectionKind::ListDefs,
79        write_section_list_defs(&story.list_defs, buf)
80    );
81    section!(
82        SectionKind::ListItems,
83        write_section_list_items(&story.list_items, buf)
84    );
85    section!(
86        SectionKind::Externals,
87        write_section_externals(&story.externals, buf)
88    );
89    section!(
90        SectionKind::Containers,
91        write_section_containers(&story.containers, buf)
92    );
93    section!(
94        SectionKind::LineTables,
95        write_section_line_tables(&story.line_tables, buf)
96    );
97    section!(
98        SectionKind::Labels,
99        write_section_addresses(&story.addresses, buf)
100    );
101    section!(
102        SectionKind::ListLiterals,
103        write_section_list_literals(&story.list_literals, buf)
104    );
105    section!(
106        SectionKind::AddressPaths,
107        write_section_address_paths(&story.address_paths, buf)
108    );
109    section!(
110        SectionKind::LiteralPool,
111        write_section_literal_pool(&story.literal_pool, buf)
112    );
113    section!(
114        SectionKind::StructShapes,
115        write_section_struct_shapes(&story.struct_shapes, buf)
116    );
117    // EffectRows (T2-3, tag 0x0D) is mandatory — always present (possibly
118    // empty), section-locally versioned. Emitted between StructShapes and the
119    // optional Visibility section so tags stay in canonical ascending order.
120    section!(
121        SectionKind::EffectRows,
122        write_section_effect_rows(&story.effect_rows, buf)
123    );
124    if has_visibility {
125        section!(
126            SectionKind::Visibility,
127            write_section_visibility(&story.private_defs, buf)
128        );
129    }
130    // AliasTable (M-3) is mandatory — always present (possibly empty) from
131    // v5 onward, unlike the optional `Visibility` section above.
132    section!(
133        SectionKind::AliasTable,
134        write_section_alias_table(&story.alias_table, buf)
135    );
136    // FrameShapes (FS-3, tag 0x10) is optional — emitted last (highest tag) so
137    // the offset table stays in canonical ascending tag order, and omitted
138    // entirely when empty so existing stories stay byte-identical.
139    if has_frame_shapes {
140        section!(
141            SectionKind::FrameShapes,
142            write_section_frame_shapes(&story.frame_shapes, buf)
143        );
144    }
145
146    let file_size = (buf.len() - base) as u32;
147    let checksum = crc32(&buf[base + header_size..]);
148
149    // Patch header in-place.
150    let h = &mut buf[base..];
151    h[0..4].copy_from_slice(MAGIC);
152    h[4..6].copy_from_slice(&VERSION.to_le_bytes());
153    h[6] = section_count as u8;
154    h[7] = 0; // reserved
155    h[8..12].copy_from_slice(&file_size.to_le_bytes());
156    h[12..16].copy_from_slice(&checksum.to_le_bytes());
157
158    for (i, (kind, offset)) in sections.iter().enumerate() {
159        let entry_base = HEADER_PREAMBLE + i * SECTION_ENTRY_SIZE;
160        h[entry_base] = *kind as u8;
161        h[entry_base + 1] = 0; // reserved
162        h[entry_base + 2] = 0;
163        h[entry_base + 3] = 0;
164        h[entry_base + 4..entry_base + 8].copy_from_slice(&offset.to_le_bytes());
165    }
166}
167
168// ── Assembly ────────────────────────────────────────────────────────────────
169
170/// Assemble a complete `.inkb` file from pre-encoded section buffers.
171///
172/// Sections should be provided in the canonical order matching [`SectionKind`]
173/// tags. The header (with offsets and checksum) is computed automatically.
174#[expect(clippy::cast_possible_truncation)]
175pub fn assemble_inkb(sections: &[(SectionKind, &[u8])], out: &mut Vec<u8>) {
176    let base = out.len();
177    let section_count = sections.len() as u8;
178    let header_size = HEADER_PREAMBLE + sections.len() * SECTION_ENTRY_SIZE;
179
180    // Placeholder header.
181    out.resize(base + header_size, 0);
182
183    // Append section data and record offsets.
184    let mut entries: Vec<(SectionKind, u32)> = Vec::with_capacity(sections.len());
185    for (kind, data) in sections {
186        let offset = (out.len() - base) as u32;
187        entries.push((*kind, offset));
188        out.extend_from_slice(data);
189    }
190
191    let file_size = (out.len() - base) as u32;
192    let checksum = crc32(&out[base + header_size..]);
193
194    // Patch header.
195    let h = &mut out[base..];
196    h[0..4].copy_from_slice(MAGIC);
197    h[4..6].copy_from_slice(&VERSION.to_le_bytes());
198    h[6] = section_count;
199    h[7] = 0;
200    h[8..12].copy_from_slice(&file_size.to_le_bytes());
201    h[12..16].copy_from_slice(&checksum.to_le_bytes());
202
203    for (i, (kind, offset)) in entries.iter().enumerate() {
204        let entry_base = HEADER_PREAMBLE + i * SECTION_ENTRY_SIZE;
205        h[entry_base] = *kind as u8;
206        h[entry_base + 1] = 0;
207        h[entry_base + 2] = 0;
208        h[entry_base + 3] = 0;
209        h[entry_base + 4..entry_base + 8].copy_from_slice(&offset.to_le_bytes());
210    }
211}
212
213// ── Section writers ─────────────────────────────────────────────────────────
214
215/// Write the name table section (no header framing).
216#[expect(clippy::cast_possible_truncation)]
217pub fn write_section_name_table(names: &[String], buf: &mut Vec<u8>) {
218    write_u32(buf, names.len() as u32);
219    for name in names {
220        write_str(buf, name);
221    }
222}
223
224/// Write the variables section (no header framing).
225#[expect(clippy::cast_possible_truncation)]
226pub fn write_section_variables(variables: &[GlobalVarDef], buf: &mut Vec<u8>) {
227    write_u32(buf, variables.len() as u32);
228    for var in variables {
229        encode_global_var(var, buf);
230    }
231}
232
233/// Write the list definitions section (no header framing).
234#[expect(clippy::cast_possible_truncation)]
235pub fn write_section_list_defs(list_defs: &[ListDef], buf: &mut Vec<u8>) {
236    write_u32(buf, list_defs.len() as u32);
237    for ld in list_defs {
238        encode_list_def(ld, buf);
239    }
240}
241
242/// Write the list items section (no header framing).
243#[expect(clippy::cast_possible_truncation)]
244pub fn write_section_list_items(list_items: &[ListItemDef], buf: &mut Vec<u8>) {
245    write_u32(buf, list_items.len() as u32);
246    for li in list_items {
247        encode_list_item(li, buf);
248    }
249}
250
251/// Write the externals section (no header framing).
252#[expect(clippy::cast_possible_truncation)]
253pub fn write_section_externals(externals: &[ExternalFnDef], buf: &mut Vec<u8>) {
254    write_u32(buf, externals.len() as u32);
255    for ext in externals {
256        encode_external(ext, buf);
257    }
258}
259
260/// Write the containers section (no header framing).
261#[expect(clippy::cast_possible_truncation)]
262pub fn write_section_containers(containers: &[ContainerDef], buf: &mut Vec<u8>) {
263    write_u32(buf, containers.len() as u32);
264    for c in containers {
265        encode_container(c, buf);
266    }
267}
268
269/// Write the addresses section (no header framing).
270#[expect(clippy::cast_possible_truncation)]
271pub fn write_section_addresses(addresses: &[AddressDef], buf: &mut Vec<u8>) {
272    write_u32(buf, addresses.len() as u32);
273    for addr in addresses {
274        write_def_id(buf, addr.id);
275        write_def_id(buf, addr.container_id);
276        write_u32(buf, addr.byte_offset);
277    }
278}
279
280/// Write the address-paths section (no header framing).
281#[expect(clippy::cast_possible_truncation)]
282pub fn write_section_address_paths(address_paths: &[AddressPath], buf: &mut Vec<u8>) {
283    write_u32(buf, address_paths.len() as u32);
284    for ap in address_paths {
285        write_u16(buf, ap.path.0);
286        write_def_id(buf, ap.target);
287    }
288}
289
290/// Write the visibility section (no header framing): a count followed by the
291/// `DefinitionId` of every `#@private` definition (M-2b). Callers only emit
292/// this section when `private_defs` is non-empty.
293#[expect(clippy::cast_possible_truncation)]
294pub fn write_section_visibility(private_defs: &[DefinitionId], buf: &mut Vec<u8>) {
295    write_u32(buf, private_defs.len() as u32);
296    for id in private_defs {
297        write_def_id(buf, *id);
298    }
299}
300
301// ── Encode helpers (private) ────────────────────────────────────────────────
302
303fn encode_global_var(v: &GlobalVarDef, buf: &mut Vec<u8>) {
304    write_def_id(buf, v.id);
305    write_u16(buf, v.name.0);
306    encode_value_type(v.value_type, buf);
307    encode_value(&v.default_value, buf);
308    write_u8(buf, u8::from(v.mutable));
309    write_u8(buf, u8::from(v.local));
310}
311
312fn encode_value_type(vt: ValueType, buf: &mut Vec<u8>) {
313    let tag = match vt {
314        ValueType::Int => VAL_INT,
315        ValueType::Float => VAL_FLOAT,
316        ValueType::Bool => VAL_BOOL,
317        ValueType::String => VAL_STRING,
318        ValueType::List => VAL_LIST,
319        ValueType::DivertTarget => VAL_DIVERT_TARGET,
320        ValueType::VariablePointer => VAL_VAR_POINTER,
321        // TempPointer is runtime-only and should never appear in .inkb files.
322        ValueType::FragmentRef => VAL_FRAGMENT_REF,
323        ValueType::TempPointer | ValueType::Null => VAL_NULL,
324        // Collection value types (v4, `docs/format-v4-rfc.md` §1).
325        ValueType::Array => VAL_ARRAY,
326        ValueType::Map => VAL_MAP,
327        // TM-4 record value type (v4, reserved tag graduated this PR).
328        ValueType::Record => VAL_RECORD,
329        // T1c function value types (v4, materialized in #700).
330        ValueType::FnRef => VAL_FN_REF,
331        ValueType::Closure => VAL_CLOSURE,
332        // T1d handle value type (v4, reserved tag graduated this PR).
333        ValueType::Handle => VAL_HANDLE,
334        // T1e projection value type (v4, reserved tag graduated this PR).
335        ValueType::Projection => VAL_PROJECTION,
336        // NS-A1 Option value type.
337        ValueType::Option => VAL_OPTION,
338        // NS-A5 range value type (F7).
339        ValueType::Range => VAL_RANGE,
340        // NS-A8 numeric tower value types.
341        ValueType::Vec2 => VAL_VEC2,
342        ValueType::Vec3 => VAL_VEC3,
343        ValueType::Vec4 => VAL_VEC4,
344        ValueType::Quat => VAL_QUAT,
345        ValueType::Mat2 => VAL_MAT2,
346        ValueType::Mat3 => VAL_MAT3,
347        ValueType::Mat4 => VAL_MAT4,
348        // NS-A7 weighted table value type.
349        ValueType::Weighted => VAL_WEIGHTED,
350    };
351    write_u8(buf, tag);
352}
353
354/// NS-A8 (`docs/tower-mini-spec.md` T5): write tower lanes as explicit
355/// little-endian f32s, one by one — the hand-serialized wire form. The
356/// caller supplies the lanes in the pinned order (vec/quat `x, y(, z, w)`;
357/// matrices column-major via `to_cols_array`), always from glam's explicit
358/// array conversions — never from glam's memory representation.
359fn write_f32_lanes(buf: &mut Vec<u8>, lanes: &[f32]) {
360    for lane in lanes {
361        buf.extend_from_slice(&lane.to_le_bytes());
362    }
363}
364
365#[expect(clippy::cast_possible_truncation)]
366#[expect(
367    clippy::too_many_lines,
368    reason = "one match arm per value variant — the NS-A1 Option arm pushed this past 100"
369)]
370fn encode_value(v: &Value, buf: &mut Vec<u8>) {
371    match v {
372        Value::Int(n) => {
373            write_u8(buf, VAL_INT);
374            write_i32(buf, *n);
375        }
376        Value::Float(n) => {
377            write_u8(buf, VAL_FLOAT);
378            buf.extend_from_slice(&n.to_le_bytes());
379        }
380        Value::Bool(b) => {
381            write_u8(buf, VAL_BOOL);
382            write_u8(buf, u8::from(*b));
383        }
384        Value::String(s) => {
385            write_u8(buf, VAL_STRING);
386            write_str(buf, s);
387        }
388        Value::List(lv) => {
389            write_u8(buf, VAL_LIST);
390            write_u32(buf, lv.items.len() as u32);
391            for item in &lv.items {
392                write_def_id(buf, *item);
393            }
394            write_u32(buf, lv.origins.len() as u32);
395            for origin in &lv.origins {
396                write_def_id(buf, *origin);
397            }
398        }
399        Value::DivertTarget(id) => {
400            write_u8(buf, VAL_DIVERT_TARGET);
401            write_def_id(buf, *id);
402        }
403        Value::VariablePointer(id) => {
404            write_u8(buf, VAL_VAR_POINTER);
405            write_def_id(buf, *id);
406        }
407        Value::FragmentRef(idx) => {
408            write_u8(buf, VAL_FRAGMENT_REF);
409            write_u32(buf, *idx);
410        }
411        // TempPointer is runtime-only and should never appear in .inkb files.
412        Value::TempPointer { .. } | Value::Null => {
413            write_u8(buf, VAL_NULL);
414        }
415        // Collections encode as trees (v4, `docs/format-v4-rfc.md` §1): a length
416        // prefix then the recursively-encoded elements / key-value pairs. Arc
417        // sharing is deliberately not preserved on the wire (value-model-spec §5).
418        Value::Array(items) => {
419            write_u8(buf, VAL_ARRAY);
420            write_u32(buf, items.len() as u32);
421            for item in items.iter() {
422                encode_value(item, buf);
423            }
424        }
425        Value::Map(map) => {
426            write_u8(buf, VAL_MAP);
427            write_u32(buf, map.len() as u32);
428            // Insertion order is semantic (keys restricted to int/string/bool).
429            for (key, val) in map.iter() {
430                encode_map_key(key, buf);
431                encode_value(val, buf);
432            }
433        }
434        // TM-4 (`docs/format-v4-rfc.md` §1): `ShapeId` then field values in
435        // shape order — no field names on the wire (they live once, in the
436        // `StructShapes` section entry the shape id references).
437        Value::Record { shape, fields } => {
438            write_u8(buf, VAL_RECORD);
439            write_u32(buf, shape.0);
440            write_u32(buf, fields.len() as u32);
441            for field in fields.iter() {
442                encode_value(field, buf);
443            }
444        }
445        // Function values (T1c, `docs/format-v4-rfc.md` §1). `FnRef` is just
446        // the fn token; `Closure` adds a u16-counted env of `{NameId, kind u8,
447        // value}` entries — the named/moded env is the redundancy rehydration
448        // validation reads (spec §6).
449        Value::FnRef(target) => {
450            write_u8(buf, VAL_FN_REF);
451            write_def_id(buf, *target);
452        }
453        Value::Closure(c) => {
454            write_u8(buf, VAL_CLOSURE);
455            write_def_id(buf, c.target);
456            write_u16(buf, c.env.len() as u16);
457            for entry in &c.env {
458                write_u16(buf, entry.name.0);
459                write_u8(buf, u8::from(entry.is_ref));
460                encode_value(&entry.payload, buf);
461            }
462        }
463        // Handle values (T1d, `docs/format-v4-rfc.md` §1: `kind NameId, u64
464        // id`). First emission of this reserved tag — the wire form is frozen
465        // by the RFC, materialized here. No opcode ever pushes one; a handle
466        // reaches this encoder only as a binding-produced global default or a
467        // literal-pool entry supplied by a future manifest-aware pipeline.
468        Value::Handle { kind, id } => {
469            write_u8(buf, VAL_HANDLE);
470            write_u16(buf, kind.0);
471            write_u64(buf, *id);
472        }
473        // Projection values (T1e, `docs/format-v4-rfc.md` §1: "cell
474        // reference, u8 segment count, then segments"). First emission of
475        // this reserved tag. Segment kind `2=range` is RESERVED and never
476        // written — `ProjSegment` has no variant to produce it.
477        Value::Projection(p) => {
478            write_u8(buf, VAL_PROJECTION);
479            write_def_id(buf, p.cell);
480            write_u8(buf, p.segments.len() as u8);
481            for seg in &p.segments {
482                encode_proj_segment(seg, buf);
483            }
484        }
485        // Option values (NS-A1, `docs/stdlib-spec.md` §1.4): flag byte
486        // (0 = none, 1 = some) then the inner value when some. No opcode
487        // literal ever produces one at compile time today (`none`/`some(x)`
488        // lower to `PushNone`/`MakeSome`, and a bare-`none` declaration
489        // default is the E107 compile error), so like `VAL_HANDLE` above
490        // this encoder leg exists for wire completeness — saves/transcripts
491        // are the live consumers.
492        Value::OptionVal(inner) => {
493            write_u8(buf, VAL_OPTION);
494            match inner {
495                None => write_u8(buf, 0),
496                Some(v) => {
497                    write_u8(buf, 1);
498                    encode_value(v, buf);
499                }
500            }
501        }
502        // Range values (NS-A5, F7): start i32, end i32, inclusive flag —
503        // the *written* form is preserved on the wire (1..=6 and 1..7 are
504        // content-equal but round-trip their own spelling). Flat: no
505        // recursion, no depth accounting.
506        Value::Range {
507            start,
508            end,
509            inclusive,
510        } => {
511            write_u8(buf, VAL_RANGE);
512            write_i32(buf, *start);
513            write_i32(buf, *end);
514            write_u8(buf, u8::from(*inclusive));
515        }
516        // Tower values (NS-A8, `docs/tower-mini-spec.md` T5): explicit
517        // little-endian f32 lanes in the pinned order — vectors and the
518        // quat `x, y(, z, w)`, matrices column-major column-by-column via
519        // glam's `to_cols_array` (an explicit conversion, never a memory
520        // cast). Fixed sizes, no counts, no recursion. Like `VAL_HANDLE`
521        // above, no opcode literal produces one at compile time today
522        // (construction is the runtime `Tower` opcode) — saves, transcripts
523        // and future const-folding are the wire consumers.
524        Value::Vec2(v) => {
525            write_u8(buf, VAL_VEC2);
526            write_f32_lanes(buf, &v.to_array());
527        }
528        Value::Vec3(v) => {
529            write_u8(buf, VAL_VEC3);
530            write_f32_lanes(buf, &v.to_array());
531        }
532        Value::Vec4(v) => {
533            write_u8(buf, VAL_VEC4);
534            write_f32_lanes(buf, &v.to_array());
535        }
536        Value::Quat(q) => {
537            write_u8(buf, VAL_QUAT);
538            write_f32_lanes(buf, &q.to_array());
539        }
540        Value::Mat2(m) => {
541            write_u8(buf, VAL_MAT2);
542            write_f32_lanes(buf, &m.to_cols_array());
543        }
544        Value::Mat3(m) => {
545            write_u8(buf, VAL_MAT3);
546            write_f32_lanes(buf, &m.to_cols_array());
547        }
548        Value::Mat4(m) => {
549            write_u8(buf, VAL_MAT4);
550            write_f32_lanes(buf, &m.to_cols_array());
551        }
552        // Weighted tables (NS-A7, `docs/stdlib-spec.md` §8): u32 entry
553        // count, then per entry an i32 weight and the recursively-encoded
554        // value, in construction order (order is semantic for display and
555        // the roll walk). Like `VAL_HANDLE` above, no opcode literal
556        // produces one at compile time today (construction is the runtime
557        // `Collect(WeightedNew)` op) — saves and transcripts are the wire
558        // consumers.
559        Value::Weighted(w) => {
560            write_u8(buf, VAL_WEIGHTED);
561            write_u32(buf, w.entries.len() as u32);
562            for (weight, value) in &w.entries {
563                write_i32(buf, *weight);
564                encode_value(value, buf);
565            }
566        }
567    }
568}
569
570/// Encode a single [`ProjSegment`] (`docs/format-v4-rfc.md` §1: `u8 kind (0
571/// = index i32, 1 = key value)`).
572fn encode_proj_segment(seg: &ProjSegment, buf: &mut Vec<u8>) {
573    match seg {
574        ProjSegment::Index(n) => {
575            write_u8(buf, PROJ_SEG_INDEX);
576            write_i32(buf, *n);
577        }
578        ProjSegment::Key(v) => {
579            write_u8(buf, PROJ_SEG_KEY);
580            encode_value(v, buf);
581        }
582    }
583}
584
585/// Encode a [`MapKey`] using the scalar `VAL_*` tag surface it maps onto
586/// (`int`/`string`/`bool` — the v1 key domain, `docs/value-model-spec.md` §4).
587/// Self-describing so the reader can reject a non-scalar key tag.
588fn encode_map_key(key: &MapKey, buf: &mut Vec<u8>) {
589    match key {
590        MapKey::Int(n) => {
591            write_u8(buf, VAL_INT);
592            write_i32(buf, *n);
593        }
594        MapKey::Str(s) => {
595            write_u8(buf, VAL_STRING);
596            write_str(buf, s);
597        }
598        MapKey::Bool(b) => {
599            write_u8(buf, VAL_BOOL);
600            write_u8(buf, u8::from(*b));
601        }
602    }
603}
604
605#[expect(clippy::cast_possible_truncation)]
606fn encode_list_def(ld: &ListDef, buf: &mut Vec<u8>) {
607    write_def_id(buf, ld.id);
608    write_u16(buf, ld.name.0);
609    write_u32(buf, ld.items.len() as u32);
610    for (name_id, ordinal) in &ld.items {
611        write_u16(buf, name_id.0);
612        write_i32(buf, *ordinal);
613    }
614}
615
616fn encode_list_item(li: &ListItemDef, buf: &mut Vec<u8>) {
617    write_def_id(buf, li.id);
618    write_def_id(buf, li.origin);
619    write_i32(buf, li.ordinal);
620    write_u16(buf, li.name.0);
621}
622
623/// Write the list literals section (no header framing).
624#[expect(clippy::cast_possible_truncation)]
625pub fn write_section_list_literals(list_literals: &[ListValue], buf: &mut Vec<u8>) {
626    write_u32(buf, list_literals.len() as u32);
627    for lv in list_literals {
628        write_u32(buf, lv.items.len() as u32);
629        for item in &lv.items {
630            write_def_id(buf, *item);
631        }
632        write_u32(buf, lv.origins.len() as u32);
633        for origin in &lv.origins {
634            write_def_id(buf, *origin);
635        }
636    }
637}
638
639/// Write the T1b literal pool section (no header framing) — a flat list of
640/// content-hash-deduplicated constant [`Value`]s referenced by
641/// `PushLiteral(idx)` (`docs/format-v4-rfc.md` §2). Each entry uses the
642/// existing generic `encode_value` (the same recursive `VAL_ARRAY`/`VAL_MAP`
643/// tree encoding as a `GlobalVarDef` default).
644#[expect(clippy::cast_possible_truncation)]
645pub fn write_section_literal_pool(literal_pool: &[Value], buf: &mut Vec<u8>) {
646    write_u32(buf, literal_pool.len() as u32);
647    for v in literal_pool {
648        encode_value(v, buf);
649    }
650}
651
652/// Write the TM-4 `StructShapes` section (no header framing): one entry per
653/// declared `STRUCT` — shape id, name, then its ordered field `NameId`s
654/// (`docs/format-v4-rfc.md` §2). Empty (count 0) until a compiler milestone
655/// emits struct declarations — see the PR description's scope note.
656#[expect(clippy::cast_possible_truncation)]
657pub fn write_section_struct_shapes(struct_shapes: &[StructShapeDef], buf: &mut Vec<u8>) {
658    write_u32(buf, struct_shapes.len() as u32);
659    for shape in struct_shapes {
660        write_u32(buf, shape.id.0);
661        write_u16(buf, shape.name.0);
662        write_u16(buf, shape.fields.len() as u16);
663        for field in &shape.fields {
664            write_u16(buf, field.0);
665        }
666    }
667}
668
669/// Section-local encoding version for `AliasTable` (`docs/modules-spec.md`
670/// §5) — independent of the `.inkb` format `VERSION`, so the row encoding
671/// can change without another whole-format bump.
672pub(crate) const ALIAS_TABLE_SECTION_VERSION: u8 = 1;
673
674/// Write the M-3 `AliasTable` section (no header framing): a one-byte
675/// section-local version, then a flat list of old→new `DefinitionId` pairs
676/// (`docs/modules-spec.md` §5). Entries are written in the order given —
677/// callers sort by `old` for the runtime's binary-search lookup.
678#[expect(clippy::cast_possible_truncation)]
679pub fn write_section_alias_table(entries: &[AliasEntry], buf: &mut Vec<u8>) {
680    write_u8(buf, ALIAS_TABLE_SECTION_VERSION);
681    write_u32(buf, entries.len() as u32);
682    for entry in entries {
683        write_def_id(buf, entry.old);
684        write_def_id(buf, entry.new);
685    }
686}
687
688/// Section-local encoding version for `FrameShapes` (FS-3,
689/// `docs/flow-suspension-spec.md` §4/§11) — independent of the `.inkb` format
690/// `VERSION`, so the shape encoding can grow (e.g. per-slot type metadata)
691/// without another whole-format bump.
692pub(crate) const FRAME_SHAPES_SECTION_VERSION: u8 = 1;
693
694/// Write the FS-3 `FrameShapes` section (no header framing): a one-byte
695/// section-local version, then one entry per `await` site
696/// (`docs/flow-suspension-spec.md` §4/§11) — the site's stable `DefinitionId`
697/// (the synthesized continuation container id) followed by its name-keyed
698/// crossing-local slots. Entries are written in the order given; callers sort
699/// by `site` for determinism. Callers emit this section only when non-empty.
700#[expect(clippy::cast_possible_truncation)]
701pub fn write_section_frame_shapes(shapes: &[FrameShapeDef], buf: &mut Vec<u8>) {
702    write_u8(buf, FRAME_SHAPES_SECTION_VERSION);
703    write_u32(buf, shapes.len() as u32);
704    for shape in shapes {
705        write_def_id(buf, shape.site);
706        write_u32(buf, shape.slots.len() as u32);
707        for slot in &shape.slots {
708            write_u16(buf, slot.0);
709        }
710    }
711}
712
713/// Section-local encoding version for `EffectRows` (T2-3,
714/// `docs/effects-spec.md` §11) — independent of the `.inkb` format `VERSION`,
715/// so the factored-row encoding can change without another whole-format bump
716/// (the reservation this section graduates was made for exactly this).
717///
718/// Bumped 1 → 2 for #882: each row gains a leading `is_entry` byte (the
719/// freeze bit — see [`EffectRowEntry::is_entry`]).
720///
721/// Bumped 2 → 3 for NS-A2 (issue #1108): each `DirectEffects` block gains a
722/// trailing extension-flags byte carrying the emits/tags/faults dimensions
723/// (bits 0–2; bits 3–7 reserved, strict-rejected — per-fault-kind
724/// granularity is the named future occupant, graduating via the next bump).
725pub(crate) const EFFECT_ROWS_SECTION_VERSION: u8 = 3;
726
727/// Write the T2-3 `EffectRows` section (no header framing): a one-byte
728/// section-local version, then the `DefinitionId → row` table of factored
729/// effect rows (`docs/effects-spec.md` §11). One entry per knot/stitch — the
730/// host's resume-scheduling estimate (§12.1). Entries are written in the order
731/// given; callers sort by `def` for determinism.
732#[expect(clippy::cast_possible_truncation)]
733pub fn write_section_effect_rows(rows: &[EffectRowEntry], buf: &mut Vec<u8>) {
734    write_u8(buf, EFFECT_ROWS_SECTION_VERSION);
735    write_u32(buf, rows.len() as u32);
736    for row in rows {
737        write_def_id(buf, row.def);
738        // #882 freeze bit: whether this row is a legitimate host entry point
739        // (see `EffectRowEntry::is_entry`'s doc — `false` only for
740        // `#@private` defs, and the row still ships either way).
741        write_u8(buf, u8::from(row.is_entry));
742        encode_direct_effects(&row.direct, buf);
743        // Per-dispatch entries (v1 emits none, but the encoding ships the
744        // structure — a flat row forecloses §7 narrowing).
745        write_u32(buf, row.dispatches.len() as u32);
746        for d in &row.dispatches {
747            write_def_id(buf, d.cell);
748            write_u8(buf, u8::from(d.narrowable));
749            encode_direct_effects(&d.fallback, buf);
750        }
751    }
752}
753
754/// Encode a [`DirectEffects`] block: reads, writes, call atoms, opaque flag.
755#[expect(clippy::cast_possible_truncation)]
756fn encode_direct_effects(direct: &DirectEffects, buf: &mut Vec<u8>) {
757    write_u32(buf, direct.reads.len() as u32);
758    for id in &direct.reads {
759        write_def_id(buf, *id);
760    }
761    write_u32(buf, direct.writes.len() as u32);
762    for id in &direct.writes {
763        write_def_id(buf, *id);
764    }
765    write_u32(buf, direct.calls.len() as u32);
766    for atom in &direct.calls {
767        encode_call_atom(atom, buf);
768    }
769    write_u8(buf, u8::from(direct.opaque));
770    // NS-A2 extension-flags byte (section version 3): emits/tags/faults.
771    let mut dims = 0u8;
772    if direct.emits {
773        dims |= super::EFFECT_DIM_EMITS;
774    }
775    if direct.tags {
776        dims |= super::EFFECT_DIM_TAGS;
777    }
778    if direct.faults {
779        dims |= super::EFFECT_DIM_FAULTS;
780    }
781    write_u8(buf, dims);
782}
783
784/// Encode a single [`CallAtom`]: interned name, the capability-parameter slot
785/// (`(any)` in v1), then the reserved handle-parameter slot (`None` in v1 —
786/// `docs/t1d-spec.md` §7). A bound handle is never emitted in this section
787/// version.
788fn encode_call_atom(atom: &CallAtom, buf: &mut Vec<u8>) {
789    write_u16(buf, atom.name.0);
790    let cap_tag = match atom.capability {
791        CapabilityParam::Any => CAP_PARAM_ANY,
792    };
793    write_u8(buf, cap_tag);
794    // Reserved handle-parameter slot: v1 is always `None`. A `Some` is
795    // structurally representable but never encoded in this section version.
796    write_u8(buf, atom.handle_param.unwrap_or(HANDLE_PARAM_NONE));
797}
798
799fn encode_external(ext: &ExternalFnDef, buf: &mut Vec<u8>) {
800    write_def_id(buf, ext.id);
801    write_u16(buf, ext.name.0);
802    write_u8(buf, ext.arg_count);
803    match ext.fallback {
804        Some(fb) => {
805            write_u8(buf, 1);
806            write_def_id(buf, fb);
807        }
808        None => {
809            write_u8(buf, 0);
810        }
811    }
812}
813
814#[expect(clippy::cast_possible_truncation)]
815fn encode_container(c: &ContainerDef, buf: &mut Vec<u8>) {
816    write_def_id(buf, c.id);
817    write_def_id(buf, c.scope_id);
818    match c.name {
819        Some(name_id) => {
820            write_u8(buf, 1);
821            write_u16(buf, name_id.0);
822        }
823        None => {
824            write_u8(buf, 0);
825        }
826    }
827    write_u8(buf, c.counting_flags.bits());
828    write_i32(buf, c.path_hash);
829    write_u8(buf, c.param_count);
830    write_u8(buf, u8::from(c.local));
831    // Per-param name/mode metadata (T1c, `docs/t1c-spec.md` §6). Additive
832    // trailing field: a `0` count for the common no-param container.
833    write_u16(buf, c.params.len() as u16);
834    for p in &c.params {
835        write_u16(buf, p.name.0);
836        write_u8(buf, u8::from(p.is_ref));
837    }
838    write_u32(buf, c.bytecode.len() as u32);
839    buf.extend_from_slice(&c.bytecode);
840}
841
842/// Write the line tables section (no header framing).
843#[expect(clippy::cast_possible_truncation)]
844pub fn write_section_line_tables(line_tables: &[ScopeLineTable], buf: &mut Vec<u8>) {
845    write_u32(buf, line_tables.len() as u32);
846    for lt in line_tables {
847        encode_scope_line_table(lt, buf);
848    }
849}
850
851#[expect(clippy::cast_possible_truncation)]
852fn encode_scope_line_table(lt: &ScopeLineTable, buf: &mut Vec<u8>) {
853    write_def_id(buf, lt.scope_id);
854    write_u32(buf, lt.lines.len() as u32);
855    for entry in &lt.lines {
856        encode_line_entry(entry, buf);
857    }
858}
859
860fn encode_line_entry(entry: &LineEntry, buf: &mut Vec<u8>) {
861    encode_line_content(&entry.content, buf);
862    write_u64(buf, entry.source_hash);
863    match &entry.audio_ref {
864        Some(audio) => {
865            write_u8(buf, 1);
866            write_str(buf, audio);
867        }
868        None => {
869            write_u8(buf, 0);
870        }
871    }
872
873    // Slot info
874    #[expect(clippy::cast_possible_truncation)]
875    write_u8(buf, entry.slot_info.len() as u8);
876    for slot in &entry.slot_info {
877        write_u8(buf, slot.index);
878        write_str(buf, &slot.name);
879    }
880
881    // Source location
882    match &entry.source_location {
883        Some(loc) => {
884            write_u8(buf, 1);
885            write_str(buf, &loc.file);
886            write_u32(buf, loc.range_start);
887            write_u32(buf, loc.range_end);
888        }
889        None => {
890            write_u8(buf, 0);
891        }
892    }
893}
894
895#[expect(clippy::cast_possible_truncation)]
896pub(crate) fn encode_line_content(content: &LineContent, buf: &mut Vec<u8>) {
897    match content {
898        LineContent::Plain(s) => {
899            write_u8(buf, LINE_PLAIN);
900            write_str(buf, s);
901        }
902        LineContent::Template(parts) => {
903            write_u8(buf, LINE_TEMPLATE);
904            write_u32(buf, parts.len() as u32);
905            for part in parts {
906                encode_line_part(part, buf);
907            }
908        }
909    }
910}
911
912#[expect(clippy::cast_possible_truncation)]
913fn encode_line_part(part: &LinePart, buf: &mut Vec<u8>) {
914    match part {
915        LinePart::Literal(s) => {
916            write_u8(buf, PART_LITERAL);
917            write_str(buf, s);
918        }
919        LinePart::Slot(idx) => {
920            write_u8(buf, PART_SLOT);
921            write_u8(buf, *idx);
922        }
923        LinePart::Select {
924            slot,
925            variants,
926            default,
927        } => {
928            write_u8(buf, PART_SELECT);
929            write_u8(buf, *slot);
930            write_u32(buf, variants.len() as u32);
931            for (key, text) in variants {
932                encode_select_key(key, buf);
933                write_str(buf, text);
934            }
935            write_str(buf, default);
936        }
937        LinePart::Span {
938            name,
939            attrs,
940            children,
941        } => {
942            write_u8(buf, PART_SPAN);
943            write_str(buf, name);
944            write_u32(buf, attrs.len() as u32);
945            for (k, v) in attrs {
946                write_str(buf, k);
947                write_str(buf, v);
948            }
949            write_u32(buf, children.len() as u32);
950            for child in children {
951                encode_line_part(child, buf);
952            }
953        }
954    }
955}
956
957fn encode_select_key(key: &SelectKey, buf: &mut Vec<u8>) {
958    match key {
959        SelectKey::Cardinal(cat) => {
960            write_u8(buf, KEY_CARDINAL);
961            encode_plural_category(*cat, buf);
962        }
963        SelectKey::Ordinal(cat) => {
964            write_u8(buf, KEY_ORDINAL);
965            encode_plural_category(*cat, buf);
966        }
967        SelectKey::Exact(n) => {
968            write_u8(buf, KEY_EXACT);
969            write_i32(buf, *n);
970        }
971        SelectKey::Keyword(k) => {
972            write_u8(buf, KEY_KEYWORD);
973            write_str(buf, k);
974        }
975    }
976}
977
978fn encode_plural_category(cat: PluralCategory, buf: &mut Vec<u8>) {
979    let tag = match cat {
980        PluralCategory::Zero => CAT_ZERO,
981        PluralCategory::One => CAT_ONE,
982        PluralCategory::Two => CAT_TWO,
983        PluralCategory::Few => CAT_FEW,
984        PluralCategory::Many => CAT_MANY,
985        PluralCategory::Other => CAT_OTHER,
986    };
987    write_u8(buf, tag);
988}