This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new fc0b2faa [FEAT][Rust] Add context policies for structural map and 
mutation (#800)
fc0b2faa is described below

commit fc0b2faad1c998f1dcdd2ae03fac5855bb28c50d
Author: Shushi Hong <[email protected]>
AuthorDate: Fri Sep 18 15:28:52 2026 -0400

    [FEAT][Rust] Add context policies for structural map and mutation (#800)
    
    This pr adds `MutationPolicy<State>` to customize default recursion with
    shared callback state. `MutateCallbacks::with_policy` applies policies
    when a callback requests default descent or no callback matches;
    `MapWithPolicy` applies them between map callback positions. Policies
    compose as `(outer, inner)` tuples and consume `MutateValue` while
    preserving `UnchangedOr` results.
    
    Continue through the next policy without redispatching the target, while
    children re-enter the full callback engine. Scoped definition-region
    changes preserve Pattern and restore the ABI context on exit. Keep
    in-place permission and ownership checks across continuation, including
    retained aliases and explicit Disallow.
---
 docs/guides/rust_lang_guide.md                     |   9 +-
 rust/tvm-ffi/src/extra/structural_mutate.rs        | 195 +++++++--
 rust/tvm-ffi/src/extra/structural_mutate/policy.rs | 280 +++++++++++++
 .../extra/structural_mutate/policy/compile_fail.rs |  50 +++
 rust/tvm-ffi/src/extra/structural_visit.rs         |   7 +-
 rust/tvm-ffi/src/extra/structural_visit/policy.rs  |  10 +-
 rust/tvm-ffi/src/lib.rs                            |  13 +-
 rust/tvm-ffi/tests/test_structural_mutate.rs       | 452 ++++++++++++++++++++-
 rust/tvm-ffi/tests/test_structural_visit.rs        |  17 +-
 9 files changed, 976 insertions(+), 57 deletions(-)

diff --git a/docs/guides/rust_lang_guide.md b/docs/guides/rust_lang_guide.md
index 6da519a3..66d64daa 100644
--- a/docs/guides/rust_lang_guide.md
+++ b/docs/guides/rust_lang_guide.md
@@ -366,7 +366,7 @@ Callbacks are `Fn`; mutable data belongs in the visitor 
state. A catch-all
 callback must call `visit_children()` explicitly, and interrupt values must be
 returned explicitly because `?` only propagates errors.
 
-`VisitCallbacks::with_policy` and `WalkWithPolicy` use `ContextPolicy<State>`
+`VisitCallbacks::with_policy` and `WalkWithContextPolicy` use 
`ContextPolicy<State>`
 to manage context around default recursion. See the `ContextPolicy` API
 documentation for composition and shared state access.
 
@@ -488,6 +488,13 @@ Within one `structural_map` call, callbacks run at every 
occurrence; their
 results are not cached. Default recursion manages identity remapping with
 the same semantics as C++.
 
+`MutateCallbacks::with_policy` and `MapWithContextPolicy` use 
`MutContextPolicy<State>`
+to manage context around default recursion. Policies consume `MutateValue`
+and return `UnchangedOr<Any>`; see the API documentation for composition and
+scoped definition regions. Pass `MapWithContextPolicy` directly to 
`structural_map`;
+it cannot be a callback tuple member or another wrapper's dispatcher. Compose
+policies as `(outer, inner)` within one wrapper.
+
 Default recursion uses the same type attributes as C++: it calls
 `__s_maybe_inplace_mutate__` for a uniquely owned object when available, or
 falls back to `__s_mutate__`. Each hook receives the active Rust-backed
diff --git a/rust/tvm-ffi/src/extra/structural_mutate.rs 
b/rust/tvm-ffi/src/extra/structural_mutate.rs
index 80998f0a..04b2cfd5 100644
--- a/rust/tvm-ffi/src/extra/structural_mutate.rs
+++ b/rust/tvm-ffi/src/extra/structural_mutate.rs
@@ -71,6 +71,9 @@ pub use super::StructuralView;
 /// Compatibility name for [`StructuralView`].
 pub use super::StructuralView as MapValue;
 
+mod policy;
+pub use policy::{DefaultMutContextPolicy, MapWithContextPolicy, 
MutContextPolicy};
+
 /// Result type produced by a structural-map callback.
 #[doc(hidden)]
 pub type MapResult = Result<Any>;
@@ -559,6 +562,24 @@ where
         self.def_region_kind
     }
 
+    /// Run recursive operations in a definition region, preserving an outer 
Pattern.
+    /// The previous context is restored on return, error, or unwinding.
+    pub fn with_def_region_kind<T>(
+        &mut self,
+        kind: DefRegionKind,
+        callback: impl FnOnce(&mut MutateContext<'_, State, Driver>) -> 
Result<T>,
+    ) -> Result<T> {
+        with_mutation_region(kind, |kind| {
+            callback(&mut MutateContext {
+                driver: &mut *self.driver,
+                def_region_kind: kind,
+                inplace_mode: self.inplace_mode,
+                _state: PhantomData,
+                _not_send_sync: PhantomData,
+            })
+        })
+    }
+
     /// Mutate a borrowed value through the same callback chain. The value and
     /// its descendants begin on the non-in-place path.
     #[inline(always)]
@@ -1019,7 +1040,8 @@ macro_rules! impl_mutate_chain_link {
 impl_callback_chain_tuple_arities!(impl_mutate_chain_link);
 
 /// A reusable typed-dispatch or callback mutator with shared user state.
-pub struct MutateCallbacks<State, Link, Marker> {
+pub struct MutateCallbacks<State, Link, Marker, Policy = 
DefaultMutContextPolicy> {
+    policy: Option<Rc<Policy>>,
     state: State,
     callbacks: Rc<Link>,
     _marker: PhantomData<fn(Marker)>,
@@ -1034,12 +1056,26 @@ where
         Self {
             state,
             callbacks: Rc::new(callbacks),
+            policy: None,
             _marker: PhantomData,
         }
     }
 }
 
-impl<State, Link, Marker> MutateCallbacks<State, Link, Marker> {
+impl<State, Link, Marker, Policy> MutateCallbacks<State, Link, Marker, Policy> 
{
+    /// Customize default descent while sharing the callback state.
+    pub fn with_policy<P: MutContextPolicy<State>>(
+        self,
+        policy: P,
+    ) -> MutateCallbacks<State, Link, Marker, P> {
+        MutateCallbacks {
+            policy: Some(Rc::new(policy)),
+            state: self.state,
+            callbacks: self.callbacks,
+            _marker: PhantomData,
+        }
+    }
+
     /// Shared access to the callback state.
     pub fn state(&self) -> &State {
         &self.state
@@ -1067,7 +1103,9 @@ trait MutateCallbackState<State> {
     fn callback_state_mut(&mut self) -> &mut State;
 }
 
-impl<State, Link, Marker> MutateCallbackState<State> for 
MutateCallbacks<State, Link, Marker> {
+impl<State, Link, Marker, Policy> MutateCallbackState<State>
+    for MutateCallbacks<State, Link, Marker, Policy>
+{
     fn callback_state(&self) -> &State {
         &self.state
     }
@@ -1119,6 +1157,26 @@ pub trait MapDispatch: Sized {
     ) -> Option<MapResult>;
 }
 
+/// Internal root-mapping protocol used by [`IntoMapper`].
+#[doc(hidden)]
+pub trait NativeMap: Sized {
+    fn map_root(&mut self, root: Any, order: WalkOrder) -> Result<Any>;
+}
+
+impl<D: MapDispatch> NativeMap for D {
+    fn map_root(&mut self, root: Any, order: WalkOrder) -> Result<Any> {
+        run_structural_mutator(
+            root,
+            &mut NativeMapper::<_, DefaultMutContextPolicy> {
+                dispatch: self,
+                order,
+                policy: None,
+                remap: StructuralVarRemap::default(),
+            },
+        )
+    }
+}
+
 impl<V: MapDispatch> MapDispatch for &mut V {
     #[inline]
     fn dispatch_map(
@@ -1134,10 +1192,10 @@ impl<V: MapDispatch> MapDispatch for &mut V {
 #[diagnostic::on_unimplemented(
     message = "unsupported structural-map callback shape",
     label = "this value cannot be used as a structural mapper",
-    note = "pass `&mut` a type implementing `MapDispatch`, a supported 
closure, or a tuple of callbacks"
+    note = "pass `&mut` a `MapDispatch`, a supported closure or callback 
tuple, or a `MapWithContextPolicy`"
 )]
 pub trait IntoMapper<Marker> {
-    type Mapper: MapDispatch;
+    type Mapper: NativeMap;
     fn into_mapper(self) -> Self::Mapper;
 }
 
@@ -1582,6 +1640,20 @@ pub trait StructuralMutator: Sized {
         self.dispatch_mutate(value.as_value(), def_region_kind)
     }
 
+    #[doc(hidden)]
+    fn dispatch_default_mutate(
+        &mut self,
+        value: MutateValue<'_>,
+        kind: DefRegionKind,
+    ) -> Result<Any> {
+        default_mutate_driver(
+            self,
+            value.value.raw(),
+            kind,
+            value.permit(value.inplace_mode()),
+        )
+    }
+
     /// Re-enter this mutator for a borrowed value. The value and all of its
     /// descendants use the non-in-place path.
     fn mutate<T>(&mut self, value: &T, def_region_kind: DefRegionKind) -> 
Result<Any>
@@ -1871,8 +1943,9 @@ where
     )
 }
 
-impl<State, Link, Marker> StructuralMutator for MutateCallbacks<State, Link, 
Marker>
+impl<State, Link, Marker, Policy> StructuralMutator for MutateCallbacks<State, 
Link, Marker, Policy>
 where
+    Policy: MutContextPolicy<State>,
     Link: MutateChainLink<State, Marker>,
     Link::Strategy: MutateCallbackStrategy<State, Link, Marker>,
 {
@@ -1915,6 +1988,27 @@ where
                 .map(Any::from),
         }
     }
+
+    fn dispatch_default_mutate(
+        &mut self,
+        value: MutateValue<'_>,
+        kind: DefRegionKind,
+    ) -> Result<Any> {
+        match self.policy.clone() {
+            Some(policy) => policy::mutate_with_policy(
+                &mut policy::MutationDescent { driver: self },
+                policy.as_ref(),
+                value,
+                kind,
+            ),
+            None => default_mutate_driver(
+                self,
+                value.value.raw(),
+                kind,
+                value.permit(value.inplace_mode()),
+            ),
+        }
+    }
 }
 
 impl<Link, Marker> StructuralMutator for DirectMutateCallbacks<'_, Link, 
Marker>
@@ -1989,7 +2083,7 @@ where
 
     #[inline(always)]
     fn default_mutate_borrowed(&mut self, value: AnyView<'_>, kind: 
DefRegionKind) -> Result<Any> {
-        default_mutate_driver(self, *value.as_raw_ffi_any(), kind, 
Permit::Copy)
+        user_default_mutate(self, *value.as_raw_ffi_any(), kind, Permit::Copy)
     }
 
     fn default_mutate_value(
@@ -1999,7 +2093,7 @@ where
         mode: InplaceMode,
     ) -> Result<Any> {
         let permit = value.permit(mode);
-        default_mutate_driver(self, value.value.raw(), kind, permit)
+        user_default_mutate(self, value.value.raw(), kind, permit)
     }
 
     #[inline(always)]
@@ -2019,6 +2113,16 @@ enum Permit {
     MaybeInPlace,
 }
 
+impl Permit {
+    fn inplace_mode(self, raw: TVMFFIAny) -> InplaceMode {
+        if self == Self::MaybeInPlace && object_is_unique(raw) {
+            InplaceMode::Allow
+        } else {
+            InplaceMode::Disallow
+        }
+    }
+}
+
 struct MemoEntry {
     // Keeps the pointer-valued key alive so its address cannot be reused
     // during the same mapping invocation.
@@ -2026,13 +2130,14 @@ struct MemoEntry {
     result: Any,
 }
 
-struct NativeMapper<D> {
-    dispatch: D,
+struct NativeMapper<'a, D, Policy> {
+    dispatch: &'a mut D,
+    policy: Option<Rc<Policy>>,
     order: WalkOrder,
     remap: StructuralVarRemap,
 }
 
-impl<D: MapDispatch> NativeMapper<D> {
+impl<D: MapDispatch, Policy: MutContextPolicy<D>> NativeMapper<'_, D, Policy> {
     fn map_raw(
         &mut self,
         raw: TVMFFIAny,
@@ -2045,7 +2150,7 @@ impl<D: MapDispatch> NativeMapper<D> {
         // Raw strings, byte-array views, and ObjectRValueRef are deliberately
         // excluded because converting those borrowed special values into an
         // Any performs normalization rather than a bitwise copy.
-        if is_plain_inline(raw.type_index) {
+        if self.policy.is_none() && is_plain_inline(raw.type_index) {
             let value = StructuralView::from_raw(raw);
             return match self.dispatch.dispatch_map(&value, def_region_kind) {
                 Some(result) => {
@@ -2661,14 +2766,34 @@ fn def_region_from_raw(kind: i32) -> 
Result<DefRegionKind> {
     }
 }
 
-impl<D: MapDispatch> MutationDriver for NativeMapper<D> {
+impl<D: MapDispatch, Policy: MutContextPolicy<D>> MutationDriver for 
NativeMapper<'_, D, Policy> {
     fn dispatch_raw(
         &mut self,
         raw: TVMFFIAny,
         def_region_kind: DefRegionKind,
         permit: Permit,
     ) -> Result<Any> {
-        self.map_raw(raw, def_region_kind, permit)
+        with_mutation_region(def_region_kind, |kind| self.map_raw(raw, kind, 
permit))
+    }
+
+    fn default_map_current_raw(
+        &mut self,
+        raw: TVMFFIAny,
+        kind: DefRegionKind,
+        permit: Permit,
+    ) -> Result<Any> {
+        match self.policy.clone() {
+            Some(policy) => {
+                let view = StructuralView::from_raw(raw);
+                policy::mutate_with_policy(
+                    &mut policy::MutationDescent { driver: self },
+                    policy.as_ref(),
+                    MutateValue::new(&view, permit.inplace_mode(raw)),
+                    kind,
+                )
+            }
+            None => default_mutate_driver(self, raw, kind, permit),
+        }
     }
 
     fn var_remap_get_raw(&mut self, raw: TVMFFIAny) -> Result<Option<Any>> {
@@ -2955,6 +3080,18 @@ fn with_mutator_def_region<T>(
     }
 }
 
+fn with_mutation_region<T>(
+    kind: DefRegionKind,
+    callback: impl FnOnce(DefRegionKind) -> Result<T>,
+) -> Result<T> {
+    let mutator = active_mutator()?;
+    with_mutator_def_region(mutator, kind, || {
+        // SAFETY: the active invocation keeps this thread's ABI mutator alive.
+        let effective = def_region_from_raw(unsafe { 
(*mutator).def_region_mode })?;
+        callback(effective)
+    })
+}
+
 #[inline(always)]
 fn dispatch_user_raw<U: StructuralMutator>(
     mutator: &mut U,
@@ -2962,14 +3099,15 @@ fn dispatch_user_raw<U: StructuralMutator>(
     def_region_kind: DefRegionKind,
     permit: Permit,
 ) -> Result<Any> {
-    let result = if permit == Permit::MaybeInPlace && object_is_unique(raw) {
-        let mut scoped_raw = raw;
-        mutator
-            .dispatch_maybe_inplace_mutate(InplaceValue::from_raw(&mut 
scoped_raw), def_region_kind)
-    } else {
-        mutator.dispatch_mutate(&StructuralView::from_raw(raw), 
def_region_kind)
-    };
-    result.map_err(|error| with_value_context(error, raw))
+    with_mutation_region(def_region_kind, |kind| {
+        let result = if permit == Permit::MaybeInPlace && 
object_is_unique(raw) {
+            let mut scoped_raw = raw;
+            mutator.dispatch_maybe_inplace_mutate(InplaceValue::from_raw(&mut 
scoped_raw), kind)
+        } else {
+            mutator.dispatch_mutate(&StructuralView::from_raw(raw), kind)
+        };
+        result.map_err(|error| with_value_context(error, raw))
+    })
 }
 
 fn user_default_mutate<U: StructuralMutator>(
@@ -2978,7 +3116,10 @@ fn user_default_mutate<U: StructuralMutator>(
     def_region_kind: DefRegionKind,
     permit: Permit,
 ) -> Result<Any> {
-    default_mutate_driver(mutator, raw, def_region_kind, permit)
+    with_mutation_region(def_region_kind, |kind| {
+        let value = StructuralView::from_raw(raw);
+        mutator.dispatch_default_mutate(MutateValue::new(&value, 
permit.inplace_mode(raw)), kind)
+    })
 }
 
 fn default_mutate_driver<D: MutationDriver>(
@@ -3055,13 +3196,7 @@ where
     R: Into<Any>,
     H: IntoMapper<M>,
 {
-    let root = root.into();
-    let mut native = NativeMapper {
-        dispatch: mapper.into_mapper(),
-        order,
-        remap: StructuralVarRemap::default(),
-    };
-    run_structural_mutator(root, &mut native)
+    mapper.into_mapper().map_root(root.into(), order)
 }
 
 fn shallow_copy(raw: TVMFFIAny) -> Result<Any> {
diff --git a/rust/tvm-ffi/src/extra/structural_mutate/policy.rs 
b/rust/tvm-ffi/src/extra/structural_mutate/policy.rs
new file mode 100644
index 00000000..1ebe7a0d
--- /dev/null
+++ b/rust/tvm-ffi/src/extra/structural_mutate/policy.rs
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Context management around default structural mutation.
+
+use super::*;
+
+#[cfg(doctest)]
+mod compile_fail;
+
+/// Default-recursion policy for [`MutateCallbacks::with_policy`] and 
[`MapWithContextPolicy`].
+///
+/// `ctx.default_maybe_inplace_mutate_result(value)` continues to the next 
policy,
+/// then hooks or reflected fields; children re-enter callback dispatch.
+/// Policies share callback state and compose as `(outer, inner)`.
+/// Restore user state before returning, including on errors; use
+/// [`MutateContext::with_def_region_kind`] for scoped definition regions.
+pub trait MutContextPolicy<State> {
+    /// Customize default descent, preserving the input permission and result 
marker.
+    fn default_mutate(
+        &self,
+        value: MutateValue<'_>,
+        ctx: &mut MutateContext<'_, State>,
+    ) -> Result<UnchangedOr<Any>>;
+}
+
+/// Default descent through registered hooks or reflected structural fields.
+pub struct DefaultMutContextPolicy;
+
+impl<State> MutContextPolicy<State> for DefaultMutContextPolicy {
+    fn default_mutate(
+        &self,
+        value: MutateValue<'_>,
+        ctx: &mut MutateContext<'_, State>,
+    ) -> Result<UnchangedOr<Any>> {
+        ctx.default_maybe_inplace_mutate_result(value)
+    }
+}
+
+impl<State, Outer: MutContextPolicy<State>, Inner: MutContextPolicy<State>> 
MutContextPolicy<State>
+    for (Outer, Inner)
+{
+    fn default_mutate(
+        &self,
+        value: MutateValue<'_>,
+        ctx: &mut MutateContext<'_, State>,
+    ) -> Result<UnchangedOr<Any>> {
+        let kind = ctx.def_region_kind();
+        mutate_with_policy(
+            &mut NextPolicy {
+                driver: &mut *ctx.driver,
+                policy: &self.1,
+            },
+            &self.0,
+            value,
+            kind,
+        )
+        .and_then(UnchangedOr::from_carrier)
+    }
+}
+
+pub(super) fn mutate_with_policy<State>(
+    driver: &mut dyn MutateContextDriver<State>,
+    policy: &impl MutContextPolicy<State>,
+    value: MutateValue<'_>,
+    kind: DefRegionKind,
+) -> Result<Any> {
+    with_mutation_region(kind, |kind| {
+        let mut ctx = MutateContext {
+            driver,
+            def_region_kind: kind,
+            inplace_mode: value.inplace_mode(),
+            _state: PhantomData,
+            _not_send_sync: PhantomData,
+        };
+        policy.default_mutate(value, &mut ctx).map(Any::from)
+    })
+}
+
+struct NextPolicy<'a, State, Policy> {
+    driver: &'a mut dyn MutateContextDriver<State>,
+    policy: &'a Policy,
+}
+
+impl<State, Policy: MutContextPolicy<State>> MutateContextDriver<State>
+    for NextPolicy<'_, State, Policy>
+{
+    fn state(&self) -> &State {
+        self.driver.state()
+    }
+    fn state_mut(&mut self) -> &mut State {
+        self.driver.state_mut()
+    }
+    fn mutate_borrowed(&mut self, value: AnyView<'_>, kind: DefRegionKind) -> 
Result<Any> {
+        self.driver.mutate_borrowed(value, kind)
+    }
+    fn mutate_owned(&mut self, value: Any, kind: DefRegionKind, mode: 
InplaceMode) -> Result<Any> {
+        self.driver.mutate_owned(value, kind, mode)
+    }
+    fn default_mutate_borrowed(&mut self, value: AnyView<'_>, kind: 
DefRegionKind) -> Result<Any> {
+        let view = StructuralView::from_raw(*value.as_raw_ffi_any());
+        mutate_with_policy(self.driver, self.policy, 
MutateValue::borrowed(&view), kind)
+    }
+    fn default_mutate_value(
+        &mut self,
+        mut value: MutateValue<'_>,
+        kind: DefRegionKind,
+        mode: InplaceMode,
+    ) -> Result<Any> {
+        value.mode = value.permit(mode).inplace_mode(value.value.raw());
+        mutate_with_policy(self.driver, self.policy, value, kind)
+    }
+    fn var_remap_get(&mut self, var: &StructuralView) -> Result<Option<Any>> {
+        self.driver.var_remap_get(var)
+    }
+    fn var_remap_set(&mut self, var: &StructuralView, replacement: &Any) -> 
Result<()> {
+        self.driver.var_remap_set(var, replacement)
+    }
+}
+
+pub(super) struct MutationDescent<'a, Driver> {
+    pub(super) driver: &'a mut Driver,
+}
+
+impl<State, Driver: MutationDriver + MutateCallbackState<State>> 
MutateContextDriver<State>
+    for MutationDescent<'_, Driver>
+{
+    fn state(&self) -> &State {
+        self.driver.callback_state()
+    }
+    fn state_mut(&mut self) -> &mut State {
+        self.driver.callback_state_mut()
+    }
+    fn mutate_borrowed(&mut self, value: AnyView<'_>, kind: DefRegionKind) -> 
Result<Any> {
+        with_mutation_region(kind, |kind| {
+            self.driver
+                .dispatch_raw(*value.as_raw_ffi_any(), kind, Permit::Copy)
+        })
+    }
+    fn mutate_owned(&mut self, value: Any, kind: DefRegionKind, mode: 
InplaceMode) -> Result<Any> {
+        let raw = *value.as_raw_ffi_any();
+        let result = with_mutation_region(kind, |kind| {
+            self.driver.dispatch_raw(raw, kind, mode.permit())
+        })?;
+        Ok(if is_unchanged(&result) { value } else { result })
+    }
+    fn default_mutate_borrowed(&mut self, value: AnyView<'_>, kind: 
DefRegionKind) -> Result<Any> {
+        with_mutation_region(kind, |kind| {
+            default_mutate_driver(self.driver, *value.as_raw_ffi_any(), kind, 
Permit::Copy)
+        })
+    }
+    fn default_mutate_value(
+        &mut self,
+        value: MutateValue<'_>,
+        kind: DefRegionKind,
+        mode: InplaceMode,
+    ) -> Result<Any> {
+        let permit = value.permit(mode);
+        with_mutation_region(kind, |kind| {
+            default_mutate_driver(self.driver, value.value.raw(), kind, permit)
+        })
+    }
+    fn var_remap_get(&mut self, var: &StructuralView) -> Result<Option<Any>> {
+        self.driver.var_remap_get_raw(var.raw())
+    }
+    fn var_remap_set(&mut self, var: &StructuralView, replacement: &Any) -> 
Result<()> {
+        self.driver.var_remap_set_raw(var.raw(), replacement)
+    }
+}
+
+impl<D, Policy> MutateCallbackState<D> for NativeMapper<'_, D, Policy> {
+    fn callback_state(&self) -> &D {
+        self.dispatch
+    }
+    fn callback_state_mut(&mut self) -> &mut D {
+        self.dispatch
+    }
+}
+
+/// A [`MapDispatch`] with a [`MutContextPolicy`], sharing the dispatcher's 
state.
+///
+/// Run with [`Self::map`] or [`structural_map`]. Each node's map callback runs
+/// outside its policy scope; its children run inside.
+/// This mapper cannot be a callback tuple member or another wrapper's 
dispatcher.
+/// Compose policies as `(outer, inner)` within one wrapper.
+pub struct MapWithContextPolicy<Mapper, Policy> {
+    mapper: Mapper,
+    policy: Rc<Policy>,
+}
+
+impl<Mapper: MapDispatch, Policy: MutContextPolicy<Mapper>> 
MapWithContextPolicy<Mapper, Policy> {
+    /// Combine a dispatcher and a default-recursion policy.
+    pub fn new(mapper: Mapper, policy: Policy) -> Self {
+        Self {
+            mapper,
+            policy: Rc::new(policy),
+        }
+    }
+
+    /// Access the dispatcher and shared state.
+    pub fn state(&self) -> &Mapper {
+        &self.mapper
+    }
+
+    /// Mutably access the dispatcher outside recursive calls.
+    pub fn state_mut(&mut self) -> &mut Mapper {
+        &mut self.mapper
+    }
+
+    /// Recover the dispatcher and its state.
+    pub fn into_state(self) -> Mapper {
+        self.mapper
+    }
+
+    /// Map an owning root with fresh invocation-local identity substitutions.
+    pub fn map<R: Into<Any>>(&mut self, root: R, order: WalkOrder) -> 
Result<Any> {
+        run_structural_mutator(
+            root.into(),
+            &mut NativeMapper {
+                dispatch: &mut self.mapper,
+                order,
+                policy: Some(self.policy.clone()),
+                remap: StructuralVarRemap::default(),
+            },
+        )
+    }
+}
+
+impl<Mapper: MapDispatch, Policy: MutContextPolicy<Mapper>> NativeMap
+    for MapWithContextPolicy<Mapper, Policy>
+{
+    fn map_root(&mut self, root: Any, order: WalkOrder) -> Result<Any> {
+        self.map(root, order)
+    }
+}
+
+impl<Mapper: MapDispatch, Policy: MutContextPolicy<Mapper>> NativeMap
+    for &mut MapWithContextPolicy<Mapper, Policy>
+{
+    fn map_root(&mut self, root: Any, order: WalkOrder) -> Result<Any> {
+        self.map(root, order)
+    }
+}
+
+#[doc(hidden)]
+pub enum ByPolicyMap {}
+
+impl<Mapper: MapDispatch, Policy: MutContextPolicy<Mapper>> 
IntoMapper<ByPolicyMap>
+    for MapWithContextPolicy<Mapper, Policy>
+{
+    type Mapper = Self;
+    fn into_mapper(self) -> Self {
+        self
+    }
+}
+
+impl<'a, Mapper: MapDispatch, Policy: MutContextPolicy<Mapper>> 
IntoMapper<ByPolicyMap>
+    for &'a mut MapWithContextPolicy<Mapper, Policy>
+{
+    type Mapper = Self;
+    fn into_mapper(self) -> Self {
+        self
+    }
+}
diff --git a/rust/tvm-ffi/src/extra/structural_mutate/policy/compile_fail.rs 
b/rust/tvm-ffi/src/extra/structural_mutate/policy/compile_fail.rs
new file mode 100644
index 00000000..16bb79be
--- /dev/null
+++ b/rust/tvm-ffi/src/extra/structural_mutate/policy/compile_fail.rs
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! A policy mapper cannot be used as a callback, even in a one-element tuple.
+//! ```compile_fail,E0277
+//! use tvm_ffi::*;
+//! fn invalid<D: MapDispatch>(mapper: &mut MapWithContextPolicy<D, 
DefaultMutContextPolicy>) {
+//!     structural_map(1_i64, (mapper,), WalkOrder::PreOrder).unwrap();
+//! }
+//! ```
+//!
+//! Adding another callback must not discard the policy.
+//! ```compile_fail,E0277
+//! use tvm_ffi::*;
+//! fn invalid<D: MapDispatch>(mapper: &mut MapWithContextPolicy<D, 
DefaultMutContextPolicy>) {
+//!     structural_map(1_i64, (mapper, |s: String| s), 
WalkOrder::PreOrder).unwrap();
+//! }
+//! ```
+//!
+//! Nested callback tuples obey the same restriction.
+//! ```compile_fail,E0277
+//! use tvm_ffi::*;
+//! fn invalid<D: MapDispatch>(mapper: &mut MapWithContextPolicy<D, 
DefaultMutContextPolicy>) {
+//!     structural_map(1_i64, (|s: String| s, ((mapper,),)), 
WalkOrder::PostOrder).unwrap();
+//! }
+//! ```
+//!
+//! Compose policies in a tuple instead of nesting mapper wrappers.
+//! ```compile_fail,E0277
+//! use tvm_ffi::*;
+//! fn invalid<D: MapDispatch>(mapper: MapWithContextPolicy<D, 
DefaultMutContextPolicy>) {
+//!     MapWithContextPolicy::new(mapper, DefaultMutContextPolicy);
+//! }
+//! ```
diff --git a/rust/tvm-ffi/src/extra/structural_visit.rs 
b/rust/tvm-ffi/src/extra/structural_visit.rs
index 5cea6591..eedce47c 100644
--- a/rust/tvm-ffi/src/extra/structural_visit.rs
+++ b/rust/tvm-ffi/src/extra/structural_visit.rs
@@ -223,12 +223,7 @@ impl From<Error> for NativeHalt {
 type NativeResult = std::result::Result<(), NativeHalt>;
 
 mod policy;
-pub use policy::{ContextPolicy, DefaultContextPolicy, WalkWithPolicy};
-
-/// Compatibility name for [`ContextPolicy`].
-pub use policy::ContextPolicy as VisitPolicy;
-/// Compatibility name for [`DefaultContextPolicy`].
-pub use policy::DefaultContextPolicy as DefaultVisitPolicy;
+pub use policy::{ContextPolicy, DefaultContextPolicy, WalkWithContextPolicy};
 
 /// State and recursive operations available to a visit callback.
 ///
diff --git a/rust/tvm-ffi/src/extra/structural_visit/policy.rs 
b/rust/tvm-ffi/src/extra/structural_visit/policy.rs
index 502f0df4..fed91afc 100644
--- a/rust/tvm-ffi/src/extra/structural_visit/policy.rs
+++ b/rust/tvm-ffi/src/extra/structural_visit/policy.rs
@@ -155,12 +155,12 @@ impl<State, V: StructuralVisitor + 
VisitCallbackState<State>> VisitContextDriver
 /// The dispatcher is also the state visible through the policy's context. Use
 /// `#[dispatch(walk)]` or implement [`WalkDispatch`] to define its callbacks.
 /// Run repeatedly with [`Self::walk`], or pass this value to 
[`structural_walk`].
-pub struct WalkWithPolicy<Walker, Policy> {
+pub struct WalkWithContextPolicy<Walker, Policy> {
     walker: Walker,
     policy: Rc<Policy>,
 }
 
-impl<Walker: WalkDispatch, Policy: ContextPolicy<Walker>> 
WalkWithPolicy<Walker, Policy> {
+impl<Walker: WalkDispatch, Policy: ContextPolicy<Walker>> 
WalkWithContextPolicy<Walker, Policy> {
     /// Combine a dispatcher and a default-recursion policy.
     pub fn new(walker: Walker, policy: Policy) -> Self {
         Self {
@@ -205,7 +205,7 @@ impl<Walker: WalkDispatch, Policy: ContextPolicy<Walker>> 
WalkWithPolicy<Walker,
 pub enum ByPolicyWalk {}
 
 impl<Walker: WalkDispatch, Policy: ContextPolicy<Walker>> 
IntoWalker<ByPolicyWalk>
-    for WalkWithPolicy<Walker, Policy>
+    for WalkWithContextPolicy<Walker, Policy>
 {
     type Walker = Self;
     fn into_walker(self) -> Self {
@@ -214,7 +214,7 @@ impl<Walker: WalkDispatch, Policy: ContextPolicy<Walker>> 
IntoWalker<ByPolicyWal
 }
 
 impl<Walker: WalkDispatch, Policy: ContextPolicy<Walker>> NativeVisit
-    for WalkWithPolicy<Walker, Policy>
+    for WalkWithContextPolicy<Walker, Policy>
 {
     const CUSTOM_DESCENT: bool = true;
 
@@ -245,7 +245,7 @@ impl<Walker: WalkDispatch, Policy: ContextPolicy<Walker>> 
NativeVisit
 }
 
 struct WalkDescent<'a, Walker, Policy, const PRE_ORDER: bool> {
-    visitor: &'a mut WalkWithPolicy<Walker, Policy>,
+    visitor: &'a mut WalkWithContextPolicy<Walker, Policy>,
 }
 
 impl<Walker: WalkDispatch, Policy: ContextPolicy<Walker>, const PRE_ORDER: 
bool>
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index f6ddd640..eadc138c 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -49,15 +49,16 @@ pub use crate::error::{
 };
 pub use crate::extra::module::Module;
 pub use crate::extra::structural_mutate::{
-    structural_map, structural_mutate, CallbackMutator, InplaceMode, 
InplaceValue, IntoMapResult,
-    IntoMapper, IntoMutator, MapChainLink, MapDispatch, MapValue, 
MutateCallbacks, MutateChainLink,
-    MutateContext, MutateDispatch, MutateValue, Mutator, StructuralMutator, 
StructuralVarRemap,
+    structural_map, structural_mutate, CallbackMutator, 
DefaultMutContextPolicy, InplaceMode,
+    InplaceValue, IntoMapResult, IntoMapper, IntoMutator, MapChainLink, 
MapDispatch, MapValue,
+    MapWithContextPolicy, MutContextPolicy, MutateCallbacks, MutateChainLink, 
MutateContext,
+    MutateDispatch, MutateValue, Mutator, StructuralMutator, 
StructuralVarRemap,
 };
 pub use crate::extra::structural_visit::{
     structural_visit, structural_walk, ContextPolicy, DefRegionKind, 
DefaultContextPolicy,
-    DefaultVisitPolicy, IntoVisitor, IntoWalkResult, IntoWalker, 
StructuralVisitor, VisitCallbacks,
-    VisitChainLink, VisitContext, VisitInterrupt, VisitPolicy, VisitValue, 
WalkChainLink,
-    WalkDispatch, WalkOrder, WalkResult, WalkWithPolicy,
+    IntoVisitor, IntoWalkResult, IntoWalker, StructuralVisitor, 
VisitCallbacks, VisitChainLink,
+    VisitContext, VisitInterrupt, VisitValue, WalkChainLink, WalkDispatch, 
WalkOrder, WalkResult,
+    WalkWithContextPolicy,
 };
 pub use crate::extra::unchanged::{Unchanged, UnchangedOr};
 pub use crate::extra::StructuralView;
diff --git a/rust/tvm-ffi/tests/test_structural_mutate.rs 
b/rust/tvm-ffi/tests/test_structural_mutate.rs
index b7647603..70183e9c 100644
--- a/rust/tvm-ffi/tests/test_structural_mutate.rs
+++ b/rust/tvm-ffi/tests/test_structural_mutate.rs
@@ -23,8 +23,9 @@ use tvm_ffi::function::FunctionObj;
 use tvm_ffi::object::ObjectRef;
 use tvm_ffi::{
     dispatch, structural_map, structural_mutate, Any, AnyView, Array, 
CallbackMutator,
-    DefRegionKind, Error, FieldGetter, Function, InplaceMode, InplaceValue, 
Map, MapDispatch,
-    MapValue, MutateCallbacks, MutateValue, Mutator, Object, ObjectArc, 
ObjectRefCore, Result,
+    DefRegionKind, DefaultMutContextPolicy, Error, FieldGetter, Function, 
InplaceMode,
+    InplaceValue, IntoMapper, Map, MapDispatch, MapValue, 
MapWithContextPolicy, MutContextPolicy,
+    MutateCallbacks, MutateValue, Mutator, Object, ObjectArc, ObjectRefCore, 
Result,
     String as FfiString, StructuralMutator, StructuralVarRemap, TypeIndex, 
Unchanged, UnchangedOr,
     WalkOrder, RUNTIME_ERROR,
 };
@@ -1649,3 +1650,450 @@ fn 
callback_mutate_panics_resume_and_leave_the_next_run_usable() {
     .unwrap();
     assert_eq!(mutated.get(0).unwrap(), 2);
 }
+
+#[derive(Default)]
+struct PolicyState {
+    depth: usize,
+    events: Vec<(&'static str, usize)>,
+}
+#[dispatch(map)]
+impl PolicyState {
+    fn map_integer(&mut self, value: i64) -> i64 {
+        self.events.push(("integer", self.depth));
+        value + 1
+    }
+    fn map_array(&mut self, value: Array<Any>) -> Any {
+        self.events.push(("callback", self.depth));
+        value.into()
+    }
+}
+
+struct ArrayPolicy;
+impl MutContextPolicy<PolicyState> for ArrayPolicy {
+    fn default_mutate(
+        &self,
+        value: MutateValue<'_>,
+        ctx: &mut CallbackMutator<PolicyState>,
+    ) -> Result<UnchangedOr<Any>> {
+        if value
+            .as_node::<tvm_ffi::collections::array::ArrayObj>()
+            .is_none()
+        {
+            return ctx.default_maybe_inplace_mutate_result(value);
+        }
+        ctx.state_mut().depth += 1;
+        let depth = ctx.state().depth;
+        ctx.state_mut().events.push(("enter", depth));
+        let result = ctx.default_maybe_inplace_mutate_result(value);
+        ctx.state_mut().depth -= 1;
+        let depth = ctx.state().depth;
+        ctx.state_mut().events.push(("exit", depth));
+        result
+    }
+}
+struct RecordPolicy;
+impl MutContextPolicy<PolicyState> for RecordPolicy {
+    fn default_mutate(
+        &self,
+        value: MutateValue<'_>,
+        ctx: &mut CallbackMutator<PolicyState>,
+    ) -> Result<UnchangedOr<Any>> {
+        if value
+            .as_node::<tvm_ffi::collections::array::ArrayObj>()
+            .is_some()
+        {
+            let depth = ctx.state().depth;
+            ctx.state_mut().events.push(("next", depth));
+        }
+        ctx.default_maybe_inplace_mutate_result(value)
+    }
+}
+
+#[test]
+fn mutation_policies_share_state_and_preserve_callback_order() {
+    let root = || Array::new(vec![Any::from(Array::new(vec![1_i64])), 
Any::from(2_i64)]);
+    let pre = vec![
+        ("callback", 0),
+        ("enter", 1),
+        ("next", 1),
+        ("callback", 1),
+        ("enter", 2),
+        ("next", 2),
+        ("integer", 2),
+        ("exit", 1),
+        ("integer", 1),
+        ("exit", 0),
+    ];
+    let post = vec![
+        ("enter", 1),
+        ("next", 1),
+        ("enter", 2),
+        ("next", 2),
+        ("integer", 2),
+        ("exit", 1),
+        ("callback", 1),
+        ("integer", 1),
+        ("exit", 0),
+        ("callback", 0),
+    ];
+    for order in [WalkOrder::PreOrder, WalkOrder::PostOrder] {
+        let mut mapper = MapWithContextPolicy::new(
+            PolicyState::default(),
+            (ArrayPolicy, (RecordPolicy, DefaultMutContextPolicy)),
+        );
+        let output = structural_map(root(), &mut mapper, order).unwrap();
+        assert_eq!(i64::try_from(array_item(&output, 1)).unwrap(), 3);
+        assert_eq!(
+            i64::try_from(array_item(&array_item(&output, 0), 0)).unwrap(),
+            2
+        );
+        assert_eq!(mapper.state().depth, 0);
+        assert_eq!(
+            &mapper.state().events,
+            if order == WalkOrder::PreOrder {
+                &pre
+            } else {
+                &post
+            }
+        );
+    }
+    let mut mutator = MutateCallbacks::new(
+        PolicyState::default(),
+        (
+            |x: i64, ctx: &mut CallbackMutator<PolicyState>| {
+                let depth = ctx.state().depth;
+                ctx.state_mut().events.push(("integer", depth));
+                x + 1
+            },
+            |value: MutateValue<'_>, ctx: &mut CallbackMutator<PolicyState>| {
+                let depth = ctx.state().depth;
+                ctx.state_mut().events.push(("callback", depth));
+                ctx.default_maybe_inplace_mutate_result(value)
+            },
+        ),
+    )
+    .with_policy((ArrayPolicy, RecordPolicy));
+    let output = structural_mutate(root(), &mut mutator).unwrap();
+    assert_eq!(i64::try_from(array_item(&output, 1)).unwrap(), 3);
+    assert_eq!(mutator.state().events, pre);
+    assert_eq!(mutator.state().depth, 0);
+}
+
+#[test]
+fn map_policy_entries_preserve_descent_and_callback_composition() {
+    struct Stop;
+    impl<State> MutContextPolicy<State> for Stop {
+        fn default_mutate(
+            &self,
+            _: MutateValue<'_>,
+            _: &mut CallbackMutator<State>,
+        ) -> Result<UnchangedOr<Any>> {
+            Ok(UnchangedOr::unchanged())
+        }
+    }
+
+    let root = || Array::new(vec![1_i64]);
+    let first = |value: Any| 
Array::<i64>::try_from(value).unwrap().get(0).unwrap();
+    for order in [WalkOrder::PreOrder, WalkOrder::PostOrder] {
+        for entry in 0..3 {
+            let mut state = PolicyState::default();
+            let output = {
+                let mut mapper =
+                    MapWithContextPolicy::new(&mut state, 
(DefaultMutContextPolicy, Stop));
+                match entry {
+                    0 => structural_map(root(), mapper, order),
+                    1 => structural_map(root(), &mut mapper, order),
+                    _ => mapper.map(root(), order),
+                }
+            }
+            .unwrap();
+            assert_eq!(first(output), 1);
+            assert_eq!(state.events, vec![("callback", 0)]);
+        }
+
+        let mut dispatch = IncrementIntegers;
+        let callbacks = (|s: FfiString| s, (&mut dispatch,));
+        assert_eq!(first(structural_map(root(), callbacks, order).unwrap()), 
2);
+
+        let callbacks = (|x: i64| x + 1, (|s: FfiString| s,));
+        assert_eq!(first(structural_map(root(), callbacks, order).unwrap()), 
2);
+        let mapper = MapWithContextPolicy::new(callbacks.into_mapper(), Stop);
+        assert_eq!(first(structural_map(root(), mapper, order).unwrap()), 1);
+    }
+}
+
+#[test]
+fn mutation_policy_continuations_preserve_ownership_and_markers() {
+    struct Ownership {
+        increment: bool,
+        retained: Option<Any>,
+    }
+    #[dispatch(map)]
+    impl Ownership {
+        fn map_integer(&mut self, x: i64) -> UnchangedOr<i64> {
+            if self.increment {
+                UnchangedOr::changed(x + 1)
+            } else {
+                UnchangedOr::unchanged()
+            }
+        }
+    }
+    struct Control {
+        mode: InplaceMode,
+        retain: bool,
+    }
+    impl MutContextPolicy<Ownership> for Control {
+        fn default_mutate(
+            &self,
+            value: MutateValue<'_>,
+            ctx: &mut CallbackMutator<Ownership>,
+        ) -> Result<UnchangedOr<Any>> {
+            let array = value
+                .as_node::<tvm_ffi::collections::array::ArrayObj>()
+                .is_some();
+            if array && self.retain {
+                ctx.state_mut().retained = Some(value.to_owned());
+            }
+            let result = ctx.default_mutate_with_mode_result(value, 
self.mode)?;
+            if array && !ctx.state().increment {
+                assert!(result.is_unchanged());
+            }
+            Ok(result)
+        }
+    }
+    for entry in 0..3 {
+        // pre-order map, post-order map, mutation callback
+        for case in 0..4 {
+            // unique, shared, alias retained by policy, forced copy
+            for increment in [false, true] {
+                let root = Array::new(vec![1_i64]);
+                let pointer = array_pointer(&root);
+                let alias = (case == 1).then(|| root.clone());
+                let policy = (
+                    Control {
+                        mode: if case == 3 {
+                            InplaceMode::Disallow
+                        } else {
+                            InplaceMode::Allow
+                        },
+                        retain: case == 2,
+                    },
+                    DefaultMutContextPolicy,
+                );
+                let state = Ownership {
+                    increment,
+                    retained: None,
+                };
+                let (output, state) = if entry < 2 {
+                    let mut mapper = MapWithContextPolicy::new(state, policy);
+                    let output = mapper
+                        .map(
+                            root,
+                            if entry == 0 {
+                                WalkOrder::PreOrder
+                            } else {
+                                WalkOrder::PostOrder
+                            },
+                        )
+                        .unwrap();
+                    (output, mapper.into_state())
+                } else {
+                    let mut mutator = MutateCallbacks::new(
+                        state,
+                        (
+                            |x: i64, ctx: &mut CallbackMutator<Ownership>| {
+                                ctx.state_mut().map_integer(x)
+                            },
+                            |value: MutateValue<'_>, ctx: &mut 
CallbackMutator<Ownership>| {
+                                ctx.default_maybe_inplace_mutate_result(value)
+                            },
+                        ),
+                    )
+                    .with_policy(policy);
+                    let output = structural_mutate(root, &mut 
mutator).unwrap();
+                    (output, mutator.into_state())
+                };
+                let output = Array::<i64>::try_from(output).unwrap();
+                assert_eq!(output.get(0).unwrap(), if increment { 2 } else { 1 
});
+                assert_eq!(array_pointer(&output) == pointer, !increment || 
case == 0);
+                if let Some(alias) = alias {
+                    assert_eq!(alias.get(0).unwrap(), 1);
+                }
+                if let Some(alias) = state.retained {
+                    
assert_eq!(Array::<i64>::try_from(alias).unwrap().get(0).unwrap(), 1);
+                }
+            }
+        }
+    }
+}
+
+#[test]
+fn mutation_policy_regions_retargeting_and_error_restore() {
+    use DefRegionKind::{None as Use, Pattern, Simple};
+    #[derive(Default)]
+    struct Regions(Vec<(i64, DefRegionKind)>);
+    #[dispatch(map)]
+    impl Regions {
+        fn map_integer(&mut self, x: i64, kind: DefRegionKind) -> i64 {
+            self.0.push((x, kind));
+            x
+        }
+        fn map_array(&mut self, value: Array<i64>, kind: DefRegionKind) -> Any 
{
+            self.0.push((200, kind));
+            value.into()
+        }
+    }
+    struct Redirect(DefRegionKind);
+    impl MutContextPolicy<Regions> for Redirect {
+        fn default_mutate(
+            &self,
+            value: MutateValue<'_>,
+            ctx: &mut CallbackMutator<Regions>,
+        ) -> Result<UnchangedOr<Any>> {
+            if value.cast::<bool>() == Some(false) {
+                // Bypass this container's callback, but enter the next policy 
and redispatch its children.
+                return ctx.with_def_region_kind(Pattern, |ctx| {
+                    ctx.default_mutate(&Array::new(vec![1_i64]))
+                        .map(UnchangedOr::changed)
+                });
+            }
+            if value.cast::<i64>() == Some(2) {
+                assert_eq!(ctx.def_region_kind(), Pattern);
+                ctx.mutate_with(&99_i64, self.0)?;
+                let error: Result<()> = ctx.with_def_region_kind(Use, |ctx| {
+                    assert_eq!(ctx.def_region_kind(), Pattern);
+                    Err(Error::new(RUNTIME_ERROR, "scoped error", ""))
+                });
+                assert!(error.is_err());
+                assert_eq!(ctx.def_region_kind(), Pattern);
+            }
+            ctx.default_maybe_inplace_mutate_result(value)
+        }
+    }
+    struct Observe;
+    impl MutContextPolicy<Regions> for Observe {
+        fn default_mutate(
+            &self,
+            value: MutateValue<'_>,
+            ctx: &mut CallbackMutator<Regions>,
+        ) -> Result<UnchangedOr<Any>> {
+            if value
+                .as_node::<tvm_ffi::collections::array::ArrayObj>()
+                .is_some()
+            {
+                assert_eq!(ctx.def_region_kind(), Pattern);
+                let kind = ctx.def_region_kind();
+                ctx.state_mut().0.push((100, kind));
+            }
+            ctx.default_maybe_inplace_mutate_result(value)
+        }
+    }
+    assert_eq!(
+        unsafe { tvm_ffi::tvm_ffi_sys::TVMFFITestingDummyTarget() },
+        0
+    );
+    for requested in [Use, Simple] {
+        for order in [WalkOrder::PreOrder, WalkOrder::PostOrder] {
+            let mut mapper =
+                MapWithContextPolicy::new(Regions::default(), 
(Redirect(requested), Observe));
+            let output = mapper.map(false, order).unwrap();
+            assert_eq!(i64::try_from(array_item(&output, 0)).unwrap(), 1);
+            let mut expected = vec![(100, Pattern), (1, Pattern)];
+            if order == WalkOrder::PostOrder {
+                expected.push((200, Use)); // The root callback sees the 
result after descent.
+            }
+            assert_eq!(mapper.state().0, expected);
+            mapper.state_mut().0.clear();
+            let graph = Function::get_global("testing.make_visit_region_graph")
+                .unwrap()
+                .call_tuple((false,))
+                .unwrap();
+            mapper.map(graph, order).unwrap();
+            let expected = if order == WalkOrder::PreOrder {
+                vec![(1, Simple), (2, Pattern), (99, Pattern), (3, Use)]
+            } else {
+                vec![(1, Simple), (99, Pattern), (2, Pattern), (3, Use)]
+            };
+            assert_eq!(mapper.state().0, expected);
+        }
+        let mut mutator = MutateCallbacks::new(
+            Regions::default(),
+            |value: MutateValue<'_>, ctx: &mut CallbackMutator<Regions>| {
+                if let Some(x) = value.cast::<i64>() {
+                    let kind = ctx.def_region_kind();
+                    ctx.state_mut().0.push((x, kind));
+                }
+                if value
+                    .as_node::<tvm_ffi::collections::array::ArrayObj>()
+                    .is_some()
+                {
+                    let kind = ctx.def_region_kind();
+                    ctx.state_mut().0.push((200, kind));
+                }
+                ctx.default_maybe_inplace_mutate_result(value)
+            },
+        )
+        .with_policy((Redirect(requested), Observe));
+        let output = structural_mutate(false, &mut mutator).unwrap();
+        assert_eq!(i64::try_from(array_item(&output, 0)).unwrap(), 1);
+        assert_eq!(mutator.state().0, vec![(100, Pattern), (1, Pattern)]);
+        mutator.state_mut().0.clear();
+        let graph = Function::get_global("testing.make_visit_region_graph")
+            .unwrap()
+            .call_tuple((false,))
+            .unwrap();
+        structural_mutate(graph, &mut mutator).unwrap();
+        assert_eq!(
+            mutator.state().0,
+            vec![(1, Simple), (2, Pattern), (99, Pattern), (3, Use)]
+        );
+    }
+}
+
+#[test]
+fn mutation_policy_halts_restore_state_and_skip_later_policies() {
+    struct Halt(bool);
+    impl MutContextPolicy<PolicyState> for Halt {
+        fn default_mutate(
+            &self,
+            _: MutateValue<'_>,
+            _: &mut CallbackMutator<PolicyState>,
+        ) -> Result<UnchangedOr<Any>> {
+            if self.0 {
+                Err(Error::new(RUNTIME_ERROR, "stop descent", ""))
+            } else {
+                Ok(UnchangedOr::unchanged())
+            }
+        }
+    }
+    for fail in [false, true] {
+        for order in [WalkOrder::PreOrder, WalkOrder::PostOrder] {
+            let mut mapper = MapWithContextPolicy::new(
+                PolicyState::default(),
+                (ArrayPolicy, (Halt(fail), RecordPolicy)),
+            );
+            let result = mapper.map(Array::new(vec![1_i64]), order);
+            assert_eq!(result.is_err(), fail);
+            assert_eq!(mapper.state().depth, 0);
+            assert!(!mapper
+                .state()
+                .events
+                .iter()
+                .any(|(tag, _)| matches!(*tag, "integer" | "next")));
+            if let Err(error) = result {
+                assert!(error.message().contains("stop descent"));
+            }
+        }
+        let mut mutator = MutateCallbacks::new(
+            PolicyState::default(),
+            |x: i64, _: &mut CallbackMutator<PolicyState>| x + 1,
+        )
+        .with_policy((ArrayPolicy, (Halt(fail), RecordPolicy)));
+        assert_eq!(
+            structural_mutate(Array::new(vec![1_i64]), &mut mutator).is_err(),
+            fail
+        );
+        assert_eq!(mutator.state().events, vec![("enter", 1), ("exit", 0)]);
+        assert_eq!(mutator.state().depth, 0);
+    }
+}
diff --git a/rust/tvm-ffi/tests/test_structural_visit.rs 
b/rust/tvm-ffi/tests/test_structural_visit.rs
index 2858844f..22137fa6 100644
--- a/rust/tvm-ffi/tests/test_structural_visit.rs
+++ b/rust/tvm-ffi/tests/test_structural_visit.rs
@@ -23,7 +23,8 @@ use tvm_ffi::{
     dispatch, get_type_attr, structural_visit, structural_walk, Any, Array, 
ContextPolicy,
     DLDataType, DLDataTypeCode, DefRegionKind, Error, FieldGetter, Function, 
Map, Object,
     ObjectRefCore, Result, String as FfiString, StructuralVisitor, TypeIndex, 
VisitCallbacks,
-    VisitContext, VisitInterrupt, VisitValue, WalkOrder, WalkResult, 
WalkWithPolicy, RUNTIME_ERROR,
+    VisitContext, VisitInterrupt, VisitValue, WalkOrder, WalkResult, 
WalkWithContextPolicy,
+    RUNTIME_ERROR,
 };
 
 fn runtime_error(message: &str) -> Error {
@@ -120,7 +121,7 @@ fn 
composed_policies_share_array_scope_with_visit_and_walk_callbacks() {
     // recursion, so default policies also run for matched integer leaves.
     for order in [WalkOrder::PreOrder, WalkOrder::PostOrder] {
         let mut walker =
-            WalkWithPolicy::new(CollectIntegers::default(), (ArrayScope, 
RecordDescent));
+            WalkWithContextPolicy::new(CollectIntegers::default(), 
(ArrayScope, RecordDescent));
         assert!(walker.walk(&root, order).unwrap().is_none());
         assert_eq!(walker.state().integers, expected);
         assert_eq!(walker.state().depth, 0);
@@ -195,7 +196,7 @@ fn 
policy_continuation_scopes_regions_and_restores_after_halts() {
             };
             let policies = (Scope(Simple, 4), (Scope(Pattern, 3), Scope(Use, 
2)));
             let (result, state) = if let Some(order) = order {
-                let mut walker = WalkWithPolicy::new(state, policies);
+                let mut walker = WalkWithContextPolicy::new(state, policies);
                 let result = walker.walk(&root, order);
                 (result, walker.into_state())
             } else {
@@ -332,7 +333,7 @@ fn 
policy_continuation_retargets_without_dispatching_the_container() {
     ];
     for order in [WalkOrder::PreOrder, WalkOrder::PostOrder] {
         for skip in [false, true] {
-            let mut walker = WalkWithPolicy::new(Probe(vec![], skip), 
(Redirect, Observe));
+            let mut walker = WalkWithContextPolicy::new(Probe(vec![], skip), 
(Redirect, Observe));
             assert!(walker.walk(&root, order).unwrap().is_none());
             let mut expected = descent.clone();
             let array_callback = ("array callback", 2, DefRegionKind::None);
@@ -473,7 +474,7 @@ fn 
policy_halts_skip_remaining_policies_and_restore_outer_state() {
         for order in [None, Some(WalkOrder::PreOrder), 
Some(WalkOrder::PostOrder)] {
             let policies = (Scope, (Stop(error), Unreachable));
             let (result, state) = if let Some(order) = order {
-                let mut walker = WalkWithPolicy::new(Probe::default(), 
policies);
+                let mut walker = WalkWithContextPolicy::new(Probe::default(), 
policies);
                 let result = walker.walk(&root, order);
                 (result, walker.into_state())
             } else {
@@ -548,7 +549,8 @@ fn 
policy_regions_compose_with_field_flags_and_function_hooks() {
             for order in [None, Some(WalkOrder::PreOrder), 
Some(WalkOrder::PostOrder)] {
                 let policy = SetRootRegion(root.type_index(), region);
                 let state = if let Some(order) = order {
-                    let mut walker = 
WalkWithPolicy::new(PolicyRegionTrace::default(), policy);
+                    let mut walker =
+                        
WalkWithContextPolicy::new(PolicyRegionTrace::default(), policy);
                     assert!(walker.walk(&root, order).unwrap().is_none());
                     walker.into_state()
                 } else {
@@ -598,7 +600,8 @@ fn 
walk_policy_preserves_reflected_pattern_before_default_descent() {
     use DefRegionKind::{None as Use, Pattern, Simple};
     for requested in [Use, Simple] {
         for order in [WalkOrder::PreOrder, WalkOrder::PostOrder] {
-            let mut walker = WalkWithPolicy::new(PolicyRegionTrace::default(), 
Reenter(requested));
+            let mut walker =
+                WalkWithContextPolicy::new(PolicyRegionTrace::default(), 
Reenter(requested));
             assert!(walker.walk(&root, order).unwrap().is_none());
             let expected = match order {
                 WalkOrder::PreOrder => vec![

Reply via email to