1use serde::{Deserialize, Serialize};
28use shape_runtime::snapshot::{SerializableVMValue, SnapshotStore};
29use shape_runtime::type_schema::TypeSchemaRegistry;
30
31use shape_wire::WireValue;
32
33use crate::bytecode::{BytecodeProgram, FunctionBlob, FunctionHash, Program};
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct RemoteCallRequest {
57 pub program: BytecodeProgram,
60
61 pub function_name: String,
63
64 pub function_id: Option<u16>,
67
68 #[serde(default)]
73 pub function_hash: Option<FunctionHash>,
74
75 pub arguments: Vec<SerializableVMValue>,
77
78 pub upvalues: Option<Vec<SerializableVMValue>>,
82
83 pub type_schemas: TypeSchemaRegistry,
86
87 pub program_hash: [u8; 32],
90
91 #[serde(default)]
96 pub function_blobs: Option<Vec<(FunctionHash, FunctionBlob)>>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct RemoteCallResponse {
102 pub result: Result<SerializableVMValue, RemoteCallError>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct RemoteCallError {
109 pub message: String,
111 pub kind: RemoteErrorKind,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub enum RemoteErrorKind {
118 FunctionNotFound,
120 ArgumentError,
122 RuntimeError,
124 MissingModuleFunction,
126}
127
128impl std::fmt::Display for RemoteCallError {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 write!(f, "{:?}: {}", self.kind, self.message)
131 }
132}
133
134impl std::error::Error for RemoteCallError {}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
145pub enum WireMessage {
146 BlobNegotiation(BlobNegotiationRequest),
148 BlobNegotiationReply(BlobNegotiationResponse),
150 Call(RemoteCallRequest),
152 CallResponse(RemoteCallResponse),
154 Sidecar(BlobSidecar),
156
157 Execute(ExecuteRequest),
160 ExecuteResponse(ExecuteResponse),
162 Validate(ValidateRequest),
164 ValidateResponse(ValidateResponse),
166 Auth(AuthRequest),
168 AuthResponse(AuthResponse),
170 ExecuteFile(ExecuteFileRequest),
172 ExecuteProject(ExecuteProjectRequest),
174 ValidatePath(ValidatePathRequest),
176 Ping(PingRequest),
178 Pong(ServerInfo),
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct PingRequest {}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct BlobNegotiationRequest {
189 pub offered_hashes: Vec<FunctionHash>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct BlobNegotiationResponse {
196 pub known_hashes: Vec<FunctionHash>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct BlobSidecar {
206 pub sidecar_id: u32,
207 pub data: Vec<u8>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct ExecuteRequest {
217 pub code: String,
219 pub request_id: u64,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct ExecuteResponse {
226 pub request_id: u64,
228 pub success: bool,
230 pub value: WireValue,
232 pub stdout: Option<String>,
234 pub error: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none", default)]
238 pub content_terminal: Option<String>,
239 #[serde(skip_serializing_if = "Option::is_none", default)]
241 pub content_html: Option<String>,
242 pub diagnostics: Vec<WireDiagnostic>,
244 pub metrics: Option<ExecutionMetrics>,
246 #[serde(skip_serializing_if = "Option::is_none", default)]
248 pub print_output: Option<Vec<shape_wire::print_result::WirePrintResult>>,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct ValidateRequest {
254 pub code: String,
256 pub request_id: u64,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct ValidateResponse {
263 pub request_id: u64,
265 pub success: bool,
267 pub diagnostics: Vec<WireDiagnostic>,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct ExecuteFileRequest {
274 pub path: String,
276 pub cwd: Option<String>,
278 pub request_id: u64,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct ExecuteProjectRequest {
285 pub project_dir: String,
287 pub request_id: u64,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct ValidatePathRequest {
294 pub path: String,
296 pub request_id: u64,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct AuthRequest {
303 pub token: String,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct AuthResponse {
310 pub authenticated: bool,
312 pub error: Option<String>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct ServerInfo {
319 pub shape_version: String,
321 pub wire_protocol: u32,
323 pub capabilities: Vec<String>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct WireDiagnostic {
330 pub severity: String,
332 pub message: String,
334 pub line: Option<u32>,
336 pub column: Option<u32>,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct ExecutionMetrics {
343 pub instructions_executed: u64,
345 pub wall_time_ms: u64,
347 pub memory_bytes_peak: u64,
349}
350
351pub struct RemoteBlobCache {
360 blobs: std::collections::HashMap<FunctionHash, FunctionBlob>,
361 order: Vec<FunctionHash>,
363 max_entries: usize,
365}
366
367impl RemoteBlobCache {
368 pub fn new(max_entries: usize) -> Self {
370 Self {
371 blobs: std::collections::HashMap::new(),
372 order: Vec::new(),
373 max_entries,
374 }
375 }
376
377 pub fn default_cache() -> Self {
379 Self::new(4096)
380 }
381
382 pub fn insert(&mut self, hash: FunctionHash, blob: FunctionBlob) {
384 if self.blobs.contains_key(&hash) {
385 self.order.retain(|h| h != &hash);
387 self.order.push(hash);
388 return;
389 }
390
391 while self.blobs.len() >= self.max_entries && !self.order.is_empty() {
393 let evicted = self.order.remove(0);
394 self.blobs.remove(&evicted);
395 }
396
397 self.blobs.insert(hash, blob);
398 self.order.push(hash);
399 }
400
401 pub fn get(&mut self, hash: &FunctionHash) -> Option<&FunctionBlob> {
403 if self.blobs.contains_key(hash) {
404 self.order.retain(|h| h != hash);
405 self.order.push(*hash);
406 self.blobs.get(hash)
407 } else {
408 None
409 }
410 }
411
412 pub fn contains(&self, hash: &FunctionHash) -> bool {
414 self.blobs.contains_key(hash)
415 }
416
417 pub fn known_hashes(&self) -> Vec<FunctionHash> {
419 self.blobs.keys().copied().collect()
420 }
421
422 pub fn filter_known(&self, offered: &[FunctionHash]) -> Vec<FunctionHash> {
424 offered
425 .iter()
426 .filter(|h| self.blobs.contains_key(h))
427 .copied()
428 .collect()
429 }
430
431 pub fn len(&self) -> usize {
433 self.blobs.len()
434 }
435
436 pub fn is_empty(&self) -> bool {
438 self.blobs.is_empty()
439 }
440
441 pub fn insert_blobs(&mut self, blobs: &[(FunctionHash, FunctionBlob)]) {
443 for (hash, blob) in blobs {
444 self.insert(*hash, blob.clone());
445 }
446 }
447}
448
449pub fn build_minimal_blobs_by_hash(
455 program: &BytecodeProgram,
456 entry_hash: FunctionHash,
457) -> Option<Vec<(FunctionHash, FunctionBlob)>> {
458 let ca = program.content_addressed.as_ref()?;
459 if !ca.function_store.contains_key(&entry_hash) {
460 return None;
461 }
462
463 let mut needed: std::collections::HashSet<FunctionHash> = std::collections::HashSet::new();
465 let mut queue = vec![entry_hash];
466 while let Some(hash) = queue.pop() {
467 if needed.insert(hash) {
468 if let Some(blob) = ca.function_store.get(&hash) {
469 for dep in &blob.dependencies {
470 if !needed.contains(dep) {
471 queue.push(*dep);
472 }
473 }
474 }
475 }
476 }
477
478 let blobs: Vec<(FunctionHash, FunctionBlob)> = needed
480 .into_iter()
481 .filter_map(|hash| {
482 ca.function_store
483 .get(&hash)
484 .map(|blob| (hash, blob.clone()))
485 })
486 .collect();
487
488 Some(blobs)
489}
490
491pub fn build_minimal_blobs(
496 program: &BytecodeProgram,
497 fn_name: &str,
498) -> Option<Vec<(FunctionHash, FunctionBlob)>> {
499 let ca = program.content_addressed.as_ref()?;
500 let mut matches = ca.function_store.iter().filter_map(|(hash, blob)| {
501 if blob.name == fn_name {
502 Some(*hash)
503 } else {
504 None
505 }
506 });
507 let first = matches.next()?;
508 if matches.next().is_some() {
509 return None;
510 }
511 build_minimal_blobs_by_hash(program, first)
512}
513
514pub fn program_from_blobs_by_hash(
519 blobs: Vec<(FunctionHash, FunctionBlob)>,
520 entry_hash: FunctionHash,
521 source: &BytecodeProgram,
522) -> Option<Program> {
523 let function_store: std::collections::HashMap<FunctionHash, FunctionBlob> =
524 blobs.into_iter().collect();
525 if !function_store.contains_key(&entry_hash) {
526 return None;
527 }
528
529 Some(Program {
530 entry: entry_hash,
531 function_store,
532 top_level_locals_count: source.top_level_locals_count,
533 top_level_local_storage_hints: source.top_level_local_storage_hints.clone(),
534 module_binding_names: source.module_binding_names.clone(),
535 module_binding_storage_hints: source.module_binding_storage_hints.clone(),
536 function_local_storage_hints: source.function_local_storage_hints.clone(),
537 top_level_frame: source.top_level_frame.clone(),
538 top_level_local_concrete_types: source.top_level_local_concrete_types.clone(),
539 function_local_concrete_types: source.function_local_concrete_types.clone(),
540 function_return_concrete_types: source.function_return_concrete_types.clone(),
541 monomorphized_method_call_sites: source.monomorphized_method_call_sites.clone(),
542 value_call_return_concrete_types:
543 source.value_call_return_concrete_types.clone(),
544 operator_trait_dispatch_sites:
545 source.operator_trait_dispatch_sites.clone(),
546 data_schema: source.data_schema.clone(),
547 type_schema_registry: source.type_schema_registry.clone(),
548 trait_method_symbols: source.trait_method_symbols.clone(),
549 foreign_functions: source.foreign_functions.clone(),
550 native_struct_layouts: source.native_struct_layouts.clone(),
551 debug_info: source.debug_info.clone(),
552 closure_function_layouts_by_name: source
556 .content_addressed
557 .as_ref()
558 .map(|ca| ca.closure_function_layouts_by_name.clone())
559 .unwrap_or_default(),
560 trait_vtables: source.trait_vtables.clone(),
564 has_imported_const_inline: source.has_imported_const_inline,
566 has_w17_marshal_residual: source.has_w17_marshal_residual,
568 })
569}
570
571pub fn program_from_blobs(
573 blobs: Vec<(FunctionHash, FunctionBlob)>,
574 fn_name: &str,
575 source: &BytecodeProgram,
576) -> Option<Program> {
577 let mut matches = blobs.iter().filter_map(|(hash, blob)| {
578 if blob.name == fn_name {
579 Some(*hash)
580 } else {
581 None
582 }
583 });
584 let entry = matches.next()?;
585 if matches.next().is_some() {
586 return None;
587 }
588 program_from_blobs_by_hash(blobs, entry, source)
589}
590
591pub fn execute_remote_call(
608 request: RemoteCallRequest,
609 store: &SnapshotStore,
610) -> RemoteCallResponse {
611 match execute_inner(request, store) {
612 Ok(value) => RemoteCallResponse { result: Ok(value) },
613 Err(err) => RemoteCallResponse { result: Err(err) },
614 }
615}
616
617pub fn execute_remote_call_with_runtimes(
624 request: RemoteCallRequest,
625 store: &SnapshotStore,
626 language_runtimes: &std::collections::HashMap<
627 String,
628 std::sync::Arc<shape_runtime::plugins::language_runtime::PluginLanguageRuntime>,
629 >,
630) -> RemoteCallResponse {
631 match execute_inner_with_runtimes(request, store, language_runtimes) {
632 Ok(value) => RemoteCallResponse { result: Ok(value) },
633 Err(err) => RemoteCallResponse { result: Err(err) },
634 }
635}
636
637fn execute_inner(
638 request: RemoteCallRequest,
639 store: &SnapshotStore,
640) -> Result<SerializableVMValue, RemoteCallError> {
641 run_remote_call(request, store, None)
655}
656
657fn execute_inner_with_runtimes(
658 request: RemoteCallRequest,
659 store: &SnapshotStore,
660 language_runtimes: &std::collections::HashMap<
661 String,
662 std::sync::Arc<shape_runtime::plugins::language_runtime::PluginLanguageRuntime>,
663 >,
664) -> Result<SerializableVMValue, RemoteCallError> {
665 run_remote_call(request, store, Some(language_runtimes))
671}
672
673fn run_remote_call(
693 request: RemoteCallRequest,
694 store: &SnapshotStore,
695 language_runtimes: Option<
696 &std::collections::HashMap<
697 String,
698 std::sync::Arc<shape_runtime::plugins::language_runtime::PluginLanguageRuntime>,
699 >,
700 >,
701) -> Result<SerializableVMValue, RemoteCallError> {
702 use crate::executor::{VMConfig, VirtualMachine};
703 use shape_runtime::snapshot::{serializable_to_slot, slot_to_serializable};
704 use shape_value::{KindedSlot, ValueSlot};
705
706 let _ = language_runtimes;
713
714 let mut program: BytecodeProgram = request.program;
717 program.type_schema_registry = request.type_schemas;
718
719 if let (Some(blobs), Some(entry_hash)) =
720 (request.function_blobs.clone(), request.function_hash)
721 {
722 if let Some(ca) = program_from_blobs_by_hash(blobs, entry_hash, &program) {
723 program.content_addressed = Some(ca);
724 }
725 }
726
727 if request.upvalues.is_some() {
731 return Err(RemoteCallError {
732 message: "remote closure dispatch requires upvalue kind track \
733 (ADR-006 §2.7.8 / Q10 cell-storage parallel-kind) — \
734 not yet wired through the remote-call boundary"
735 .to_string(),
736 kind: RemoteErrorKind::RuntimeError,
737 });
738 }
739
740 let mut vm = VirtualMachine::new(VMConfig::default());
742 vm.load_program(program);
743 vm.populate_module_objects();
744
745 let func_id: u16 = if let Some(hash) = request.function_hash {
747 vm.program
748 .function_blob_hashes
749 .iter()
750 .position(|h| *h == Some(hash))
751 .map(|p| p as u16)
752 .or_else(|| request.function_id)
753 .or_else(|| {
754 vm.program
755 .functions
756 .iter()
757 .position(|f| f.name == request.function_name)
758 .map(|p| p as u16)
759 })
760 .ok_or_else(|| RemoteCallError {
761 message: format!(
762 "function not found by hash; name='{}', id={:?}",
763 request.function_name, request.function_id,
764 ),
765 kind: RemoteErrorKind::FunctionNotFound,
766 })?
767 } else if let Some(id) = request.function_id {
768 id
769 } else {
770 vm.program
771 .functions
772 .iter()
773 .position(|f| f.name == request.function_name)
774 .map(|p| p as u16)
775 .ok_or_else(|| RemoteCallError {
776 message: format!("function '{}' not found", request.function_name),
777 kind: RemoteErrorKind::FunctionNotFound,
778 })?
779 };
780
781 let function = vm
785 .program
786 .functions
787 .get(func_id as usize)
788 .ok_or_else(|| RemoteCallError {
789 message: format!("function_id {} out of range", func_id),
790 kind: RemoteErrorKind::FunctionNotFound,
791 })?;
792
793 let arity = function.arity as usize;
794 if request.arguments.len() != arity {
795 return Err(RemoteCallError {
796 message: format!(
797 "argument count mismatch for function '{}': expected {}, got {}",
798 function.name,
799 arity,
800 request.arguments.len(),
801 ),
802 kind: RemoteErrorKind::ArgumentError,
803 });
804 }
805
806 let frame_desc = function.frame_descriptor.clone();
807 let arg_kinds: Vec<shape_value::NativeKind> = if let Some(ref fd) = frame_desc {
808 if fd.slots.len() < arity {
809 return Err(RemoteCallError {
810 message: format!(
811 "function '{}' frame_descriptor has {} slots but arity is {}",
812 function.name,
813 fd.slots.len(),
814 arity,
815 ),
816 kind: RemoteErrorKind::ArgumentError,
817 });
818 }
819 fd.slots.iter().take(arity).copied().collect()
820 } else if arity == 0 {
821 Vec::new()
822 } else {
823 return Err(RemoteCallError {
824 message: format!(
825 "function '{}' has no frame_descriptor — cannot derive \
826 per-arg NativeKind for marshal protocol (ADR-006 §2.7.5.1)",
827 function.name,
828 ),
829 kind: RemoteErrorKind::ArgumentError,
830 });
831 };
832
833 let return_kind = frame_desc.as_ref().and_then(|fd| fd.return_kind);
834 let function_name_owned = function.name.clone();
835 let _ = function; let mut args: Vec<KindedSlot> = Vec::with_capacity(arity);
843 for (idx, sv) in request.arguments.iter().enumerate() {
844 let expected = arg_kinds[idx];
845 let (bits, kind) = serializable_to_slot(sv, expected, store).map_err(|e| {
846 RemoteCallError {
847 message: format!(
848 "arg {} marshal failure (expected kind {:?}): {}",
849 idx, expected, e,
850 ),
851 kind: RemoteErrorKind::ArgumentError,
852 }
853 })?;
854 args.push(KindedSlot::new(ValueSlot::from_raw(bits), kind));
855 }
856
857 let result = vm
859 .execute_function_by_id(func_id, args, None)
860 .map_err(|e| RemoteCallError {
861 message: format!(
862 "remote execution of '{}' failed: {:?}",
863 function_name_owned, e,
864 ),
865 kind: RemoteErrorKind::RuntimeError,
866 })?;
867
868 let (bits, kind) = (result.slot.raw(), result.kind);
873 if let Some(declared) = return_kind {
877 if kind != declared {
878 return Err(RemoteCallError {
879 message: format!(
880 "function '{}' returned kind {:?} but frame_descriptor \
881 declared return_kind {:?}",
882 function_name_owned, kind, declared,
883 ),
884 kind: RemoteErrorKind::RuntimeError,
885 });
886 }
887 }
888 let serialized = slot_to_serializable(bits, kind, store).map_err(|e| RemoteCallError {
889 message: format!("return-value marshal failure: {}", e),
890 kind: RemoteErrorKind::RuntimeError,
891 })?;
892 drop(result);
895 Ok(serialized)
896}
897
898pub fn program_hash(program: &BytecodeProgram) -> [u8; 32] {
903 use sha2::{Digest, Sha256};
904 let bytes =
905 rmp_serde::to_vec_named(program).expect("BytecodeProgram serialization should not fail");
906 let hash = Sha256::digest(&bytes);
907 let mut out = [0u8; 32];
908 out.copy_from_slice(&hash);
909 out
910}
911
912fn create_stub_program(program: &BytecodeProgram) -> BytecodeProgram {
917 let mut stub = BytecodeProgram::default();
918 stub.type_schema_registry = program.type_schema_registry.clone();
919 if let Some(ref ca) = program.content_addressed {
921 stub.content_addressed = Some(Program {
922 entry: ca.entry,
923 function_store: std::collections::HashMap::new(),
924 top_level_locals_count: ca.top_level_locals_count,
925 top_level_local_storage_hints: ca.top_level_local_storage_hints.clone(),
926 module_binding_names: ca.module_binding_names.clone(),
927 module_binding_storage_hints: ca.module_binding_storage_hints.clone(),
928 function_local_storage_hints: ca.function_local_storage_hints.clone(),
929 top_level_frame: ca.top_level_frame.clone(),
930 top_level_local_concrete_types: ca.top_level_local_concrete_types.clone(),
931 function_local_concrete_types: ca.function_local_concrete_types.clone(),
932 function_return_concrete_types: ca.function_return_concrete_types.clone(),
933 monomorphized_method_call_sites: ca.monomorphized_method_call_sites.clone(),
934 value_call_return_concrete_types:
935 ca.value_call_return_concrete_types.clone(),
936 operator_trait_dispatch_sites:
937 ca.operator_trait_dispatch_sites.clone(),
938 data_schema: ca.data_schema.clone(),
939 type_schema_registry: ca.type_schema_registry.clone(),
940 trait_method_symbols: ca.trait_method_symbols.clone(),
941 foreign_functions: ca.foreign_functions.clone(),
942 native_struct_layouts: ca.native_struct_layouts.clone(),
943 debug_info: ca.debug_info.clone(),
944 closure_function_layouts_by_name: ca
945 .closure_function_layouts_by_name
946 .clone(),
947 trait_vtables: ca.trait_vtables.clone(),
948 has_imported_const_inline: ca.has_imported_const_inline,
950 has_w17_marshal_residual: ca.has_w17_marshal_residual,
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 negotiate_blobs(
988 blobs: &[(FunctionHash, FunctionBlob)],
989 remote_cache: &RemoteBlobCache,
990) -> BlobNegotiationResponse {
991 let request = BlobNegotiationRequest {
992 offered_hashes: blobs.iter().map(|(h, _)| *h).collect(),
993 };
994 handle_negotiation(&request, remote_cache)
998}
999
1000pub fn build_call_request_with_negotiation(
1006 program: &BytecodeProgram,
1007 function_name: &str,
1008 arguments: Vec<SerializableVMValue>,
1009 remote_cache: Option<&RemoteBlobCache>,
1010) -> RemoteCallRequest {
1011 let mut request = build_call_request(program, function_name, arguments);
1012
1013 if let (Some(cache), Some(blobs)) = (remote_cache, &mut request.function_blobs) {
1014 let response = negotiate_blobs(blobs, cache);
1015 let known_set: std::collections::HashSet<FunctionHash> =
1016 response.known_hashes.into_iter().collect();
1017 blobs.retain(|(hash, _)| !known_set.contains(hash));
1018 }
1019
1020 request
1021}
1022
1023pub fn build_call_request(
1029 program: &BytecodeProgram,
1030 function_name: &str,
1031 arguments: Vec<SerializableVMValue>,
1032) -> RemoteCallRequest {
1033 let hash = program_hash(program);
1034 let function_id = program
1035 .functions
1036 .iter()
1037 .position(|f| f.name == function_name)
1038 .map(|id| id as u16);
1039 let function_hash = function_id
1040 .and_then(|fid| {
1041 program
1042 .function_blob_hashes
1043 .get(fid as usize)
1044 .copied()
1045 .flatten()
1046 })
1047 .or_else(|| {
1048 program.content_addressed.as_ref().and_then(|ca| {
1049 let mut matches = ca.function_store.iter().filter_map(|(hash, blob)| {
1050 if blob.name == function_name {
1051 Some(*hash)
1052 } else {
1053 None
1054 }
1055 });
1056 let first = matches.next()?;
1057 if matches.next().is_some() {
1058 None
1059 } else {
1060 Some(first)
1061 }
1062 })
1063 });
1064 let blobs = function_hash.and_then(|h| build_minimal_blobs_by_hash(program, h));
1065
1066 let request_program = if blobs.is_some() {
1069 create_stub_program(program)
1070 } else {
1071 program.clone()
1072 };
1073
1074 RemoteCallRequest {
1075 program: request_program,
1076 function_name: function_name.to_string(),
1077 function_id,
1078 function_hash,
1079 arguments,
1080 upvalues: None,
1081 type_schemas: program.type_schema_registry.clone(),
1082 program_hash: hash,
1083 function_blobs: blobs,
1084 }
1085}
1086
1087pub fn build_closure_call_request(
1093 program: &BytecodeProgram,
1094 function_id: u16,
1095 arguments: Vec<SerializableVMValue>,
1096 upvalues: Vec<SerializableVMValue>,
1097) -> RemoteCallRequest {
1098 let hash = program_hash(program);
1099
1100 let function_hash = program
1101 .function_blob_hashes
1102 .get(function_id as usize)
1103 .copied()
1104 .flatten();
1105 let blobs = function_hash.and_then(|h| build_minimal_blobs_by_hash(program, h));
1106
1107 RemoteCallRequest {
1108 program: if blobs.is_some() {
1109 create_stub_program(program)
1110 } else {
1111 program.clone()
1112 },
1113 function_name: String::new(),
1114 function_id: Some(function_id),
1115 function_hash,
1116 arguments,
1117 upvalues: Some(upvalues),
1118 type_schemas: program.type_schema_registry.clone(),
1119 program_hash: hash,
1120 function_blobs: blobs,
1121 }
1122}
1123
1124pub fn build_call_request_negotiated(
1130 program: &BytecodeProgram,
1131 function_name: &str,
1132 arguments: Vec<SerializableVMValue>,
1133 known_hashes: &[FunctionHash],
1134) -> RemoteCallRequest {
1135 let mut request = build_call_request(program, function_name, arguments);
1136
1137 if let Some(ref mut blobs) = request.function_blobs {
1139 let known_set: std::collections::HashSet<FunctionHash> =
1140 known_hashes.iter().copied().collect();
1141 blobs.retain(|(hash, _)| !known_set.contains(hash));
1142 }
1143
1144 request
1145}
1146
1147pub fn handle_negotiation(
1151 request: &BlobNegotiationRequest,
1152 cache: &RemoteBlobCache,
1153) -> BlobNegotiationResponse {
1154 BlobNegotiationResponse {
1155 known_hashes: cache.filter_known(&request.offered_hashes),
1156 }
1157}
1158
1159pub fn handle_wire_message(
1169 msg: WireMessage,
1170 store: &SnapshotStore,
1171 cache: &mut RemoteBlobCache,
1172) -> WireMessage {
1173 match msg {
1174 WireMessage::BlobNegotiation(req) => {
1175 let response = handle_negotiation(&req, cache);
1176 WireMessage::BlobNegotiationReply(response)
1177 }
1178 WireMessage::BlobNegotiationReply(_) => {
1179 WireMessage::ExecuteResponse(ExecuteResponse {
1182 request_id: 0,
1183 success: false,
1184 value: WireValue::Null,
1185 stdout: None,
1186 error: Some("Unexpected BlobNegotiationReply on server side".to_string()),
1187 content_terminal: None,
1188 content_html: None,
1189 diagnostics: vec![],
1190 metrics: None,
1191 print_output: None,
1192 })
1193 }
1194 WireMessage::Call(req) => {
1195 if let Some(ref blobs) = req.function_blobs {
1197 cache.insert_blobs(blobs);
1198 }
1199 let response = execute_remote_call(req, store);
1200 WireMessage::CallResponse(response)
1201 }
1202 WireMessage::CallResponse(_) => {
1203 WireMessage::ExecuteResponse(ExecuteResponse {
1205 request_id: 0,
1206 success: false,
1207 value: WireValue::Null,
1208 stdout: None,
1209 error: Some("Unexpected CallResponse on server side".to_string()),
1210 content_terminal: None,
1211 content_html: None,
1212 diagnostics: vec![],
1213 metrics: None,
1214 print_output: None,
1215 })
1216 }
1217 WireMessage::Sidecar(_sidecar) => {
1218 WireMessage::ExecuteResponse(ExecuteResponse {
1222 request_id: 0,
1223 success: false,
1224 value: WireValue::Null,
1225 stdout: None,
1226 error: Some("Unexpected standalone Sidecar message".to_string()),
1227 content_terminal: None,
1228 content_html: None,
1229 diagnostics: vec![],
1230 metrics: None,
1231 print_output: None,
1232 })
1233 }
1234
1235 WireMessage::Execute(req) => WireMessage::ExecuteResponse(ExecuteResponse {
1238 request_id: req.request_id,
1239 success: false,
1240 value: WireValue::Null,
1241 stdout: None,
1242 error: Some("V2 Execute not yet implemented".to_string()),
1243 content_terminal: None,
1244 content_html: None,
1245 diagnostics: vec![WireDiagnostic {
1246 severity: "error".to_string(),
1247 message: "V2 Execute handler not yet implemented".to_string(),
1248 line: None,
1249 column: None,
1250 }],
1251 metrics: None,
1252 print_output: None,
1253 }),
1254 WireMessage::ExecuteResponse(_) => {
1255 WireMessage::ExecuteResponse(ExecuteResponse {
1257 request_id: 0,
1258 success: false,
1259 value: WireValue::Null,
1260 stdout: None,
1261 error: Some("Unexpected ExecuteResponse on server side".to_string()),
1262 content_terminal: None,
1263 content_html: None,
1264 diagnostics: vec![],
1265 metrics: None,
1266 print_output: None,
1267 })
1268 }
1269 WireMessage::Validate(req) => WireMessage::ValidateResponse(ValidateResponse {
1270 request_id: req.request_id,
1271 success: false,
1272 diagnostics: vec![WireDiagnostic {
1273 severity: "error".to_string(),
1274 message: "V2 Validate handler not yet implemented".to_string(),
1275 line: None,
1276 column: None,
1277 }],
1278 }),
1279 WireMessage::ValidateResponse(_) => {
1280 WireMessage::ExecuteResponse(ExecuteResponse {
1281 request_id: 0,
1282 success: false,
1283 value: WireValue::Null,
1284 stdout: None,
1285 error: Some("Unexpected ValidateResponse on server side".to_string()),
1286 content_terminal: None,
1287 content_html: None,
1288 diagnostics: vec![],
1289 metrics: None,
1290 print_output: None,
1291 })
1292 }
1293 WireMessage::Auth(_req) => WireMessage::AuthResponse(AuthResponse {
1294 authenticated: false,
1295 error: Some("V2 Auth handler not yet implemented".to_string()),
1296 }),
1297 WireMessage::AuthResponse(_) => {
1298 WireMessage::ExecuteResponse(ExecuteResponse {
1299 request_id: 0,
1300 success: false,
1301 value: WireValue::Null,
1302 stdout: None,
1303 error: Some("Unexpected AuthResponse on server side".to_string()),
1304 content_terminal: None,
1305 content_html: None,
1306 diagnostics: vec![],
1307 metrics: None,
1308 print_output: None,
1309 })
1310 }
1311 WireMessage::ExecuteFile(req) => WireMessage::ExecuteResponse(ExecuteResponse {
1312 request_id: req.request_id,
1313 success: false,
1314 value: WireValue::Null,
1315 stdout: None,
1316 error: Some("V2 ExecuteFile handler not yet implemented".to_string()),
1317 content_terminal: None,
1318 content_html: None,
1319 diagnostics: vec![WireDiagnostic {
1320 severity: "error".to_string(),
1321 message: "V2 ExecuteFile handler not yet implemented".to_string(),
1322 line: None,
1323 column: None,
1324 }],
1325 metrics: None,
1326 print_output: None,
1327 }),
1328 WireMessage::ExecuteProject(req) => WireMessage::ExecuteResponse(ExecuteResponse {
1329 request_id: req.request_id,
1330 success: false,
1331 value: WireValue::Null,
1332 stdout: None,
1333 error: Some("V2 ExecuteProject handler not yet implemented".to_string()),
1334 content_terminal: None,
1335 content_html: None,
1336 diagnostics: vec![WireDiagnostic {
1337 severity: "error".to_string(),
1338 message: "V2 ExecuteProject handler not yet implemented".to_string(),
1339 line: None,
1340 column: None,
1341 }],
1342 metrics: None,
1343 print_output: None,
1344 }),
1345 WireMessage::ValidatePath(req) => WireMessage::ValidateResponse(ValidateResponse {
1346 request_id: req.request_id,
1347 success: false,
1348 diagnostics: vec![WireDiagnostic {
1349 severity: "error".to_string(),
1350 message: "V2 ValidatePath handler not yet implemented".to_string(),
1351 line: None,
1352 column: None,
1353 }],
1354 }),
1355 WireMessage::Ping(_) => WireMessage::Pong(ServerInfo {
1356 shape_version: env!("CARGO_PKG_VERSION").to_string(),
1357 wire_protocol: shape_wire::WIRE_PROTOCOL_V2,
1358 capabilities: vec![
1359 "call".to_string(),
1360 "blob-negotiation".to_string(),
1361 "sidecar".to_string(),
1362 ],
1363 }),
1364 WireMessage::Pong(_) => {
1365 WireMessage::ExecuteResponse(ExecuteResponse {
1367 request_id: 0,
1368 success: false,
1369 value: WireValue::Null,
1370 stdout: None,
1371 error: Some("Unexpected Pong on server side".to_string()),
1372 content_terminal: None,
1373 content_html: None,
1374 diagnostics: vec![],
1375 metrics: None,
1376 print_output: None,
1377 })
1378 }
1379 }
1380}
1381
1382pub const SIDECAR_THRESHOLD: usize = 1024 * 1024; pub fn extract_sidecars(
1398 args: &mut Vec<SerializableVMValue>,
1399 store: &SnapshotStore,
1400) -> Vec<BlobSidecar> {
1401 let mut sidecars = Vec::new();
1402 let mut next_id: u32 = 0;
1403 for arg in args.iter_mut() {
1404 extract_sidecars_recursive(arg, store, &mut sidecars, &mut next_id);
1405 }
1406 sidecars
1407}
1408
1409fn get_blob_ref(value: &SerializableVMValue) -> Option<&shape_runtime::snapshot::BlobRef> {
1411 use shape_runtime::snapshot::SerializableVMValue as SV;
1412 match value {
1413 SV::DataTable(blob)
1414 | SV::TypedTable { table: blob, .. }
1415 | SV::RowView { table: blob, .. }
1416 | SV::ColumnRef { table: blob, .. }
1417 | SV::IndexedTable { table: blob, .. } => Some(blob),
1418 SV::TypedArray { blob, .. } | SV::Matrix { blob, .. } => Some(blob),
1419 _ => None,
1420 }
1421}
1422
1423fn extract_sidecars_recursive(
1424 value: &mut SerializableVMValue,
1425 store: &SnapshotStore,
1426 sidecars: &mut Vec<BlobSidecar>,
1427 next_id: &mut u32,
1428) {
1429 use shape_runtime::snapshot::SerializableVMValue as SV;
1430
1431 let meta = match &*value {
1434 SV::TypedArray { len, .. } => (*len as u32, 0u32),
1435 SV::Matrix { rows, cols, .. } => (*rows, *cols),
1436 _ => (0, 0),
1437 };
1438 if let Some(blob) = get_blob_ref(value) {
1440 let blob_kind = blob.kind.clone();
1441 let blob_hash = blob.hash.clone();
1442 if let Some(sidecar) = try_extract_blob(blob, store, next_id) {
1443 let sidecar_id = sidecar.sidecar_id;
1444 sidecars.push(sidecar);
1445 *value = SV::SidecarRef {
1446 sidecar_id,
1447 blob_kind,
1448 original_hash: blob_hash,
1449 meta_a: meta.0,
1450 meta_b: meta.1,
1451 };
1452 return;
1453 }
1454 }
1455
1456 match value {
1458 SV::Array(items) => {
1459 for item in items.iter_mut() {
1460 extract_sidecars_recursive(item, store, sidecars, next_id);
1461 }
1462 }
1463 SV::HashMap { keys, values } => {
1464 for k in keys.iter_mut() {
1465 extract_sidecars_recursive(k, store, sidecars, next_id);
1466 }
1467 for v in values.iter_mut() {
1468 extract_sidecars_recursive(v, store, sidecars, next_id);
1469 }
1470 }
1471 SV::TypedObject { slot_data, .. } => {
1472 for slot in slot_data.iter_mut() {
1473 extract_sidecars_recursive(slot, store, sidecars, next_id);
1474 }
1475 }
1476 SV::Some(inner) | SV::Ok(inner) | SV::Err(inner) => {
1477 extract_sidecars_recursive(inner, store, sidecars, next_id);
1478 }
1479 SV::TypeAnnotatedValue { value: inner, .. } => {
1480 extract_sidecars_recursive(inner, store, sidecars, next_id);
1481 }
1482 SV::Closure { upvalues, .. } => {
1483 for uv in upvalues.iter_mut() {
1484 extract_sidecars_recursive(uv, store, sidecars, next_id);
1485 }
1486 }
1487 SV::Enum(ev) => match &mut ev.payload {
1488 shape_runtime::snapshot::EnumPayloadSnapshot::Unit => {}
1489 shape_runtime::snapshot::EnumPayloadSnapshot::Tuple(items) => {
1490 for item in items.iter_mut() {
1491 extract_sidecars_recursive(item, store, sidecars, next_id);
1492 }
1493 }
1494 shape_runtime::snapshot::EnumPayloadSnapshot::Struct(fields) => {
1495 for (_, v) in fields.iter_mut() {
1496 extract_sidecars_recursive(v, store, sidecars, next_id);
1497 }
1498 }
1499 },
1500 SV::PrintResult(pr) => {
1501 for span in pr.spans.iter_mut() {
1502 if let shape_runtime::snapshot::PrintSpanSnapshot::Value {
1503 raw_value,
1504 format_params,
1505 ..
1506 } = span
1507 {
1508 extract_sidecars_recursive(raw_value, store, sidecars, next_id);
1509 for (_, v) in format_params.iter_mut() {
1510 extract_sidecars_recursive(v, store, sidecars, next_id);
1511 }
1512 }
1513 }
1514 }
1515 SV::SimulationCall { params, .. } => {
1516 for (_, v) in params.iter_mut() {
1517 extract_sidecars_recursive(v, store, sidecars, next_id);
1518 }
1519 }
1520 SV::FunctionRef { closure, .. } => {
1521 if let Some(c) = closure {
1522 extract_sidecars_recursive(c, store, sidecars, next_id);
1523 }
1524 }
1525 SV::Range { start, end, .. } => {
1526 if let Some(s) = start {
1527 extract_sidecars_recursive(s, store, sidecars, next_id);
1528 }
1529 if let Some(e) = end {
1530 extract_sidecars_recursive(e, store, sidecars, next_id);
1531 }
1532 }
1533
1534 _ => {}
1536 }
1537}
1538
1539fn try_extract_blob(
1541 blob: &shape_runtime::snapshot::BlobRef,
1542 store: &SnapshotStore,
1543 next_id: &mut u32,
1544) -> Option<BlobSidecar> {
1545 let chunked: shape_runtime::snapshot::ChunkedBlob = store.get_struct(&blob.hash).ok()?;
1547 if chunked.total_len < SIDECAR_THRESHOLD {
1548 return None;
1549 }
1550
1551 let data = shape_runtime::snapshot::load_chunked_bytes(&chunked, store).ok()?;
1553 let sidecar_id = *next_id;
1554 *next_id += 1;
1555
1556 Some(BlobSidecar { sidecar_id, data })
1557}
1558
1559pub fn reassemble_sidecars(
1565 args: &mut Vec<SerializableVMValue>,
1566 sidecars: &std::collections::HashMap<u32, BlobSidecar>,
1567 store: &SnapshotStore,
1568) -> anyhow::Result<()> {
1569 for arg in args.iter_mut() {
1570 reassemble_recursive(arg, sidecars, store)?;
1571 }
1572 Ok(())
1573}
1574
1575fn reassemble_recursive(
1576 value: &mut SerializableVMValue,
1577 sidecars: &std::collections::HashMap<u32, BlobSidecar>,
1578 store: &SnapshotStore,
1579) -> anyhow::Result<()> {
1580 use shape_runtime::snapshot::{BlobRef, SerializableVMValue as SV};
1581
1582 match value {
1583 SV::SidecarRef {
1584 sidecar_id,
1585 blob_kind,
1586 original_hash: _,
1587 meta_a,
1588 meta_b,
1589 } => {
1590 let sidecar = sidecars
1591 .get(sidecar_id)
1592 .ok_or_else(|| anyhow::anyhow!("missing sidecar with id {}", sidecar_id))?;
1593 let meta_a = *meta_a;
1594 let meta_b = *meta_b;
1595
1596 let chunked = shape_runtime::snapshot::store_chunked_bytes(&sidecar.data, store)?;
1599 let hash = store.put_struct(&chunked)?;
1600
1601 let blob = BlobRef {
1602 hash,
1603 kind: blob_kind.clone(),
1604 };
1605 *value = match blob_kind {
1606 shape_runtime::snapshot::BlobKind::DataTable => SV::DataTable(blob),
1607 shape_runtime::snapshot::BlobKind::TypedArray(ek) => SV::TypedArray {
1608 element_kind: *ek,
1609 blob,
1610 len: meta_a as usize,
1611 },
1612 shape_runtime::snapshot::BlobKind::Matrix => SV::Matrix {
1613 blob,
1614 rows: meta_a,
1615 cols: meta_b,
1616 },
1617 };
1618 }
1619
1620 SV::Array(items) => {
1622 for item in items.iter_mut() {
1623 reassemble_recursive(item, sidecars, store)?;
1624 }
1625 }
1626 SV::HashMap { keys, values } => {
1627 for k in keys.iter_mut() {
1628 reassemble_recursive(k, sidecars, store)?;
1629 }
1630 for v in values.iter_mut() {
1631 reassemble_recursive(v, sidecars, store)?;
1632 }
1633 }
1634 SV::TypedObject { slot_data, .. } => {
1635 for slot in slot_data.iter_mut() {
1636 reassemble_recursive(slot, sidecars, store)?;
1637 }
1638 }
1639 SV::Some(inner) | SV::Ok(inner) | SV::Err(inner) => {
1640 reassemble_recursive(inner, sidecars, store)?;
1641 }
1642 SV::TypeAnnotatedValue { value: inner, .. } => {
1643 reassemble_recursive(inner, sidecars, store)?;
1644 }
1645 SV::Closure { upvalues, .. } => {
1646 for uv in upvalues.iter_mut() {
1647 reassemble_recursive(uv, sidecars, store)?;
1648 }
1649 }
1650 SV::Enum(ev) => match &mut ev.payload {
1651 shape_runtime::snapshot::EnumPayloadSnapshot::Unit => {}
1652 shape_runtime::snapshot::EnumPayloadSnapshot::Tuple(items) => {
1653 for item in items.iter_mut() {
1654 reassemble_recursive(item, sidecars, store)?;
1655 }
1656 }
1657 shape_runtime::snapshot::EnumPayloadSnapshot::Struct(fields) => {
1658 for (_, v) in fields.iter_mut() {
1659 reassemble_recursive(v, sidecars, store)?;
1660 }
1661 }
1662 },
1663 SV::PrintResult(pr) => {
1664 for span in pr.spans.iter_mut() {
1665 if let shape_runtime::snapshot::PrintSpanSnapshot::Value {
1666 raw_value,
1667 format_params,
1668 ..
1669 } = span
1670 {
1671 reassemble_recursive(raw_value, sidecars, store)?;
1672 for (_, v) in format_params.iter_mut() {
1673 reassemble_recursive(v, sidecars, store)?;
1674 }
1675 }
1676 }
1677 }
1678 SV::SimulationCall { params, .. } => {
1679 for (_, v) in params.iter_mut() {
1680 reassemble_recursive(v, sidecars, store)?;
1681 }
1682 }
1683 SV::FunctionRef { closure, .. } => {
1684 if let Some(c) = closure {
1685 reassemble_recursive(c, sidecars, store)?;
1686 }
1687 }
1688 SV::Range { start, end, .. } => {
1689 if let Some(s) = start {
1690 reassemble_recursive(s, sidecars, store)?;
1691 }
1692 if let Some(e) = end {
1693 reassemble_recursive(e, sidecars, store)?;
1694 }
1695 }
1696
1697 _ => {}
1699 }
1700 Ok(())
1701}
1702
1703#[cfg(test)]
1704mod tests {
1705 use super::*;
1706 use crate::bytecode::{FunctionBlob, FunctionHash, Instruction, OpCode, Program};
1707 use crate::compiler::BytecodeCompiler;
1708 use shape_abi_v1::PermissionSet;
1709 use std::collections::HashMap;
1710
1711 fn compile(source: &str) -> BytecodeProgram {
1713 let program = shape_ast::parser::parse_program(source).expect("parse failed");
1714 let compiler = BytecodeCompiler::new();
1715 compiler.compile(&program).expect("compile failed")
1716 }
1717
1718 fn temp_store() -> SnapshotStore {
1720 let dir = std::env::temp_dir().join(format!("shape_remote_test_{}", std::process::id()));
1721 SnapshotStore::new(dir).expect("create snapshot store")
1722 }
1723
1724 fn mk_hash(tag: u8) -> FunctionHash {
1725 let mut bytes = [0u8; 32];
1726 bytes[0] = tag;
1727 FunctionHash(bytes)
1728 }
1729
1730 fn mk_blob(name: &str, hash: FunctionHash, dependencies: Vec<FunctionHash>) -> FunctionBlob {
1731 FunctionBlob {
1732 content_hash: hash,
1733 name: name.to_string(),
1734 arity: 0,
1735 param_names: Vec::new(),
1736 locals_count: 0,
1737 is_closure: false,
1738 captures_count: 0,
1739 is_async: false,
1740 ref_params: Vec::new(),
1741 ref_mutates: Vec::new(),
1742 mutable_captures: Vec::new(),
1743 frame_descriptor: None,
1744 instructions: vec![
1745 Instruction::simple(OpCode::PushNull),
1746 Instruction::simple(OpCode::ReturnValue),
1747 ],
1748 constants: Vec::new(),
1749 strings: Vec::new(),
1750 required_permissions: PermissionSet::pure(),
1751 dependencies,
1752 callee_names: Vec::new(),
1753 type_schemas: Vec::new(),
1754 foreign_dependencies: Vec::new(),
1755 source_map: Vec::new(),
1756 }
1757 }
1758
1759 #[test]
1769 fn test_program_hash_deterministic() {
1770 let bytecode = compile("function f(x) { x * 2 }");
1771 let hash1 = program_hash(&bytecode);
1772 let hash2 = program_hash(&bytecode);
1773 assert_eq!(hash1, hash2, "Same program should produce same hash");
1774 }
1775
1776 #[test]
1777 fn test_request_response_serialization_roundtrip() {
1778 let bytecode = compile("function double(x) { x * 2 }");
1779 let request =
1780 build_call_request(&bytecode, "double", vec![SerializableVMValue::Number(21.0)]);
1781
1782 let bytes = shape_wire::encode_message(&request).expect("encode request");
1784 let decoded: RemoteCallRequest =
1785 shape_wire::decode_message(&bytes).expect("decode request");
1786
1787 assert_eq!(decoded.function_name, "double");
1788 assert_eq!(decoded.arguments.len(), 1);
1789 assert_eq!(decoded.program_hash, request.program_hash);
1790 }
1791
1792 #[test]
1793 fn test_response_serialization_roundtrip() {
1794 let response = RemoteCallResponse {
1795 result: Ok(SerializableVMValue::String("hello".to_string())),
1796 };
1797
1798 let bytes = shape_wire::encode_message(&response).expect("encode response");
1799 let decoded: RemoteCallResponse =
1800 shape_wire::decode_message(&bytes).expect("decode response");
1801
1802 match decoded.result {
1803 Ok(SerializableVMValue::String(s)) => assert_eq!(s, "hello"),
1804 other => panic!("Expected Ok(String), got {:?}", other),
1805 }
1806 }
1807
1808 #[test]
1809 fn test_type_schema_registry_roundtrip() {
1810 use shape_runtime::type_schema::{FieldType, TypeSchemaRegistry};
1811
1812 let mut registry = TypeSchemaRegistry::new();
1813 registry.register_type(
1814 "Point",
1815 vec![
1816 ("x".to_string(), FieldType::F64),
1817 ("y".to_string(), FieldType::F64),
1818 ],
1819 );
1820
1821 let bytes = shape_wire::encode_message(®istry).expect("encode registry");
1822 let decoded: TypeSchemaRegistry =
1823 shape_wire::decode_message(&bytes).expect("decode registry");
1824
1825 assert!(decoded.has_type("Point"));
1826 let schema = decoded.get("Point").unwrap();
1827 assert_eq!(schema.field_count(), 2);
1828 assert_eq!(schema.field_offset("x"), Some(0));
1829 assert_eq!(schema.field_offset("y"), Some(8));
1830 }
1831
1832 #[test]
1833 fn test_build_minimal_blobs_rejects_ambiguous_function_name() {
1834 let h1 = mk_hash(1);
1835 let h2 = mk_hash(2);
1836 let blob1 = mk_blob("dup", h1, vec![]);
1837 let blob2 = mk_blob("dup", h2, vec![]);
1838
1839 let mut function_store = HashMap::new();
1840 function_store.insert(h1, blob1.clone());
1841 function_store.insert(h2, blob2.clone());
1842
1843 let mut program = BytecodeProgram::default();
1844 program.content_addressed = Some(Program {
1845 entry: h1,
1846 function_store,
1847 top_level_locals_count: 0,
1848 top_level_local_storage_hints: Vec::new(),
1849 module_binding_names: Vec::new(),
1850 module_binding_storage_hints: Vec::new(),
1851 function_local_storage_hints: Vec::new(),
1852 top_level_frame: None,
1853 top_level_local_concrete_types: Vec::new(),
1854 function_local_concrete_types: Vec::new(),
1855 function_return_concrete_types: Vec::new(),
1856 monomorphized_method_call_sites: HashMap::new(),
1857 value_call_return_concrete_types: HashMap::new(),
1858 operator_trait_dispatch_sites: HashMap::new(),
1859 data_schema: None,
1860 type_schema_registry: shape_runtime::type_schema::TypeSchemaRegistry::new(),
1861 trait_method_symbols: HashMap::new(),
1862 foreign_functions: Vec::new(),
1863 native_struct_layouts: Vec::new(),
1864 debug_info: crate::bytecode::DebugInfo::new("<test>".to_string()),
1865 closure_function_layouts_by_name: HashMap::new(),
1866 trait_vtables: HashMap::new(),
1867 has_imported_const_inline: false,
1868 has_w17_marshal_residual: false,
1869 });
1870
1871 assert!(
1872 build_minimal_blobs(&program, "dup").is_none(),
1873 "name-based selection must reject ambiguous function names"
1874 );
1875
1876 let by_hash = build_minimal_blobs_by_hash(&program, h2)
1877 .expect("hash-based selection should work with duplicate names");
1878 assert_eq!(by_hash.len(), 1);
1879 assert_eq!(by_hash[0].0, h2);
1880 assert_eq!(by_hash[0].1.name, "dup");
1881 }
1882
1883 #[test]
1884 fn test_program_from_blobs_by_hash_requires_entry_blob() {
1885 let h1 = mk_hash(1);
1886 let h_missing = mk_hash(9);
1887 let blob = mk_blob("f", h1, vec![]);
1888 let source = BytecodeProgram::default();
1889
1890 let reconstructed = program_from_blobs_by_hash(vec![(h1, blob)], h_missing, &source);
1891 assert!(
1892 reconstructed.is_none(),
1893 "reconstruction must fail when the requested entry hash is absent"
1894 );
1895 }
1896
1897 #[test]
1900 fn test_blob_cache_insert_and_get() {
1901 let mut cache = RemoteBlobCache::new(10);
1902 let h1 = mk_hash(1);
1903 let blob1 = mk_blob("f1", h1, vec![]);
1904
1905 cache.insert(h1, blob1.clone());
1906 assert_eq!(cache.len(), 1);
1907 assert!(cache.contains(&h1));
1908 assert_eq!(cache.get(&h1).unwrap().name, "f1");
1909 }
1910
1911 #[test]
1912 fn test_blob_cache_lru_eviction() {
1913 let mut cache = RemoteBlobCache::new(2);
1914 let h1 = mk_hash(1);
1915 let h2 = mk_hash(2);
1916 let h3 = mk_hash(3);
1917
1918 cache.insert(h1, mk_blob("f1", h1, vec![]));
1919 cache.insert(h2, mk_blob("f2", h2, vec![]));
1920 assert_eq!(cache.len(), 2);
1921
1922 cache.insert(h3, mk_blob("f3", h3, vec![]));
1924 assert_eq!(cache.len(), 2);
1925 assert!(!cache.contains(&h1), "h1 should be evicted");
1926 assert!(cache.contains(&h2));
1927 assert!(cache.contains(&h3));
1928 }
1929
1930 #[test]
1931 fn test_blob_cache_access_updates_order() {
1932 let mut cache = RemoteBlobCache::new(2);
1933 let h1 = mk_hash(1);
1934 let h2 = mk_hash(2);
1935 let h3 = mk_hash(3);
1936
1937 cache.insert(h1, mk_blob("f1", h1, vec![]));
1938 cache.insert(h2, mk_blob("f2", h2, vec![]));
1939
1940 cache.get(&h1);
1942
1943 cache.insert(h3, mk_blob("f3", h3, vec![]));
1945 assert!(
1946 cache.contains(&h1),
1947 "h1 was accessed, should not be evicted"
1948 );
1949 assert!(!cache.contains(&h2), "h2 should be evicted");
1950 assert!(cache.contains(&h3));
1951 }
1952
1953 #[test]
1954 fn test_blob_cache_filter_known() {
1955 let mut cache = RemoteBlobCache::new(10);
1956 let h1 = mk_hash(1);
1957 let h2 = mk_hash(2);
1958 let h3 = mk_hash(3);
1959
1960 cache.insert(h1, mk_blob("f1", h1, vec![]));
1961 cache.insert(h3, mk_blob("f3", h3, vec![]));
1962
1963 let known = cache.filter_known(&[h1, h2, h3]);
1964 assert_eq!(known.len(), 2);
1965 assert!(known.contains(&h1));
1966 assert!(known.contains(&h3));
1967 assert!(!known.contains(&h2));
1968 }
1969
1970 #[test]
1971 fn test_handle_negotiation() {
1972 let mut cache = RemoteBlobCache::new(10);
1973 let h1 = mk_hash(1);
1974 let h2 = mk_hash(2);
1975 cache.insert(h1, mk_blob("f1", h1, vec![]));
1976
1977 let request = BlobNegotiationRequest {
1978 offered_hashes: vec![h1, h2],
1979 };
1980 let response = handle_negotiation(&request, &cache);
1981 assert_eq!(response.known_hashes.len(), 1);
1982 assert!(response.known_hashes.contains(&h1));
1983 }
1984
1985 #[test]
1986 fn test_build_call_request_negotiated_strips_known_blobs() {
1987 let h1 = mk_hash(1);
1989 let h2 = mk_hash(2);
1990 let blob1 = mk_blob("entry", h1, vec![h2]);
1991 let blob2 = mk_blob("helper", h2, vec![]);
1992
1993 let mut function_store = HashMap::new();
1994 function_store.insert(h1, blob1.clone());
1995 function_store.insert(h2, blob2.clone());
1996
1997 let mut program = BytecodeProgram::default();
1998 program.content_addressed = Some(Program {
1999 entry: h1,
2000 function_store,
2001 top_level_locals_count: 0,
2002 top_level_local_storage_hints: Vec::new(),
2003 module_binding_names: Vec::new(),
2004 module_binding_storage_hints: Vec::new(),
2005 function_local_storage_hints: Vec::new(),
2006 top_level_frame: None,
2007 top_level_local_concrete_types: Vec::new(),
2008 function_local_concrete_types: Vec::new(),
2009 function_return_concrete_types: Vec::new(),
2010 monomorphized_method_call_sites: HashMap::new(),
2011 value_call_return_concrete_types: HashMap::new(),
2012 operator_trait_dispatch_sites: HashMap::new(),
2013 data_schema: None,
2014 type_schema_registry: shape_runtime::type_schema::TypeSchemaRegistry::new(),
2015 trait_method_symbols: HashMap::new(),
2016 foreign_functions: Vec::new(),
2017 native_struct_layouts: Vec::new(),
2018 debug_info: crate::bytecode::DebugInfo::new("<test>".to_string()),
2019 closure_function_layouts_by_name: HashMap::new(),
2020 trait_vtables: HashMap::new(),
2021 has_imported_const_inline: false,
2022 has_w17_marshal_residual: false,
2023 });
2024 program.functions = vec![crate::bytecode::Function {
2025 name: "entry".to_string(),
2026 arity: 0,
2027 param_names: vec![],
2028 locals_count: 0,
2029 entry_point: 0,
2030 body_length: 0,
2031 is_closure: false,
2032 captures_count: 0,
2033 is_async: false,
2034 ref_params: vec![],
2035 ref_mutates: vec![],
2036 mutable_captures: vec![],
2037 frame_descriptor: None,
2038 osr_entry_points: vec![],
2039 mir_data: None,
2040 }];
2041 program.function_blob_hashes = vec![Some(h1)];
2042
2043 let req1 = build_call_request_negotiated(&program, "entry", vec![], &[]);
2045 let blobs1 = req1.function_blobs.as_ref().unwrap();
2046 assert_eq!(blobs1.len(), 2, "first call should send all blobs");
2047
2048 let req2 = build_call_request_negotiated(&program, "entry", vec![], &[h2]);
2050 let blobs2 = req2.function_blobs.as_ref().unwrap();
2051 assert_eq!(blobs2.len(), 1, "second call should skip known blobs");
2052 assert_eq!(blobs2[0].0, h1);
2053 }
2054
2055 #[test]
2056 fn test_wire_message_serialization_roundtrip() {
2057 let msg = WireMessage::BlobNegotiation(BlobNegotiationRequest {
2058 offered_hashes: vec![mk_hash(1), mk_hash(2)],
2059 });
2060 let bytes = shape_wire::encode_message(&msg).expect("encode WireMessage");
2061 let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode WireMessage");
2062 match decoded {
2063 WireMessage::BlobNegotiation(req) => {
2064 assert_eq!(req.offered_hashes.len(), 2);
2065 }
2066 _ => panic!("Expected BlobNegotiation"),
2067 }
2068 }
2069
2070 #[test]
2073 fn test_execute_request_roundtrip() {
2074 let msg = WireMessage::Execute(ExecuteRequest {
2075 code: "fn main() { 42 }".to_string(),
2076 request_id: 7,
2077 });
2078 let bytes = shape_wire::encode_message(&msg).expect("encode Execute");
2079 let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode Execute");
2080 match decoded {
2081 WireMessage::Execute(req) => {
2082 assert_eq!(req.code, "fn main() { 42 }");
2083 assert_eq!(req.request_id, 7);
2084 }
2085 _ => panic!("Expected Execute"),
2086 }
2087 }
2088
2089 #[test]
2090 fn test_execute_response_roundtrip() {
2091 let msg = WireMessage::ExecuteResponse(ExecuteResponse {
2092 request_id: 7,
2093 success: true,
2094 value: WireValue::Number(42.0),
2095 stdout: Some("hello\n".to_string()),
2096 error: None,
2097 content_terminal: None,
2098 content_html: None,
2099 diagnostics: vec![WireDiagnostic {
2100 severity: "warning".to_string(),
2101 message: "unused variable".to_string(),
2102 line: Some(1),
2103 column: Some(5),
2104 }],
2105 metrics: Some(ExecutionMetrics {
2106 instructions_executed: 100,
2107 wall_time_ms: 3,
2108 memory_bytes_peak: 4096,
2109 }),
2110 print_output: None,
2111 });
2112 let bytes = shape_wire::encode_message(&msg).expect("encode ExecuteResponse");
2113 let decoded: WireMessage =
2114 shape_wire::decode_message(&bytes).expect("decode ExecuteResponse");
2115 match decoded {
2116 WireMessage::ExecuteResponse(resp) => {
2117 assert_eq!(resp.request_id, 7);
2118 assert!(resp.success);
2119 assert!(matches!(resp.value, WireValue::Number(n) if n == 42.0));
2120 assert_eq!(resp.stdout.as_deref(), Some("hello\n"));
2121 assert!(resp.error.is_none());
2122 assert_eq!(resp.diagnostics.len(), 1);
2123 assert_eq!(resp.diagnostics[0].severity, "warning");
2124 assert_eq!(resp.diagnostics[0].line, Some(1));
2125 let m = resp.metrics.unwrap();
2126 assert_eq!(m.instructions_executed, 100);
2127 assert_eq!(m.wall_time_ms, 3);
2128 }
2129 _ => panic!("Expected ExecuteResponse"),
2130 }
2131 }
2132
2133 #[test]
2134 fn test_ping_pong_roundtrip() {
2135 let ping = WireMessage::Ping(PingRequest {});
2136 let bytes = shape_wire::encode_message(&ping).expect("encode Ping");
2137 let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode Ping");
2138 assert!(matches!(decoded, WireMessage::Ping(_)));
2139
2140 let pong = WireMessage::Pong(ServerInfo {
2141 shape_version: "0.1.3".to_string(),
2142 wire_protocol: 2,
2143 capabilities: vec!["execute".to_string(), "validate".to_string()],
2144 });
2145 let bytes = shape_wire::encode_message(&pong).expect("encode Pong");
2146 let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode Pong");
2147 match decoded {
2148 WireMessage::Pong(info) => {
2149 assert_eq!(info.shape_version, "0.1.3");
2150 assert_eq!(info.wire_protocol, 2);
2151 assert_eq!(info.capabilities.len(), 2);
2152 }
2153 _ => panic!("Expected Pong"),
2154 }
2155 }
2156
2157 #[test]
2158 fn test_auth_roundtrip() {
2159 let msg = WireMessage::Auth(AuthRequest {
2160 token: "secret-token".to_string(),
2161 });
2162 let bytes = shape_wire::encode_message(&msg).expect("encode Auth");
2163 let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode Auth");
2164 match decoded {
2165 WireMessage::Auth(req) => assert_eq!(req.token, "secret-token"),
2166 _ => panic!("Expected Auth"),
2167 }
2168
2169 let resp = WireMessage::AuthResponse(AuthResponse {
2170 authenticated: true,
2171 error: None,
2172 });
2173 let bytes = shape_wire::encode_message(&resp).expect("encode AuthResponse");
2174 let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode AuthResponse");
2175 match decoded {
2176 WireMessage::AuthResponse(r) => {
2177 assert!(r.authenticated);
2178 assert!(r.error.is_none());
2179 }
2180 _ => panic!("Expected AuthResponse"),
2181 }
2182 }
2183
2184 #[test]
2185 fn test_validate_roundtrip() {
2186 let msg = WireMessage::Validate(ValidateRequest {
2187 code: "let x = 1".to_string(),
2188 request_id: 99,
2189 });
2190 let bytes = shape_wire::encode_message(&msg).expect("encode Validate");
2191 let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode Validate");
2192 match decoded {
2193 WireMessage::Validate(req) => {
2194 assert_eq!(req.code, "let x = 1");
2195 assert_eq!(req.request_id, 99);
2196 }
2197 _ => panic!("Expected Validate"),
2198 }
2199
2200 let resp = WireMessage::ValidateResponse(ValidateResponse {
2201 request_id: 99,
2202 success: false,
2203 diagnostics: vec![WireDiagnostic {
2204 severity: "error".to_string(),
2205 message: "parse error".to_string(),
2206 line: None,
2207 column: None,
2208 }],
2209 });
2210 let bytes = shape_wire::encode_message(&resp).expect("encode ValidateResponse");
2211 let decoded: WireMessage =
2212 shape_wire::decode_message(&bytes).expect("decode ValidateResponse");
2213 match decoded {
2214 WireMessage::ValidateResponse(r) => {
2215 assert_eq!(r.request_id, 99);
2216 assert!(!r.success);
2217 assert_eq!(r.diagnostics.len(), 1);
2218 }
2219 _ => panic!("Expected ValidateResponse"),
2220 }
2221 }
2222
2223 #[test]
2224 fn test_ping_framing_roundtrip() {
2225 use shape_wire::transport::framing::{decode_framed, encode_framed};
2226
2227 let ping = WireMessage::Ping(PingRequest {});
2228 let mp = shape_wire::encode_message(&ping).expect("encode Ping");
2229 eprintln!("Ping msgpack bytes ({} bytes): {:02x?}", mp.len(), &mp);
2230
2231 let framed = encode_framed(&mp);
2232 eprintln!("Framed bytes ({} bytes): {:02x?}", framed.len(), &framed);
2233
2234 let decompressed = decode_framed(&framed).expect("decode_framed");
2235 assert_eq!(mp, decompressed, "framing roundtrip should preserve bytes");
2236
2237 let decoded: WireMessage =
2238 shape_wire::decode_message(&decompressed).expect("decode Ping after framing");
2239 assert!(matches!(decoded, WireMessage::Ping(_)));
2240 }
2241
2242 #[test]
2243 fn test_execute_framing_roundtrip() {
2244 use shape_wire::transport::framing::{decode_framed, encode_framed};
2245
2246 let exec = WireMessage::Execute(ExecuteRequest {
2247 code: "42".to_string(),
2248 request_id: 1,
2249 });
2250 let mp = shape_wire::encode_message(&exec).expect("encode Execute");
2251 eprintln!("Execute msgpack bytes ({} bytes): {:02x?}", mp.len(), &mp);
2252
2253 let framed = encode_framed(&mp);
2254 let decompressed = decode_framed(&framed).expect("decode_framed");
2255 let decoded: WireMessage =
2256 shape_wire::decode_message(&decompressed).expect("decode Execute after framing");
2257 match decoded {
2258 WireMessage::Execute(req) => {
2259 assert_eq!(req.code, "42");
2260 assert_eq!(req.request_id, 1);
2261 }
2262 _ => panic!("Expected Execute"),
2263 }
2264 }
2265
2266 #[test]
2269 fn test_extract_sidecars_no_large_blobs() {
2270 let store = temp_store();
2271 let mut args = vec![
2272 SerializableVMValue::Int(42),
2273 SerializableVMValue::String("hello".to_string()),
2274 SerializableVMValue::Array(vec![
2275 SerializableVMValue::Number(1.0),
2276 SerializableVMValue::Number(2.0),
2277 ]),
2278 ];
2279 let sidecars = extract_sidecars(&mut args, &store);
2280 assert!(sidecars.is_empty(), "no large blobs → no sidecars");
2281 assert!(matches!(args[0], SerializableVMValue::Int(42)));
2283 }
2284
2285 #[test]
2299 fn test_sidecar_ref_serialization_roundtrip() {
2300 use shape_runtime::hashing::HashDigest;
2301 use shape_runtime::snapshot::{BlobKind, TypedArrayElementKind};
2302
2303 let value = SerializableVMValue::SidecarRef {
2304 sidecar_id: 7,
2305 blob_kind: BlobKind::TypedArray(TypedArrayElementKind::F64),
2306 original_hash: HashDigest::from_hex("abc123"),
2307 meta_a: 1000,
2308 meta_b: 0,
2309 };
2310
2311 let bytes = shape_wire::encode_message(&value).expect("encode SidecarRef");
2312 let decoded: SerializableVMValue =
2313 shape_wire::decode_message(&bytes).expect("decode SidecarRef");
2314 match decoded {
2315 SerializableVMValue::SidecarRef { sidecar_id, .. } => {
2316 assert_eq!(sidecar_id, 7);
2317 }
2318 _ => panic!("Expected SidecarRef"),
2319 }
2320 }
2321
2322 #[test]
2325 fn test_negotiate_blobs_returns_known_hashes() {
2326 let h1 = mk_hash(1);
2327 let h2 = mk_hash(2);
2328 let h3 = mk_hash(3);
2329
2330 let mut cache = RemoteBlobCache::new(10);
2331 cache.insert(h1, mk_blob("f1", h1, vec![]));
2332 cache.insert(h3, mk_blob("f3", h3, vec![]));
2333
2334 let blobs = vec![
2335 (h1, mk_blob("f1", h1, vec![])),
2336 (h2, mk_blob("f2", h2, vec![])),
2337 (h3, mk_blob("f3", h3, vec![])),
2338 ];
2339 let response = negotiate_blobs(&blobs, &cache);
2340 assert_eq!(response.known_hashes.len(), 2);
2341 assert!(response.known_hashes.contains(&h1));
2342 assert!(response.known_hashes.contains(&h3));
2343 assert!(!response.known_hashes.contains(&h2));
2344 }
2345
2346 #[test]
2347 fn test_build_call_request_with_negotiation_strips_known() {
2348 let h1 = mk_hash(1);
2349 let h2 = mk_hash(2);
2350 let blob1 = mk_blob("entry", h1, vec![h2]);
2351 let blob2 = mk_blob("helper", h2, vec![]);
2352
2353 let mut function_store = HashMap::new();
2354 function_store.insert(h1, blob1.clone());
2355 function_store.insert(h2, blob2.clone());
2356
2357 let mut program = BytecodeProgram::default();
2358 program.content_addressed = Some(Program {
2359 entry: h1,
2360 function_store,
2361 top_level_locals_count: 0,
2362 top_level_local_storage_hints: Vec::new(),
2363 module_binding_names: Vec::new(),
2364 module_binding_storage_hints: Vec::new(),
2365 function_local_storage_hints: Vec::new(),
2366 top_level_frame: None,
2367 top_level_local_concrete_types: Vec::new(),
2368 function_local_concrete_types: Vec::new(),
2369 function_return_concrete_types: Vec::new(),
2370 monomorphized_method_call_sites: HashMap::new(),
2371 value_call_return_concrete_types: HashMap::new(),
2372 operator_trait_dispatch_sites: HashMap::new(),
2373 data_schema: None,
2374 type_schema_registry: shape_runtime::type_schema::TypeSchemaRegistry::new(),
2375 trait_method_symbols: HashMap::new(),
2376 foreign_functions: Vec::new(),
2377 native_struct_layouts: Vec::new(),
2378 debug_info: crate::bytecode::DebugInfo::new("<test>".to_string()),
2379 closure_function_layouts_by_name: HashMap::new(),
2380 trait_vtables: HashMap::new(),
2381 has_imported_const_inline: false,
2382 has_w17_marshal_residual: false,
2383 });
2384 program.functions = vec![crate::bytecode::Function {
2385 name: "entry".to_string(),
2386 arity: 0,
2387 param_names: vec![],
2388 locals_count: 0,
2389 entry_point: 0,
2390 body_length: 0,
2391 is_closure: false,
2392 captures_count: 0,
2393 is_async: false,
2394 ref_params: vec![],
2395 ref_mutates: vec![],
2396 mutable_captures: vec![],
2397 frame_descriptor: None,
2398 osr_entry_points: vec![],
2399 mir_data: None,
2400 }];
2401 program.function_blob_hashes = vec![Some(h1)];
2402
2403 let mut cache = RemoteBlobCache::new(10);
2405 cache.insert(h2, blob2.clone());
2406
2407 let req = build_call_request_with_negotiation(&program, "entry", vec![], Some(&cache));
2408 let blobs = req.function_blobs.as_ref().unwrap();
2409 assert_eq!(blobs.len(), 1, "should strip known blob h2");
2410 assert_eq!(blobs[0].0, h1, "only h1 should remain");
2411 }
2412
2413 #[test]
2414 fn test_build_call_request_with_negotiation_no_cache() {
2415 let h1 = mk_hash(1);
2416 let blob1 = mk_blob("entry", h1, vec![]);
2417
2418 let mut function_store = HashMap::new();
2419 function_store.insert(h1, blob1.clone());
2420
2421 let mut program = BytecodeProgram::default();
2422 program.content_addressed = Some(Program {
2423 entry: h1,
2424 function_store,
2425 top_level_locals_count: 0,
2426 top_level_local_storage_hints: Vec::new(),
2427 module_binding_names: Vec::new(),
2428 module_binding_storage_hints: Vec::new(),
2429 function_local_storage_hints: Vec::new(),
2430 top_level_frame: None,
2431 top_level_local_concrete_types: Vec::new(),
2432 function_local_concrete_types: Vec::new(),
2433 function_return_concrete_types: Vec::new(),
2434 monomorphized_method_call_sites: HashMap::new(),
2435 value_call_return_concrete_types: HashMap::new(),
2436 operator_trait_dispatch_sites: HashMap::new(),
2437 data_schema: None,
2438 type_schema_registry: shape_runtime::type_schema::TypeSchemaRegistry::new(),
2439 trait_method_symbols: HashMap::new(),
2440 foreign_functions: Vec::new(),
2441 native_struct_layouts: Vec::new(),
2442 debug_info: crate::bytecode::DebugInfo::new("<test>".to_string()),
2443 closure_function_layouts_by_name: HashMap::new(),
2444 trait_vtables: HashMap::new(),
2445 has_imported_const_inline: false,
2446 has_w17_marshal_residual: false,
2447 });
2448 program.functions = vec![crate::bytecode::Function {
2449 name: "entry".to_string(),
2450 arity: 0,
2451 param_names: vec![],
2452 locals_count: 0,
2453 entry_point: 0,
2454 body_length: 0,
2455 is_closure: false,
2456 captures_count: 0,
2457 is_async: false,
2458 ref_params: vec![],
2459 ref_mutates: vec![],
2460 mutable_captures: vec![],
2461 frame_descriptor: None,
2462 osr_entry_points: vec![],
2463 mir_data: None,
2464 }];
2465 program.function_blob_hashes = vec![Some(h1)];
2466
2467 let req = build_call_request_with_negotiation(&program, "entry", vec![], None);
2469 let blobs = req.function_blobs.as_ref().unwrap();
2470 assert_eq!(blobs.len(), 1, "all blobs should be sent when no cache");
2471 }
2472
2473 #[test]
2476 fn test_handle_wire_message_ping_returns_pong() {
2477 let store = temp_store();
2478 let mut cache = RemoteBlobCache::default_cache();
2479 let msg = WireMessage::Ping(PingRequest {});
2480 let response = handle_wire_message(msg, &store, &mut cache);
2481 match response {
2482 WireMessage::Pong(info) => {
2483 assert_eq!(info.wire_protocol, shape_wire::WIRE_PROTOCOL_V2);
2484 assert!(info.capabilities.contains(&"call".to_string()));
2485 assert!(info.capabilities.contains(&"blob-negotiation".to_string()));
2486 }
2487 _ => panic!("Expected Pong response"),
2488 }
2489 }
2490
2491 #[test]
2492 fn test_handle_wire_message_execute_returns_v2_stub() {
2493 let store = temp_store();
2494 let mut cache = RemoteBlobCache::default_cache();
2495 let msg = WireMessage::Execute(ExecuteRequest {
2496 code: "42".to_string(),
2497 request_id: 5,
2498 });
2499 let response = handle_wire_message(msg, &store, &mut cache);
2500 match response {
2501 WireMessage::ExecuteResponse(resp) => {
2502 assert_eq!(resp.request_id, 5);
2503 assert!(!resp.success);
2504 assert!(resp.error.as_ref().unwrap().contains("not yet implemented"));
2505 }
2506 _ => panic!("Expected ExecuteResponse"),
2507 }
2508 }
2509
2510 #[test]
2511 fn test_handle_wire_message_validate_returns_v2_stub() {
2512 let store = temp_store();
2513 let mut cache = RemoteBlobCache::default_cache();
2514 let msg = WireMessage::Validate(ValidateRequest {
2515 code: "let x = 1".to_string(),
2516 request_id: 10,
2517 });
2518 let response = handle_wire_message(msg, &store, &mut cache);
2519 match response {
2520 WireMessage::ValidateResponse(resp) => {
2521 assert_eq!(resp.request_id, 10);
2522 assert!(!resp.success);
2523 assert!(resp.diagnostics[0].message.contains("not yet implemented"));
2524 }
2525 _ => panic!("Expected ValidateResponse"),
2526 }
2527 }
2528
2529 #[test]
2530 fn test_handle_wire_message_auth_returns_v2_stub() {
2531 let store = temp_store();
2532 let mut cache = RemoteBlobCache::default_cache();
2533 let msg = WireMessage::Auth(AuthRequest {
2534 token: "test".to_string(),
2535 });
2536 let response = handle_wire_message(msg, &store, &mut cache);
2537 match response {
2538 WireMessage::AuthResponse(resp) => {
2539 assert!(!resp.authenticated);
2540 assert!(resp.error.as_ref().unwrap().contains("not yet implemented"));
2541 }
2542 _ => panic!("Expected AuthResponse"),
2543 }
2544 }
2545
2546 #[test]
2547 fn test_handle_wire_message_blob_negotiation() {
2548 let store = temp_store();
2549 let mut cache = RemoteBlobCache::new(10);
2550 let h1 = mk_hash(1);
2551 let h2 = mk_hash(2);
2552 cache.insert(h1, mk_blob("f1", h1, vec![]));
2553
2554 let msg = WireMessage::BlobNegotiation(BlobNegotiationRequest {
2555 offered_hashes: vec![h1, h2],
2556 });
2557 let response = handle_wire_message(msg, &store, &mut cache);
2558 match response {
2559 WireMessage::BlobNegotiationReply(resp) => {
2560 assert_eq!(resp.known_hashes.len(), 1);
2561 assert!(resp.known_hashes.contains(&h1));
2562 }
2563 _ => panic!("Expected BlobNegotiationReply"),
2564 }
2565 }
2566
2567 }