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