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(®ion.start_ip.to_le_bytes());
873 out.extend_from_slice(®ion.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}