1use serde::{Deserialize, Serialize};
27use shape_runtime::snapshot::{
28 SerializableVMValue, SnapshotStore, nanboxed_to_serializable, serializable_to_nanboxed,
29};
30use shape_runtime::type_schema::TypeSchemaRegistry;
31use shape_value::ValueWord;
32
33use shape_wire::WireValue;
34
35use crate::bytecode::{BytecodeProgram, FunctionBlob, FunctionHash, Program};
36use crate::executor::{VMConfig, VirtualMachine};
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct RemoteCallRequest {
45 pub program: BytecodeProgram,
48
49 pub function_name: String,
51
52 pub function_id: Option<u16>,
55
56 #[serde(default)]
61 pub function_hash: Option<FunctionHash>,
62
63 pub arguments: Vec<SerializableVMValue>,
65
66 pub upvalues: Option<Vec<SerializableVMValue>>,
69
70 pub type_schemas: TypeSchemaRegistry,
73
74 pub program_hash: [u8; 32],
77
78 #[serde(default)]
83 pub function_blobs: Option<Vec<(FunctionHash, FunctionBlob)>>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct RemoteCallResponse {
89 pub result: Result<SerializableVMValue, RemoteCallError>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct RemoteCallError {
96 pub message: String,
98 pub kind: RemoteErrorKind,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub enum RemoteErrorKind {
105 FunctionNotFound,
107 ArgumentError,
109 RuntimeError,
111 MissingModuleFunction,
113}
114
115impl std::fmt::Display for RemoteCallError {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 write!(f, "{:?}: {}", self.kind, self.message)
118 }
119}
120
121impl std::error::Error for RemoteCallError {}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
132pub enum WireMessage {
133 BlobNegotiation(BlobNegotiationRequest),
135 BlobNegotiationReply(BlobNegotiationResponse),
137 Call(RemoteCallRequest),
139 CallResponse(RemoteCallResponse),
141 Sidecar(BlobSidecar),
143
144 Execute(ExecuteRequest),
147 ExecuteResponse(ExecuteResponse),
149 Validate(ValidateRequest),
151 ValidateResponse(ValidateResponse),
153 Auth(AuthRequest),
155 AuthResponse(AuthResponse),
157 Ping(PingRequest),
159 Pong(ServerInfo),
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct PingRequest {}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct BlobNegotiationRequest {
170 pub offered_hashes: Vec<FunctionHash>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct BlobNegotiationResponse {
177 pub known_hashes: Vec<FunctionHash>,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct BlobSidecar {
187 pub sidecar_id: u32,
188 pub data: Vec<u8>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ExecuteRequest {
198 pub code: String,
200 pub request_id: u64,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct ExecuteResponse {
207 pub request_id: u64,
209 pub success: bool,
211 pub value: WireValue,
213 pub stdout: Option<String>,
215 pub error: Option<String>,
217 #[serde(skip_serializing_if = "Option::is_none", default)]
219 pub content_terminal: Option<String>,
220 #[serde(skip_serializing_if = "Option::is_none", default)]
222 pub content_html: Option<String>,
223 pub diagnostics: Vec<WireDiagnostic>,
225 pub metrics: Option<ExecutionMetrics>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct ValidateRequest {
232 pub code: String,
234 pub request_id: u64,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct ValidateResponse {
241 pub request_id: u64,
243 pub success: bool,
245 pub diagnostics: Vec<WireDiagnostic>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct AuthRequest {
252 pub token: String,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct AuthResponse {
259 pub authenticated: bool,
261 pub error: Option<String>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct ServerInfo {
268 pub shape_version: String,
270 pub wire_protocol: u32,
272 pub capabilities: Vec<String>,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct WireDiagnostic {
279 pub severity: String,
281 pub message: String,
283 pub line: Option<u32>,
285 pub column: Option<u32>,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct ExecutionMetrics {
292 pub instructions_executed: u64,
294 pub wall_time_ms: u64,
296 pub memory_bytes_peak: u64,
298}
299
300pub struct RemoteBlobCache {
309 blobs: std::collections::HashMap<FunctionHash, FunctionBlob>,
310 order: Vec<FunctionHash>,
312 max_entries: usize,
314}
315
316impl RemoteBlobCache {
317 pub fn new(max_entries: usize) -> Self {
319 Self {
320 blobs: std::collections::HashMap::new(),
321 order: Vec::new(),
322 max_entries,
323 }
324 }
325
326 pub fn default_cache() -> Self {
328 Self::new(4096)
329 }
330
331 pub fn insert(&mut self, hash: FunctionHash, blob: FunctionBlob) {
333 if self.blobs.contains_key(&hash) {
334 self.order.retain(|h| h != &hash);
336 self.order.push(hash);
337 return;
338 }
339
340 while self.blobs.len() >= self.max_entries && !self.order.is_empty() {
342 let evicted = self.order.remove(0);
343 self.blobs.remove(&evicted);
344 }
345
346 self.blobs.insert(hash, blob);
347 self.order.push(hash);
348 }
349
350 pub fn get(&mut self, hash: &FunctionHash) -> Option<&FunctionBlob> {
352 if self.blobs.contains_key(hash) {
353 self.order.retain(|h| h != hash);
354 self.order.push(*hash);
355 self.blobs.get(hash)
356 } else {
357 None
358 }
359 }
360
361 pub fn contains(&self, hash: &FunctionHash) -> bool {
363 self.blobs.contains_key(hash)
364 }
365
366 pub fn known_hashes(&self) -> Vec<FunctionHash> {
368 self.blobs.keys().copied().collect()
369 }
370
371 pub fn filter_known(&self, offered: &[FunctionHash]) -> Vec<FunctionHash> {
373 offered
374 .iter()
375 .filter(|h| self.blobs.contains_key(h))
376 .copied()
377 .collect()
378 }
379
380 pub fn len(&self) -> usize {
382 self.blobs.len()
383 }
384
385 pub fn is_empty(&self) -> bool {
387 self.blobs.is_empty()
388 }
389
390 pub fn insert_blobs(&mut self, blobs: &[(FunctionHash, FunctionBlob)]) {
392 for (hash, blob) in blobs {
393 self.insert(*hash, blob.clone());
394 }
395 }
396}
397
398pub fn build_minimal_blobs_by_hash(
404 program: &BytecodeProgram,
405 entry_hash: FunctionHash,
406) -> Option<Vec<(FunctionHash, FunctionBlob)>> {
407 let ca = program.content_addressed.as_ref()?;
408 if !ca.function_store.contains_key(&entry_hash) {
409 return None;
410 }
411
412 let mut needed: std::collections::HashSet<FunctionHash> = std::collections::HashSet::new();
414 let mut queue = vec![entry_hash];
415 while let Some(hash) = queue.pop() {
416 if needed.insert(hash) {
417 if let Some(blob) = ca.function_store.get(&hash) {
418 for dep in &blob.dependencies {
419 if !needed.contains(dep) {
420 queue.push(*dep);
421 }
422 }
423 }
424 }
425 }
426
427 let blobs: Vec<(FunctionHash, FunctionBlob)> = needed
429 .into_iter()
430 .filter_map(|hash| {
431 ca.function_store
432 .get(&hash)
433 .map(|blob| (hash, blob.clone()))
434 })
435 .collect();
436
437 Some(blobs)
438}
439
440pub fn build_minimal_blobs(
445 program: &BytecodeProgram,
446 fn_name: &str,
447) -> Option<Vec<(FunctionHash, FunctionBlob)>> {
448 let ca = program.content_addressed.as_ref()?;
449 let mut matches = ca.function_store.iter().filter_map(|(hash, blob)| {
450 if blob.name == fn_name {
451 Some(*hash)
452 } else {
453 None
454 }
455 });
456 let first = matches.next()?;
457 if matches.next().is_some() {
458 return None;
459 }
460 build_minimal_blobs_by_hash(program, first)
461}
462
463pub fn program_from_blobs_by_hash(
468 blobs: Vec<(FunctionHash, FunctionBlob)>,
469 entry_hash: FunctionHash,
470 source: &BytecodeProgram,
471) -> Option<Program> {
472 let function_store: std::collections::HashMap<FunctionHash, FunctionBlob> =
473 blobs.into_iter().collect();
474 if !function_store.contains_key(&entry_hash) {
475 return None;
476 }
477
478 Some(Program {
479 entry: entry_hash,
480 function_store,
481 top_level_locals_count: source.top_level_locals_count,
482 top_level_local_storage_hints: source.top_level_local_storage_hints.clone(),
483 module_binding_names: source.module_binding_names.clone(),
484 module_binding_storage_hints: source.module_binding_storage_hints.clone(),
485 function_local_storage_hints: source.function_local_storage_hints.clone(),
486 top_level_frame: source.top_level_frame.clone(),
487 data_schema: source.data_schema.clone(),
488 type_schema_registry: source.type_schema_registry.clone(),
489 trait_method_symbols: source.trait_method_symbols.clone(),
490 foreign_functions: source.foreign_functions.clone(),
491 native_struct_layouts: source.native_struct_layouts.clone(),
492 debug_info: source.debug_info.clone(),
493 })
494}
495
496pub fn program_from_blobs(
498 blobs: Vec<(FunctionHash, FunctionBlob)>,
499 fn_name: &str,
500 source: &BytecodeProgram,
501) -> Option<Program> {
502 let mut matches = blobs.iter().filter_map(|(hash, blob)| {
503 if blob.name == fn_name {
504 Some(*hash)
505 } else {
506 None
507 }
508 });
509 let entry = matches.next()?;
510 if matches.next().is_some() {
511 return None;
512 }
513 program_from_blobs_by_hash(blobs, entry, source)
514}
515
516pub fn execute_remote_call(
528 request: RemoteCallRequest,
529 store: &SnapshotStore,
530) -> RemoteCallResponse {
531 match execute_inner(request, store) {
532 Ok(value) => RemoteCallResponse { result: Ok(value) },
533 Err(err) => RemoteCallResponse { result: Err(err) },
534 }
535}
536
537pub fn execute_remote_call_with_runtimes(
544 request: RemoteCallRequest,
545 store: &SnapshotStore,
546 language_runtimes: &std::collections::HashMap<String, std::sync::Arc<shape_runtime::plugins::language_runtime::PluginLanguageRuntime>>,
547) -> RemoteCallResponse {
548 match execute_inner_with_runtimes(request, store, language_runtimes) {
549 Ok(value) => RemoteCallResponse { result: Ok(value) },
550 Err(err) => RemoteCallResponse { result: Err(err) },
551 }
552}
553
554fn execute_inner(
555 request: RemoteCallRequest,
556 store: &SnapshotStore,
557) -> Result<SerializableVMValue, RemoteCallError> {
558 let mut program = if let Some(blobs) = request.function_blobs {
562 let entry_hash = request
563 .function_hash
564 .or_else(|| {
565 if let Some(fid) = request.function_id {
566 request
567 .program
568 .function_blob_hashes
569 .get(fid as usize)
570 .copied()
571 .flatten()
572 } else {
573 None
574 }
575 })
576 .or_else(|| {
577 request.program.content_addressed.as_ref().and_then(|ca| {
579 let mut matches = ca.function_store.iter().filter_map(|(hash, blob)| {
580 if blob.name == request.function_name {
581 Some(*hash)
582 } else {
583 None
584 }
585 });
586 let first = matches.next()?;
587 if matches.next().is_some() {
588 None
589 } else {
590 Some(first)
591 }
592 })
593 })
594 .ok_or_else(|| RemoteCallError {
595 message: format!(
596 "Could not resolve entry hash for remote function '{}'",
597 request.function_name
598 ),
599 kind: RemoteErrorKind::FunctionNotFound,
600 })?;
601
602 let ca_program = program_from_blobs_by_hash(blobs, entry_hash, &request.program)
605 .ok_or_else(|| RemoteCallError {
606 message: format!(
607 "Could not reconstruct program from blobs for '{}'",
608 request.function_name
609 ),
610 kind: RemoteErrorKind::FunctionNotFound,
611 })?;
612 let linked = crate::linker::link(&ca_program).map_err(|e| RemoteCallError {
614 message: format!("Linker error: {}", e),
615 kind: RemoteErrorKind::RuntimeError,
616 })?;
617 crate::linker::linked_to_bytecode_program(&linked)
619 } else {
620 request.program
621 };
622 program.type_schema_registry = request.type_schemas;
623
624 let args: Vec<ValueWord> = request
626 .arguments
627 .iter()
628 .map(|sv| {
629 serializable_to_nanboxed(sv, store).map_err(|e| RemoteCallError {
630 message: format!("Failed to deserialize argument: {}", e),
631 kind: RemoteErrorKind::ArgumentError,
632 })
633 })
634 .collect::<Result<Vec<_>, _>>()?;
635
636 let mut vm = VirtualMachine::new(VMConfig::default());
638 vm.load_program(program);
639 vm.populate_module_objects();
640
641 let result = if let Some(ref upvalue_data) = request.upvalues {
650 let upvalues: Vec<shape_value::Upvalue> = upvalue_data
652 .iter()
653 .map(|sv| {
654 let nb = serializable_to_nanboxed(sv, store).map_err(|e| RemoteCallError {
655 message: format!("Failed to deserialize upvalue: {}", e),
656 kind: RemoteErrorKind::ArgumentError,
657 })?;
658 Ok(shape_value::Upvalue::new(nb))
659 })
660 .collect::<Result<Vec<_>, RemoteCallError>>()?;
661
662 let function_id = request.function_id.ok_or_else(|| RemoteCallError {
663 message: "Closure call requires function_id".to_string(),
664 kind: RemoteErrorKind::FunctionNotFound,
665 })?;
666
667 vm.execute_closure(function_id, upvalues, args, None)
668 .map_err(|e| RemoteCallError {
669 message: e.to_string(),
670 kind: RemoteErrorKind::RuntimeError,
671 })?
672 } else if let Some(hash) = request.function_hash {
673 let func_id = vm
677 .program()
678 .function_blob_hashes
679 .iter()
680 .enumerate()
681 .find_map(|(idx, maybe_hash)| {
682 if maybe_hash == &Some(hash) {
683 Some(idx as u16)
684 } else {
685 None
686 }
687 })
688 .ok_or_else(|| RemoteCallError {
689 message: format!("Function hash not found in program: {}", hash),
690 kind: RemoteErrorKind::FunctionNotFound,
691 })?;
692 vm.execute_function_by_id(func_id, args, None)
693 .map_err(|e| RemoteCallError {
694 message: e.to_string(),
695 kind: RemoteErrorKind::RuntimeError,
696 })?
697 } else if let Some(func_id) = request.function_id {
698 vm.execute_function_by_id(func_id, args, None)
700 .map_err(|e| RemoteCallError {
701 message: e.to_string(),
702 kind: RemoteErrorKind::RuntimeError,
703 })?
704 } else {
705 vm.execute_function_by_name(&request.function_name, args, None)
707 .map_err(|e| RemoteCallError {
708 message: e.to_string(),
709 kind: RemoteErrorKind::RuntimeError,
710 })?
711 };
712
713 nanboxed_to_serializable(&result, store).map_err(|e| RemoteCallError {
715 message: format!("Failed to serialize result: {}", e),
716 kind: RemoteErrorKind::RuntimeError,
717 })
718}
719
720fn execute_inner_with_runtimes(
721 request: RemoteCallRequest,
722 store: &SnapshotStore,
723 language_runtimes: &std::collections::HashMap<String, std::sync::Arc<shape_runtime::plugins::language_runtime::PluginLanguageRuntime>>,
724) -> Result<SerializableVMValue, RemoteCallError> {
725 let mut program = if let Some(blobs) = request.function_blobs {
727 let entry_hash = request
728 .function_hash
729 .or_else(|| {
730 if let Some(fid) = request.function_id {
731 request
732 .program
733 .function_blob_hashes
734 .get(fid as usize)
735 .copied()
736 .flatten()
737 } else {
738 None
739 }
740 })
741 .or_else(|| {
742 request.program.content_addressed.as_ref().and_then(|ca| {
743 let mut matches = ca.function_store.iter().filter_map(|(hash, blob)| {
744 if blob.name == request.function_name {
745 Some(*hash)
746 } else {
747 None
748 }
749 });
750 let first = matches.next()?;
751 if matches.next().is_some() {
752 None
753 } else {
754 Some(first)
755 }
756 })
757 })
758 .ok_or_else(|| RemoteCallError {
759 message: format!(
760 "Could not resolve entry hash for remote function '{}'",
761 request.function_name
762 ),
763 kind: RemoteErrorKind::FunctionNotFound,
764 })?;
765
766 let ca_program = program_from_blobs_by_hash(blobs, entry_hash, &request.program)
767 .ok_or_else(|| RemoteCallError {
768 message: format!(
769 "Could not reconstruct program from blobs for '{}'",
770 request.function_name
771 ),
772 kind: RemoteErrorKind::FunctionNotFound,
773 })?;
774 let linked = crate::linker::link(&ca_program).map_err(|e| RemoteCallError {
775 message: format!("Linker error: {}", e),
776 kind: RemoteErrorKind::RuntimeError,
777 })?;
778 crate::linker::linked_to_bytecode_program(&linked)
779 } else {
780 request.program
781 };
782 program.type_schema_registry = request.type_schemas;
783
784 let args: Vec<ValueWord> = request
786 .arguments
787 .iter()
788 .map(|sv| {
789 serializable_to_nanboxed(sv, store).map_err(|e| RemoteCallError {
790 message: format!("Failed to deserialize argument: {}", e),
791 kind: RemoteErrorKind::ArgumentError,
792 })
793 })
794 .collect::<Result<Vec<_>, _>>()?;
795
796 let mut vm = VirtualMachine::new(VMConfig::default());
798 vm.load_program(program);
799 vm.populate_module_objects();
800
801 if !vm.program.foreign_functions.is_empty() && !language_runtimes.is_empty() {
803 let entries = vm.program.foreign_functions.clone();
804 let mut handles = Vec::with_capacity(entries.len());
805
806 for (idx, entry) in entries.iter().enumerate() {
807 if entry.native_abi.is_some() {
809 handles.push(None);
810 continue;
811 }
812
813 if let Some(lang_runtime) = language_runtimes.get(&entry.language) {
814 vm.program.foreign_functions[idx].dynamic_errors =
815 lang_runtime.has_dynamic_errors();
816
817 let compiled = lang_runtime.compile(
818 &entry.name,
819 &entry.body_text,
820 &entry.param_names,
821 &entry.param_types,
822 entry.return_type.as_deref(),
823 entry.is_async,
824 ).map_err(|e| RemoteCallError {
825 message: format!("Failed to compile foreign function '{}': {}", entry.name, e),
826 kind: RemoteErrorKind::RuntimeError,
827 })?;
828 handles.push(Some(crate::executor::ForeignFunctionHandle::Runtime {
829 runtime: std::sync::Arc::clone(lang_runtime),
830 compiled,
831 }));
832 } else {
833 return Err(RemoteCallError {
834 message: format!(
835 "No language runtime for '{}' on this server. \
836 Install the {} extension.",
837 entry.language, entry.language
838 ),
839 kind: RemoteErrorKind::RuntimeError,
840 });
841 }
842 }
843 vm.foreign_fn_handles = handles;
844 }
845
846 let result = if let Some(ref upvalue_data) = request.upvalues {
848 let upvalues: Vec<shape_value::Upvalue> = upvalue_data
849 .iter()
850 .map(|sv| {
851 let nb = serializable_to_nanboxed(sv, store).map_err(|e| RemoteCallError {
852 message: format!("Failed to deserialize upvalue: {}", e),
853 kind: RemoteErrorKind::ArgumentError,
854 })?;
855 Ok(shape_value::Upvalue::new(nb))
856 })
857 .collect::<Result<Vec<_>, RemoteCallError>>()?;
858
859 let function_id = request.function_id.ok_or_else(|| RemoteCallError {
860 message: "Closure call requires function_id".to_string(),
861 kind: RemoteErrorKind::FunctionNotFound,
862 })?;
863
864 vm.execute_closure(function_id, upvalues, args, None)
865 .map_err(|e| RemoteCallError {
866 message: e.to_string(),
867 kind: RemoteErrorKind::RuntimeError,
868 })?
869 } else if let Some(hash) = request.function_hash {
870 let func_id = vm
873 .program()
874 .function_blob_hashes
875 .iter()
876 .enumerate()
877 .find_map(|(idx, maybe_hash)| {
878 if maybe_hash == &Some(hash) {
879 Some(idx as u16)
880 } else {
881 None
882 }
883 })
884 .ok_or_else(|| RemoteCallError {
885 message: format!("Function hash not found in program: {}", hash),
886 kind: RemoteErrorKind::FunctionNotFound,
887 })?;
888 vm.execute_function_by_id(func_id, args, None)
889 .map_err(|e| RemoteCallError {
890 message: e.to_string(),
891 kind: RemoteErrorKind::RuntimeError,
892 })?
893 } else if let Some(func_id) = request.function_id {
894 vm.execute_function_by_id(func_id, args, None)
895 .map_err(|e| RemoteCallError {
896 message: e.to_string(),
897 kind: RemoteErrorKind::RuntimeError,
898 })?
899 } else {
900 vm.execute_function_by_name(&request.function_name, args, None)
901 .map_err(|e| RemoteCallError {
902 message: e.to_string(),
903 kind: RemoteErrorKind::RuntimeError,
904 })?
905 };
906
907 nanboxed_to_serializable(&result, store).map_err(|e| RemoteCallError {
909 message: format!("Failed to serialize result: {}", e),
910 kind: RemoteErrorKind::RuntimeError,
911 })
912}
913
914pub fn program_hash(program: &BytecodeProgram) -> [u8; 32] {
919 use sha2::{Digest, Sha256};
920 let bytes =
921 rmp_serde::to_vec_named(program).expect("BytecodeProgram serialization should not fail");
922 let hash = Sha256::digest(&bytes);
923 let mut out = [0u8; 32];
924 out.copy_from_slice(&hash);
925 out
926}
927
928fn create_stub_program(program: &BytecodeProgram) -> BytecodeProgram {
933 let mut stub = BytecodeProgram::default();
934 stub.type_schema_registry = program.type_schema_registry.clone();
935 if let Some(ref ca) = program.content_addressed {
937 stub.content_addressed = Some(Program {
938 entry: ca.entry,
939 function_store: std::collections::HashMap::new(),
940 top_level_locals_count: ca.top_level_locals_count,
941 top_level_local_storage_hints: ca.top_level_local_storage_hints.clone(),
942 module_binding_names: ca.module_binding_names.clone(),
943 module_binding_storage_hints: ca.module_binding_storage_hints.clone(),
944 function_local_storage_hints: ca.function_local_storage_hints.clone(),
945 top_level_frame: ca.top_level_frame.clone(),
946 data_schema: ca.data_schema.clone(),
947 type_schema_registry: ca.type_schema_registry.clone(),
948 trait_method_symbols: ca.trait_method_symbols.clone(),
949 foreign_functions: ca.foreign_functions.clone(),
950 native_struct_layouts: ca.native_struct_layouts.clone(),
951 debug_info: ca.debug_info.clone(),
952 });
953 }
954 stub.top_level_locals_count = program.top_level_locals_count;
956 stub.top_level_local_storage_hints = program.top_level_local_storage_hints.clone();
957 stub.module_binding_names = program.module_binding_names.clone();
958 stub.module_binding_storage_hints = program.module_binding_storage_hints.clone();
959 stub.function_local_storage_hints = program.function_local_storage_hints.clone();
960 stub.data_schema = program.data_schema.clone();
961 stub.trait_method_symbols = program.trait_method_symbols.clone();
962 stub.foreign_functions = program.foreign_functions.clone();
963 stub.native_struct_layouts = program.native_struct_layouts.clone();
964 stub.debug_info = program.debug_info.clone();
965 stub.function_blob_hashes = program.function_blob_hashes.clone();
966 stub
967}
968
969pub fn build_call_request(
975 program: &BytecodeProgram,
976 function_name: &str,
977 arguments: Vec<SerializableVMValue>,
978) -> RemoteCallRequest {
979 let hash = program_hash(program);
980 let function_id = program
981 .functions
982 .iter()
983 .position(|f| f.name == function_name)
984 .map(|id| id as u16);
985 let function_hash = function_id
986 .and_then(|fid| {
987 program
988 .function_blob_hashes
989 .get(fid as usize)
990 .copied()
991 .flatten()
992 })
993 .or_else(|| {
994 program.content_addressed.as_ref().and_then(|ca| {
995 let mut matches = ca.function_store.iter().filter_map(|(hash, blob)| {
996 if blob.name == function_name {
997 Some(*hash)
998 } else {
999 None
1000 }
1001 });
1002 let first = matches.next()?;
1003 if matches.next().is_some() {
1004 None
1005 } else {
1006 Some(first)
1007 }
1008 })
1009 });
1010 let blobs = function_hash.and_then(|h| build_minimal_blobs_by_hash(program, h));
1011
1012 let request_program = if blobs.is_some() {
1015 create_stub_program(program)
1016 } else {
1017 program.clone()
1018 };
1019
1020 RemoteCallRequest {
1021 program: request_program,
1022 function_name: function_name.to_string(),
1023 function_id,
1024 function_hash,
1025 arguments,
1026 upvalues: None,
1027 type_schemas: program.type_schema_registry.clone(),
1028 program_hash: hash,
1029 function_blobs: blobs,
1030 }
1031}
1032
1033pub fn build_closure_call_request(
1039 program: &BytecodeProgram,
1040 function_id: u16,
1041 arguments: Vec<SerializableVMValue>,
1042 upvalues: Vec<SerializableVMValue>,
1043) -> RemoteCallRequest {
1044 let hash = program_hash(program);
1045
1046 let function_hash = program
1047 .function_blob_hashes
1048 .get(function_id as usize)
1049 .copied()
1050 .flatten();
1051 let blobs = function_hash.and_then(|h| build_minimal_blobs_by_hash(program, h));
1052
1053 RemoteCallRequest {
1054 program: if blobs.is_some() {
1055 create_stub_program(program)
1056 } else {
1057 program.clone()
1058 },
1059 function_name: String::new(),
1060 function_id: Some(function_id),
1061 function_hash,
1062 arguments,
1063 upvalues: Some(upvalues),
1064 type_schemas: program.type_schema_registry.clone(),
1065 program_hash: hash,
1066 function_blobs: blobs,
1067 }
1068}
1069
1070pub fn build_call_request_negotiated(
1076 program: &BytecodeProgram,
1077 function_name: &str,
1078 arguments: Vec<SerializableVMValue>,
1079 known_hashes: &[FunctionHash],
1080) -> RemoteCallRequest {
1081 let mut request = build_call_request(program, function_name, arguments);
1082
1083 if let Some(ref mut blobs) = request.function_blobs {
1085 let known_set: std::collections::HashSet<FunctionHash> =
1086 known_hashes.iter().copied().collect();
1087 blobs.retain(|(hash, _)| !known_set.contains(hash));
1088 }
1089
1090 request
1091}
1092
1093pub fn handle_negotiation(
1097 request: &BlobNegotiationRequest,
1098 cache: &RemoteBlobCache,
1099) -> BlobNegotiationResponse {
1100 BlobNegotiationResponse {
1101 known_hashes: cache.filter_known(&request.offered_hashes),
1102 }
1103}
1104
1105pub const SIDECAR_THRESHOLD: usize = 1024 * 1024; pub fn extract_sidecars(
1121 args: &mut Vec<SerializableVMValue>,
1122 store: &SnapshotStore,
1123) -> Vec<BlobSidecar> {
1124 let mut sidecars = Vec::new();
1125 let mut next_id: u32 = 0;
1126 for arg in args.iter_mut() {
1127 extract_sidecars_recursive(arg, store, &mut sidecars, &mut next_id);
1128 }
1129 sidecars
1130}
1131
1132fn get_blob_ref(value: &SerializableVMValue) -> Option<&shape_runtime::snapshot::BlobRef> {
1134 use shape_runtime::snapshot::SerializableVMValue as SV;
1135 match value {
1136 SV::DataTable(blob)
1137 | SV::TypedTable { table: blob, .. }
1138 | SV::RowView { table: blob, .. }
1139 | SV::ColumnRef { table: blob, .. }
1140 | SV::IndexedTable { table: blob, .. } => Some(blob),
1141 SV::TypedArray { blob, .. } | SV::Matrix { blob, .. } => Some(blob),
1142 _ => None,
1143 }
1144}
1145
1146fn extract_sidecars_recursive(
1147 value: &mut SerializableVMValue,
1148 store: &SnapshotStore,
1149 sidecars: &mut Vec<BlobSidecar>,
1150 next_id: &mut u32,
1151) {
1152 use shape_runtime::snapshot::SerializableVMValue as SV;
1153
1154 let meta = match &*value {
1157 SV::TypedArray { len, .. } => (*len as u32, 0u32),
1158 SV::Matrix { rows, cols, .. } => (*rows, *cols),
1159 _ => (0, 0),
1160 };
1161 if let Some(blob) = get_blob_ref(value) {
1163 let blob_kind = blob.kind.clone();
1164 let blob_hash = blob.hash.clone();
1165 if let Some(sidecar) = try_extract_blob(blob, store, next_id) {
1166 let sidecar_id = sidecar.sidecar_id;
1167 sidecars.push(sidecar);
1168 *value = SV::SidecarRef {
1169 sidecar_id,
1170 blob_kind,
1171 original_hash: blob_hash,
1172 meta_a: meta.0,
1173 meta_b: meta.1,
1174 };
1175 return;
1176 }
1177 }
1178
1179 match value {
1181 SV::Array(items) => {
1182 for item in items.iter_mut() {
1183 extract_sidecars_recursive(item, store, sidecars, next_id);
1184 }
1185 }
1186 SV::HashMap { keys, values } => {
1187 for k in keys.iter_mut() {
1188 extract_sidecars_recursive(k, store, sidecars, next_id);
1189 }
1190 for v in values.iter_mut() {
1191 extract_sidecars_recursive(v, store, sidecars, next_id);
1192 }
1193 }
1194 SV::TypedObject { slot_data, .. } => {
1195 for slot in slot_data.iter_mut() {
1196 extract_sidecars_recursive(slot, store, sidecars, next_id);
1197 }
1198 }
1199 SV::Some(inner) | SV::Ok(inner) | SV::Err(inner) => {
1200 extract_sidecars_recursive(inner, store, sidecars, next_id);
1201 }
1202 SV::TypeAnnotatedValue { value: inner, .. } => {
1203 extract_sidecars_recursive(inner, store, sidecars, next_id);
1204 }
1205 SV::Closure { upvalues, .. } => {
1206 for uv in upvalues.iter_mut() {
1207 extract_sidecars_recursive(uv, store, sidecars, next_id);
1208 }
1209 }
1210 SV::Enum(ev) => match &mut ev.payload {
1211 shape_runtime::snapshot::EnumPayloadSnapshot::Unit => {}
1212 shape_runtime::snapshot::EnumPayloadSnapshot::Tuple(items) => {
1213 for item in items.iter_mut() {
1214 extract_sidecars_recursive(item, store, sidecars, next_id);
1215 }
1216 }
1217 shape_runtime::snapshot::EnumPayloadSnapshot::Struct(fields) => {
1218 for (_, v) in fields.iter_mut() {
1219 extract_sidecars_recursive(v, store, sidecars, next_id);
1220 }
1221 }
1222 },
1223 SV::PrintResult(pr) => {
1224 for span in pr.spans.iter_mut() {
1225 if let shape_runtime::snapshot::PrintSpanSnapshot::Value {
1226 raw_value,
1227 format_params,
1228 ..
1229 } = span
1230 {
1231 extract_sidecars_recursive(raw_value, store, sidecars, next_id);
1232 for (_, v) in format_params.iter_mut() {
1233 extract_sidecars_recursive(v, store, sidecars, next_id);
1234 }
1235 }
1236 }
1237 }
1238 SV::SimulationCall { params, .. } => {
1239 for (_, v) in params.iter_mut() {
1240 extract_sidecars_recursive(v, store, sidecars, next_id);
1241 }
1242 }
1243 SV::FunctionRef { closure, .. } => {
1244 if let Some(c) = closure {
1245 extract_sidecars_recursive(c, store, sidecars, next_id);
1246 }
1247 }
1248 SV::Range { start, end, .. } => {
1249 if let Some(s) = start {
1250 extract_sidecars_recursive(s, store, sidecars, next_id);
1251 }
1252 if let Some(e) = end {
1253 extract_sidecars_recursive(e, store, sidecars, next_id);
1254 }
1255 }
1256
1257 _ => {}
1259 }
1260}
1261
1262fn try_extract_blob(
1264 blob: &shape_runtime::snapshot::BlobRef,
1265 store: &SnapshotStore,
1266 next_id: &mut u32,
1267) -> Option<BlobSidecar> {
1268 let chunked: shape_runtime::snapshot::ChunkedBlob = store.get_struct(&blob.hash).ok()?;
1270 if chunked.total_len < SIDECAR_THRESHOLD {
1271 return None;
1272 }
1273
1274 let data = shape_runtime::snapshot::load_chunked_bytes(&chunked, store).ok()?;
1276 let sidecar_id = *next_id;
1277 *next_id += 1;
1278
1279 Some(BlobSidecar { sidecar_id, data })
1280}
1281
1282fn typed_array_element_size(kind: shape_runtime::snapshot::TypedArrayElementKind) -> usize {
1284 use shape_runtime::snapshot::TypedArrayElementKind as EK;
1285 match kind {
1286 EK::I8 | EK::U8 | EK::Bool => 1,
1287 EK::I16 | EK::U16 => 2,
1288 EK::I32 | EK::U32 | EK::F32 => 4,
1289 EK::I64 | EK::U64 | EK::F64 => 8,
1290 }
1291}
1292
1293pub fn reassemble_sidecars(
1299 args: &mut Vec<SerializableVMValue>,
1300 sidecars: &std::collections::HashMap<u32, BlobSidecar>,
1301 store: &SnapshotStore,
1302) -> anyhow::Result<()> {
1303 for arg in args.iter_mut() {
1304 reassemble_recursive(arg, sidecars, store)?;
1305 }
1306 Ok(())
1307}
1308
1309fn reassemble_recursive(
1310 value: &mut SerializableVMValue,
1311 sidecars: &std::collections::HashMap<u32, BlobSidecar>,
1312 store: &SnapshotStore,
1313) -> anyhow::Result<()> {
1314 use shape_runtime::snapshot::{BlobRef, SerializableVMValue as SV};
1315
1316 match value {
1317 SV::SidecarRef {
1318 sidecar_id,
1319 blob_kind,
1320 original_hash: _,
1321 meta_a,
1322 meta_b,
1323 } => {
1324 let sidecar = sidecars
1325 .get(sidecar_id)
1326 .ok_or_else(|| anyhow::anyhow!("missing sidecar with id {}", sidecar_id))?;
1327 let meta_a = *meta_a;
1328 let meta_b = *meta_b;
1329
1330 let chunked = shape_runtime::snapshot::store_chunked_bytes(&sidecar.data, store)?;
1333 let hash = store.put_struct(&chunked)?;
1334
1335 let blob = BlobRef {
1336 hash,
1337 kind: blob_kind.clone(),
1338 };
1339 *value = match blob_kind {
1340 shape_runtime::snapshot::BlobKind::DataTable => SV::DataTable(blob),
1341 shape_runtime::snapshot::BlobKind::TypedArray(ek) => SV::TypedArray {
1342 element_kind: *ek,
1343 blob,
1344 len: meta_a as usize,
1345 },
1346 shape_runtime::snapshot::BlobKind::Matrix => SV::Matrix {
1347 blob,
1348 rows: meta_a,
1349 cols: meta_b,
1350 },
1351 };
1352 }
1353
1354 SV::Array(items) => {
1356 for item in items.iter_mut() {
1357 reassemble_recursive(item, sidecars, store)?;
1358 }
1359 }
1360 SV::HashMap { keys, values } => {
1361 for k in keys.iter_mut() {
1362 reassemble_recursive(k, sidecars, store)?;
1363 }
1364 for v in values.iter_mut() {
1365 reassemble_recursive(v, sidecars, store)?;
1366 }
1367 }
1368 SV::TypedObject { slot_data, .. } => {
1369 for slot in slot_data.iter_mut() {
1370 reassemble_recursive(slot, sidecars, store)?;
1371 }
1372 }
1373 SV::Some(inner) | SV::Ok(inner) | SV::Err(inner) => {
1374 reassemble_recursive(inner, sidecars, store)?;
1375 }
1376 SV::TypeAnnotatedValue { value: inner, .. } => {
1377 reassemble_recursive(inner, sidecars, store)?;
1378 }
1379 SV::Closure { upvalues, .. } => {
1380 for uv in upvalues.iter_mut() {
1381 reassemble_recursive(uv, sidecars, store)?;
1382 }
1383 }
1384 SV::Enum(ev) => match &mut ev.payload {
1385 shape_runtime::snapshot::EnumPayloadSnapshot::Unit => {}
1386 shape_runtime::snapshot::EnumPayloadSnapshot::Tuple(items) => {
1387 for item in items.iter_mut() {
1388 reassemble_recursive(item, sidecars, store)?;
1389 }
1390 }
1391 shape_runtime::snapshot::EnumPayloadSnapshot::Struct(fields) => {
1392 for (_, v) in fields.iter_mut() {
1393 reassemble_recursive(v, sidecars, store)?;
1394 }
1395 }
1396 },
1397 SV::PrintResult(pr) => {
1398 for span in pr.spans.iter_mut() {
1399 if let shape_runtime::snapshot::PrintSpanSnapshot::Value {
1400 raw_value,
1401 format_params,
1402 ..
1403 } = span
1404 {
1405 reassemble_recursive(raw_value, sidecars, store)?;
1406 for (_, v) in format_params.iter_mut() {
1407 reassemble_recursive(v, sidecars, store)?;
1408 }
1409 }
1410 }
1411 }
1412 SV::SimulationCall { params, .. } => {
1413 for (_, v) in params.iter_mut() {
1414 reassemble_recursive(v, sidecars, store)?;
1415 }
1416 }
1417 SV::FunctionRef { closure, .. } => {
1418 if let Some(c) = closure {
1419 reassemble_recursive(c, sidecars, store)?;
1420 }
1421 }
1422 SV::Range { start, end, .. } => {
1423 if let Some(s) = start {
1424 reassemble_recursive(s, sidecars, store)?;
1425 }
1426 if let Some(e) = end {
1427 reassemble_recursive(e, sidecars, store)?;
1428 }
1429 }
1430
1431 _ => {}
1433 }
1434 Ok(())
1435}
1436
1437#[cfg(test)]
1438mod tests {
1439 use super::*;
1440 use crate::bytecode::{FunctionBlob, FunctionHash, Instruction, OpCode, Program};
1441 use crate::compiler::BytecodeCompiler;
1442 use shape_abi_v1::PermissionSet;
1443 use std::collections::HashMap;
1444
1445 fn compile(source: &str) -> BytecodeProgram {
1447 let program = shape_ast::parser::parse_program(source).expect("parse failed");
1448 let compiler = BytecodeCompiler::new();
1449 compiler.compile(&program).expect("compile failed")
1450 }
1451
1452 fn temp_store() -> SnapshotStore {
1454 let dir = std::env::temp_dir().join(format!("shape_remote_test_{}", std::process::id()));
1455 SnapshotStore::new(dir).expect("create snapshot store")
1456 }
1457
1458 fn mk_hash(tag: u8) -> FunctionHash {
1459 let mut bytes = [0u8; 32];
1460 bytes[0] = tag;
1461 FunctionHash(bytes)
1462 }
1463
1464 fn mk_blob(name: &str, hash: FunctionHash, dependencies: Vec<FunctionHash>) -> FunctionBlob {
1465 FunctionBlob {
1466 content_hash: hash,
1467 name: name.to_string(),
1468 arity: 0,
1469 param_names: Vec::new(),
1470 locals_count: 0,
1471 is_closure: false,
1472 captures_count: 0,
1473 is_async: false,
1474 ref_params: Vec::new(),
1475 ref_mutates: Vec::new(),
1476 mutable_captures: Vec::new(),
1477 frame_descriptor: None,
1478 instructions: vec![
1479 Instruction::simple(OpCode::PushNull),
1480 Instruction::simple(OpCode::ReturnValue),
1481 ],
1482 constants: Vec::new(),
1483 strings: Vec::new(),
1484 required_permissions: PermissionSet::pure(),
1485 dependencies,
1486 callee_names: Vec::new(),
1487 type_schemas: Vec::new(),
1488 foreign_dependencies: Vec::new(),
1489 source_map: Vec::new(),
1490 }
1491 }
1492
1493 #[test]
1494 fn test_remote_call_simple_function() {
1495 let bytecode = compile(
1496 r#"
1497 function add(a, b) { a + b }
1498 "#,
1499 );
1500 let store = temp_store();
1501
1502 let request = build_call_request(
1503 &bytecode,
1504 "add",
1505 vec![
1506 SerializableVMValue::Number(10.0),
1507 SerializableVMValue::Number(32.0),
1508 ],
1509 );
1510
1511 let response = execute_remote_call(request, &store);
1512 match response.result {
1513 Ok(SerializableVMValue::Number(n)) => assert_eq!(n, 42.0),
1514 other => panic!("Expected Number(42.0), got {:?}", other),
1515 }
1516 }
1517
1518 #[test]
1519 fn test_remote_call_function_not_found() {
1520 let bytecode = compile("function foo() { 1 }");
1521 let store = temp_store();
1522
1523 let request = build_call_request(&bytecode, "nonexistent", vec![]);
1524
1525 let response = execute_remote_call(request, &store);
1526 assert!(response.result.is_err());
1527 let err = response.result.unwrap_err();
1528 assert!(matches!(err.kind, RemoteErrorKind::RuntimeError));
1529 }
1530
1531 #[test]
1532 fn test_program_hash_deterministic() {
1533 let bytecode = compile("function f(x) { x * 2 }");
1534 let hash1 = program_hash(&bytecode);
1535 let hash2 = program_hash(&bytecode);
1536 assert_eq!(hash1, hash2, "Same program should produce same hash");
1537 }
1538
1539 #[test]
1540 fn test_request_response_serialization_roundtrip() {
1541 let bytecode = compile("function double(x) { x * 2 }");
1542 let request =
1543 build_call_request(&bytecode, "double", vec![SerializableVMValue::Number(21.0)]);
1544
1545 let bytes = shape_wire::encode_message(&request).expect("encode request");
1547 let decoded: RemoteCallRequest =
1548 shape_wire::decode_message(&bytes).expect("decode request");
1549
1550 assert_eq!(decoded.function_name, "double");
1551 assert_eq!(decoded.arguments.len(), 1);
1552 assert_eq!(decoded.program_hash, request.program_hash);
1553 }
1554
1555 #[test]
1556 fn test_response_serialization_roundtrip() {
1557 let response = RemoteCallResponse {
1558 result: Ok(SerializableVMValue::String("hello".to_string())),
1559 };
1560
1561 let bytes = shape_wire::encode_message(&response).expect("encode response");
1562 let decoded: RemoteCallResponse =
1563 shape_wire::decode_message(&bytes).expect("decode response");
1564
1565 match decoded.result {
1566 Ok(SerializableVMValue::String(s)) => assert_eq!(s, "hello"),
1567 other => panic!("Expected Ok(String), got {:?}", other),
1568 }
1569 }
1570
1571 #[test]
1572 fn test_type_schema_registry_roundtrip() {
1573 use shape_runtime::type_schema::{FieldType, TypeSchemaRegistry};
1574
1575 let mut registry = TypeSchemaRegistry::new();
1576 registry.register_type(
1577 "Point",
1578 vec![
1579 ("x".to_string(), FieldType::F64),
1580 ("y".to_string(), FieldType::F64),
1581 ],
1582 );
1583
1584 let bytes = shape_wire::encode_message(®istry).expect("encode registry");
1585 let decoded: TypeSchemaRegistry =
1586 shape_wire::decode_message(&bytes).expect("decode registry");
1587
1588 assert!(decoded.has_type("Point"));
1589 let schema = decoded.get("Point").unwrap();
1590 assert_eq!(schema.field_count(), 2);
1591 assert_eq!(schema.field_offset("x"), Some(0));
1592 assert_eq!(schema.field_offset("y"), Some(8));
1593 }
1594
1595 #[test]
1596 fn test_build_minimal_blobs_rejects_ambiguous_function_name() {
1597 let h1 = mk_hash(1);
1598 let h2 = mk_hash(2);
1599 let blob1 = mk_blob("dup", h1, vec![]);
1600 let blob2 = mk_blob("dup", h2, vec![]);
1601
1602 let mut function_store = HashMap::new();
1603 function_store.insert(h1, blob1.clone());
1604 function_store.insert(h2, blob2.clone());
1605
1606 let mut program = BytecodeProgram::default();
1607 program.content_addressed = Some(Program {
1608 entry: h1,
1609 function_store,
1610 top_level_locals_count: 0,
1611 top_level_local_storage_hints: Vec::new(),
1612 module_binding_names: Vec::new(),
1613 module_binding_storage_hints: Vec::new(),
1614 function_local_storage_hints: Vec::new(),
1615 top_level_frame: None,
1616 data_schema: None,
1617 type_schema_registry: shape_runtime::type_schema::TypeSchemaRegistry::new(),
1618 trait_method_symbols: HashMap::new(),
1619 foreign_functions: Vec::new(),
1620 native_struct_layouts: Vec::new(),
1621 debug_info: crate::bytecode::DebugInfo::new("<test>".to_string()),
1622 });
1623
1624 assert!(
1625 build_minimal_blobs(&program, "dup").is_none(),
1626 "name-based selection must reject ambiguous function names"
1627 );
1628
1629 let by_hash = build_minimal_blobs_by_hash(&program, h2)
1630 .expect("hash-based selection should work with duplicate names");
1631 assert_eq!(by_hash.len(), 1);
1632 assert_eq!(by_hash[0].0, h2);
1633 assert_eq!(by_hash[0].1.name, "dup");
1634 }
1635
1636 #[test]
1637 fn test_program_from_blobs_by_hash_requires_entry_blob() {
1638 let h1 = mk_hash(1);
1639 let h_missing = mk_hash(9);
1640 let blob = mk_blob("f", h1, vec![]);
1641 let source = BytecodeProgram::default();
1642
1643 let reconstructed = program_from_blobs_by_hash(vec![(h1, blob)], h_missing, &source);
1644 assert!(
1645 reconstructed.is_none(),
1646 "reconstruction must fail when the requested entry hash is absent"
1647 );
1648 }
1649
1650 #[test]
1653 fn test_blob_cache_insert_and_get() {
1654 let mut cache = RemoteBlobCache::new(10);
1655 let h1 = mk_hash(1);
1656 let blob1 = mk_blob("f1", h1, vec![]);
1657
1658 cache.insert(h1, blob1.clone());
1659 assert_eq!(cache.len(), 1);
1660 assert!(cache.contains(&h1));
1661 assert_eq!(cache.get(&h1).unwrap().name, "f1");
1662 }
1663
1664 #[test]
1665 fn test_blob_cache_lru_eviction() {
1666 let mut cache = RemoteBlobCache::new(2);
1667 let h1 = mk_hash(1);
1668 let h2 = mk_hash(2);
1669 let h3 = mk_hash(3);
1670
1671 cache.insert(h1, mk_blob("f1", h1, vec![]));
1672 cache.insert(h2, mk_blob("f2", h2, vec![]));
1673 assert_eq!(cache.len(), 2);
1674
1675 cache.insert(h3, mk_blob("f3", h3, vec![]));
1677 assert_eq!(cache.len(), 2);
1678 assert!(!cache.contains(&h1), "h1 should be evicted");
1679 assert!(cache.contains(&h2));
1680 assert!(cache.contains(&h3));
1681 }
1682
1683 #[test]
1684 fn test_blob_cache_access_updates_order() {
1685 let mut cache = RemoteBlobCache::new(2);
1686 let h1 = mk_hash(1);
1687 let h2 = mk_hash(2);
1688 let h3 = mk_hash(3);
1689
1690 cache.insert(h1, mk_blob("f1", h1, vec![]));
1691 cache.insert(h2, mk_blob("f2", h2, vec![]));
1692
1693 cache.get(&h1);
1695
1696 cache.insert(h3, mk_blob("f3", h3, vec![]));
1698 assert!(
1699 cache.contains(&h1),
1700 "h1 was accessed, should not be evicted"
1701 );
1702 assert!(!cache.contains(&h2), "h2 should be evicted");
1703 assert!(cache.contains(&h3));
1704 }
1705
1706 #[test]
1707 fn test_blob_cache_filter_known() {
1708 let mut cache = RemoteBlobCache::new(10);
1709 let h1 = mk_hash(1);
1710 let h2 = mk_hash(2);
1711 let h3 = mk_hash(3);
1712
1713 cache.insert(h1, mk_blob("f1", h1, vec![]));
1714 cache.insert(h3, mk_blob("f3", h3, vec![]));
1715
1716 let known = cache.filter_known(&[h1, h2, h3]);
1717 assert_eq!(known.len(), 2);
1718 assert!(known.contains(&h1));
1719 assert!(known.contains(&h3));
1720 assert!(!known.contains(&h2));
1721 }
1722
1723 #[test]
1724 fn test_handle_negotiation() {
1725 let mut cache = RemoteBlobCache::new(10);
1726 let h1 = mk_hash(1);
1727 let h2 = mk_hash(2);
1728 cache.insert(h1, mk_blob("f1", h1, vec![]));
1729
1730 let request = BlobNegotiationRequest {
1731 offered_hashes: vec![h1, h2],
1732 };
1733 let response = handle_negotiation(&request, &cache);
1734 assert_eq!(response.known_hashes.len(), 1);
1735 assert!(response.known_hashes.contains(&h1));
1736 }
1737
1738 #[test]
1739 fn test_build_call_request_negotiated_strips_known_blobs() {
1740 let h1 = mk_hash(1);
1742 let h2 = mk_hash(2);
1743 let blob1 = mk_blob("entry", h1, vec![h2]);
1744 let blob2 = mk_blob("helper", h2, vec![]);
1745
1746 let mut function_store = HashMap::new();
1747 function_store.insert(h1, blob1.clone());
1748 function_store.insert(h2, blob2.clone());
1749
1750 let mut program = BytecodeProgram::default();
1751 program.content_addressed = Some(Program {
1752 entry: h1,
1753 function_store,
1754 top_level_locals_count: 0,
1755 top_level_local_storage_hints: Vec::new(),
1756 module_binding_names: Vec::new(),
1757 module_binding_storage_hints: Vec::new(),
1758 function_local_storage_hints: Vec::new(),
1759 top_level_frame: None,
1760 data_schema: None,
1761 type_schema_registry: shape_runtime::type_schema::TypeSchemaRegistry::new(),
1762 trait_method_symbols: HashMap::new(),
1763 foreign_functions: Vec::new(),
1764 native_struct_layouts: Vec::new(),
1765 debug_info: crate::bytecode::DebugInfo::new("<test>".to_string()),
1766 });
1767 program.functions = vec![crate::bytecode::Function {
1768 name: "entry".to_string(),
1769 arity: 0,
1770 param_names: vec![],
1771 locals_count: 0,
1772 entry_point: 0,
1773 body_length: 0,
1774 is_closure: false,
1775 captures_count: 0,
1776 is_async: false,
1777 ref_params: vec![],
1778 ref_mutates: vec![],
1779 mutable_captures: vec![],
1780 frame_descriptor: None,
1781 osr_entry_points: vec![],
1782 }];
1783 program.function_blob_hashes = vec![Some(h1)];
1784
1785 let req1 = build_call_request_negotiated(&program, "entry", vec![], &[]);
1787 let blobs1 = req1.function_blobs.as_ref().unwrap();
1788 assert_eq!(blobs1.len(), 2, "first call should send all blobs");
1789
1790 let req2 = build_call_request_negotiated(&program, "entry", vec![], &[h2]);
1792 let blobs2 = req2.function_blobs.as_ref().unwrap();
1793 assert_eq!(blobs2.len(), 1, "second call should skip known blobs");
1794 assert_eq!(blobs2[0].0, h1);
1795 }
1796
1797 #[test]
1798 fn test_wire_message_serialization_roundtrip() {
1799 let msg = WireMessage::BlobNegotiation(BlobNegotiationRequest {
1800 offered_hashes: vec![mk_hash(1), mk_hash(2)],
1801 });
1802 let bytes = shape_wire::encode_message(&msg).expect("encode WireMessage");
1803 let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode WireMessage");
1804 match decoded {
1805 WireMessage::BlobNegotiation(req) => {
1806 assert_eq!(req.offered_hashes.len(), 2);
1807 }
1808 _ => panic!("Expected BlobNegotiation"),
1809 }
1810 }
1811
1812 #[test]
1815 fn test_execute_request_roundtrip() {
1816 let msg = WireMessage::Execute(ExecuteRequest {
1817 code: "fn main() { 42 }".to_string(),
1818 request_id: 7,
1819 });
1820 let bytes = shape_wire::encode_message(&msg).expect("encode Execute");
1821 let decoded: WireMessage =
1822 shape_wire::decode_message(&bytes).expect("decode Execute");
1823 match decoded {
1824 WireMessage::Execute(req) => {
1825 assert_eq!(req.code, "fn main() { 42 }");
1826 assert_eq!(req.request_id, 7);
1827 }
1828 _ => panic!("Expected Execute"),
1829 }
1830 }
1831
1832 #[test]
1833 fn test_execute_response_roundtrip() {
1834 let msg = WireMessage::ExecuteResponse(ExecuteResponse {
1835 request_id: 7,
1836 success: true,
1837 value: WireValue::Number(42.0),
1838 stdout: Some("hello\n".to_string()),
1839 error: None,
1840 content_terminal: None,
1841 content_html: None,
1842 diagnostics: vec![WireDiagnostic {
1843 severity: "warning".to_string(),
1844 message: "unused variable".to_string(),
1845 line: Some(1),
1846 column: Some(5),
1847 }],
1848 metrics: Some(ExecutionMetrics {
1849 instructions_executed: 100,
1850 wall_time_ms: 3,
1851 memory_bytes_peak: 4096,
1852 }),
1853 });
1854 let bytes = shape_wire::encode_message(&msg).expect("encode ExecuteResponse");
1855 let decoded: WireMessage =
1856 shape_wire::decode_message(&bytes).expect("decode ExecuteResponse");
1857 match decoded {
1858 WireMessage::ExecuteResponse(resp) => {
1859 assert_eq!(resp.request_id, 7);
1860 assert!(resp.success);
1861 assert!(matches!(resp.value, WireValue::Number(n) if n == 42.0));
1862 assert_eq!(resp.stdout.as_deref(), Some("hello\n"));
1863 assert!(resp.error.is_none());
1864 assert_eq!(resp.diagnostics.len(), 1);
1865 assert_eq!(resp.diagnostics[0].severity, "warning");
1866 assert_eq!(resp.diagnostics[0].line, Some(1));
1867 let m = resp.metrics.unwrap();
1868 assert_eq!(m.instructions_executed, 100);
1869 assert_eq!(m.wall_time_ms, 3);
1870 }
1871 _ => panic!("Expected ExecuteResponse"),
1872 }
1873 }
1874
1875 #[test]
1876 fn test_ping_pong_roundtrip() {
1877 let ping = WireMessage::Ping(PingRequest {});
1878 let bytes = shape_wire::encode_message(&ping).expect("encode Ping");
1879 let decoded: WireMessage =
1880 shape_wire::decode_message(&bytes).expect("decode Ping");
1881 assert!(matches!(decoded, WireMessage::Ping(_)));
1882
1883 let pong = WireMessage::Pong(ServerInfo {
1884 shape_version: "0.1.3".to_string(),
1885 wire_protocol: 2,
1886 capabilities: vec!["execute".to_string(), "validate".to_string()],
1887 });
1888 let bytes = shape_wire::encode_message(&pong).expect("encode Pong");
1889 let decoded: WireMessage =
1890 shape_wire::decode_message(&bytes).expect("decode Pong");
1891 match decoded {
1892 WireMessage::Pong(info) => {
1893 assert_eq!(info.shape_version, "0.1.3");
1894 assert_eq!(info.wire_protocol, 2);
1895 assert_eq!(info.capabilities.len(), 2);
1896 }
1897 _ => panic!("Expected Pong"),
1898 }
1899 }
1900
1901 #[test]
1902 fn test_auth_roundtrip() {
1903 let msg = WireMessage::Auth(AuthRequest {
1904 token: "secret-token".to_string(),
1905 });
1906 let bytes = shape_wire::encode_message(&msg).expect("encode Auth");
1907 let decoded: WireMessage =
1908 shape_wire::decode_message(&bytes).expect("decode Auth");
1909 match decoded {
1910 WireMessage::Auth(req) => assert_eq!(req.token, "secret-token"),
1911 _ => panic!("Expected Auth"),
1912 }
1913
1914 let resp = WireMessage::AuthResponse(AuthResponse {
1915 authenticated: true,
1916 error: None,
1917 });
1918 let bytes = shape_wire::encode_message(&resp).expect("encode AuthResponse");
1919 let decoded: WireMessage =
1920 shape_wire::decode_message(&bytes).expect("decode AuthResponse");
1921 match decoded {
1922 WireMessage::AuthResponse(r) => {
1923 assert!(r.authenticated);
1924 assert!(r.error.is_none());
1925 }
1926 _ => panic!("Expected AuthResponse"),
1927 }
1928 }
1929
1930 #[test]
1931 fn test_validate_roundtrip() {
1932 let msg = WireMessage::Validate(ValidateRequest {
1933 code: "let x = 1".to_string(),
1934 request_id: 99,
1935 });
1936 let bytes = shape_wire::encode_message(&msg).expect("encode Validate");
1937 let decoded: WireMessage =
1938 shape_wire::decode_message(&bytes).expect("decode Validate");
1939 match decoded {
1940 WireMessage::Validate(req) => {
1941 assert_eq!(req.code, "let x = 1");
1942 assert_eq!(req.request_id, 99);
1943 }
1944 _ => panic!("Expected Validate"),
1945 }
1946
1947 let resp = WireMessage::ValidateResponse(ValidateResponse {
1948 request_id: 99,
1949 success: false,
1950 diagnostics: vec![WireDiagnostic {
1951 severity: "error".to_string(),
1952 message: "parse error".to_string(),
1953 line: None,
1954 column: None,
1955 }],
1956 });
1957 let bytes = shape_wire::encode_message(&resp).expect("encode ValidateResponse");
1958 let decoded: WireMessage =
1959 shape_wire::decode_message(&bytes).expect("decode ValidateResponse");
1960 match decoded {
1961 WireMessage::ValidateResponse(r) => {
1962 assert_eq!(r.request_id, 99);
1963 assert!(!r.success);
1964 assert_eq!(r.diagnostics.len(), 1);
1965 }
1966 _ => panic!("Expected ValidateResponse"),
1967 }
1968 }
1969
1970 #[test]
1971 fn test_ping_framing_roundtrip() {
1972 use shape_wire::transport::framing::{encode_framed, decode_framed};
1973
1974 let ping = WireMessage::Ping(PingRequest {});
1975 let mp = shape_wire::encode_message(&ping).expect("encode Ping");
1976 eprintln!("Ping msgpack bytes ({} bytes): {:02x?}", mp.len(), &mp);
1977
1978 let framed = encode_framed(&mp);
1979 eprintln!("Framed bytes ({} bytes): {:02x?}", framed.len(), &framed);
1980
1981 let decompressed = decode_framed(&framed).expect("decode_framed");
1982 assert_eq!(mp, decompressed, "framing roundtrip should preserve bytes");
1983
1984 let decoded: WireMessage =
1985 shape_wire::decode_message(&decompressed).expect("decode Ping after framing");
1986 assert!(matches!(decoded, WireMessage::Ping(_)));
1987 }
1988
1989 #[test]
1990 fn test_execute_framing_roundtrip() {
1991 use shape_wire::transport::framing::{encode_framed, decode_framed};
1992
1993 let exec = WireMessage::Execute(ExecuteRequest {
1994 code: "42".to_string(),
1995 request_id: 1,
1996 });
1997 let mp = shape_wire::encode_message(&exec).expect("encode Execute");
1998 eprintln!("Execute msgpack bytes ({} bytes): {:02x?}", mp.len(), &mp);
1999
2000 let framed = encode_framed(&mp);
2001 let decompressed = decode_framed(&framed).expect("decode_framed");
2002 let decoded: WireMessage =
2003 shape_wire::decode_message(&decompressed).expect("decode Execute after framing");
2004 match decoded {
2005 WireMessage::Execute(req) => {
2006 assert_eq!(req.code, "42");
2007 assert_eq!(req.request_id, 1);
2008 }
2009 _ => panic!("Expected Execute"),
2010 }
2011 }
2012
2013 #[test]
2016 fn test_extract_sidecars_no_large_blobs() {
2017 let store = temp_store();
2018 let mut args = vec![
2019 SerializableVMValue::Int(42),
2020 SerializableVMValue::String("hello".to_string()),
2021 SerializableVMValue::Array(vec![
2022 SerializableVMValue::Number(1.0),
2023 SerializableVMValue::Number(2.0),
2024 ]),
2025 ];
2026 let sidecars = extract_sidecars(&mut args, &store);
2027 assert!(sidecars.is_empty(), "no large blobs → no sidecars");
2028 assert!(matches!(args[0], SerializableVMValue::Int(42)));
2030 }
2031
2032 #[test]
2033 fn test_extract_sidecars_large_typed_array() {
2034 let store = temp_store();
2035
2036 let data = vec![0f64; 256 * 1024]; let aligned = shape_value::AlignedVec::from_vec(data);
2039 let buf = shape_value::AlignedTypedBuffer::from_aligned(aligned);
2040 let nb = shape_value::ValueWord::from_float_array(std::sync::Arc::new(buf));
2041 let serialized = shape_runtime::snapshot::nanboxed_to_serializable(&nb, &store).unwrap();
2042
2043 let mut args = vec![serialized];
2044 let sidecars = extract_sidecars(&mut args, &store);
2045 assert_eq!(
2046 sidecars.len(),
2047 1,
2048 "should extract one sidecar for 2MB array"
2049 );
2050 assert!(
2051 matches!(args[0], SerializableVMValue::SidecarRef { .. }),
2052 "original should be replaced with SidecarRef"
2053 );
2054 assert!(
2055 sidecars[0].data.len() >= 1024 * 1024,
2056 "sidecar data should be >= 1MB"
2057 );
2058 }
2059
2060 #[test]
2061 fn test_reassemble_sidecars_roundtrip() {
2062 let store = temp_store();
2063
2064 let data: Vec<f64> = (0..256 * 1024).map(|i| i as f64).collect();
2066 let aligned = shape_value::AlignedVec::from_vec(data.clone());
2067 let buf = shape_value::AlignedTypedBuffer::from_aligned(aligned);
2068 let nb = shape_value::ValueWord::from_float_array(std::sync::Arc::new(buf));
2069 let original = shape_runtime::snapshot::nanboxed_to_serializable(&nb, &store).unwrap();
2070
2071 let mut args = vec![original.clone()];
2072 let sidecars = extract_sidecars(&mut args, &store);
2073 assert_eq!(sidecars.len(), 1);
2074
2075 let sidecar_map: HashMap<u32, BlobSidecar> =
2077 sidecars.into_iter().map(|s| (s.sidecar_id, s)).collect();
2078
2079 reassemble_sidecars(&mut args, &sidecar_map, &store).unwrap();
2081
2082 let restored = shape_runtime::snapshot::serializable_to_nanboxed(&args[0], &store).unwrap();
2084 let hv = restored.as_heap_ref().unwrap();
2085 match hv {
2086 shape_value::heap_value::HeapValue::FloatArray(a) => {
2087 assert_eq!(a.len(), 256 * 1024);
2088 assert!((a.as_slice()[0] - 0.0).abs() < f64::EPSILON);
2089 assert!((a.as_slice()[1000] - 1000.0).abs() < f64::EPSILON);
2090 }
2091 _ => {
2093 }
2097 }
2098 }
2099
2100 #[test]
2101 fn test_extract_sidecars_nested_in_array() {
2102 let store = temp_store();
2103
2104 let data = vec![0f64; 256 * 1024]; let aligned = shape_value::AlignedVec::from_vec(data);
2107 let buf = shape_value::AlignedTypedBuffer::from_aligned(aligned);
2108 let nb = shape_value::ValueWord::from_float_array(std::sync::Arc::new(buf));
2109 let serialized = shape_runtime::snapshot::nanboxed_to_serializable(&nb, &store).unwrap();
2110
2111 let mut args = vec![SerializableVMValue::Array(vec![
2112 SerializableVMValue::Int(1),
2113 serialized,
2114 SerializableVMValue::String("end".to_string()),
2115 ])];
2116
2117 let sidecars = extract_sidecars(&mut args, &store);
2118 assert_eq!(sidecars.len(), 1, "should find nested large blob");
2119
2120 match &args[0] {
2122 SerializableVMValue::Array(items) => {
2123 assert_eq!(items.len(), 3);
2124 assert!(matches!(items[0], SerializableVMValue::Int(1)));
2125 assert!(matches!(items[1], SerializableVMValue::SidecarRef { .. }));
2126 assert!(matches!(items[2], SerializableVMValue::String(_)));
2127 }
2128 _ => panic!("Expected Array wrapper to be preserved"),
2129 }
2130 }
2131
2132 #[test]
2133 fn test_sidecar_ref_serialization_roundtrip() {
2134 use shape_runtime::hashing::HashDigest;
2135 use shape_runtime::snapshot::{BlobKind, TypedArrayElementKind};
2136
2137 let value = SerializableVMValue::SidecarRef {
2138 sidecar_id: 7,
2139 blob_kind: BlobKind::TypedArray(TypedArrayElementKind::F64),
2140 original_hash: HashDigest::from_hex("abc123"),
2141 meta_a: 1000,
2142 meta_b: 0,
2143 };
2144
2145 let bytes = shape_wire::encode_message(&value).expect("encode SidecarRef");
2146 let decoded: SerializableVMValue =
2147 shape_wire::decode_message(&bytes).expect("decode SidecarRef");
2148 match decoded {
2149 SerializableVMValue::SidecarRef { sidecar_id, .. } => {
2150 assert_eq!(sidecar_id, 7);
2151 }
2152 _ => panic!("Expected SidecarRef"),
2153 }
2154 }
2155}