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