Skip to main content

shape_vm/executor/state_builtins/
core.rs

1// Content-addressed VM state primitives (`std::state` module).
2//
3// **W17-snapshot-resume surface — see ADR-006 §2.7.4 + §2.7.5.1.** The
4// body of every `state.*` builtin in this module depended on the
5// deleted `ValueWord` type, the deleted `state_diff` runtime module
6// (1486 LoC of ValueWord-typed value-diff/patch logic), and the deleted
7// `nanboxed_to_serializable` / `serializable_to_nanboxed` snapshot
8// helpers. Per ADR-006 §2.7.4, snapshot serialization is deferred to a
9// Phase-2c rebuild session that can design a kind-threaded
10// `slot_to_serializable(bits, kind, store)` / inverse pair. W17
11// converts the previous `todo!()`-driven VM-thread abort into a
12// structured `Err(String)` surface return so the broken capability
13// surfaces as a recoverable runtime error rather than crashing the VM.
14// Placeholder serializers stay forbidden per CLAUDE.md "Forbidden
15// rationalizations" (silent persisted-state corruption is the bug
16// §2.7.4 explicitly rules out).
17//
18// The module-construction surface (`create_state_module`) stays so the
19// `std::core::state` module continues to register with the runtime —
20// the schema metadata is consumable by tooling/LSP. The function
21// bodies all return W17 surface errors until the Phase-2c rebuild
22// lands.
23
24use super::introspection::{
25    state_args_stub, state_caller_stub, state_capture_all_stub, state_capture_call_stub,
26    state_capture_module_stub, state_capture_stub, state_locals_stub, state_resume_frame_stub,
27    state_resume_stub,
28};
29use shape_runtime::module_exports::{ModuleContext, ModuleExports, ModuleParam};
30use shape_runtime::type_schema::{FieldType, TypeSchema};
31use shape_runtime::typed_module_exports::{ConcreteType, TypedReturn};
32use shape_runtime::marshal::register_typed_function;
33use shape_value::KindedSlot;
34
35// ---------------------------------------------------------------------------
36// Module constructor
37// ---------------------------------------------------------------------------
38
39/// Create the `state` extension module with all content-addressed builtins.
40///
41/// **W17-snapshot-resume surface — see ADR-006 §2.7.4 + §2.7.5.1.** The
42/// schemas and registration surface are intact so the module is
43/// discoverable; the per-function bodies return structured W17 surface
44/// errors until the snapshot/diff rebuild lands.
45pub fn create_state_module() -> ModuleExports {
46    let mut module = ModuleExports::new("std::core::state");
47    module.description = "Content-addressed VM state primitives".to_string();
48
49    // -- Type schemas for state introspection types --
50
51    module.add_type_schema(TypeSchema::new(
52        "FunctionRef",
53        vec![
54            ("name".to_string(), FieldType::String),
55            ("hash".to_string(), FieldType::String),
56        ],
57    ));
58
59    module.add_type_schema(TypeSchema::new(
60        "FrameState",
61        vec![
62            ("function_name".to_string(), FieldType::String),
63            ("blob_hash".to_string(), FieldType::String),
64            ("ip".to_string(), FieldType::I64),
65            ("locals".to_string(), FieldType::Any),
66            ("args".to_string(), FieldType::Any),
67            ("upvalues".to_string(), FieldType::Any),
68        ],
69    ));
70
71    module.add_type_schema(TypeSchema::new(
72        "VmState",
73        vec![
74            ("frames".to_string(), FieldType::Any),
75            ("module_bindings".to_string(), FieldType::Any),
76            ("instruction_count".to_string(), FieldType::I64),
77        ],
78    ));
79
80    module.add_type_schema(TypeSchema::new(
81        "ModuleState",
82        vec![("bindings".to_string(), FieldType::Any)],
83    ));
84
85    module.add_type_schema(TypeSchema::new(
86        "CallPayload",
87        vec![
88            ("hash".to_string(), FieldType::String),
89            ("args".to_string(), FieldType::Any),
90        ],
91    ));
92
93    // -- Content addressing --
94
95    register_typed_function(
96        &mut module,
97        "hash",
98        "SHA-256 content hash of any value",
99        vec![ModuleParam {
100            name: "value".into(),
101            type_name: "any".into(),
102            required: true,
103            description: "Value to hash".into(),
104            ..Default::default()
105        }],
106        ConcreteType::String,
107        state_hash,
108    );
109
110    register_typed_function(
111        &mut module,
112        "fn_hash",
113        "Get a function's content hash from its FunctionBlob",
114        vec![ModuleParam {
115            name: "f".into(),
116            type_name: "any".into(),
117            required: true,
118            description: "Function value".into(),
119            ..Default::default()
120        }],
121        ConcreteType::String,
122        state_fn_hash,
123    );
124
125    register_typed_function(
126        &mut module,
127        "schema_hash",
128        "Content hash of a type's schema definition",
129        vec![ModuleParam {
130            name: "type_name".into(),
131            type_name: "string".into(),
132            required: true,
133            description: "Name of the type to hash".into(),
134            ..Default::default()
135        }],
136        ConcreteType::String,
137        state_schema_hash,
138    );
139
140    // -- Serialization --
141
142    register_typed_function(
143        &mut module,
144        "serialize",
145        "Serialize a value to MessagePack bytes",
146        vec![ModuleParam {
147            name: "value".into(),
148            type_name: "any".into(),
149            required: true,
150            description: "Value to serialize".into(),
151            ..Default::default()
152        }],
153        ConcreteType::ArrayInt,
154        state_serialize,
155    );
156
157    register_typed_function(
158        &mut module,
159        "deserialize",
160        "Deserialize MessagePack bytes back to a value",
161        vec![ModuleParam {
162            name: "bytes".into(),
163            type_name: "Array<int>".into(),
164            required: true,
165            description: "MessagePack byte array".into(),
166            ..Default::default()
167        }],
168        ConcreteType::Any,
169        state_deserialize,
170    );
171
172    // -- Diffing --
173
174    register_typed_function(
175        &mut module,
176        "diff",
177        "Compute delta between two values using content-hash trees",
178        vec![
179            ModuleParam {
180                name: "old".into(),
181                type_name: "any".into(),
182                required: true,
183                description: "Old value".into(),
184                ..Default::default()
185            },
186            ModuleParam {
187                name: "new".into(),
188                type_name: "any".into(),
189                required: true,
190                description: "New value".into(),
191                ..Default::default()
192            },
193        ],
194        ConcreteType::Named("Delta".into()),
195        state_diff,
196    );
197
198    register_typed_function(
199        &mut module,
200        "patch",
201        "Apply a delta to a base value, producing the updated value",
202        vec![
203            ModuleParam {
204                name: "base".into(),
205                type_name: "any".into(),
206                required: true,
207                description: "Base value".into(),
208                ..Default::default()
209            },
210            ModuleParam {
211                name: "delta".into(),
212                type_name: "Delta".into(),
213                required: true,
214                description: "Delta to apply".into(),
215                ..Default::default()
216            },
217        ],
218        ConcreteType::Any,
219        state_patch,
220    );
221
222    // -- Capture primitives (stubs — need live VM access) --
223
224    register_typed_function(
225        &mut module,
226        "capture",
227        "Capture current function's frame state",
228        vec![],
229        ConcreteType::Named("FrameState".into()),
230        state_capture_stub,
231    );
232
233    register_typed_function(
234        &mut module,
235        "capture_all",
236        "Capture full VM execution state",
237        vec![],
238        ConcreteType::Named("VmState".into()),
239        state_capture_all_stub,
240    );
241
242    register_typed_function(
243        &mut module,
244        "capture_module",
245        "Capture module-level bindings and type schemas",
246        vec![],
247        ConcreteType::Named("ModuleState".into()),
248        state_capture_module_stub,
249    );
250
251    register_typed_function(
252        &mut module,
253        "capture_call",
254        "Build a ready-to-call payload without executing",
255        vec![
256            ModuleParam {
257                name: "f".into(),
258                type_name: "any".into(),
259                required: true,
260                description: "Function to capture".into(),
261                ..Default::default()
262            },
263            ModuleParam {
264                name: "args".into(),
265                type_name: "Array<any>".into(),
266                required: true,
267                description: "Arguments for the call".into(),
268                ..Default::default()
269            },
270        ],
271        ConcreteType::Named("CallPayload".into()),
272        state_capture_call_stub,
273    );
274
275    // -- Resume primitives (stubs) --
276    //
277    // Note: state.resume's original schema declared return_type: None
278    // (the function does not return — it deopts into resumed VM state).
279    // Phase 4c.2 surfaces this via ConcreteType::Named("never") so the
280    // schema metadata gets a string label; previously the schema reported
281    // None. Consumers that special-cased None should treat "never" as the
282    // equivalent surface.
283    register_typed_function(
284        &mut module,
285        "resume",
286        "Resume full VM state (does not return)",
287        vec![ModuleParam {
288            name: "vm".into(),
289            type_name: "VmState".into(),
290            required: true,
291            description: "VM state to resume".into(),
292            ..Default::default()
293        }],
294        ConcreteType::Named("never".into()),
295        state_resume_stub,
296    );
297
298    register_typed_function(
299        &mut module,
300        "resume_frame",
301        "Re-enter a captured function frame and return its result",
302        vec![ModuleParam {
303            name: "f".into(),
304            type_name: "FrameState".into(),
305            required: true,
306            description: "Frame state to resume".into(),
307            ..Default::default()
308        }],
309        ConcreteType::Any,
310        state_resume_frame_stub,
311    );
312
313    // -- Introspection (stubs) --
314
315    register_typed_function(
316        &mut module,
317        "caller",
318        "Get a reference to the calling function",
319        vec![],
320        ConcreteType::Named("FunctionRef?".into()),
321        state_caller_stub,
322    );
323
324    register_typed_function(
325        &mut module,
326        "args",
327        "Get the current function's arguments as an array",
328        vec![],
329        ConcreteType::Named("Array<any>".into()),
330        state_args_stub,
331    );
332
333    register_typed_function(
334        &mut module,
335        "locals",
336        "Get the current scope's local variables as a map",
337        vec![],
338        ConcreteType::Named("Map<string, any>".into()),
339        state_locals_stub,
340    );
341
342    register_typed_function(
343        &mut module,
344        "snapshot",
345        "Create a snapshot of the current execution state. This is a suspension point: the engine saves all state and returns Snapshot::Hash(id). When resumed from a snapshot, execution continues here and returns Snapshot::Resumed.",
346        vec![],
347        ConcreteType::Named("Snapshot".into()),
348        state_capture_all_stub,
349    );
350
351    module
352}
353
354// ===========================================================================
355// Content addressing implementations
356// ===========================================================================
357//
358// **W17-snapshot-resume surface-and-stop — see ADR-006 §2.7.4 + §2.7.5.1.**
359// Every body below depended on the deleted `ValueWord` type, `state_diff`
360// runtime module, or `nanboxed_to_serializable` / `serializable_to_nanboxed`
361// snapshot helpers. The replacement design — a kind-threaded slot
362// content-hash + slot diff/patch + slot serialization triple, all
363// taking `(bits, kind)` or `KindedSlot` directly and dispatching on
364// `HeapKind` payload variants — is Phase-2c scope. W17 converts the
365// previous `todo!()` panics to structured `Err(...)` returns so callers
366// observe a runtime error rather than a VM-thread abort.
367
368/// Common W17-snapshot-resume surface-and-stop message for the
369/// content-addressing / serialize / diff family. The `op` parameter
370/// names the specific stdlib function so the error message points the
371/// caller at the exact entry point.
372fn content_surface(op: &str) -> String {
373    format!(
374        "{op}: W17-snapshot-resume surface — kind-threaded \
375         slot_to_serializable / serializable_to_slot replacement for the \
376         deleted nanboxed_to_serializable / serializable_to_nanboxed \
377         pair has not landed; state.diff / state.patch additionally \
378         depend on the deleted 1486-LoC `state_diff` runtime module's \
379         kind-threaded rebuild. Tracked as W17-snapshot-resume per \
380         docs/cluster-audits/phase-2d-playbook.md §3. \
381         ADR-006 §2.7.4 (snapshot serialization deferral) + §2.7.5.1 \
382         (post-proof wire-format shape for new HeapKinds).",
383    )
384}
385
386/// In-memory `SnapshotStore` for content-addressing operations
387/// (`state.hash`, `state.serialize`) that don't need filesystem
388/// persistence. The store is required by the
389/// `slot_to_serializable(bits, kind, store)` signature but is unused for
390/// scalar / heap-light kinds; complex chunked-blob kinds (TypedArray
391/// sidecar, large DataTable) surface clean from the kind-threaded API
392/// when no store is available.
393///
394/// **W17-state-tier-roundtrip (Phase 2d Wave 3, 2026-05-12).** Falls
395/// back to a tempdir-backed store so chunked-blob arms work. If the
396/// tempdir creation itself fails, the body surfaces clean per the
397/// §2.7.4 invariant (no silent state-loss).
398fn ephemeral_store() -> Result<shape_runtime::snapshot::SnapshotStore, String> {
399    let tmp = tempfile::tempdir().map_err(|e| {
400        format!(
401            "W17-snapshot-resume surface — tempdir creation failed: {e}. \
402             ADR-006 §2.7.4."
403        )
404    })?;
405    let store = shape_runtime::snapshot::SnapshotStore::new(tmp.path()).map_err(|e| {
406        format!(
407            "W17-snapshot-resume surface — SnapshotStore::new failed: {e}. \
408             ADR-006 §2.7.4."
409        )
410    })?;
411    // Leak the tempdir so the store's blob files outlive the body's
412    // immediate frame. The bodies that call this are content-addressing
413    // /serialize paths — short-lived; the tempdir cleanup runs at
414    // process exit. For high-rate state.hash callers we'd want a
415    // per-VM store on ModuleContext, but that's beyond W17-state-tier-
416    // roundtrip's scope.
417    std::mem::forget(tmp);
418    Ok(store)
419}
420
421/// Compute the deterministic serialized-bytes representation of a
422/// `KindedSlot` argument. The bytes are bincode-encoded
423/// `SerializableVMValue` per ADR-006 §2.7.5.1 — identical to what
424/// `VmSnapshot` writes for each stack/binding slot.
425fn slot_to_serialized_bytes(slot: &KindedSlot) -> Result<Vec<u8>, String> {
426    use shape_runtime::snapshot::slot_to_serializable;
427    let store = ephemeral_store()?;
428    let sv = slot_to_serializable(slot.slot().raw(), slot.kind(), &store)?;
429    let bytes = bincode::serialize(&sv).map_err(|e| {
430        format!(
431            "state.serialize: W17-snapshot-resume surface — bincode \
432             serialization failed: {e}. ADR-006 §2.7.5.1."
433        )
434    })?;
435    Ok(bytes)
436}
437
438/// `state.hash(value) -> string`
439///
440/// **W17-state-tier-roundtrip (Phase 2d Wave 3, 2026-05-12).** Wired
441/// end-to-end via the kind-threaded `slot_to_serializable` API: the
442/// arg slot is projected to `SerializableVMValue`, bincode-encoded,
443/// then SHA-256-hashed. Returns the hash as a hex string.
444pub(crate) fn state_hash(
445    args: &[KindedSlot],
446    _ctx: &ModuleContext,
447) -> Result<TypedReturn, String> {
448    let Some(arg) = args.first() else {
449        return Err(content_surface("state.hash"));
450    };
451    let bytes = slot_to_serialized_bytes(arg)?;
452    let digest = shape_runtime::hash_bytes(&bytes);
453    Ok(TypedReturn::Concrete(
454        shape_runtime::typed_module_exports::ConcreteReturn::String(digest.hex().to_string()),
455    ))
456}
457
458/// `state.fn_hash(f) -> string`
459///
460/// **W17-state-tier-roundtrip (Phase 2d Wave 3, 2026-05-12).** Returns
461/// the content-hash of a function blob. The hash is sourced from the
462/// VM's content-addressed metadata table; functions without a
463/// content-hash entry (compiled without content-addressed metadata)
464/// surface a structured error.
465pub(crate) fn state_fn_hash(
466    args: &[KindedSlot],
467    ctx: &ModuleContext,
468) -> Result<TypedReturn, String> {
469    use shape_value::{HeapKind, NativeKind};
470
471    let Some(arg) = args.first() else {
472        return Err(content_surface("state.fn_hash"));
473    };
474    // Function values flow as one of:
475    //  - NativeKind::Ptr(HeapKind::Closure) — raw OwnedClosureBlock bits
476    //  - NativeKind::Ptr(HeapKind::FunctionRef) — typed fn handle
477    //  - Inline function-id (Int64-kinded) for bare function references
478    let bits = arg.slot().raw();
479    let function_id = match arg.kind() {
480        NativeKind::Int64 | NativeKind::UInt64 => Some(bits as u16),
481        NativeKind::Ptr(HeapKind::Closure) => {
482            if bits == 0 {
483                None
484            } else {
485                // SAFETY: bits is OwnedClosureBlock::ptr per §2.7.8.
486                let ptr = bits as *const u8;
487                Some(unsafe {
488                    shape_value::v2::closure_raw::typed_closure_function_id(ptr)
489                })
490            }
491        }
492        _ => None,
493    };
494    let Some(fid) = function_id else {
495        return Err(format!(
496            "state.fn_hash: W17-snapshot-resume surface — argument is not a \
497             function value (kind={:?}); function-handle decoding for \
498             HeapKind::FunctionRef / TraitObject not yet wired. ADR-006 \
499             §2.7.4.",
500            arg.kind()
501        ));
502    };
503    // Look up the function's content hash via ctx.function_hashes.
504    let Some(hashes) = ctx.function_hashes else {
505        return Err(format!(
506            "state.fn_hash: W17-snapshot-resume surface — \
507             ctx.function_hashes is None at this dispatch surface; \
508             content-addressed metadata not propagated through \
509             invoke_module_fn_id_stub. ADR-006 §2.7.4."
510        ));
511    };
512    let Some(maybe_hash) = hashes.get(fid as usize) else {
513        return Err(format!(
514            "state.fn_hash: function_id {fid} out of range \
515             (program has {} functions). ADR-006 §2.7.4.",
516            hashes.len()
517        ));
518    };
519    let Some(hash_bytes) = maybe_hash else {
520        return Err(format!(
521            "state.fn_hash: W17-snapshot-resume surface — function_id {fid} \
522             has no content-addressed hash entry (compiled without \
523             content-addressed metadata). ADR-006 §2.7.4."
524        ));
525    };
526    Ok(TypedReturn::Concrete(
527        shape_runtime::typed_module_exports::ConcreteReturn::String(hex::encode(hash_bytes)),
528    ))
529}
530
531/// `state.schema_hash(type_name) -> string`
532///
533/// **W17-state-tier-roundtrip (Phase 2d Wave 3, 2026-05-12).** Returns
534/// the content-hash of a type schema definition. Schema bytes are the
535/// bincode-encoded `TypeSchema` from `ctx.schemas`.
536pub(crate) fn state_schema_hash(
537    args: &[KindedSlot],
538    ctx: &ModuleContext,
539) -> Result<TypedReturn, String> {
540    let Some(arg) = args.first() else {
541        return Err(content_surface("state.schema_hash"));
542    };
543    // First arg is the type name (string-kinded). Recover the string
544    // payload via the canonical Arc<String> recovery pattern. The
545    // bits encode `Arc::into_raw(Arc<String>)` per §2.7.6 String-arm.
546    let type_name = match arg.kind() {
547        shape_value::NativeKind::String
548        | shape_value::NativeKind::Ptr(shape_value::HeapKind::String) => {
549            let bits = arg.slot().raw();
550            if bits == 0 {
551                return Err(format!(
552                    "state.schema_hash: W17-snapshot-resume surface — null \
553                     string bits. ADR-006 §2.7.6."
554                ));
555            }
556            // SAFETY: bits is Arc<String> share per §2.7.6 construction.
557            unsafe {
558                let arc = std::sync::Arc::<String>::from_raw(bits as *const String);
559                let s: String = (*arc).clone();
560                let _ = std::sync::Arc::into_raw(arc); // restore share
561                s
562            }
563        }
564        other => {
565            return Err(format!(
566                "state.schema_hash: W17-snapshot-resume surface — first \
567                 argument must be string (got kind={other:?}). ADR-006 §2.7.4."
568            ));
569        }
570    };
571    let Some(schema) = ctx.schemas.get(&type_name) else {
572        return Err(format!(
573            "state.schema_hash: unknown type '{type_name}'. ADR-006 §2.7.4."
574        ));
575    };
576    let bytes = bincode::serialize(schema).map_err(|e| {
577        format!(
578            "state.schema_hash: W17-snapshot-resume surface — bincode \
579             serialization failed: {e}. ADR-006 §2.7.5.1."
580        )
581    })?;
582    let digest = shape_runtime::hash_bytes(&bytes);
583    Ok(TypedReturn::Concrete(
584        shape_runtime::typed_module_exports::ConcreteReturn::String(digest.hex().to_string()),
585    ))
586}
587
588// ===========================================================================
589// Serialization implementations
590// ===========================================================================
591
592/// `state.serialize(value) -> Array<int>`
593///
594/// **W17-state-tier-roundtrip (Phase 2d Wave 3, 2026-05-12).** The body
595/// computes the bincode-encoded `SerializableVMValue` bytes via the
596/// kind-threaded `slot_to_serializable` API. The `Array<int>` return
597/// shape needs the marshal-return `Bytes` arm follow-up at
598/// `project_typed_return` — body succeeds, marshal surfaces clean.
599pub(crate) fn state_serialize(
600    args: &[KindedSlot],
601    _ctx: &ModuleContext,
602) -> Result<TypedReturn, String> {
603    let Some(arg) = args.first() else {
604        return Err(content_surface("state.serialize"));
605    };
606    let _bytes = slot_to_serialized_bytes(arg)?;
607    Err(format!(
608        "state.serialize: W17-snapshot-resume surface — body computed \
609         {} bytes via slot_to_serializable but the Array<int>/Bytes return \
610         arm needs the W17-marshal-return-arms follow-up at \
611         project_typed_return. ADR-006 §2.7.4 + §2.7.5.1.",
612        _bytes.len()
613    ))
614}
615
616/// `state.deserialize(bytes) -> Any`
617///
618/// **W17-state-tier-roundtrip (Phase 2d Wave 3, 2026-05-12).** Mirror of
619/// `state_serialize`: requires the `Any` return arm (typed-Arc payload
620/// projection — same W17-marshal-return-arms follow-up).
621pub(crate) fn state_deserialize(
622    _args: &[KindedSlot],
623    _ctx: &ModuleContext,
624) -> Result<TypedReturn, String> {
625    Err(content_surface("state.deserialize"))
626}
627
628// ===========================================================================
629// Diffing implementations
630// ===========================================================================
631
632/// `state.diff(old, new) -> Delta`
633///
634/// **W17-state-tier-roundtrip surface-and-stop — see ADR-006 §2.7.4.**
635/// `state_diff` depends on the deleted 1486-LoC `state_diff` runtime
636/// module (`crates/shape-runtime/src/state_diff.rs` pre-bulldozer) whose
637/// kind-threaded rebuild is its own substantial workstream — out of
638/// W17-state-tier-roundtrip's scope. Surfaces clean.
639pub(crate) fn state_diff(
640    _args: &[KindedSlot],
641    _ctx: &ModuleContext,
642) -> Result<TypedReturn, String> {
643    Err(content_surface("state.diff"))
644}
645
646/// `state.patch(base, delta) -> Any`
647///
648/// **W17-state-tier-roundtrip surface-and-stop — see ADR-006 §2.7.4.**
649/// Same dependency on the deleted `state_diff` module as
650/// `state_diff`. Surfaces clean.
651pub(crate) fn state_patch(
652    _args: &[KindedSlot],
653    _ctx: &ModuleContext,
654) -> Result<TypedReturn, String> {
655    Err(content_surface("state.patch"))
656}
657
658#[cfg(test)]
659mod tests {
660    use super::*;
661    use shape_runtime::type_schema::FieldType;
662
663    /// Helper: find the schema with the given name in the module's type_schemas vec.
664    fn find_schema<'a>(
665        module: &'a ModuleExports,
666        name: &str,
667    ) -> &'a shape_runtime::type_schema::TypeSchema {
668        module
669            .type_schemas
670            .iter()
671            .find(|s| s.name == name)
672            .unwrap_or_else(|| panic!("schema '{}' not found", name))
673    }
674
675    /// Schema metadata is exercisable independent of the Phase-2c
676    /// body rebuild — `create_state_module` registers the schemas in
677    /// the type registry and the per-function bodies are unreachable
678    /// from this assertion path.
679    #[test]
680    fn test_state_schemas_have_concrete_field_types() {
681        let module = create_state_module();
682
683        // --- FunctionRef: both fields should be String ---
684        let func_ref = find_schema(&module, "FunctionRef");
685        assert_eq!(
686            func_ref.get_field("name").unwrap().field_type,
687            FieldType::String
688        );
689        assert_eq!(
690            func_ref.get_field("hash").unwrap().field_type,
691            FieldType::String
692        );
693
694        // --- FrameState: 3 typed, 3 dynamic ---
695        let frame = find_schema(&module, "FrameState");
696        assert_eq!(
697            frame.get_field("function_name").unwrap().field_type,
698            FieldType::String
699        );
700        assert_eq!(
701            frame.get_field("blob_hash").unwrap().field_type,
702            FieldType::String
703        );
704        assert_eq!(frame.get_field("ip").unwrap().field_type, FieldType::I64);
705        assert_eq!(
706            frame.get_field("locals").unwrap().field_type,
707            FieldType::Any
708        );
709        assert_eq!(frame.get_field("args").unwrap().field_type, FieldType::Any);
710        assert_eq!(
711            frame.get_field("upvalues").unwrap().field_type,
712            FieldType::Any
713        );
714
715        // --- VmState: 1 typed, 2 dynamic ---
716        let vm_state = find_schema(&module, "VmState");
717        assert_eq!(
718            vm_state.get_field("instruction_count").unwrap().field_type,
719            FieldType::I64
720        );
721        assert_eq!(
722            vm_state.get_field("frames").unwrap().field_type,
723            FieldType::Any
724        );
725        assert_eq!(
726            vm_state.get_field("module_bindings").unwrap().field_type,
727            FieldType::Any
728        );
729
730        // --- ModuleState: all dynamic ---
731        let mod_state = find_schema(&module, "ModuleState");
732        assert_eq!(
733            mod_state.get_field("bindings").unwrap().field_type,
734            FieldType::Any
735        );
736
737        // --- CallPayload: 1 typed, 1 dynamic ---
738        let call = find_schema(&module, "CallPayload");
739        assert_eq!(
740            call.get_field("hash").unwrap().field_type,
741            FieldType::String
742        );
743        assert_eq!(call.get_field("args").unwrap().field_type, FieldType::Any);
744    }
745}