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::{TypeMap, ValueType};
6use crate::compiler::ir::TypeSchema;
7use crate::debug_info::{ArgInfo, DebugFunction, DebugInfo, LineInfo, LocalInfo};
8use crate::vm::{HostImport, OpCode, Program, Value};
9
10const MAGIC: [u8; 4] = *b"VMBC";
11const VERSION_V2: u16 = 2;
12const VERSION_V3: u16 = 3;
13const VERSION_V4: u16 = 4;
14const VERSION_V5: u16 = 5;
15const VERSION_V6: u16 = 6;
16const VERSION_V8: u16 = 8;
17const ENCODE_VERSION: u16 = VERSION_V8;
18const FLAGS: u16 = 0;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum WireError {
22    UnexpectedEof,
23    InvalidMagic([u8; 4]),
24    UnsupportedVersion(u16),
25    UnsupportedFlags(u16),
26    InvalidConstantTag(u8),
27    InvalidBool(u8),
28    InvalidTypeMapFlag(u8),
29    InvalidDebugFlag(u8),
30    InvalidValueType(u8),
31    InvalidUtf8,
32    StringTooLong(usize),
33    CodeTooLong(usize),
34    UnsupportedConstantType(&'static str),
35    LengthTooLarge(&'static str, usize),
36    TrailingBytes,
37}
38
39impl std::fmt::Display for WireError {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            WireError::UnexpectedEof => write!(f, "unexpected end of input"),
43            WireError::InvalidMagic(found) => write!(f, "invalid magic: {found:?}"),
44            WireError::UnsupportedVersion(version) => {
45                write!(f, "unsupported version: {version}")
46            }
47            WireError::UnsupportedFlags(flags) => write!(f, "unsupported flags: {flags}"),
48            WireError::InvalidConstantTag(tag) => write!(f, "invalid constant tag: {tag}"),
49            WireError::InvalidBool(value) => write!(f, "invalid bool value: {value}"),
50            WireError::InvalidTypeMapFlag(value) => write!(f, "invalid type-map flag: {value}"),
51            WireError::InvalidDebugFlag(value) => write!(f, "invalid debug flag: {value}"),
52            WireError::InvalidValueType(value) => write!(f, "invalid value type: {value}"),
53            WireError::InvalidUtf8 => write!(f, "invalid utf-8 string"),
54            WireError::StringTooLong(len) => write!(f, "string too long: {len}"),
55            WireError::CodeTooLong(len) => write!(f, "code too long: {len}"),
56            WireError::UnsupportedConstantType(kind) => {
57                write!(f, "unsupported constant type for wire format: {kind}")
58            }
59            WireError::LengthTooLarge(field, len) => {
60                write!(f, "{field} length too large: {len}")
61            }
62            WireError::TrailingBytes => write!(f, "trailing bytes after program payload"),
63        }
64    }
65}
66
67impl std::error::Error for WireError {}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum ValidationError {
71    TruncatedOperand {
72        offset: usize,
73        opcode: u8,
74        expected_bytes: usize,
75    },
76    InvalidOpcode {
77        offset: usize,
78        opcode: u8,
79    },
80    InvalidConstant {
81        offset: usize,
82        index: u32,
83    },
84    InvalidCall {
85        offset: usize,
86        index: u16,
87    },
88    InvalidCallArity {
89        offset: usize,
90        index: u16,
91        expected: u8,
92        got: u8,
93    },
94    InvalidJumpTarget {
95        offset: usize,
96        target: u32,
97    },
98}
99
100impl std::fmt::Display for ValidationError {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        match self {
103            ValidationError::TruncatedOperand {
104                offset,
105                opcode,
106                expected_bytes,
107            } => write!(
108                f,
109                "truncated operand at offset {offset} for opcode {opcode:#04x}, expected {expected_bytes} bytes",
110            ),
111            ValidationError::InvalidOpcode { offset, opcode } => {
112                write!(f, "invalid opcode {opcode:#04x} at offset {offset}")
113            }
114            ValidationError::InvalidConstant { offset, index } => write!(
115                f,
116                "invalid constant index {index} for ldc instruction at offset {offset}",
117            ),
118            ValidationError::InvalidCall { offset, index } => {
119                write!(f, "invalid call index {index} at offset {offset}")
120            }
121            ValidationError::InvalidCallArity {
122                offset,
123                index,
124                expected,
125                got,
126            } => write!(
127                f,
128                "invalid call arity {got} for import index {index} at offset {offset}, expected {expected}",
129            ),
130            ValidationError::InvalidJumpTarget { offset, target } => write!(
131                f,
132                "invalid jump target {target} referenced by instruction at offset {offset}",
133            ),
134        }
135    }
136}
137
138impl std::error::Error for ValidationError {}
139
140pub fn encode_program(program: &Program) -> Result<Vec<u8>, WireError> {
141    let mut out = Vec::new();
142    out.extend_from_slice(&MAGIC);
143    out.extend_from_slice(&ENCODE_VERSION.to_le_bytes());
144    out.extend_from_slice(&FLAGS.to_le_bytes());
145    write_u32_count("constants", program.constants.len(), &mut out)?;
146
147    for constant in &program.constants {
148        match constant {
149            Value::Null => {
150                out.push(4);
151            }
152            Value::Int(value) => {
153                out.push(0);
154                out.extend_from_slice(&value.to_le_bytes());
155            }
156            Value::Float(value) => {
157                out.push(3);
158                out.extend_from_slice(&value.to_le_bytes());
159            }
160            Value::Bool(value) => {
161                out.push(1);
162                out.push(u8::from(*value));
163            }
164            Value::String(value) => {
165                out.push(2);
166                write_u32_len("constant string", value.len(), &mut out)?;
167                out.extend_from_slice(value.as_bytes());
168            }
169            Value::Bytes(value) => {
170                out.push(5);
171                write_u32_len("constant bytes", value.len(), &mut out)?;
172                out.extend_from_slice(value.as_slice());
173            }
174            Value::Array(_) => {
175                return Err(WireError::UnsupportedConstantType("array"));
176            }
177            Value::Map(_) => {
178                return Err(WireError::UnsupportedConstantType("map"));
179            }
180        }
181    }
182
183    write_u32_len("code", program.code.len(), &mut out)?;
184    out.extend_from_slice(&program.code);
185
186    if ENCODE_VERSION >= VERSION_V4 {
187        write_u32_count("imports", program.imports.len(), &mut out)?;
188        for import in &program.imports {
189            write_string("import name", &import.name, &mut out)?;
190            out.push(import.arity);
191            out.push(import.return_type as u8);
192        }
193    }
194
195    if ENCODE_VERSION >= VERSION_V6 {
196        write_type_map(&mut out, program.type_map.as_ref())?;
197    }
198
199    if ENCODE_VERSION >= VERSION_V2 {
200        write_debug_info(&mut out, program.debug.as_ref())?;
201    }
202
203    Ok(out)
204}
205
206pub fn decode_program(bytes: &[u8]) -> Result<Program, WireError> {
207    let mut cursor = Cursor::new(bytes);
208
209    let magic = cursor.read_exact_array::<4>()?;
210    if magic != MAGIC {
211        return Err(WireError::InvalidMagic(magic));
212    }
213
214    let version = cursor.read_u16()?;
215    if version != VERSION_V8 {
216        return Err(WireError::UnsupportedVersion(version));
217    }
218
219    let flags = cursor.read_u16()?;
220    if flags != FLAGS {
221        return Err(WireError::UnsupportedFlags(flags));
222    }
223
224    let constant_count = cursor.read_u32()? as usize;
225    let mut constants = Vec::with_capacity(constant_count);
226    for _ in 0..constant_count {
227        let tag = cursor.read_u8()?;
228        let value = match tag {
229            4 => Value::Null,
230            0 => Value::Int(cursor.read_i64()?),
231            3 => Value::Float(cursor.read_f64()?),
232            1 => {
233                let raw = cursor.read_u8()?;
234                match raw {
235                    0 => Value::Bool(false),
236                    1 => Value::Bool(true),
237                    other => return Err(WireError::InvalidBool(other)),
238                }
239            }
240            2 => {
241                let len = cursor.read_u32()? as usize;
242                let text_bytes = cursor.read_exact(len)?;
243                let text =
244                    String::from_utf8(text_bytes.to_vec()).map_err(|_| WireError::InvalidUtf8)?;
245                Value::string(text)
246            }
247            5 => {
248                let len = cursor.read_u32()? as usize;
249                Value::bytes(cursor.read_exact(len)?.to_vec())
250            }
251            other => return Err(WireError::InvalidConstantTag(other)),
252        };
253        constants.push(value);
254    }
255
256    let code_len = cursor.read_u32()? as usize;
257    let code = cursor.read_exact(code_len)?.to_vec();
258    let import_count = cursor.read_u32()? as usize;
259    let mut imports = Vec::with_capacity(import_count);
260    for _ in 0..import_count {
261        imports.push(HostImport {
262            name: cursor.read_string()?,
263            arity: cursor.read_u8()?,
264            return_type: read_value_type(cursor.read_u8()?)?,
265        });
266    }
267    let type_map = if version >= VERSION_V6 {
268        read_type_map(&mut cursor)?
269    } else {
270        None
271    };
272    let debug = if version >= VERSION_V2 {
273        read_debug_info(&mut cursor, version)?
274    } else {
275        None
276    };
277
278    if !cursor.is_eof() {
279        return Err(WireError::TrailingBytes);
280    }
281
282    let mut program = Program::with_imports_and_debug(constants, code, imports, debug);
283    program.type_map = type_map;
284    Ok(program)
285}
286
287pub fn validate_program(program: &Program, host_fn_count: u16) -> Result<(), ValidationError> {
288    analyze_program(program, Some(host_fn_count)).map(|_| ())
289}
290
291pub fn infer_local_count(program: &Program) -> Result<usize, ValidationError> {
292    let analysis = analyze_program(program, None)?;
293    Ok(match analysis.max_local_index {
294        Some(index) => index as usize + 1,
295        None => 0,
296    })
297}
298
299#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
300pub struct DisassembleOptions {
301    pub show_source: bool,
302}
303
304pub fn disassemble_vmbc(bytes: &[u8]) -> Result<String, WireError> {
305    disassemble_vmbc_with_options(bytes, DisassembleOptions::default())
306}
307
308pub fn disassemble_vmbc_with_options(
309    bytes: &[u8],
310    options: DisassembleOptions,
311) -> Result<String, WireError> {
312    let program = decode_program(bytes)?;
313    Ok(disassemble_program_with_options(&program, options))
314}
315
316pub fn disassemble_program(program: &Program) -> String {
317    disassemble_program_with_options(program, DisassembleOptions::default())
318}
319
320pub fn disassemble_program_with_options(program: &Program, options: DisassembleOptions) -> String {
321    let mut out = String::new();
322    let _ = writeln!(&mut out, "constants ({}):", program.constants.len());
323    for (index, constant) in program.constants.iter().enumerate() {
324        let _ = writeln!(&mut out, "  [{index:04}] {constant:?}");
325    }
326
327    let _ = writeln!(&mut out, "imports ({}):", program.imports.len());
328    for (index, import) in program.imports.iter().enumerate() {
329        let _ = writeln!(&mut out, "  [{index:04}] {}/{}", import.name, import.arity);
330    }
331    let _ = writeln!(&mut out, "code ({} bytes):", program.code.len());
332    let mut source_annotations = source_annotations(program, options.show_source);
333    if options.show_source && source_annotations.is_none() {
334        let _ = writeln!(&mut out, "      ; source: <none>");
335    }
336    let code = &program.code;
337    let mut ip = 0usize;
338    while ip < code.len() {
339        let start = ip;
340        if let Some(lines_at_offset) = source_annotations
341            .as_mut()
342            .and_then(|annotations| annotations.remove(&start))
343        {
344            for (line, text) in lines_at_offset {
345                let _ = writeln!(&mut out, "      ; src {line:04}  {text}");
346            }
347        }
348        let opcode = code[ip];
349        ip += 1;
350
351        let mut instruction = String::new();
352        let mut truncated = false;
353        match opcode {
354            x if x == OpCode::Nop as u8 => instruction.push_str("nop"),
355            x if x == OpCode::Ret as u8 => instruction.push_str("ret"),
356            x if x == OpCode::Ldc as u8 => {
357                if let Some(index) = read_u32(code, &mut ip) {
358                    instruction.push_str(&format!("ldc {index}"));
359                    if let Some(value) = program.constants.get(index as usize) {
360                        instruction.push_str(&format!(" ; const[{index}]={value:?}"));
361                    }
362                } else {
363                    instruction.push_str("ldc <truncated>");
364                    truncated = true;
365                }
366            }
367            x if x == OpCode::Add as u8 => instruction.push_str("add"),
368            x if x == OpCode::Sub as u8 => instruction.push_str("sub"),
369            x if x == OpCode::Mul as u8 => instruction.push_str("mul"),
370            x if x == OpCode::Div as u8 => instruction.push_str("div"),
371            x if x == OpCode::Neg as u8 => instruction.push_str("neg"),
372            x if x == OpCode::Not as u8 => instruction.push_str("not"),
373            x if x == OpCode::Ceq as u8 => instruction.push_str("ceq"),
374            x if x == OpCode::Clt as u8 => instruction.push_str("clt"),
375            x if x == OpCode::Cgt as u8 => instruction.push_str("cgt"),
376            x if x == OpCode::Br as u8 => {
377                if let Some(target) = read_u32(code, &mut ip) {
378                    instruction.push_str(&format!("br {target}"));
379                } else {
380                    instruction.push_str("br <truncated>");
381                    truncated = true;
382                }
383            }
384            x if x == OpCode::Brfalse as u8 => {
385                if let Some(target) = read_u32(code, &mut ip) {
386                    instruction.push_str(&format!("brfalse {target}"));
387                } else {
388                    instruction.push_str("brfalse <truncated>");
389                    truncated = true;
390                }
391            }
392            x if x == OpCode::Pop as u8 => instruction.push_str("pop"),
393            x if x == OpCode::Dup as u8 => instruction.push_str("dup"),
394            x if x == OpCode::Ldloc as u8 => {
395                if let Some(index) = read_u8(code, &mut ip) {
396                    instruction.push_str(&format!("ldloc {index}"));
397                } else {
398                    instruction.push_str("ldloc <truncated>");
399                    truncated = true;
400                }
401            }
402            x if x == OpCode::Stloc as u8 => {
403                if let Some(index) = read_u8(code, &mut ip) {
404                    instruction.push_str(&format!("stloc {index}"));
405                } else {
406                    instruction.push_str("stloc <truncated>");
407                    truncated = true;
408                }
409            }
410            x if x == OpCode::Call as u8 => {
411                if let Some(index) = read_u16(code, &mut ip) {
412                    if let Some(argc) = read_u8(code, &mut ip) {
413                        instruction.push_str(&format!("call {index} {argc}"));
414                        if let Some(comment) = format_call_target(program, index, argc) {
415                            instruction.push_str(&format!(" ; {comment}"));
416                        }
417                    } else {
418                        instruction.push_str("call <truncated>");
419                        truncated = true;
420                    }
421                } else {
422                    instruction.push_str("call <truncated>");
423                    truncated = true;
424                }
425            }
426            x if x == OpCode::Shl as u8 => instruction.push_str("shl"),
427            x if x == OpCode::Shr as u8 => instruction.push_str("shr"),
428            x if x == OpCode::Lshr as u8 => instruction.push_str("lshr"),
429            x if x == OpCode::Mod as u8 => instruction.push_str("mod"),
430            x if x == OpCode::And as u8 => instruction.push_str("and"),
431            x if x == OpCode::Or as u8 => instruction.push_str("or"),
432            other => instruction.push_str(&format!(".byte 0x{other:02X} ; invalid opcode")),
433        }
434
435        let encoded = format_hex_bytes(&code[start..ip]);
436        let _ = writeln!(&mut out, "{start:04}\t{encoded:<14}\t{instruction}");
437        if truncated {
438            break;
439        }
440    }
441
442    out
443}
444
445fn source_annotations(
446    program: &Program,
447    show_source: bool,
448) -> Option<BTreeMap<usize, Vec<(u32, String)>>> {
449    if !show_source {
450        return None;
451    }
452    let debug = program.debug.as_ref()?;
453    let source = debug.source.as_ref()?;
454    let source_lines = source.lines().collect::<Vec<_>>();
455    let mut first_offset_by_line = HashMap::<u32, u32>::new();
456    for info in &debug.lines {
457        first_offset_by_line.entry(info.line).or_insert(info.offset);
458    }
459    let mut pairs = first_offset_by_line
460        .into_iter()
461        .map(|(line, offset)| (offset, line))
462        .collect::<Vec<_>>();
463    pairs.sort_by_key(|(offset, line)| (*offset, *line));
464
465    let mut annotations = BTreeMap::<usize, Vec<(u32, String)>>::new();
466    for (offset, line) in pairs {
467        let text = source_lines
468            .get(line.saturating_sub(1) as usize)
469            .copied()
470            .unwrap_or("<missing source line>")
471            .to_string();
472        annotations
473            .entry(offset as usize)
474            .or_default()
475            .push((line, text));
476    }
477    Some(annotations)
478}
479
480struct ProgramAnalysis {
481    max_local_index: Option<u8>,
482}
483
484fn analyze_program(
485    program: &Program,
486    host_fn_count: Option<u16>,
487) -> Result<ProgramAnalysis, ValidationError> {
488    let mut ip = 0usize;
489    let mut instruction_starts = HashSet::new();
490    let mut jump_targets: Vec<(usize, u32)> = Vec::new();
491    let mut max_local_index: Option<u8> = None;
492    let code = &program.code;
493
494    while ip < code.len() {
495        let start = ip;
496        instruction_starts.insert(start);
497        let opcode = code[ip];
498        ip += 1;
499
500        match opcode {
501            x if x == OpCode::Nop as u8 || x == OpCode::Ret as u8 => {}
502            x if x == OpCode::Ldc as u8 => {
503                let index = read_u32(code, &mut ip).ok_or(ValidationError::TruncatedOperand {
504                    offset: start,
505                    opcode,
506                    expected_bytes: 4,
507                })?;
508                if index as usize >= program.constants.len() {
509                    return Err(ValidationError::InvalidConstant {
510                        offset: start,
511                        index,
512                    });
513                }
514            }
515            x if x == OpCode::Add as u8
516                || x == OpCode::Sub as u8
517                || x == OpCode::Mul as u8
518                || x == OpCode::Div as u8
519                || x == OpCode::Shl as u8
520                || x == OpCode::Shr as u8
521                || x == OpCode::Lshr as u8
522                || x == OpCode::Mod as u8
523                || x == OpCode::And as u8
524                || x == OpCode::Or as u8
525                || x == OpCode::Neg as u8
526                || x == OpCode::Not as u8
527                || x == OpCode::Ceq as u8
528                || x == OpCode::Clt as u8
529                || x == OpCode::Cgt as u8
530                || x == OpCode::Pop as u8
531                || x == OpCode::Dup as u8 => {}
532            x if x == OpCode::Br as u8 || x == OpCode::Brfalse as u8 => {
533                let target = read_u32(code, &mut ip).ok_or(ValidationError::TruncatedOperand {
534                    offset: start,
535                    opcode,
536                    expected_bytes: 4,
537                })?;
538                jump_targets.push((start, target));
539            }
540            x if x == OpCode::Ldloc as u8 || x == OpCode::Stloc as u8 => {
541                let index = read_u8(code, &mut ip).ok_or(ValidationError::TruncatedOperand {
542                    offset: start,
543                    opcode,
544                    expected_bytes: 1,
545                })?;
546                max_local_index = Some(max_local_index.map_or(index, |prev| prev.max(index)));
547            }
548            x if x == OpCode::Call as u8 => {
549                let index = read_u16(code, &mut ip).ok_or(ValidationError::TruncatedOperand {
550                    offset: start,
551                    opcode,
552                    expected_bytes: 3,
553                })?;
554                let argc = read_u8(code, &mut ip).ok_or(ValidationError::TruncatedOperand {
555                    offset: start,
556                    opcode,
557                    expected_bytes: 3,
558                })?;
559                if let Some(builtin) = BuiltinFunction::from_call_index(index) {
560                    if !builtin.accepts_arity(argc) {
561                        return Err(ValidationError::InvalidCallArity {
562                            offset: start,
563                            index,
564                            expected: builtin.arity(),
565                            got: argc,
566                        });
567                    }
568                    continue;
569                }
570                if program.imports.is_empty() {
571                    if let Some(host_fn_count) = host_fn_count
572                        && index >= host_fn_count
573                    {
574                        return Err(ValidationError::InvalidCall {
575                            offset: start,
576                            index,
577                        });
578                    }
579                } else {
580                    let Some(import) = program.imports.get(index as usize) else {
581                        return Err(ValidationError::InvalidCall {
582                            offset: start,
583                            index,
584                        });
585                    };
586                    if argc != import.arity {
587                        return Err(ValidationError::InvalidCallArity {
588                            offset: start,
589                            index,
590                            expected: import.arity,
591                            got: argc,
592                        });
593                    }
594                }
595            }
596            other => {
597                return Err(ValidationError::InvalidOpcode {
598                    offset: start,
599                    opcode: other,
600                });
601            }
602        }
603    }
604
605    for (offset, target) in &jump_targets {
606        let target = *target as usize;
607        if target >= code.len() || !instruction_starts.contains(&target) {
608            return Err(ValidationError::InvalidJumpTarget {
609                offset: *offset,
610                target: target as u32,
611            });
612        }
613    }
614
615    Ok(ProgramAnalysis { max_local_index })
616}
617
618fn write_debug_info(out: &mut Vec<u8>, debug: Option<&DebugInfo>) -> Result<(), WireError> {
619    match debug {
620        None => {
621            out.push(0);
622            Ok(())
623        }
624        Some(debug) => {
625            out.push(1);
626
627            match &debug.source {
628                None => out.push(0),
629                Some(source) => {
630                    out.push(1);
631                    write_string("debug source", source, out)?;
632                }
633            }
634
635            write_u32_count("debug lines", debug.lines.len(), out)?;
636            for line in &debug.lines {
637                out.extend_from_slice(&line.offset.to_le_bytes());
638                out.extend_from_slice(&line.line.to_le_bytes());
639            }
640
641            write_u32_count("debug functions", debug.functions.len(), out)?;
642            for function in &debug.functions {
643                write_string("debug function name", &function.name, out)?;
644                write_u32_count("debug function args", function.args.len(), out)?;
645                for arg in &function.args {
646                    write_string("debug arg name", &arg.name, out)?;
647                    out.push(arg.position);
648                }
649            }
650
651            write_u32_count("debug locals", debug.locals.len(), out)?;
652            for local in &debug.locals {
653                write_string("debug local name", &local.name, out)?;
654                out.push(local.index);
655                if ENCODE_VERSION >= VERSION_V5 {
656                    write_optional_u32(local.declared_line, out);
657                    write_optional_u32(local.last_line, out);
658                }
659            }
660
661            Ok(())
662        }
663    }
664}
665
666fn read_debug_info(cursor: &mut Cursor<'_>, version: u16) -> Result<Option<DebugInfo>, WireError> {
667    let flag = cursor.read_u8()?;
668    match flag {
669        0 => Ok(None),
670        1 => {
671            let source = match cursor.read_u8()? {
672                0 => None,
673                1 => Some(cursor.read_string()?),
674                other => return Err(WireError::InvalidDebugFlag(other)),
675            };
676
677            let line_count = cursor.read_u32()? as usize;
678            let mut lines = Vec::with_capacity(line_count);
679            for _ in 0..line_count {
680                lines.push(LineInfo {
681                    offset: cursor.read_u32()?,
682                    line: cursor.read_u32()?,
683                });
684            }
685
686            let function_count = cursor.read_u32()? as usize;
687            let mut functions = Vec::with_capacity(function_count);
688            for _ in 0..function_count {
689                let name = cursor.read_string()?;
690                let arg_count = cursor.read_u32()? as usize;
691                let mut args = Vec::with_capacity(arg_count);
692                for _ in 0..arg_count {
693                    args.push(ArgInfo {
694                        name: cursor.read_string()?,
695                        position: cursor.read_u8()?,
696                    });
697                }
698                functions.push(DebugFunction { name, args });
699            }
700
701            let locals = if version >= VERSION_V3 {
702                let local_count = cursor.read_u32()? as usize;
703                let mut locals = Vec::with_capacity(local_count);
704                for _ in 0..local_count {
705                    let name = cursor.read_string()?;
706                    let index = cursor.read_u8()?;
707                    let (declared_line, last_line) = if version >= VERSION_V5 {
708                        (read_optional_u32(cursor)?, read_optional_u32(cursor)?)
709                    } else {
710                        (None, None)
711                    };
712                    locals.push(LocalInfo {
713                        name,
714                        index,
715                        declared_line,
716                        last_line,
717                    });
718                }
719                locals
720            } else {
721                Vec::new()
722            };
723
724            Ok(Some(DebugInfo {
725                source,
726                lines,
727                functions,
728                locals,
729            }))
730        }
731        other => Err(WireError::InvalidDebugFlag(other)),
732    }
733}
734
735fn write_type_map(out: &mut Vec<u8>, type_map: Option<&TypeMap>) -> Result<(), WireError> {
736    let Some(type_map) = type_map else {
737        out.push(0);
738        return Ok(());
739    };
740
741    out.push(1);
742    out.push(u8::from(type_map.strict_types));
743    write_u32_count("type map locals", type_map.local_types.len(), out)?;
744    for ty in &type_map.local_types {
745        out.push(*ty as u8);
746    }
747    for schema in &type_map.local_schemas {
748        write_optional_schema(schema.as_ref(), out)?;
749    }
750    write_bool_slice("type map callable slots", &type_map.callable_slots, out)?;
751    write_bool_slice("type map optional slots", &type_map.optional_slots, out)?;
752
753    write_u32_count("type map operands", type_map.operand_types.len(), out)?;
754    let mut operand_entries = type_map
755        .operand_types
756        .iter()
757        .map(|(offset, pair)| (*offset, *pair))
758        .collect::<Vec<_>>();
759    operand_entries.sort_unstable_by_key(|(offset, _)| *offset);
760    for (offset, (lhs, rhs)) in operand_entries {
761        write_u32_count("type map operand offset", offset, out)?;
762        out.push(lhs as u8);
763        out.push(rhs as u8);
764    }
765    Ok(())
766}
767
768fn read_type_map(cursor: &mut Cursor<'_>) -> Result<Option<TypeMap>, WireError> {
769    match cursor.read_u8()? {
770        0 => Ok(None),
771        1 => {
772            let strict_types = match cursor.read_u8()? {
773                0 => false,
774                1 => true,
775                other => return Err(WireError::InvalidBool(other)),
776            };
777            let local_count = cursor.read_u32()? as usize;
778            let mut local_types = Vec::with_capacity(local_count);
779            for _ in 0..local_count {
780                local_types.push(read_value_type(cursor.read_u8()?)?);
781            }
782            let mut local_schemas = Vec::with_capacity(local_count);
783            for _ in 0..local_count {
784                local_schemas.push(read_optional_schema(cursor)?);
785            }
786            let callable_slots = read_bool_vec(cursor, local_count)?;
787            let optional_slots = read_bool_vec(cursor, local_count)?;
788
789            let operand_count = cursor.read_u32()? as usize;
790            let mut operand_types = HashMap::with_capacity(operand_count);
791            for _ in 0..operand_count {
792                let offset = cursor.read_u32()? as usize;
793                let lhs = read_value_type(cursor.read_u8()?)?;
794                let rhs = read_value_type(cursor.read_u8()?)?;
795                operand_types.insert(offset, (lhs, rhs));
796            }
797
798            Ok(Some(TypeMap {
799                strict_types,
800                local_types,
801                local_schemas,
802                callable_slots,
803                optional_slots,
804                operand_types,
805            }))
806        }
807        other => Err(WireError::InvalidTypeMapFlag(other)),
808    }
809}
810
811fn read_value_type(raw: u8) -> Result<ValueType, WireError> {
812    match raw {
813        0 => Ok(ValueType::Unknown),
814        1 => Ok(ValueType::Null),
815        2 => Ok(ValueType::Int),
816        3 => Ok(ValueType::Float),
817        4 => Ok(ValueType::Bool),
818        5 => Ok(ValueType::String),
819        6 => Ok(ValueType::Bytes),
820        7 => Ok(ValueType::Array),
821        8 => Ok(ValueType::Map),
822        other => Err(WireError::InvalidValueType(other)),
823    }
824}
825
826fn write_optional_u32(value: Option<u32>, out: &mut Vec<u8>) {
827    match value {
828        Some(value) => {
829            out.push(1);
830            out.extend_from_slice(&value.to_le_bytes());
831        }
832        None => out.push(0),
833    }
834}
835
836fn read_optional_u32(cursor: &mut Cursor<'_>) -> Result<Option<u32>, WireError> {
837    match cursor.read_u8()? {
838        0 => Ok(None),
839        1 => Ok(Some(cursor.read_u32()?)),
840        other => Err(WireError::InvalidDebugFlag(other)),
841    }
842}
843
844fn write_bool_slice(
845    field: &'static str,
846    values: &[bool],
847    out: &mut Vec<u8>,
848) -> Result<(), WireError> {
849    write_u32_count(field, values.len(), out)?;
850    out.extend(values.iter().map(|value| u8::from(*value)));
851    Ok(())
852}
853
854fn read_bool_vec(cursor: &mut Cursor<'_>, expected_len: usize) -> Result<Vec<bool>, WireError> {
855    let count = cursor.read_u32()? as usize;
856    if count != expected_len {
857        return Err(WireError::TrailingBytes);
858    }
859    let mut values = Vec::with_capacity(count);
860    for _ in 0..count {
861        values.push(match cursor.read_u8()? {
862            0 => false,
863            1 => true,
864            other => return Err(WireError::InvalidBool(other)),
865        });
866    }
867    Ok(values)
868}
869
870fn write_optional_schema(schema: Option<&TypeSchema>, out: &mut Vec<u8>) -> Result<(), WireError> {
871    match schema {
872        Some(schema) => {
873            out.push(1);
874            write_schema(schema, out)?;
875        }
876        None => out.push(0),
877    }
878    Ok(())
879}
880
881fn read_optional_schema(cursor: &mut Cursor<'_>) -> Result<Option<TypeSchema>, WireError> {
882    match cursor.read_u8()? {
883        0 => Ok(None),
884        1 => Ok(Some(read_schema(cursor)?)),
885        other => Err(WireError::InvalidBool(other)),
886    }
887}
888
889fn write_schema(schema: &TypeSchema, out: &mut Vec<u8>) -> Result<(), WireError> {
890    match schema {
891        TypeSchema::Unknown => out.push(0),
892        TypeSchema::Null => out.push(1),
893        TypeSchema::Int => out.push(2),
894        TypeSchema::Float => out.push(3),
895        TypeSchema::Number => out.push(4),
896        TypeSchema::Bool => out.push(5),
897        TypeSchema::String => out.push(6),
898        TypeSchema::Bytes => out.push(7),
899        TypeSchema::Optional(inner) => {
900            out.push(16);
901            write_schema(inner, out)?;
902        }
903        TypeSchema::GenericParam(name) => {
904            out.push(8);
905            write_string("schema generic", name, out)?;
906        }
907        TypeSchema::Named(name, type_args) => {
908            out.push(9);
909            write_string("schema name", name, out)?;
910            write_u32_count("schema type args", type_args.len(), out)?;
911            for type_arg in type_args {
912                write_schema(type_arg, out)?;
913            }
914        }
915        TypeSchema::Array(item) => {
916            out.push(10);
917            write_schema(item, out)?;
918        }
919        TypeSchema::ArrayTuple(items) => {
920            out.push(11);
921            write_u32_count("schema tuple items", items.len(), out)?;
922            for item in items {
923                write_schema(item, out)?;
924            }
925        }
926        TypeSchema::ArrayTupleRest { prefix, rest } => {
927            out.push(12);
928            write_u32_count("schema tuple prefix", prefix.len(), out)?;
929            for item in prefix {
930                write_schema(item, out)?;
931            }
932            write_schema(rest, out)?;
933        }
934        TypeSchema::Map(item) => {
935            out.push(13);
936            write_schema(item, out)?;
937        }
938        TypeSchema::Object(fields) => {
939            out.push(14);
940            let mut entries = fields.iter().collect::<Vec<_>>();
941            entries.sort_unstable_by(|(lhs, _), (rhs, _)| lhs.cmp(rhs));
942            write_u32_count("schema object fields", entries.len(), out)?;
943            for (name, value) in entries {
944                write_string("schema object field", name, out)?;
945                write_schema(value, out)?;
946            }
947        }
948        TypeSchema::Callable { params, result } => {
949            out.push(15);
950            write_u32_count("schema callable params", params.len(), out)?;
951            for param in params {
952                write_schema(param, out)?;
953            }
954            write_schema(result, out)?;
955        }
956    }
957    Ok(())
958}
959
960fn read_schema(cursor: &mut Cursor<'_>) -> Result<TypeSchema, WireError> {
961    match cursor.read_u8()? {
962        0 => Ok(TypeSchema::Unknown),
963        1 => Ok(TypeSchema::Null),
964        2 => Ok(TypeSchema::Int),
965        3 => Ok(TypeSchema::Float),
966        4 => Ok(TypeSchema::Number),
967        5 => Ok(TypeSchema::Bool),
968        6 => Ok(TypeSchema::String),
969        7 => Ok(TypeSchema::Bytes),
970        16 => Ok(TypeSchema::Optional(Box::new(read_schema(cursor)?))),
971        8 => Ok(TypeSchema::GenericParam(cursor.read_string()?)),
972        9 => {
973            let name = cursor.read_string()?;
974            let count = cursor.read_u32()? as usize;
975            let mut type_args = Vec::with_capacity(count);
976            for _ in 0..count {
977                type_args.push(read_schema(cursor)?);
978            }
979            Ok(TypeSchema::Named(name, type_args))
980        }
981        10 => Ok(TypeSchema::Array(Box::new(read_schema(cursor)?))),
982        11 => {
983            let count = cursor.read_u32()? as usize;
984            let mut items = Vec::with_capacity(count);
985            for _ in 0..count {
986                items.push(read_schema(cursor)?);
987            }
988            Ok(TypeSchema::ArrayTuple(items))
989        }
990        12 => {
991            let count = cursor.read_u32()? as usize;
992            let mut prefix = Vec::with_capacity(count);
993            for _ in 0..count {
994                prefix.push(read_schema(cursor)?);
995            }
996            let rest = Box::new(read_schema(cursor)?);
997            Ok(TypeSchema::ArrayTupleRest { prefix, rest })
998        }
999        13 => Ok(TypeSchema::Map(Box::new(read_schema(cursor)?))),
1000        14 => {
1001            let count = cursor.read_u32()? as usize;
1002            let mut fields = HashMap::with_capacity(count);
1003            for _ in 0..count {
1004                let name = cursor.read_string()?;
1005                let value = read_schema(cursor)?;
1006                fields.insert(name, value);
1007            }
1008            Ok(TypeSchema::Object(fields))
1009        }
1010        15 => {
1011            let count = cursor.read_u32()? as usize;
1012            let mut params = Vec::with_capacity(count);
1013            for _ in 0..count {
1014                params.push(read_schema(cursor)?);
1015            }
1016            let result = Box::new(read_schema(cursor)?);
1017            Ok(TypeSchema::Callable { params, result })
1018        }
1019        other => Err(WireError::InvalidValueType(other)),
1020    }
1021}
1022
1023fn write_string(field: &'static str, value: &str, out: &mut Vec<u8>) -> Result<(), WireError> {
1024    write_u32_len(field, value.len(), out)?;
1025    out.extend_from_slice(value.as_bytes());
1026    Ok(())
1027}
1028
1029fn write_u32_len(field: &'static str, len: usize, out: &mut Vec<u8>) -> Result<(), WireError> {
1030    let len_u32 = u32::try_from(len).map_err(|_| WireError::LengthTooLarge(field, len))?;
1031    out.extend_from_slice(&len_u32.to_le_bytes());
1032    Ok(())
1033}
1034
1035fn write_u32_count(field: &'static str, count: usize, out: &mut Vec<u8>) -> Result<(), WireError> {
1036    write_u32_len(field, count, out)
1037}
1038
1039struct Cursor<'a> {
1040    bytes: &'a [u8],
1041    offset: usize,
1042}
1043
1044impl<'a> Cursor<'a> {
1045    fn new(bytes: &'a [u8]) -> Self {
1046        Self { bytes, offset: 0 }
1047    }
1048
1049    fn read_u8(&mut self) -> Result<u8, WireError> {
1050        let value = self
1051            .bytes
1052            .get(self.offset)
1053            .ok_or(WireError::UnexpectedEof)?;
1054        self.offset += 1;
1055        Ok(*value)
1056    }
1057
1058    fn read_u16(&mut self) -> Result<u16, WireError> {
1059        let bytes = self.read_exact_array::<2>()?;
1060        Ok(u16::from_le_bytes(bytes))
1061    }
1062
1063    fn read_u32(&mut self) -> Result<u32, WireError> {
1064        let bytes = self.read_exact_array::<4>()?;
1065        Ok(u32::from_le_bytes(bytes))
1066    }
1067
1068    fn read_i64(&mut self) -> Result<i64, WireError> {
1069        let bytes = self.read_exact_array::<8>()?;
1070        Ok(i64::from_le_bytes(bytes))
1071    }
1072
1073    fn read_f64(&mut self) -> Result<f64, WireError> {
1074        let bytes = self.read_exact_array::<8>()?;
1075        Ok(f64::from_le_bytes(bytes))
1076    }
1077
1078    fn read_string(&mut self) -> Result<String, WireError> {
1079        let len = self.read_u32()? as usize;
1080        let bytes = self.read_exact(len)?;
1081        String::from_utf8(bytes.to_vec()).map_err(|_| WireError::InvalidUtf8)
1082    }
1083
1084    fn read_exact_array<const N: usize>(&mut self) -> Result<[u8; N], WireError> {
1085        let bytes = self.read_exact(N)?;
1086        let mut out = [0u8; N];
1087        out.copy_from_slice(bytes);
1088        Ok(out)
1089    }
1090
1091    fn read_exact(&mut self, len: usize) -> Result<&'a [u8], WireError> {
1092        let end = self
1093            .offset
1094            .checked_add(len)
1095            .ok_or(WireError::UnexpectedEof)?;
1096        if end > self.bytes.len() {
1097            return Err(WireError::UnexpectedEof);
1098        }
1099        let slice = &self.bytes[self.offset..end];
1100        self.offset = end;
1101        Ok(slice)
1102    }
1103
1104    fn is_eof(&self) -> bool {
1105        self.offset == self.bytes.len()
1106    }
1107}
1108
1109fn read_u8(code: &[u8], ip: &mut usize) -> Option<u8> {
1110    let value = *code.get(*ip)?;
1111    *ip += 1;
1112    Some(value)
1113}
1114
1115fn read_u16(code: &[u8], ip: &mut usize) -> Option<u16> {
1116    let bytes = code.get(*ip..(*ip + 2))?;
1117    *ip += 2;
1118    Some(u16::from_le_bytes([bytes[0], bytes[1]]))
1119}
1120
1121fn read_u32(code: &[u8], ip: &mut usize) -> Option<u32> {
1122    let bytes = code.get(*ip..(*ip + 4))?;
1123    *ip += 4;
1124    Some(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
1125}
1126
1127fn format_hex_bytes(bytes: &[u8]) -> String {
1128    let mut out = String::new();
1129    for (idx, byte) in bytes.iter().enumerate() {
1130        if idx > 0 {
1131            out.push(' ');
1132        }
1133        out.push_str(&format!("{byte:02X}"));
1134    }
1135    out
1136}
1137
1138fn format_call_target(program: &Program, index: u16, argc: u8) -> Option<String> {
1139    if let Some(builtin) = BuiltinFunction::from_call_index(index) {
1140        return Some(format!("builtin {}/{}", builtin.name(), builtin.arity()));
1141    }
1142    program
1143        .imports
1144        .get(index as usize)
1145        .map(|import| format!("import {}/{} (argc={argc})", import.name, import.arity))
1146}