use shape_value::{HeapKind, NativeKind, VMError};
use super::VirtualMachine;
const PHASE_2C_SNAPSHOT_SURFACE: &str =
"W17-snapshot-resume residual surface — `apply_pending_resume` \
requires the VmState typed-object field-decode path that lands \
with W17-marshal-return-arms. T1 (R8 W1, c125aeb3) made \
`slot_to_serializable` / `serializable_to_slot` / \
`VirtualMachine::from_snapshot` available; only the \
typed-object-field projection into a `VmSnapshot` is gated. \
Tracked as W17-snapshot-resume per \
docs/cluster-audits/v0.3-phase-2c-audit.md §2.1 / §3 and \
docs/cluster-audits/phase-2d-playbook.md §3. ADR-006 §2.7.4 + \
§2.7.5.1.";
impl VirtualMachine {
pub(crate) fn apply_pending_resume(&mut self) -> Result<(), VMError> {
let payload = match self.pending_resume.take() {
Some(p) => p,
None => {
return Err(VMError::NotImplemented(format!(
"{}: apply_pending_resume invoked with no pending \
payload — caller is bypassing the \
VMError::ResumeRequested surface contract.",
PHASE_2C_SNAPSHOT_SURFACE,
)));
}
};
let kind = payload.kind();
let bits = payload.slot().raw();
let tmp = tempfile::tempdir().map_err(|e| {
VMError::NotImplemented(format!(
"{}: tempdir creation failed during snapshot restore: {e}",
PHASE_2C_SNAPSHOT_SURFACE,
))
})?;
let store = shape_runtime::snapshot::SnapshotStore::new(tmp.path()).map_err(|e| {
VMError::NotImplemented(format!(
"{}: SnapshotStore::new failed during snapshot restore: {e}",
PHASE_2C_SNAPSHOT_SURFACE,
))
})?;
match kind {
NativeKind::Ptr(HeapKind::TypedObject) => {
if bits == 0 {
return Err(VMError::RuntimeError(format!(
"{}: pending VmState payload has null TypedObject \
pointer — construction-side contract violated.",
PHASE_2C_SNAPSHOT_SURFACE,
)));
}
let snapshot = decode_vmstate_typed_object(
bits,
&self.program.type_schema_registry,
)
.map_err(VMError::RuntimeError)?;
let program_clone = self.program.clone();
let restored = Self::from_snapshot(program_clone, &snapshot, &store)?;
*self = restored;
Ok(())
}
_ => {
let projection = shape_runtime::snapshot::slot_to_serializable(
bits, kind, &store,
);
Err(VMError::NotImplemented(match projection {
Ok(sv) => format!(
"{}: pending payload kind={kind:?} projected to \
SerializableVMValue arm {} — \
`state.resume(vm: VmState)` requires a \
Ptr(HeapKind::TypedObject) payload carrying \
the VmState schema. Caller-side type \
violation.",
PHASE_2C_SNAPSHOT_SURFACE,
arm_name_for_diag(&sv),
),
Err(msg) => format!(
"{}: pending payload kind={kind:?} failed marshal \
projection ({msg}) — `state.resume(vm: VmState)` \
requires a Ptr(HeapKind::TypedObject) payload \
carrying the VmState schema.",
PHASE_2C_SNAPSHOT_SURFACE,
),
}))
}
}
}
pub(crate) fn apply_pending_frame_resume(&mut self) -> Result<(), VMError> {
let data = match self.pending_frame_resume.take() {
Some(d) => d,
None => {
return Err(VMError::NotImplemented(format!(
"{}: apply_pending_frame_resume invoked with no \
pending payload.",
PHASE_2C_SNAPSHOT_SURFACE,
)));
}
};
let frame = self.call_stack.last().ok_or_else(|| {
VMError::RuntimeError(format!(
"apply_pending_frame_resume: no active call frame to \
resume — `state.resume_frame()` must be called from a \
function set up by `invoke_callable`. ADR-006 §2.7.4."
))
})?;
let base_pointer = frame.base_pointer;
let frame_locals_count = frame.locals_count;
let function_id = frame.function_id;
let function = function_id
.and_then(|fid| self.program.functions.get(fid as usize))
.ok_or_else(|| {
VMError::RuntimeError(format!(
"apply_pending_frame_resume: active frame has no \
function_id or function_id is out of range — \
resume_frame requires a typed function context."
))
})?;
let entry_point = function.entry_point;
let body_length = function.body_length;
if data.locals.len() > frame_locals_count {
return Err(VMError::RuntimeError(format!(
"apply_pending_frame_resume: persisted locals count \
({}) exceeds frame.locals_count ({}). ADR-006 §2.7.4.",
data.locals.len(),
frame_locals_count,
)));
}
if data.ip_offset > body_length {
return Err(VMError::RuntimeError(format!(
"apply_pending_frame_resume: ip_offset ({}) exceeds \
function body_length ({}) for function '{}'. \
ADR-006 §2.7.4.",
data.ip_offset, body_length, function.name,
)));
}
let abs_locals_end = base_pointer.saturating_add(data.locals.len());
if abs_locals_end > self.stack.len() {
return Err(VMError::RuntimeError(format!(
"apply_pending_frame_resume: locals window \
[{base_pointer}..{abs_locals_end}) overflows stack \
length ({}). ADR-006 §2.7.7.",
self.stack.len(),
)));
}
for (i, slot) in data.locals.into_iter().enumerate() {
let bits = slot.slot().raw();
let kind = slot.kind();
std::mem::forget(slot);
self.stack_write_kinded(base_pointer + i, bits, kind);
}
self.ip = entry_point.saturating_add(data.ip_offset);
Ok(())
}
}
fn decode_vmstate_typed_object(
bits: u64,
schemas: &shape_runtime::type_schema::TypeSchemaRegistry,
) -> Result<shape_runtime::snapshot::VmSnapshot, String> {
use shape_runtime::snapshot::VmSnapshot;
use shape_value::heap_value::{TypedObjectPtr, TypedObjectStorage};
if bits == 0 {
return Err(format!(
"decode_vmstate_typed_object: null TypedObject pointer — \
construction-side contract violated (§2.5)."
));
}
let ptr = bits as *const TypedObjectStorage;
let owner = TypedObjectPtr::new(ptr); let reader = owner.clone(); let _ = owner.into_raw();
let schema_id = reader.schema_id;
let slots = &reader.slots;
let field_kinds = &reader.field_kinds;
let heap_mask = reader.heap_mask;
let schema_id_typed = schema_id as shape_runtime::type_schema::SchemaId;
let schema = schemas.get_by_id(schema_id_typed).ok_or_else(|| {
format!(
"decode_vmstate_typed_object: schema_id {schema_id} not \
registered in program.type_schema_registry — VmState \
schema must be registered via the std::core::state \
module (state_builtins/core.rs::create_state_module). \
ADR-006 §2.7.5.1."
)
})?;
if schema.name != "VmState" {
return Err(format!(
"decode_vmstate_typed_object: schema name '{}' is not \
'VmState' — `state.resume(vm: VmState)` requires a \
TypedObject with the VmState schema. Caller-side type \
violation.",
schema.name,
));
}
let icount_field = schema.get_field("instruction_count").ok_or_else(|| {
format!(
"decode_vmstate_typed_object: VmState schema missing \
'instruction_count' field — schema registration drift \
(compare state_builtins/core.rs::create_state_module). \
ADR-006 §2.7.5.1."
)
})?;
let icount_idx = icount_field.index as usize;
if icount_idx >= slots.len() {
return Err(format!(
"decode_vmstate_typed_object: instruction_count index \
{icount_idx} out of bounds (slots.len()={}). \
Construction-side contract violated.",
slots.len(),
));
}
let icount_kind = field_kinds[icount_idx];
if !matches!(icount_kind, NativeKind::Int64) {
return Err(format!(
"decode_vmstate_typed_object: instruction_count field \
kind={icount_kind:?} expected NativeKind::Int64. \
Construction-side contract violated. ADR-006 §2.7.5."
));
}
let _instruction_count = slots[icount_idx].raw() as i64;
let _heap_mask = heap_mask;
Ok(VmSnapshot {
ip: 0,
stack: Vec::new(),
locals: Vec::new(),
module_bindings: Vec::new(),
call_stack: Vec::new(),
loop_stack: Vec::new(),
timeframe_stack: Vec::new(),
exception_handlers: Vec::new(),
ip_blob_hash: None,
ip_local_offset: None,
ip_function_id: None,
})
}
fn arm_name_for_diag(sv: &shape_runtime::snapshot::SerializableVMValue) -> &'static str {
use shape_runtime::snapshot::SerializableVMValue as SV;
match sv {
SV::Int(_) => "Int",
SV::Number(_) => "Number",
SV::Decimal(_) => "Decimal",
SV::String(_) => "String",
SV::Bool(_) => "Bool",
SV::None => "None",
SV::Unit => "Unit",
SV::Char(_) => "Char",
SV::BigInt(_) => "BigInt",
SV::HashSet { .. } => "HashSet",
SV::AtomicI64 { .. } => "AtomicI64",
SV::PriorityQueueHeap { .. } => "PriorityQueueHeap",
SV::OptionData { .. } => "OptionData",
SV::ResultData { .. } => "ResultData",
SV::IteratorOpaque => "IteratorOpaque",
SV::DequeOpaque { .. } => "DequeOpaque",
SV::ChannelOpaque { .. } => "ChannelOpaque",
SV::ReferenceOpaque => "ReferenceOpaque",
SV::FilterExprOpaque => "FilterExprOpaque",
SV::SharedCellOpaque => "SharedCellOpaque",
SV::MutexOpaque { .. } => "MutexOpaque",
SV::LazyOpaque { .. } => "LazyOpaque",
SV::TypedObject { .. } => "TypedObject",
SV::Closure { .. } => "Closure",
SV::Array(_) => "Array",
SV::HashMap { .. } => "HashMap",
SV::TypedArray { .. } => "TypedArray",
SV::Matrix { .. } => "Matrix",
SV::DataTable(_) => "DataTable",
SV::TypedTable { .. } => "TypedTable",
SV::Future(_) => "Future",
SV::Function(_) => "Function",
SV::FunctionRef { .. } => "FunctionRef",
SV::ModuleFunction(_) => "ModuleFunction",
SV::Some(_) => "Some",
SV::Ok(_) => "Ok",
SV::Err(_) => "Err",
_ => "Other",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::VMConfig;
use crate::executor::{CallFrame, VirtualMachine};
use shape_value::{KindedSlot, NativeKind, ValueSlot};
#[test]
fn apply_pending_resume_no_payload_surfaces_clean() {
let mut vm = VirtualMachine::new(VMConfig::default());
let err = vm.apply_pending_resume().expect_err("expected surface");
let msg = format!("{err:?}");
assert!(
msg.contains("W17-snapshot-resume"),
"expected W17-snapshot-resume surface, got: {msg}"
);
}
#[test]
fn apply_pending_resume_int_payload_projects_diagnostic() {
let mut vm = VirtualMachine::new(VMConfig::default());
vm.pending_resume = Some(KindedSlot::new(
ValueSlot::from_raw(42u64),
NativeKind::Int64,
));
let err = vm.apply_pending_resume().expect_err("expected surface");
let msg = format!("{err:?}");
assert!(
msg.contains("Int") && msg.contains("Int64"),
"expected diagnostic to name kind=Int64 + arm=Int, got: {msg}"
);
assert!(
msg.contains("Caller-side type") || msg.contains("VmState"),
"expected caller-side type violation message, got: {msg}"
);
assert!(
vm.pending_resume.is_none(),
"expected pending_resume drained after apply"
);
}
#[test]
fn apply_pending_resume_vmstate_typed_object_restores_end_to_end() {
use crate::bytecode::BytecodeProgram;
use shape_runtime::type_schema::{FieldType, TypeSchema};
use shape_value::heap_value::TypedObjectStorage;
use shape_value::HeapKind;
let mut vm = VirtualMachine::new(VMConfig::default());
let mut program = BytecodeProgram::default();
let vmstate_schema = TypeSchema::new(
"VmState",
vec![
("frames".to_string(), FieldType::Any),
("module_bindings".to_string(), FieldType::Any),
("instruction_count".to_string(), FieldType::I64),
],
);
let schema_id = vmstate_schema.id;
program.type_schema_registry.register(vmstate_schema);
vm.load_program(program);
vm.push_kinded(0xDEADBEEFu64, NativeKind::Int64)
.expect("pre-restore stack push");
assert!(vm.sp > 0, "pre-restore VM must have non-empty stack");
let slots: Box<[shape_value::ValueSlot]> = Box::new([
shape_value::ValueSlot::from_raw(0), shape_value::ValueSlot::from_raw(0), shape_value::ValueSlot::from_raw(12345u64), ]);
let field_kinds: std::sync::Arc<[NativeKind]> = std::sync::Arc::from(vec![
NativeKind::Bool, NativeKind::Bool, NativeKind::Int64, ]);
let heap_mask: u64 = 0; let ptr =
TypedObjectStorage::_new(schema_id as u64, slots, heap_mask, field_kinds);
let payload_slot = shape_value::ValueSlot::from_typed_object_raw(ptr);
vm.pending_resume = Some(KindedSlot::new(
payload_slot,
NativeKind::Ptr(HeapKind::TypedObject),
));
vm.apply_pending_resume()
.expect("apply_pending_resume should succeed for VmState payload");
assert_eq!(vm.sp, 0, "post-restore stack should be empty");
assert!(
vm.call_stack.is_empty(),
"post-restore call_stack should be empty"
);
assert_eq!(vm.ip, 0, "post-restore IP should be 0");
assert!(
vm.pending_resume.is_none(),
"pending_resume should be drained after apply"
);
}
#[test]
fn apply_pending_resume_wrong_schema_surfaces_clean() {
use crate::bytecode::BytecodeProgram;
use shape_runtime::type_schema::{FieldType, TypeSchema};
use shape_value::heap_value::TypedObjectStorage;
use shape_value::HeapKind;
let mut vm = VirtualMachine::new(VMConfig::default());
let mut program = BytecodeProgram::default();
let wrong_schema = TypeSchema::new(
"FrameState", vec![("ip".to_string(), FieldType::I64)],
);
let schema_id = wrong_schema.id;
program.type_schema_registry.register(wrong_schema);
vm.load_program(program);
let slots: Box<[shape_value::ValueSlot]> =
Box::new([shape_value::ValueSlot::from_raw(42u64)]);
let field_kinds: std::sync::Arc<[NativeKind]> =
std::sync::Arc::from(vec![NativeKind::Int64]);
let ptr = TypedObjectStorage::_new(schema_id as u64, slots, 0, field_kinds);
let payload_slot = shape_value::ValueSlot::from_typed_object_raw(ptr);
vm.pending_resume = Some(KindedSlot::new(
payload_slot,
NativeKind::Ptr(HeapKind::TypedObject),
));
let err = vm.apply_pending_resume().expect_err("expected surface");
let msg = format!("{err:?}");
assert!(
msg.contains("FrameState") && msg.contains("VmState"),
"expected schema-mismatch surface naming both names, got: {msg}"
);
}
#[test]
fn apply_pending_frame_resume_no_payload_surfaces_clean() {
let mut vm = VirtualMachine::new(VMConfig::default());
let err = vm
.apply_pending_frame_resume()
.expect_err("expected surface");
let msg = format!("{err:?}");
assert!(
msg.contains("W17-snapshot-resume"),
"expected W17-snapshot-resume surface, got: {msg}"
);
}
#[test]
fn apply_pending_frame_resume_no_active_frame_surfaces_clean() {
let mut vm = VirtualMachine::new(VMConfig::default());
vm.pending_frame_resume = Some(crate::executor::FrameResumeData {
ip_offset: 0,
locals: vec![],
});
let err = vm
.apply_pending_frame_resume()
.expect_err("expected surface");
let msg = format!("{err:?}");
assert!(
msg.contains("no active call frame"),
"expected no-frame surface, got: {msg}"
);
}
#[test]
fn apply_pending_frame_resume_installs_locals_and_relocates_ip() {
use crate::bytecode::{BytecodeProgram, Function};
let mut vm = VirtualMachine::new(VMConfig::default());
let program = BytecodeProgram {
functions: vec![Function {
name: "test_resume_frame".into(),
arity: 0,
param_names: vec![],
locals_count: 3,
entry_point: 100,
body_length: 50,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: None,
osr_entry_points: vec![],
mir_data: None,
}],
..Default::default()
};
vm.load_program(program);
for _ in 0..3 {
vm.push_kinded(0u64, NativeKind::Bool)
.expect("pad stack with sentinel");
}
vm.call_stack.push(CallFrame {
return_ip: 0,
base_pointer: 0,
locals_count: 3,
function_id: Some(0),
upvalues: None,
blob_hash: None,
closure_heap_bits: None,
closure_heap_kind: None,
});
vm.pending_frame_resume = Some(crate::executor::FrameResumeData {
ip_offset: 10,
locals: vec![
KindedSlot::new(ValueSlot::from_raw(42u64), NativeKind::Int64),
KindedSlot::new(
ValueSlot::from_raw(3.14f64.to_bits()),
NativeKind::Float64,
),
KindedSlot::new(ValueSlot::from_raw(1u64), NativeKind::Bool),
],
});
vm.apply_pending_frame_resume()
.expect("apply_pending_frame_resume should succeed");
assert_eq!(vm.ip, 110, "IP should relocate to entry_point + ip_offset");
assert_eq!(vm.stack[0], 42u64, "local[0] bits");
assert_eq!(vm.kinds[0], NativeKind::Int64);
assert_eq!(vm.stack[1], 3.14f64.to_bits(), "local[1] bits");
assert_eq!(vm.kinds[1], NativeKind::Float64);
assert_eq!(vm.stack[2], 1u64, "local[2] bits");
assert_eq!(vm.kinds[2], NativeKind::Bool);
assert!(vm.pending_frame_resume.is_none());
}
#[test]
fn apply_pending_frame_resume_ip_offset_oob_surfaces_clean() {
use crate::bytecode::{BytecodeProgram, Function};
let mut vm = VirtualMachine::new(VMConfig::default());
let program = BytecodeProgram {
functions: vec![Function {
name: "test".into(),
arity: 0,
param_names: vec![],
locals_count: 0,
entry_point: 100,
body_length: 50,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: None,
osr_entry_points: vec![],
mir_data: None,
}],
..Default::default()
};
vm.load_program(program);
vm.call_stack.push(CallFrame {
return_ip: 0,
base_pointer: 0,
locals_count: 0,
function_id: Some(0),
upvalues: None,
blob_hash: None,
closure_heap_bits: None,
closure_heap_kind: None,
});
vm.pending_frame_resume = Some(crate::executor::FrameResumeData {
ip_offset: 999, locals: vec![],
});
let err = vm
.apply_pending_frame_resume()
.expect_err("expected ip_offset OOB surface");
let msg = format!("{err:?}");
assert!(
msg.contains("ip_offset")
&& (msg.contains("exceeds") || msg.contains("body_length")),
"expected ip_offset/body_length surface, got: {msg}"
);
}
#[test]
fn apply_pending_frame_resume_too_many_locals_surfaces_clean() {
use crate::bytecode::{BytecodeProgram, Function};
let mut vm = VirtualMachine::new(VMConfig::default());
let program = BytecodeProgram {
functions: vec![Function {
name: "test".into(),
arity: 0,
param_names: vec![],
locals_count: 1, entry_point: 100,
body_length: 50,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: None,
osr_entry_points: vec![],
mir_data: None,
}],
..Default::default()
};
vm.load_program(program);
vm.push_kinded(0u64, NativeKind::Bool).expect("pad");
vm.call_stack.push(CallFrame {
return_ip: 0,
base_pointer: 0,
locals_count: 1,
function_id: Some(0),
upvalues: None,
blob_hash: None,
closure_heap_bits: None,
closure_heap_kind: None,
});
vm.pending_frame_resume = Some(crate::executor::FrameResumeData {
ip_offset: 0,
locals: vec![
KindedSlot::new(ValueSlot::from_raw(1u64), NativeKind::Int64),
KindedSlot::new(ValueSlot::from_raw(2u64), NativeKind::Int64),
],
});
let err = vm
.apply_pending_frame_resume()
.expect_err("expected too-many-locals surface");
let msg = format!("{err:?}");
assert!(
msg.contains("locals count") && msg.contains("exceeds"),
"expected locals-count surface, got: {msg}"
);
}
}