Skip to main content

vm/
vmbc.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::fmt::Write;
3
4use crate::builtins::BuiltinFunction;
5use crate::bytecode::{
6    CallableKind, CallablePrototype, CallableTarget, CaptureBindingMode, ExportedCallable,
7    FunctionRegion, RootCallableBinding, ScriptFunction, TypeMap, ValueType,
8};
9use crate::compiler::ir::TypeSchema;
10use crate::debug_info::{ArgInfo, DebugFunction, DebugInfo, LineInfo, LocalInfo};
11use crate::vm::{HostImport, OpCode, Program, Value};
12
13const MAGIC: [u8; 4] = *b"VMBC";
14const VERSION_V10: u16 = 10;
15const FLAGS: u16 = 0;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum WireError {
19    UnexpectedEof,
20    InvalidMagic([u8; 4]),
21    UnsupportedVersion(u16),
22    UnsupportedFlags(u16),
23    InvalidConstantTag(u8),
24    InvalidBool(u8),
25    InvalidTypeMapFlag(u8),
26    InvalidDebugFlag(u8),
27    InvalidValueType(u8),
28    InvalidCaptureBindingMode(u8),
29    InvalidUtf8,
30    StringTooLong(usize),
31    CodeTooLong(usize),
32    UnsupportedConstantType(&'static str),
33    LengthTooLarge(&'static str, usize),
34    TrailingBytes,
35}
36
37impl std::fmt::Display for WireError {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            WireError::UnexpectedEof => write!(f, "unexpected end of input"),
41            WireError::InvalidMagic(found) => write!(f, "invalid magic: {found:?}"),
42            WireError::UnsupportedVersion(version) => {
43                write!(f, "unsupported version: {version}")
44            }
45            WireError::UnsupportedFlags(flags) => write!(f, "unsupported flags: {flags}"),
46            WireError::InvalidConstantTag(tag) => write!(f, "invalid constant tag: {tag}"),
47            WireError::InvalidBool(value) => write!(f, "invalid bool value: {value}"),
48            WireError::InvalidTypeMapFlag(value) => write!(f, "invalid type-map flag: {value}"),
49            WireError::InvalidDebugFlag(value) => write!(f, "invalid debug flag: {value}"),
50            WireError::InvalidValueType(value) => write!(f, "invalid value type: {value}"),
51            WireError::InvalidCaptureBindingMode(value) => {
52                write!(f, "invalid capture binding mode: {value}")
53            }
54            WireError::InvalidUtf8 => write!(f, "invalid utf-8 string"),
55            WireError::StringTooLong(len) => write!(f, "string too long: {len}"),
56            WireError::CodeTooLong(len) => write!(f, "code too long: {len}"),
57            WireError::UnsupportedConstantType(kind) => {
58                write!(f, "unsupported constant type for wire format: {kind}")
59            }
60            WireError::LengthTooLarge(field, len) => {
61                write!(f, "{field} length too large: {len}")
62            }
63            WireError::TrailingBytes => write!(f, "trailing bytes after program payload"),
64        }
65    }
66}
67
68impl std::error::Error for WireError {}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum ValidationError {
72    TruncatedOperand {
73        offset: usize,
74        opcode: u8,
75        expected_bytes: usize,
76    },
77    InvalidOpcode {
78        offset: usize,
79        opcode: u8,
80    },
81    InvalidConstant {
82        offset: usize,
83        index: u32,
84    },
85    InvalidCall {
86        offset: usize,
87        index: u16,
88    },
89    InvalidCallArity {
90        offset: usize,
91        index: u16,
92        expected: u8,
93        got: u8,
94    },
95    InvalidJumpTarget {
96        offset: usize,
97        target: u32,
98    },
99    InvalidCallableMetadata(&'static str),
100}
101
102impl std::fmt::Display for ValidationError {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        match self {
105            ValidationError::TruncatedOperand {
106                offset,
107                opcode,
108                expected_bytes,
109            } => write!(
110                f,
111                "truncated operand at offset {offset} for opcode {opcode:#04x}, expected {expected_bytes} bytes",
112            ),
113            ValidationError::InvalidOpcode { offset, opcode } => {
114                write!(f, "invalid opcode {opcode:#04x} at offset {offset}")
115            }
116            ValidationError::InvalidConstant { offset, index } => write!(
117                f,
118                "invalid constant index {index} for ldc instruction at offset {offset}",
119            ),
120            ValidationError::InvalidCall { offset, index } => {
121                write!(f, "invalid call index {index} at offset {offset}")
122            }
123            ValidationError::InvalidCallArity {
124                offset,
125                index,
126                expected,
127                got,
128            } => write!(
129                f,
130                "invalid call arity {got} for import index {index} at offset {offset}, expected {expected}",
131            ),
132            ValidationError::InvalidJumpTarget { offset, target } => write!(
133                f,
134                "invalid jump target {target} referenced by instruction at offset {offset}",
135            ),
136            ValidationError::InvalidCallableMetadata(message) => {
137                write!(f, "invalid callable metadata: {message}")
138            }
139        }
140    }
141}
142
143impl std::error::Error for ValidationError {}
144
145const MAX_CONSTANT_DEPTH: usize = 64;
146
147fn write_constant(value: &Value, out: &mut Vec<u8>, depth: usize) -> Result<(), WireError> {
148    if depth >= MAX_CONSTANT_DEPTH {
149        return Err(WireError::LengthTooLarge("constant nesting depth", depth));
150    }
151    match value {
152        Value::Int(value) => {
153            out.push(0);
154            out.extend_from_slice(&value.to_le_bytes());
155        }
156        Value::Bool(value) => {
157            out.push(1);
158            out.push(u8::from(*value));
159        }
160        Value::String(value) => {
161            out.push(2);
162            write_u32_len("constant string", value.len(), out)?;
163            out.extend_from_slice(value.as_bytes());
164        }
165        Value::Float(value) => {
166            out.push(3);
167            out.extend_from_slice(&value.to_le_bytes());
168        }
169        Value::Null => out.push(4),
170        Value::Bytes(value) => {
171            out.push(5);
172            write_u32_len("constant bytes", value.len(), out)?;
173            out.extend_from_slice(value.as_slice());
174        }
175        Value::Array(values) => {
176            out.push(6);
177            write_u32_count("constant array", values.len(), out)?;
178            for value in values.iter() {
179                write_constant(value, out, depth + 1)?;
180            }
181        }
182        Value::Map(entries) => {
183            out.push(7);
184            write_u32_count("constant map", entries.len(), out)?;
185            for (key, value) in entries.iter() {
186                write_constant(key, out, depth + 1)?;
187                write_constant(value, out, depth + 1)?;
188            }
189        }
190        Value::Callable(_) => return Err(WireError::UnsupportedConstantType("callable")),
191    }
192    Ok(())
193}
194
195fn read_constant(cursor: &mut Cursor<'_>, depth: usize) -> Result<Value, WireError> {
196    if depth >= MAX_CONSTANT_DEPTH {
197        return Err(WireError::LengthTooLarge("constant nesting depth", depth));
198    }
199    match cursor.read_u8()? {
200        0 => Ok(Value::Int(cursor.read_i64()?)),
201        1 => match cursor.read_u8()? {
202            0 => Ok(Value::Bool(false)),
203            1 => Ok(Value::Bool(true)),
204            other => Err(WireError::InvalidBool(other)),
205        },
206        2 => {
207            let len = cursor.read_u32()? as usize;
208            let bytes = cursor.read_exact(len)?;
209            let text = String::from_utf8(bytes.to_vec()).map_err(|_| WireError::InvalidUtf8)?;
210            Ok(Value::string(text))
211        }
212        3 => Ok(Value::Float(cursor.read_f64()?)),
213        4 => Ok(Value::Null),
214        5 => {
215            let len = cursor.read_u32()? as usize;
216            Ok(Value::bytes(cursor.read_exact(len)?.to_vec()))
217        }
218        6 => {
219            let count = cursor.read_u32()? as usize;
220            let mut values = Vec::with_capacity(count);
221            for _ in 0..count {
222                values.push(read_constant(cursor, depth + 1)?);
223            }
224            Ok(Value::array(values))
225        }
226        7 => {
227            let count = cursor.read_u32()? as usize;
228            let mut entries = Vec::with_capacity(count);
229            for _ in 0..count {
230                entries.push((
231                    read_constant(cursor, depth + 1)?,
232                    read_constant(cursor, depth + 1)?,
233                ));
234            }
235            Ok(Value::map(entries))
236        }
237        tag => Err(WireError::InvalidConstantTag(tag)),
238    }
239}
240
241pub fn encode_program(program: &Program) -> Result<Vec<u8>, WireError> {
242    let mut out = Vec::new();
243    out.extend_from_slice(&MAGIC);
244    out.extend_from_slice(&VERSION_V10.to_le_bytes());
245    out.extend_from_slice(&FLAGS.to_le_bytes());
246    write_u32_count("constants", program.constants.len(), &mut out)?;
247
248    for constant in &program.constants {
249        write_constant(constant, &mut out, 0)?;
250    }
251
252    write_u32_len("code", program.code.len(), &mut out)?;
253    out.extend_from_slice(&program.code);
254
255    write_u32_count("imports", program.imports.len(), &mut out)?;
256    for import in &program.imports {
257        write_string("import name", &import.name, &mut out)?;
258        out.push(import.arity);
259        out.push(import.return_type as u8);
260    }
261
262    write_type_map(&mut out, program.type_map.as_ref())?;
263    write_debug_info(&mut out, program.debug.as_ref())?;
264    write_callable_metadata(&mut out, program)?;
265
266    Ok(out)
267}
268
269pub fn decode_program(bytes: &[u8]) -> Result<Program, WireError> {
270    let mut cursor = Cursor::new(bytes);
271
272    let magic = cursor.read_exact_array::<4>()?;
273    if magic != MAGIC {
274        return Err(WireError::InvalidMagic(magic));
275    }
276
277    let version = cursor.read_u16()?;
278    if version != VERSION_V10 {
279        return Err(WireError::UnsupportedVersion(version));
280    }
281
282    let flags = cursor.read_u16()?;
283    if flags != FLAGS {
284        return Err(WireError::UnsupportedFlags(flags));
285    }
286
287    let constant_count = cursor.read_u32()? as usize;
288    let mut constants = Vec::with_capacity(constant_count);
289    for _ in 0..constant_count {
290        constants.push(read_constant(&mut cursor, 0)?);
291    }
292
293    let code_len = cursor.read_u32()? as usize;
294    let code = cursor.read_exact(code_len)?.to_vec();
295    let import_count = cursor.read_u32()? as usize;
296    let mut imports = Vec::with_capacity(import_count);
297    for _ in 0..import_count {
298        imports.push(HostImport {
299            name: cursor.read_string()?,
300            arity: cursor.read_u8()?,
301            return_type: read_value_type(cursor.read_u8()?)?,
302        });
303    }
304    let type_map = read_type_map(&mut cursor)?;
305    let debug = read_debug_info(&mut cursor)?;
306    let (
307        script_functions,
308        callable_prototypes,
309        function_regions,
310        root_callable_bindings,
311        exported_callables,
312    ) = read_callable_metadata(&mut cursor)?;
313
314    if !cursor.is_eof() {
315        return Err(WireError::TrailingBytes);
316    }
317
318    let mut program = Program::with_imports_and_debug(constants, code, imports, debug);
319    program.type_map = type_map;
320    program.script_functions = script_functions;
321    program.callable_prototypes = callable_prototypes;
322    program.function_regions = function_regions;
323    program.root_callable_bindings = root_callable_bindings;
324    program.exported_callables = exported_callables;
325    Ok(program)
326}
327
328pub fn validate_program(program: &Program, host_fn_count: u16) -> Result<(), ValidationError> {
329    analyze_program(program, Some(host_fn_count)).map(|_| ())
330}
331
332pub fn infer_local_count(program: &Program) -> Result<usize, ValidationError> {
333    let analysis = analyze_program(program, None)?;
334    Ok(match analysis.max_local_index {
335        Some(index) => index as usize + 1,
336        None => 0,
337    })
338}
339
340#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
341pub struct DisassembleOptions {
342    pub show_source: bool,
343}
344
345pub fn disassemble_vmbc(bytes: &[u8]) -> Result<String, WireError> {
346    disassemble_vmbc_with_options(bytes, DisassembleOptions::default())
347}
348
349pub fn disassemble_vmbc_with_options(
350    bytes: &[u8],
351    options: DisassembleOptions,
352) -> Result<String, WireError> {
353    let program = decode_program(bytes)?;
354    Ok(disassemble_program_with_options(&program, options))
355}
356
357pub fn disassemble_program(program: &Program) -> String {
358    disassemble_program_with_options(program, DisassembleOptions::default())
359}
360
361pub fn disassemble_program_with_options(program: &Program, options: DisassembleOptions) -> String {
362    let mut out = String::new();
363    let _ = writeln!(&mut out, "constants ({}):", program.constants.len());
364    for (index, constant) in program.constants.iter().enumerate() {
365        let _ = writeln!(&mut out, "  [{index:04}] {constant:?}");
366    }
367
368    let _ = writeln!(&mut out, "imports ({}):", program.imports.len());
369    for (index, import) in program.imports.iter().enumerate() {
370        let _ = writeln!(&mut out, "  [{index:04}] {}/{}", import.name, import.arity);
371    }
372    let _ = writeln!(&mut out, "code ({} bytes):", program.code.len());
373    let mut source_annotations = source_annotations(program, options.show_source);
374    if options.show_source && source_annotations.is_none() {
375        let _ = writeln!(&mut out, "      ; source: <none>");
376    }
377    let code = &program.code;
378    let mut ip = 0usize;
379    while ip < code.len() {
380        let start = ip;
381        if let Some(lines_at_offset) = source_annotations
382            .as_mut()
383            .and_then(|annotations| annotations.remove(&start))
384        {
385            for (line, text) in lines_at_offset {
386                let _ = writeln!(&mut out, "      ; src {line:04}  {text}");
387            }
388        }
389        let opcode = code[ip];
390        ip += 1;
391
392        let mut instruction = String::new();
393        let mut truncated = false;
394        match opcode {
395            x if x == OpCode::Nop as u8 => instruction.push_str("nop"),
396            x if x == OpCode::Ret as u8 => instruction.push_str("ret"),
397            x if x == OpCode::Ldc as u8 => {
398                if let Some(index) = read_u32(code, &mut ip) {
399                    instruction.push_str(&format!("ldc {index}"));
400                    if let Some(value) = program.constants.get(index as usize) {
401                        instruction.push_str(&format!(" ; const[{index}]={value:?}"));
402                    }
403                } else {
404                    instruction.push_str("ldc <truncated>");
405                    truncated = true;
406                }
407            }
408            x if x == OpCode::Add as u8 => instruction.push_str("add"),
409            x if x == OpCode::Sub as u8 => instruction.push_str("sub"),
410            x if x == OpCode::Mul as u8 => instruction.push_str("mul"),
411            x if x == OpCode::Div as u8 => instruction.push_str("div"),
412            x if x == OpCode::Neg as u8 => instruction.push_str("neg"),
413            x if x == OpCode::Not as u8 => instruction.push_str("not"),
414            x if x == OpCode::Ceq as u8 => instruction.push_str("ceq"),
415            x if x == OpCode::Clt as u8 => instruction.push_str("clt"),
416            x if x == OpCode::Cgt as u8 => instruction.push_str("cgt"),
417            x if x == OpCode::Br as u8 => {
418                if let Some(target) = read_u32(code, &mut ip) {
419                    instruction.push_str(&format!("br {target}"));
420                } else {
421                    instruction.push_str("br <truncated>");
422                    truncated = true;
423                }
424            }
425            x if x == OpCode::Brfalse as u8 => {
426                if let Some(target) = read_u32(code, &mut ip) {
427                    instruction.push_str(&format!("brfalse {target}"));
428                } else {
429                    instruction.push_str("brfalse <truncated>");
430                    truncated = true;
431                }
432            }
433            x if x == OpCode::Pop as u8 => instruction.push_str("pop"),
434            x if x == OpCode::Dup as u8 => instruction.push_str("dup"),
435            x if x == OpCode::Ldloc as u8 => {
436                if let Some(index) = read_u8(code, &mut ip) {
437                    instruction.push_str(&format!("ldloc {index}"));
438                } else {
439                    instruction.push_str("ldloc <truncated>");
440                    truncated = true;
441                }
442            }
443            x if x == OpCode::Stloc as u8 => {
444                if let Some(index) = read_u8(code, &mut ip) {
445                    instruction.push_str(&format!("stloc {index}"));
446                } else {
447                    instruction.push_str("stloc <truncated>");
448                    truncated = true;
449                }
450            }
451            x if x == OpCode::Call as u8 => {
452                if let Some(index) = read_u16(code, &mut ip) {
453                    if let Some(argc) = read_u8(code, &mut ip) {
454                        instruction.push_str(&format!("call {index} {argc}"));
455                        if let Some(comment) = format_call_target(program, index, argc) {
456                            instruction.push_str(&format!(" ; {comment}"));
457                        }
458                    } else {
459                        instruction.push_str("call <truncated>");
460                        truncated = true;
461                    }
462                } else {
463                    instruction.push_str("call <truncated>");
464                    truncated = true;
465                }
466            }
467            x if x == OpCode::CallValue as u8 => {
468                if let Some(argc) = read_u8(code, &mut ip) {
469                    instruction.push_str(&format!("callvalue {argc}"));
470                } else {
471                    instruction.push_str("callvalue <truncated>");
472                    truncated = true;
473                }
474            }
475
476            x if x == OpCode::Shl as u8 => instruction.push_str("shl"),
477            x if x == OpCode::Shr as u8 => instruction.push_str("shr"),
478            x if x == OpCode::Lshr as u8 => instruction.push_str("lshr"),
479            x if x == OpCode::Mod as u8 => instruction.push_str("mod"),
480            x if x == OpCode::And as u8 => instruction.push_str("and"),
481            x if x == OpCode::Or as u8 => instruction.push_str("or"),
482            other => instruction.push_str(&format!(".byte 0x{other:02X} ; invalid opcode")),
483        }
484
485        let encoded = format_hex_bytes(&code[start..ip]);
486        let _ = writeln!(&mut out, "{start:04}\t{encoded:<14}\t{instruction}");
487        if truncated {
488            break;
489        }
490    }
491
492    out
493}
494
495fn source_annotations(
496    program: &Program,
497    show_source: bool,
498) -> Option<BTreeMap<usize, Vec<(u32, String)>>> {
499    if !show_source {
500        return None;
501    }
502    let debug = program.debug.as_ref()?;
503    let source = debug.source.as_ref()?;
504    let source_lines = source.lines().collect::<Vec<_>>();
505    let mut first_offset_by_line = HashMap::<u32, u32>::new();
506    for info in &debug.lines {
507        first_offset_by_line.entry(info.line).or_insert(info.offset);
508    }
509    let mut pairs = first_offset_by_line
510        .into_iter()
511        .map(|(line, offset)| (offset, line))
512        .collect::<Vec<_>>();
513    pairs.sort_by_key(|(offset, line)| (*offset, *line));
514
515    let mut annotations = BTreeMap::<usize, Vec<(u32, String)>>::new();
516    for (offset, line) in pairs {
517        let text = source_lines
518            .get(line.saturating_sub(1) as usize)
519            .copied()
520            .unwrap_or("<missing source line>")
521            .to_string();
522        annotations
523            .entry(offset as usize)
524            .or_default()
525            .push((line, text));
526    }
527    Some(annotations)
528}
529
530struct ProgramAnalysis {
531    max_local_index: Option<u8>,
532}
533
534fn region_index_for_ip(regions: &[FunctionRegion], ip: usize) -> Option<usize> {
535    regions
536        .iter()
537        .position(|region| (region.start_ip as usize) <= ip && ip < region.end_ip as usize)
538}
539
540fn validate_callable_metadata(program: &Program) -> Result<(), ValidationError> {
541    let code_len = program.code.len();
542    let mut previous_end = 0usize;
543    for region in &program.function_regions {
544        let start = region.start_ip as usize;
545        let end = region.end_ip as usize;
546        if start < previous_end || start >= end || end > code_len {
547            return Err(ValidationError::InvalidCallableMetadata(
548                "function regions overlap or exceed bytecode bounds",
549            ));
550        }
551        if let Some(prototype_id) = region.prototype_id
552            && prototype_id as usize >= program.callable_prototypes.len()
553        {
554            return Err(ValidationError::InvalidCallableMetadata(
555                "function region references an invalid prototype",
556            ));
557        }
558        previous_end = end;
559    }
560    if !program.function_regions.is_empty()
561        && (program.function_regions[0].start_ip != 0 || previous_end != code_len)
562    {
563        return Err(ValidationError::InvalidCallableMetadata(
564            "function regions do not cover the complete bytecode",
565        ));
566    }
567
568    for prototype in &program.callable_prototypes {
569        if matches!(prototype.target, CallableTarget::ScriptFunction(_))
570            && prototype.parameter_slots.len() != prototype.arity as usize
571            || prototype.capture_source_slots.len() != prototype.capture_slots.len()
572            || prototype.capture_modes.len() != prototype.capture_slots.len()
573            || prototype
574                .parameter_slots
575                .iter()
576                .chain(prototype.capture_source_slots.iter())
577                .chain(prototype.capture_slots.iter())
578                .any(|slot| *slot as usize >= prototype.frame_local_count)
579            || prototype
580                .self_slot
581                .is_some_and(|slot| slot as usize >= prototype.frame_local_count)
582        {
583            return Err(ValidationError::InvalidCallableMetadata(
584                "callable frame layout is invalid",
585            ));
586        }
587        match prototype.target {
588            CallableTarget::ScriptFunction(id) if id as usize >= program.script_functions.len() => {
589                return Err(ValidationError::InvalidCallableMetadata(
590                    "callable references an invalid script function",
591                ));
592            }
593            CallableTarget::HostImport(id)
594                if id as usize >= program.imports.len()
595                    && BuiltinFunction::from_call_index(id).is_none() =>
596            {
597                return Err(ValidationError::InvalidCallableMetadata(
598                    "callable references an invalid host import",
599                ));
600            }
601            _ => {}
602        }
603    }
604
605    for binding in &program.root_callable_bindings {
606        if binding.local_slot as usize >= program.local_count
607            || binding.prototype_id as usize >= program.callable_prototypes.len()
608        {
609            return Err(ValidationError::InvalidCallableMetadata(
610                "root callable binding is invalid",
611            ));
612        }
613    }
614    let mut export_names = HashSet::new();
615    for exported in &program.exported_callables {
616        if exported.name.is_empty()
617            || exported.local_slot as usize >= program.local_count
618            || !export_names.insert(exported.name.as_str())
619        {
620            return Err(ValidationError::InvalidCallableMetadata(
621                "exported callable metadata is invalid",
622            ));
623        }
624    }
625    Ok(())
626}
627
628fn analyze_program(
629    program: &Program,
630    host_fn_count: Option<u16>,
631) -> Result<ProgramAnalysis, ValidationError> {
632    validate_callable_metadata(program)?;
633    let mut ip = 0usize;
634    let mut instruction_starts = HashSet::new();
635    let mut jump_targets: Vec<(usize, u32)> = Vec::new();
636    let mut max_local_index: Option<u8> = None;
637    let code = &program.code;
638
639    while ip < code.len() {
640        let start = ip;
641        instruction_starts.insert(start);
642        let opcode = code[ip];
643        ip += 1;
644
645        match opcode {
646            x if x == OpCode::Nop as u8 || x == OpCode::Ret as u8 => {}
647            x if x == OpCode::Ldc as u8 => {
648                let index = read_u32(code, &mut ip).ok_or(ValidationError::TruncatedOperand {
649                    offset: start,
650                    opcode,
651                    expected_bytes: 4,
652                })?;
653                if index as usize >= program.constants.len() {
654                    return Err(ValidationError::InvalidConstant {
655                        offset: start,
656                        index,
657                    });
658                }
659            }
660            x if x == OpCode::Add as u8
661                || x == OpCode::Sub as u8
662                || x == OpCode::Mul as u8
663                || x == OpCode::Div as u8
664                || x == OpCode::Shl as u8
665                || x == OpCode::Shr as u8
666                || x == OpCode::Lshr as u8
667                || x == OpCode::Mod as u8
668                || x == OpCode::And as u8
669                || x == OpCode::Or as u8
670                || x == OpCode::Neg as u8
671                || x == OpCode::Not as u8
672                || x == OpCode::Ceq as u8
673                || x == OpCode::Clt as u8
674                || x == OpCode::Cgt as u8
675                || x == OpCode::Pop as u8
676                || x == OpCode::Dup as u8 => {}
677            x if x == OpCode::Br as u8 || x == OpCode::Brfalse as u8 => {
678                let target = read_u32(code, &mut ip).ok_or(ValidationError::TruncatedOperand {
679                    offset: start,
680                    opcode,
681                    expected_bytes: 4,
682                })?;
683                jump_targets.push((start, target));
684            }
685            x if x == OpCode::Ldloc as u8 || x == OpCode::Stloc as u8 => {
686                let index = read_u8(code, &mut ip).ok_or(ValidationError::TruncatedOperand {
687                    offset: start,
688                    opcode,
689                    expected_bytes: 1,
690                })?;
691                max_local_index = Some(max_local_index.map_or(index, |prev| prev.max(index)));
692            }
693            x if x == OpCode::Call as u8 => {
694                let index = read_u16(code, &mut ip).ok_or(ValidationError::TruncatedOperand {
695                    offset: start,
696                    opcode,
697                    expected_bytes: 3,
698                })?;
699                let argc = read_u8(code, &mut ip).ok_or(ValidationError::TruncatedOperand {
700                    offset: start,
701                    opcode,
702                    expected_bytes: 3,
703                })?;
704                if let Some(builtin) = BuiltinFunction::from_call_index(index) {
705                    if !builtin.accepts_arity(argc) {
706                        return Err(ValidationError::InvalidCallArity {
707                            offset: start,
708                            index,
709                            expected: builtin.arity(),
710                            got: argc,
711                        });
712                    }
713                    continue;
714                }
715                if program.imports.is_empty() {
716                    if let Some(host_fn_count) = host_fn_count
717                        && index >= host_fn_count
718                    {
719                        return Err(ValidationError::InvalidCall {
720                            offset: start,
721                            index,
722                        });
723                    }
724                } else {
725                    let Some(import) = program.imports.get(index as usize) else {
726                        return Err(ValidationError::InvalidCall {
727                            offset: start,
728                            index,
729                        });
730                    };
731                    if argc != import.arity {
732                        return Err(ValidationError::InvalidCallArity {
733                            offset: start,
734                            index,
735                            expected: import.arity,
736                            got: argc,
737                        });
738                    }
739                }
740            }
741            x if x == OpCode::CallValue as u8 => {
742                read_u8(code, &mut ip).ok_or(ValidationError::TruncatedOperand {
743                    offset: start,
744                    opcode,
745                    expected_bytes: 1,
746                })?;
747            }
748
749            other => {
750                return Err(ValidationError::InvalidOpcode {
751                    offset: start,
752                    opcode: other,
753                });
754            }
755        }
756    }
757
758    for (offset, target) in &jump_targets {
759        let target = *target as usize;
760        if target >= code.len() || !instruction_starts.contains(&target) {
761            return Err(ValidationError::InvalidJumpTarget {
762                offset: *offset,
763                target: target as u32,
764            });
765        }
766        if !program.function_regions.is_empty()
767            && region_index_for_ip(&program.function_regions, *offset)
768                != region_index_for_ip(&program.function_regions, target)
769        {
770            return Err(ValidationError::InvalidJumpTarget {
771                offset: *offset,
772                target: target as u32,
773            });
774        }
775    }
776
777    for function in &program.script_functions {
778        let entry = function.entry_ip as usize;
779        let end = function.end_ip as usize;
780        if !instruction_starts.contains(&entry)
781            || end > code.len()
782            || (end < code.len() && !instruction_starts.contains(&end))
783        {
784            return Err(ValidationError::InvalidCallableMetadata(
785                "script function boundary is not an instruction boundary",
786            ));
787        }
788    }
789
790    Ok(ProgramAnalysis { max_local_index })
791}
792
793fn write_callable_metadata(out: &mut Vec<u8>, program: &Program) -> Result<(), WireError> {
794    write_u32_count("script functions", program.script_functions.len(), out)?;
795    for function in &program.script_functions {
796        out.extend_from_slice(&function.entry_ip.to_le_bytes());
797        out.extend_from_slice(&function.end_ip.to_le_bytes());
798    }
799
800    write_u32_count(
801        "callable prototypes",
802        program.callable_prototypes.len(),
803        out,
804    )?;
805    for prototype in &program.callable_prototypes {
806        out.push(match prototype.kind {
807            CallableKind::FunctionItem => 0,
808            CallableKind::Closure => 1,
809            CallableKind::HostFunction => 2,
810        });
811        match prototype.target {
812            CallableTarget::ScriptFunction(id) => {
813                out.push(0);
814                out.extend_from_slice(&id.to_le_bytes());
815            }
816            CallableTarget::HostImport(id) => {
817                out.push(1);
818                out.extend_from_slice(&u32::from(id).to_le_bytes());
819            }
820        }
821        out.push(prototype.arity);
822        write_u32_count("callable frame locals", prototype.frame_local_count, out)?;
823        write_u16_list("callable parameters", &prototype.parameter_slots, out)?;
824        write_u16_list(
825            "callable capture sources",
826            &prototype.capture_source_slots,
827            out,
828        )?;
829        write_u16_list("callable captures", &prototype.capture_slots, out)?;
830        write_u32_count("callable capture modes", prototype.capture_modes.len(), out)?;
831        for mode in &prototype.capture_modes {
832            out.push(*mode as u8);
833        }
834        match prototype.self_slot {
835            Some(slot) => {
836                out.push(1);
837                out.extend_from_slice(&slot.to_le_bytes());
838            }
839            None => out.push(0),
840        }
841        match &prototype.schema {
842            Some(schema) => {
843                out.push(1);
844                write_schema(schema, out)?;
845            }
846            None => out.push(0),
847        }
848    }
849
850    write_u32_count("function regions", program.function_regions.len(), out)?;
851    for region in &program.function_regions {
852        out.extend_from_slice(&region.start_ip.to_le_bytes());
853        out.extend_from_slice(&region.end_ip.to_le_bytes());
854        match region.prototype_id {
855            Some(id) => {
856                out.push(1);
857                out.extend_from_slice(&id.to_le_bytes());
858            }
859            None => out.push(0),
860        }
861    }
862
863    write_u32_count(
864        "root callable bindings",
865        program.root_callable_bindings.len(),
866        out,
867    )?;
868    for binding in &program.root_callable_bindings {
869        out.extend_from_slice(&binding.local_slot.to_le_bytes());
870        out.extend_from_slice(&binding.prototype_id.to_le_bytes());
871    }
872    write_u32_count("exported callables", program.exported_callables.len(), out)?;
873    for exported in &program.exported_callables {
874        write_string("exported callable name", &exported.name, out)?;
875        out.extend_from_slice(&exported.local_slot.to_le_bytes());
876    }
877    Ok(())
878}
879
880fn write_u16_list(field: &'static str, values: &[u16], out: &mut Vec<u8>) -> Result<(), WireError> {
881    write_u32_count(field, values.len(), out)?;
882    for value in values {
883        out.extend_from_slice(&value.to_le_bytes());
884    }
885    Ok(())
886}
887
888type CallableMetadata = (
889    Vec<ScriptFunction>,
890    Vec<CallablePrototype>,
891    Vec<FunctionRegion>,
892    Vec<RootCallableBinding>,
893    Vec<ExportedCallable>,
894);
895
896fn read_callable_metadata(cursor: &mut Cursor<'_>) -> Result<CallableMetadata, WireError> {
897    let function_count = cursor.read_u32()? as usize;
898    let mut script_functions = Vec::with_capacity(function_count);
899    for _ in 0..function_count {
900        script_functions.push(ScriptFunction {
901            entry_ip: cursor.read_u32()?,
902            end_ip: cursor.read_u32()?,
903        });
904    }
905
906    let prototype_count = cursor.read_u32()? as usize;
907    let mut callable_prototypes = Vec::with_capacity(prototype_count);
908    for _ in 0..prototype_count {
909        let kind = match cursor.read_u8()? {
910            0 => CallableKind::FunctionItem,
911            1 => CallableKind::Closure,
912            2 => CallableKind::HostFunction,
913            other => return Err(WireError::InvalidValueType(other)),
914        };
915        let target_tag = cursor.read_u8()?;
916        let target_id = cursor.read_u32()?;
917        let target = match target_tag {
918            0 => CallableTarget::ScriptFunction(target_id),
919            1 => CallableTarget::HostImport(
920                u16::try_from(target_id).map_err(|_| WireError::InvalidValueType(target_tag))?,
921            ),
922            other => return Err(WireError::InvalidValueType(other)),
923        };
924        let arity = cursor.read_u8()?;
925        let frame_local_count = cursor.read_u32()? as usize;
926        let parameter_slots = read_u16_list(cursor)?;
927        let capture_source_slots = read_u16_list(cursor)?;
928        let capture_slots = read_u16_list(cursor)?;
929        let capture_mode_count = cursor.read_u32()? as usize;
930        let mut capture_modes = Vec::with_capacity(capture_mode_count);
931        for _ in 0..capture_mode_count {
932            capture_modes.push(match cursor.read_u8()? {
933                0 => CaptureBindingMode::Copy,
934                1 => CaptureBindingMode::Borrow,
935                2 => CaptureBindingMode::BorrowMut,
936                3 => CaptureBindingMode::Move,
937                other => return Err(WireError::InvalidCaptureBindingMode(other)),
938            });
939        }
940        let self_slot = match cursor.read_u8()? {
941            0 => None,
942            1 => Some(cursor.read_u16()?),
943            other => return Err(WireError::InvalidBool(other)),
944        };
945        let schema = match cursor.read_u8()? {
946            0 => None,
947            1 => Some(read_schema(cursor)?),
948            other => return Err(WireError::InvalidBool(other)),
949        };
950        callable_prototypes.push(CallablePrototype {
951            kind,
952            target,
953            arity,
954            frame_local_count,
955            parameter_slots,
956            capture_source_slots,
957            capture_slots,
958            capture_modes,
959            self_slot,
960            schema,
961        });
962    }
963
964    let region_count = cursor.read_u32()? as usize;
965    let mut function_regions = Vec::with_capacity(region_count);
966    for _ in 0..region_count {
967        let start_ip = cursor.read_u32()?;
968        let end_ip = cursor.read_u32()?;
969        let prototype_id = match cursor.read_u8()? {
970            0 => None,
971            1 => Some(cursor.read_u32()?),
972            other => return Err(WireError::InvalidBool(other)),
973        };
974        function_regions.push(FunctionRegion {
975            start_ip,
976            end_ip,
977            prototype_id,
978        });
979    }
980
981    let binding_count = cursor.read_u32()? as usize;
982    let mut root_callable_bindings = Vec::with_capacity(binding_count);
983    for _ in 0..binding_count {
984        root_callable_bindings.push(RootCallableBinding {
985            local_slot: cursor.read_u16()?,
986            prototype_id: cursor.read_u32()?,
987        });
988    }
989    let export_count = cursor.read_u32()? as usize;
990    let mut exported_callables = Vec::with_capacity(export_count);
991    for _ in 0..export_count {
992        exported_callables.push(ExportedCallable {
993            name: cursor.read_string()?,
994            local_slot: cursor.read_u16()?,
995        });
996    }
997    Ok((
998        script_functions,
999        callable_prototypes,
1000        function_regions,
1001        root_callable_bindings,
1002        exported_callables,
1003    ))
1004}
1005
1006fn read_u16_list(cursor: &mut Cursor<'_>) -> Result<Vec<u16>, WireError> {
1007    let len = cursor.read_u32()? as usize;
1008    let mut values = Vec::with_capacity(len);
1009    for _ in 0..len {
1010        values.push(cursor.read_u16()?);
1011    }
1012    Ok(values)
1013}
1014
1015fn write_debug_info(out: &mut Vec<u8>, debug: Option<&DebugInfo>) -> Result<(), WireError> {
1016    match debug {
1017        None => {
1018            out.push(0);
1019            Ok(())
1020        }
1021        Some(debug) => {
1022            out.push(1);
1023
1024            match &debug.source {
1025                None => out.push(0),
1026                Some(source) => {
1027                    out.push(1);
1028                    write_string("debug source", source, out)?;
1029                }
1030            }
1031
1032            write_u32_count("debug lines", debug.lines.len(), out)?;
1033            for line in &debug.lines {
1034                out.extend_from_slice(&line.offset.to_le_bytes());
1035                out.extend_from_slice(&line.line.to_le_bytes());
1036            }
1037
1038            write_u32_count("debug functions", debug.functions.len(), out)?;
1039            for function in &debug.functions {
1040                write_string("debug function name", &function.name, out)?;
1041                write_u32_count("debug function args", function.args.len(), out)?;
1042                for arg in &function.args {
1043                    write_string("debug arg name", &arg.name, out)?;
1044                    out.push(arg.position);
1045                }
1046            }
1047
1048            write_u32_count("debug locals", debug.locals.len(), out)?;
1049            for local in &debug.locals {
1050                write_string("debug local name", &local.name, out)?;
1051                out.push(local.index);
1052                write_optional_u32(local.declared_line, out);
1053                write_optional_u32(local.last_line, out);
1054            }
1055
1056            Ok(())
1057        }
1058    }
1059}
1060
1061fn read_debug_info(cursor: &mut Cursor<'_>) -> Result<Option<DebugInfo>, WireError> {
1062    let flag = cursor.read_u8()?;
1063    match flag {
1064        0 => Ok(None),
1065        1 => {
1066            let source = match cursor.read_u8()? {
1067                0 => None,
1068                1 => Some(cursor.read_string()?),
1069                other => return Err(WireError::InvalidDebugFlag(other)),
1070            };
1071
1072            let line_count = cursor.read_u32()? as usize;
1073            let mut lines = Vec::with_capacity(line_count);
1074            for _ in 0..line_count {
1075                lines.push(LineInfo {
1076                    offset: cursor.read_u32()?,
1077                    line: cursor.read_u32()?,
1078                });
1079            }
1080
1081            let function_count = cursor.read_u32()? as usize;
1082            let mut functions = Vec::with_capacity(function_count);
1083            for _ in 0..function_count {
1084                let name = cursor.read_string()?;
1085                let arg_count = cursor.read_u32()? as usize;
1086                let mut args = Vec::with_capacity(arg_count);
1087                for _ in 0..arg_count {
1088                    args.push(ArgInfo {
1089                        name: cursor.read_string()?,
1090                        position: cursor.read_u8()?,
1091                    });
1092                }
1093                functions.push(DebugFunction { name, args });
1094            }
1095
1096            let local_count = cursor.read_u32()? as usize;
1097            let mut locals = Vec::with_capacity(local_count);
1098            for _ in 0..local_count {
1099                locals.push(LocalInfo {
1100                    name: cursor.read_string()?,
1101                    index: cursor.read_u8()?,
1102                    declared_line: read_optional_u32(cursor)?,
1103                    last_line: read_optional_u32(cursor)?,
1104                });
1105            }
1106
1107            Ok(Some(DebugInfo {
1108                source,
1109                lines,
1110                functions,
1111                locals,
1112            }))
1113        }
1114        other => Err(WireError::InvalidDebugFlag(other)),
1115    }
1116}
1117
1118fn write_type_map(out: &mut Vec<u8>, type_map: Option<&TypeMap>) -> Result<(), WireError> {
1119    let Some(type_map) = type_map else {
1120        out.push(0);
1121        return Ok(());
1122    };
1123
1124    out.push(1);
1125    out.push(u8::from(type_map.strict_types));
1126    write_u32_count("type map locals", type_map.local_types.len(), out)?;
1127    for ty in &type_map.local_types {
1128        out.push(*ty as u8);
1129    }
1130    for schema in &type_map.local_schemas {
1131        write_optional_schema(schema.as_ref(), out)?;
1132    }
1133    write_bool_slice("type map callable slots", &type_map.callable_slots, out)?;
1134    write_bool_slice("type map optional slots", &type_map.optional_slots, out)?;
1135
1136    write_u32_count("type map operands", type_map.operand_types.len(), out)?;
1137    let mut operand_entries = type_map
1138        .operand_types
1139        .iter()
1140        .map(|(offset, pair)| (*offset, *pair))
1141        .collect::<Vec<_>>();
1142    operand_entries.sort_unstable_by_key(|(offset, _)| *offset);
1143    for (offset, (lhs, rhs)) in operand_entries {
1144        write_u32_count("type map operand offset", offset, out)?;
1145        out.push(lhs as u8);
1146        out.push(rhs as u8);
1147    }
1148    Ok(())
1149}
1150
1151fn read_type_map(cursor: &mut Cursor<'_>) -> Result<Option<TypeMap>, WireError> {
1152    match cursor.read_u8()? {
1153        0 => Ok(None),
1154        1 => {
1155            let strict_types = match cursor.read_u8()? {
1156                0 => false,
1157                1 => true,
1158                other => return Err(WireError::InvalidBool(other)),
1159            };
1160            let local_count = cursor.read_u32()? as usize;
1161            let mut local_types = Vec::with_capacity(local_count);
1162            for _ in 0..local_count {
1163                local_types.push(read_value_type(cursor.read_u8()?)?);
1164            }
1165            let mut local_schemas = Vec::with_capacity(local_count);
1166            for _ in 0..local_count {
1167                local_schemas.push(read_optional_schema(cursor)?);
1168            }
1169            let callable_slots = read_bool_vec(cursor, local_count)?;
1170            let optional_slots = read_bool_vec(cursor, local_count)?;
1171
1172            let operand_count = cursor.read_u32()? as usize;
1173            let mut operand_types = HashMap::with_capacity(operand_count);
1174            for _ in 0..operand_count {
1175                let offset = cursor.read_u32()? as usize;
1176                let lhs = read_value_type(cursor.read_u8()?)?;
1177                let rhs = read_value_type(cursor.read_u8()?)?;
1178                operand_types.insert(offset, (lhs, rhs));
1179            }
1180
1181            Ok(Some(TypeMap {
1182                strict_types,
1183                local_types,
1184                local_schemas,
1185                callable_slots,
1186                optional_slots,
1187                operand_types,
1188            }))
1189        }
1190        other => Err(WireError::InvalidTypeMapFlag(other)),
1191    }
1192}
1193
1194fn read_value_type(raw: u8) -> Result<ValueType, WireError> {
1195    match raw {
1196        0 => Ok(ValueType::Unknown),
1197        1 => Ok(ValueType::Null),
1198        2 => Ok(ValueType::Int),
1199        3 => Ok(ValueType::Float),
1200        4 => Ok(ValueType::Bool),
1201        5 => Ok(ValueType::String),
1202        6 => Ok(ValueType::Bytes),
1203        7 => Ok(ValueType::Array),
1204        8 => Ok(ValueType::Map),
1205        9 => Ok(ValueType::Callable),
1206        other => Err(WireError::InvalidValueType(other)),
1207    }
1208}
1209
1210fn write_optional_u32(value: Option<u32>, out: &mut Vec<u8>) {
1211    match value {
1212        Some(value) => {
1213            out.push(1);
1214            out.extend_from_slice(&value.to_le_bytes());
1215        }
1216        None => out.push(0),
1217    }
1218}
1219
1220fn read_optional_u32(cursor: &mut Cursor<'_>) -> Result<Option<u32>, WireError> {
1221    match cursor.read_u8()? {
1222        0 => Ok(None),
1223        1 => Ok(Some(cursor.read_u32()?)),
1224        other => Err(WireError::InvalidDebugFlag(other)),
1225    }
1226}
1227
1228fn write_bool_slice(
1229    field: &'static str,
1230    values: &[bool],
1231    out: &mut Vec<u8>,
1232) -> Result<(), WireError> {
1233    write_u32_count(field, values.len(), out)?;
1234    out.extend(values.iter().map(|value| u8::from(*value)));
1235    Ok(())
1236}
1237
1238fn read_bool_vec(cursor: &mut Cursor<'_>, expected_len: usize) -> Result<Vec<bool>, WireError> {
1239    let count = cursor.read_u32()? as usize;
1240    if count != expected_len {
1241        return Err(WireError::TrailingBytes);
1242    }
1243    let mut values = Vec::with_capacity(count);
1244    for _ in 0..count {
1245        values.push(match cursor.read_u8()? {
1246            0 => false,
1247            1 => true,
1248            other => return Err(WireError::InvalidBool(other)),
1249        });
1250    }
1251    Ok(values)
1252}
1253
1254fn write_optional_schema(schema: Option<&TypeSchema>, out: &mut Vec<u8>) -> Result<(), WireError> {
1255    match schema {
1256        Some(schema) => {
1257            out.push(1);
1258            write_schema(schema, out)?;
1259        }
1260        None => out.push(0),
1261    }
1262    Ok(())
1263}
1264
1265fn read_optional_schema(cursor: &mut Cursor<'_>) -> Result<Option<TypeSchema>, WireError> {
1266    match cursor.read_u8()? {
1267        0 => Ok(None),
1268        1 => Ok(Some(read_schema(cursor)?)),
1269        other => Err(WireError::InvalidBool(other)),
1270    }
1271}
1272
1273fn write_schema(schema: &TypeSchema, out: &mut Vec<u8>) -> Result<(), WireError> {
1274    match schema {
1275        TypeSchema::Unknown => out.push(0),
1276        TypeSchema::Null => out.push(1),
1277        TypeSchema::Int => out.push(2),
1278        TypeSchema::Float => out.push(3),
1279        TypeSchema::Number => out.push(4),
1280        TypeSchema::Bool => out.push(5),
1281        TypeSchema::String => out.push(6),
1282        TypeSchema::Bytes => out.push(7),
1283        TypeSchema::Optional(inner) => {
1284            out.push(16);
1285            write_schema(inner, out)?;
1286        }
1287        TypeSchema::GenericParam(name) => {
1288            out.push(8);
1289            write_string("schema generic", name, out)?;
1290        }
1291        TypeSchema::Named(name, type_args) => {
1292            out.push(9);
1293            write_string("schema name", name, out)?;
1294            write_u32_count("schema type args", type_args.len(), out)?;
1295            for type_arg in type_args {
1296                write_schema(type_arg, out)?;
1297            }
1298        }
1299        TypeSchema::Array(item) => {
1300            out.push(10);
1301            write_schema(item, out)?;
1302        }
1303        TypeSchema::ArrayTuple(items) => {
1304            out.push(11);
1305            write_u32_count("schema tuple items", items.len(), out)?;
1306            for item in items {
1307                write_schema(item, out)?;
1308            }
1309        }
1310        TypeSchema::ArrayTupleRest { prefix, rest } => {
1311            out.push(12);
1312            write_u32_count("schema tuple prefix", prefix.len(), out)?;
1313            for item in prefix {
1314                write_schema(item, out)?;
1315            }
1316            write_schema(rest, out)?;
1317        }
1318        TypeSchema::Map(item) => {
1319            out.push(13);
1320            write_schema(item, out)?;
1321        }
1322        TypeSchema::Object(fields) => {
1323            out.push(14);
1324            let mut entries = fields.iter().collect::<Vec<_>>();
1325            entries.sort_unstable_by(|(lhs, _), (rhs, _)| lhs.cmp(rhs));
1326            write_u32_count("schema object fields", entries.len(), out)?;
1327            for (name, value) in entries {
1328                write_string("schema object field", name, out)?;
1329                write_schema(value, out)?;
1330            }
1331        }
1332        TypeSchema::Callable { params, result } => {
1333            out.push(15);
1334            write_u32_count("schema callable params", params.len(), out)?;
1335            for param in params {
1336                write_schema(param, out)?;
1337            }
1338            write_schema(result, out)?;
1339        }
1340    }
1341    Ok(())
1342}
1343
1344fn read_schema(cursor: &mut Cursor<'_>) -> Result<TypeSchema, WireError> {
1345    match cursor.read_u8()? {
1346        0 => Ok(TypeSchema::Unknown),
1347        1 => Ok(TypeSchema::Null),
1348        2 => Ok(TypeSchema::Int),
1349        3 => Ok(TypeSchema::Float),
1350        4 => Ok(TypeSchema::Number),
1351        5 => Ok(TypeSchema::Bool),
1352        6 => Ok(TypeSchema::String),
1353        7 => Ok(TypeSchema::Bytes),
1354        16 => Ok(TypeSchema::Optional(Box::new(read_schema(cursor)?))),
1355        8 => Ok(TypeSchema::GenericParam(cursor.read_string()?)),
1356        9 => {
1357            let name = cursor.read_string()?;
1358            let count = cursor.read_u32()? as usize;
1359            let mut type_args = Vec::with_capacity(count);
1360            for _ in 0..count {
1361                type_args.push(read_schema(cursor)?);
1362            }
1363            Ok(TypeSchema::Named(name, type_args))
1364        }
1365        10 => Ok(TypeSchema::Array(Box::new(read_schema(cursor)?))),
1366        11 => {
1367            let count = cursor.read_u32()? as usize;
1368            let mut items = Vec::with_capacity(count);
1369            for _ in 0..count {
1370                items.push(read_schema(cursor)?);
1371            }
1372            Ok(TypeSchema::ArrayTuple(items))
1373        }
1374        12 => {
1375            let count = cursor.read_u32()? as usize;
1376            let mut prefix = Vec::with_capacity(count);
1377            for _ in 0..count {
1378                prefix.push(read_schema(cursor)?);
1379            }
1380            let rest = Box::new(read_schema(cursor)?);
1381            Ok(TypeSchema::ArrayTupleRest { prefix, rest })
1382        }
1383        13 => Ok(TypeSchema::Map(Box::new(read_schema(cursor)?))),
1384        14 => {
1385            let count = cursor.read_u32()? as usize;
1386            let mut fields = HashMap::with_capacity(count);
1387            for _ in 0..count {
1388                let name = cursor.read_string()?;
1389                let value = read_schema(cursor)?;
1390                fields.insert(name, value);
1391            }
1392            Ok(TypeSchema::Object(fields))
1393        }
1394        15 => {
1395            let count = cursor.read_u32()? as usize;
1396            let mut params = Vec::with_capacity(count);
1397            for _ in 0..count {
1398                params.push(read_schema(cursor)?);
1399            }
1400            let result = Box::new(read_schema(cursor)?);
1401            Ok(TypeSchema::Callable { params, result })
1402        }
1403        other => Err(WireError::InvalidValueType(other)),
1404    }
1405}
1406
1407fn write_string(field: &'static str, value: &str, out: &mut Vec<u8>) -> Result<(), WireError> {
1408    write_u32_len(field, value.len(), out)?;
1409    out.extend_from_slice(value.as_bytes());
1410    Ok(())
1411}
1412
1413fn write_u32_len(field: &'static str, len: usize, out: &mut Vec<u8>) -> Result<(), WireError> {
1414    let len_u32 = u32::try_from(len).map_err(|_| WireError::LengthTooLarge(field, len))?;
1415    out.extend_from_slice(&len_u32.to_le_bytes());
1416    Ok(())
1417}
1418
1419fn write_u32_count(field: &'static str, count: usize, out: &mut Vec<u8>) -> Result<(), WireError> {
1420    write_u32_len(field, count, out)
1421}
1422
1423struct Cursor<'a> {
1424    bytes: &'a [u8],
1425    offset: usize,
1426}
1427
1428impl<'a> Cursor<'a> {
1429    fn new(bytes: &'a [u8]) -> Self {
1430        Self { bytes, offset: 0 }
1431    }
1432
1433    fn read_u8(&mut self) -> Result<u8, WireError> {
1434        let value = self
1435            .bytes
1436            .get(self.offset)
1437            .ok_or(WireError::UnexpectedEof)?;
1438        self.offset += 1;
1439        Ok(*value)
1440    }
1441
1442    fn read_u16(&mut self) -> Result<u16, WireError> {
1443        let bytes = self.read_exact_array::<2>()?;
1444        Ok(u16::from_le_bytes(bytes))
1445    }
1446
1447    fn read_u32(&mut self) -> Result<u32, WireError> {
1448        let bytes = self.read_exact_array::<4>()?;
1449        Ok(u32::from_le_bytes(bytes))
1450    }
1451
1452    fn read_i64(&mut self) -> Result<i64, WireError> {
1453        let bytes = self.read_exact_array::<8>()?;
1454        Ok(i64::from_le_bytes(bytes))
1455    }
1456
1457    fn read_f64(&mut self) -> Result<f64, WireError> {
1458        let bytes = self.read_exact_array::<8>()?;
1459        Ok(f64::from_le_bytes(bytes))
1460    }
1461
1462    fn read_string(&mut self) -> Result<String, WireError> {
1463        let len = self.read_u32()? as usize;
1464        let bytes = self.read_exact(len)?;
1465        String::from_utf8(bytes.to_vec()).map_err(|_| WireError::InvalidUtf8)
1466    }
1467
1468    fn read_exact_array<const N: usize>(&mut self) -> Result<[u8; N], WireError> {
1469        let bytes = self.read_exact(N)?;
1470        let mut out = [0u8; N];
1471        out.copy_from_slice(bytes);
1472        Ok(out)
1473    }
1474
1475    fn read_exact(&mut self, len: usize) -> Result<&'a [u8], WireError> {
1476        let end = self
1477            .offset
1478            .checked_add(len)
1479            .ok_or(WireError::UnexpectedEof)?;
1480        if end > self.bytes.len() {
1481            return Err(WireError::UnexpectedEof);
1482        }
1483        let slice = &self.bytes[self.offset..end];
1484        self.offset = end;
1485        Ok(slice)
1486    }
1487
1488    fn is_eof(&self) -> bool {
1489        self.offset == self.bytes.len()
1490    }
1491}
1492
1493fn read_u8(code: &[u8], ip: &mut usize) -> Option<u8> {
1494    let value = *code.get(*ip)?;
1495    *ip += 1;
1496    Some(value)
1497}
1498
1499fn read_u16(code: &[u8], ip: &mut usize) -> Option<u16> {
1500    let bytes = code.get(*ip..(*ip + 2))?;
1501    *ip += 2;
1502    Some(u16::from_le_bytes([bytes[0], bytes[1]]))
1503}
1504
1505fn read_u32(code: &[u8], ip: &mut usize) -> Option<u32> {
1506    let bytes = code.get(*ip..(*ip + 4))?;
1507    *ip += 4;
1508    Some(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
1509}
1510
1511fn format_hex_bytes(bytes: &[u8]) -> String {
1512    let mut out = String::new();
1513    for (idx, byte) in bytes.iter().enumerate() {
1514        if idx > 0 {
1515            out.push(' ');
1516        }
1517        out.push_str(&format!("{byte:02X}"));
1518    }
1519    out
1520}
1521
1522fn format_call_target(program: &Program, index: u16, argc: u8) -> Option<String> {
1523    if let Some(builtin) = BuiltinFunction::from_call_index(index) {
1524        return Some(format!("builtin {}/{}", builtin.name(), builtin.arity()));
1525    }
1526    program
1527        .imports
1528        .get(index as usize)
1529        .map(|import| format!("import {}/{} (argc={argc})", import.name, import.arity))
1530}