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 ca8803c9 [FEAT] Add structural map, maybe_inplace_mutate, and
var_remap APIs (#649)
ca8803c9 is described below
commit ca8803c9f34d26cdc665f5dc5adfc0bc816771eb
Author: Kathryn (Jinqi) Chen <[email protected]>
AuthorDate: Thu Aug 13 20:03:29 2026 -0700
[FEAT] Add structural map, maybe_inplace_mutate, and var_remap APIs (#649)
## Summary
This PR adds structural transformation support to TVM FFI, complementing
the existing structural equality, hashing, and walking APIs.
The new `StructuralMutator` recursively transforms reflected object
graphs, while `structural_map` provides a convenient callback-based
interface for compiler passes.
## Structural mapping
`structural_map` follows the same typed callback model as
`structural_walk`. Callbacks are matched by runtime type and return
either the unchanged value or a replacement.
Mapping defaults to post-order, so callbacks observe values whose
children have already been transformed. This makes bottom-up rewrites
such as constant folding straightforward:
```py
def fold_add(expr):
if isinstance(expr.lhs, IntImm) and isinstance(expr.rhs, IntImm):
return IntImm(expr.lhs.value + expr.rhs.value)
return expr
optimized = tvm_ffi.structural_map(function, (Add, fold_add))
```
The PR exposes:
- structural_map in Python.
- StructuralMap and StructuralMapExpected in C++.
- Ordered typed callbacks and grouped callback types.
- Pre-order and post-order transformation.
- Callbacks receiving either value or (value, def_region_kind).
- Structural error context when transformation fails.
## StructuralMutator
`StructuralMutator` provides the low-level transformation engine through
two operations:
- `Mutate` transforms a value without intentionally modifying the input.
- `MaybeInplaceMutate` permits implementations to reuse an object when
doing so is safe.
## Custom mutation behavior
Object types can customize mutation using:
- `__s_mutate__` for canonical non-in-place transformation.
- `__s_maybe_inplace_mutate__` for an optional type-specific in-place
optimization.
The maybe-in-place hook owns its safety policy and may reuse the input,
delegate to normal mutation, or return another value. A type providing
it must also provide `__s_mutate__`.
Built-in hooks are registered for Array, List, Map, and Dict.
## Variable identity remapping
The mutator maintains an identity-substitution environment for FreeVar
objects. Once an identity is mapped, later occurrences reuse the same
result.
For example:
```py
def specialize_shape_var(var):
if var.name == "n":
return IntImm(10)
return var
specialized = tvm_ffi.structural_map(
function,
(Var, specialize_shape_var),
)
```
If the same `Var` occurs in a function parameter and its body, both
occurrences resolve to the same mapped result.
The mutator also exposes `get_var_remap` and `set_var_remap`, allowing
custom DAG-style variable wrappers to use their underlying identity
object as the remapping key.
---
CMakeLists.txt | 1 +
docs/concepts/structural_eq_hash.rst | 362 +++++++----
docs/guides/rust_lang_guide.md | 4 +
docs/reference/python/index.rst | 3 +
include/tvm/ffi/container/dict.h | 9 +
include/tvm/ffi/container/map.h | 9 +
include/tvm/ffi/extra/structural_mutate.h | 919 ++++++++++++++++++++++++++++
include/tvm/ffi/extra/structural_visit.h | 89 +--
include/tvm/ffi/reflection/accessor.h | 44 ++
python/tvm_ffi/__init__.py | 4 +
python/tvm_ffi/_ffi_api.py | 16 +-
python/tvm_ffi/dataclasses/py_class.py | 2 +
python/tvm_ffi/structural.py | 305 +++++++--
rust/tvm-ffi/src/extra/structural_visit.rs | 50 +-
rust/tvm-ffi/tests/test_structural_visit.rs | 10 +-
src/ffi/extra/structural_mutate.cc | 378 ++++++++++++
src/ffi/extra/structural_visit.cc | 7 +-
tests/cpp/extra/test_structural_mutate.cc | 259 ++++++++
tests/cpp/extra/test_structural_visit.cc | 7 +-
tests/python/test_structural.py | 223 ++++++-
20 files changed, 2433 insertions(+), 268 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 96f214c2..2e76a758 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -77,6 +77,7 @@ set(_tvm_ffi_extra_objs_sources
"${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/structural_equal.cc"
"${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/structural_hash.cc"
"${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/structural_visit.cc"
+ "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/structural_mutate.cc"
"${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/visit_error_context.cc"
"${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/json_parser.cc"
"${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/json_writer.cc"
diff --git a/docs/concepts/structural_eq_hash.rst
b/docs/concepts/structural_eq_hash.rst
index f3acc3a7..6b998918 100644
--- a/docs/concepts/structural_eq_hash.rst
+++ b/docs/concepts/structural_eq_hash.rst
@@ -15,13 +15,18 @@
specific language governing permissions and limitations
under the License.
-Structural Equality, Hashing, and Walk
-======================================
+Structural Equality, Hashing, Walking, and Mapping
+==================================================
TVM FFI provides ``structural_equal`` and ``structural_hash`` for the
object graph. These compare objects by **content** — recursively walking
fields — rather than by pointer identity.
+The same reflection metadata also drives ``structural_walk`` for analyses and
+``structural_map`` for rewrites. Their low-level engines,
+``StructuralVisitor`` and ``StructuralMutator``, let custom object types
+participate in the same traversal protocol.
+
The behavior is controlled by two layers of annotation on
:func:`~tvm_ffi.dataclasses.py_class`:
@@ -971,185 +976,294 @@ And in Python:
assert structural_hash(f1) == structural_hash(f2) # same hash
-Structural Walk and Visit
--------------------------
+Structural Walk and Map
+------------------------------
-``structural_equal`` and ``structural_hash`` are built on a structural
traversal
-of the value graph. ``structural_walk`` exposes that traversal directly: it
-visits containers, object fields, and POD leaves, and invokes user callbacks
for
-values whose runtime type matches a callback entry.
+Structural walk and map use the same type metadata, field flags, and
+container registrations as structural equality and hashing. The default
+reflected traversal visits only structural fields, skips fields marked
+``structural_eq="ignore"``, and preserves definition-region information from
+fields marked as definitions.
-It is useful when you want to collect information, validate a tree, find a
node, or
-stop traversal early without writing a custom equality/hash hook.
+``Map`` and ``Dict`` keys are structural anchors. Both APIs recurse through
+container values only: walk callbacks do not observe keys, and map callbacks do
+not replace them. The map or dict object itself still participates in callback
+dispatch normally.
-Basic Walk
-~~~~~~~~~~
+.. code-block:: python
+
+ import tvm_ffi
+
+ table = tvm_ffi.Map({1: 2})
+ visited = []
+
+ tvm_ffi.structural_walk(table, (int, visited.append))
+ assert visited == [2]
+
+ mapped = tvm_ffi.structural_map(table, (int, lambda value: value + 10))
+ assert mapped[1] == 12
+ assert 11 not in mapped
+
+There are two layers of API:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 24 38 38
+
+ * - API
+ - Purpose
+ - Typical use
+ * - :func:`~tvm_ffi.structural_walk`
+ - Inspect a value graph without replacing values
+ - Collect information, validate IR, or stop at a match
+ * - :func:`~tvm_ffi.structural_map`
+ - Recursively replace values and rebuild changed paths
+ - Rewriting and compiler optimization passes
+ * - :class:`~tvm_ffi.StructuralVisitor`
+ - Low-level recursive visit engine
+ - Implementing ``__s_visit__`` or a language binding
+ * - :class:`~tvm_ffi.StructuralMutator`
+ - Low-level recursive mutation engine
+ - Implementing custom mutation hooks and identity substitution
+
+``structural_walk`` and ``structural_map`` construct the corresponding
low-level
+object, install callback-aware dispatch, run it on the root, and return the
final
+result. Applications normally use these two functions directly. Custom object
+hooks receive the low-level visitor or mutator so that recursive calls remain
in
+the same traversal.
+
+StructuralVisitor and StructuralMutator
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A :class:`~tvm_ffi.StructuralVisitor` carries recursive dispatch, the current
+definition-region kind, and any early-interruption state. Its main operations
+are:
+
+- ``visitor.visit(value)`` visits a child with the same visitor.
+- ``visitor.def_region_kind()`` reports the active definition-region kind.
+- ``visitor.with_def_region_kind(kind, callback)`` temporarily changes that
kind
+ while ``callback`` performs recursive visits.
+
+The default visitor dispatches to a type's ``__s_visit__`` hook when present.
+Otherwise POD values are leaves and object-backed values are visited through
+their reflected structural fields. Array and List have built-in hooks that
+visit their elements; Map and Dict hooks visit values while skipping keys.
+
+A :class:`~tvm_ffi.StructuralMutator` adds ownership and replacement semantics.
+Its main operations are:
+
+- ``mutator.mutate(value)`` maps without intentionally modifying ``value``.
+- ``mutator.maybe_inplace_mutate(value)`` permits a type-specific
implementation
+ to reuse a safely mutable value and otherwise falls back to ``mutate``.
+- ``mutator.var_remap_get(var)`` and ``mutator.var_remap_set(var, mapped)``
+ access the current identity-substitution environment.
+- ``def_region_kind`` and ``with_def_region_kind`` have the same role as on the
+ visitor.
+
+String and Bytes are returned unchanged by default and are never mutated in
+place. For reflected objects, ``mutate`` starts from a shallow copy,
+recursively maps each structural field, and installs mapped fields in that
copy.
+If no field changes, it returns the original object instead. A nested change
+therefore copies only the objects along the changed path; unchanged children
+remain shared.
+``maybe_inplace_mutate`` is an explicit optimization path. A type-specific
+``__s_maybe_inplace_mutate__`` hook owns the safety policy and may reuse its
+input. Without that hook, the default implementation calls ``mutate``.
+
+.. note::
+
+ Visitor and mutator instances are supplied by an active traversal. Python
+ code normally receives them as arguments to ``__s_visit__``,
``__s_mutate__``,
+ or ``__s_maybe_inplace_mutate__`` rather than constructing them directly.
+
+Structural Walk
+~~~~~~~~~~~~~~~
+
+:func:`~tvm_ffi.structural_walk` invokes an analysis callback at each matching
+value. Callback entries are ordered, and only the first matching entry runs.
+A walk is post-order by default.
+A callback may return:
+
+- :attr:`~tvm_ffi.WalkResult.ADVANCE` or ``None`` to continue.
+- :attr:`~tvm_ffi.WalkResult.SKIP` to skip the current value's children. This
is
+ primarily useful with pre-order traversal.
+- :class:`~tvm_ffi.VisitInterrupt` to stop the entire traversal and return a
+ payload.
-Pass callbacks as ordered ``(type, callback)`` entries. The first matching
-entry runs for each visited value. Normal Python callbacks receive one
-argument, ``value``.
+For example, the following analysis records integer leaves and stops at the
+first negative value:
.. code-block:: python
import tvm_ffi
- visited = []
+ integers = []
- def on_int(value):
- visited.append(value)
- if value == 0:
+ def visit_int(value):
+ integers.append(value)
+ if value < 0:
return tvm_ffi.VisitInterrupt(value)
return tvm_ffi.WalkResult.ADVANCE
- result = tvm_ffi.structural_walk(root, (int, on_int), order="pre")
+ interrupted = tvm_ffi.structural_walk(
+ function,
+ (int, visit_int),
+ )
- if result is not None:
- print("stopped at", result.value)
+ if interrupted is not None:
+ print("first negative value:", interrupted.value)
-Callbacks may return:
+Walking never replaces a value. Side effects should be limited to the
+analysis state owned by the callback.
-- ``WalkResult.ADVANCE`` to continue into children.
-- ``WalkResult.SKIP`` to skip the current value's children.
-- ``VisitInterrupt(payload)`` to stop the entire walk and return an interrupt
- carrying ``payload``.
-- ``None`` as shorthand for ``WalkResult.ADVANCE``.
+Structural Map
+~~~~~~~~~~~~~~
-Grouped Types
-~~~~~~~~~~~~~
+:func:`~tvm_ffi.structural_map` uses the same typed callback selection but each
+callback returns the mapped value: either its input unchanged or a replacement.
+Mapping always visits all structural children; there is no ``SKIP`` or
+``VisitInterrupt`` result. A callback exception aborts the mapping and
+is propagated with structural visit context. Mapping is post-order by default,
+so a callback receives a value whose children have already been mapped.
-Several types can share one callback by passing a tuple of types:
+Post-order is natural for bottom-up compiler rewrites because children have
+already been mapped when the callback runs:
.. code-block:: python
- numbers = []
- strings = []
+ def fold_add(add):
+ if isinstance(add.lhs, IntImm) and isinstance(add.rhs, IntImm):
+ return IntImm(add.lhs.value + add.rhs.value)
+ return add
- tvm_ffi.structural_walk(
- root,
- [
- ((int, float), lambda value: numbers.append(value)),
- (str, lambda value: strings.append(value)),
- ],
+ optimized = tvm_ffi.structural_map(
+ function,
+ (Add, fold_add),
)
-This is normalized as if the same callback had been registered separately for
-``int`` and ``float``. Callback entries are still tried in order, so broad
-callbacks should usually come after more specific ones.
-
-Catch-All and Object Callbacks
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Map callbacks must follow map semantics: they must not mutate their input in
+place. The surrounding traversal may still reuse storage through an explicit
+``__s_maybe_inplace_mutate__`` hook. In pre-order, an unchanged or uniquely
+owned callback result may continue through that hook; in post-order, optional
+in-place mutation happens before the callback runs.
-``object`` and ``typing.Any`` are catch-all callbacks. They match POD leaves
-and object-backed values.
+Callback Selection and Order
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-.. code-block:: python
+Both Python functions accept the same callback forms:
- from typing import Any
+- ``(Type, callback)`` for one type.
+- ``((TypeA, TypeB), callback)`` to share one callback across types.
+- A sequence of callback entries.
+- A bare callable as a ``typing.Any`` catch-all.
- seen = []
- tvm_ffi.structural_walk(root, (Any, lambda value: seen.append(value)))
+``typing.Any`` and ``object`` match both POD and object-backed values.
+``tvm_ffi.Object`` matches only object-backed FFI values. Entries are tested
in
+the order supplied, so place specific types before broad catch-all callbacks.
-``tvm_ffi.Object`` is different: it matches only object-backed FFI values, such
-as ``Array``, ``Map``, ``Function``, ``String`` objects, or registered object
-classes. It does not match POD leaves such as ``int`` or ``float``.
+Both APIs default to post-order. The ``order`` argument controls the
+relationship between callbacks and children:
-.. code-block:: python
+- In pre-order, a walk callback runs before the children. For mapping, the
+ callback result becomes the value whose children are subsequently mapped.
+- In post-order, children are processed first. A map callback therefore
receives
+ the value with its mapped children already installed.
- objects = []
- leaves = []
+Definition Regions
+~~~~~~~~~~~~~~~~~~
- tvm_ffi.structural_walk(
- root,
- [
- (tvm_ffi.Object, lambda value: objects.append(value)),
- (object, lambda value: leaves.append(value)),
- ],
- )
+Callbacks passed through ``with_def_region_kind`` receive
+``(value, def_region_kind)``. The kind is one of:
-Def-Region Aware Walk
-~~~~~~~~~~~~~~~~~~~~~
+- ``DefRegionKind.NONE`` for an ordinary use.
+- ``DefRegionKind.DEF_RECURSIVE`` for a recursive definition region.
+- ``DefRegionKind.DEF_NON_RECURSIVE`` for a non-recursive definition.
-Callbacks passed to ``with_def_region_kind`` receive a second argument that
-reports whether the current value is visited as a definition or a use. This is
-useful for analyses such as collecting variable uses while skipping binders:
+The field annotations described earlier in this document establish these
+regions. Recursive definitions propagate the definition mode into the defined
+value's children. A non-recursive definition applies to the FreeVar identity
+itself, while its unannotated children are treated as ordinary uses. Explicit
+nested definition annotations establish their own region.
.. code-block:: python
uses = []
tvm_ffi.structural_walk(
- func,
+ function,
with_def_region_kind=(
Var,
lambda var, kind: (
- uses.append(var) if kind == tvm_ffi.DefRegionKind.NONE else None
+ uses.append(var)
+ if kind == tvm_ffi.DefRegionKind.NONE
+ else None
),
),
)
-For a function node, parameters are visited in a definition region, while
-occurrences in the body are visited with ``DefRegionKind.NONE``.
+``structural_map`` accepts the same def-region-aware callback form, but the
+callback must return the mapped value.
-Traversal Order
-~~~~~~~~~~~~~~~
+Custom Visit and Mutation Hooks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-The default order is pre-order: callbacks run before visiting children.
-Post-order callbacks run after children.
+A type with non-standard child storage can define ``__s_visit__``. The hook
+receives the active visitor and the current value, recursively visits every
+structural child, and returns an interrupt if one occurs:
.. code-block:: python
- trace = []
-
- tvm_ffi.structural_walk(
- tvm_ffi.Array([tvm_ffi.Array([1]), 2]),
- [
- (tvm_ffi.Array, lambda value: trace.append(f"array:{len(value)}")),
- (int, lambda value: trace.append(f"int:{value}")),
- ],
- order="post",
- )
-
- assert trace == ["int:1", "array:1", "int:2", "array:2"]
-
-C++ Walk
+ @staticmethod
+ def __s_visit__(visitor, value):
+ return visitor.visit(value.children)
+
+A custom ``__s_mutate__`` hook similarly receives the active mutator. It
should
+recursively call ``mutator.mutate`` and return a new value only when needed.
+An optional ``__s_maybe_inplace_mutate__`` hook may implement an in-place
+optimization. Callers use ``mutate`` for shared objects and call
+``maybe_inplace_mutate`` only when the input is safe to mutate, so the optional
+hook may rely on that ownership guarantee. A type defining it must also define
+``__s_mutate__``. If the optional hook is absent, ``maybe_inplace_mutate``
uses
+the default non-in-place mutation; generic reflected fields are never mutated
+in place automatically.
+
+When an object marked ``structural_eq="var"`` or ``structural_eq="dag"``
registers
+either ``__s_mutate__`` or ``__s_maybe_inplace_mutate__`` hooks, it should:
+
+1. call ``var_remap_get`` before recursively mutating the value;
+2. immediately return the mapped value on a hit;
+3. compute the final result on a miss; and
+4. call ``var_remap_set`` with that final result before returning it.
+
+Structural-map callbacks apply the same identity rule automatically: the
complete
+callback/default result is recorded on the first occurrence and reused for
later
+occurrences of the same FreeVar or DAG node.
+
+C++ APIs
~~~~~~~~
-C++ code can use ``StructuralWalk`` with typed callbacks. Callbacks are tried
-in order and dispatch on the first argument type:
+C++ provides typed counterparts. Callback dispatch uses the first argument
+type and accepts an optional second ``TVMFFIDefRegionKind`` argument. The
+``Expected`` forms report failures without throwing:
.. code-block:: cpp
- Optional<VisitInterrupt> result = StructuralWalk<WalkOrder::kPreOrder>(
+ Expected<Optional<VisitInterrupt>> walked =
+ StructuralWalkExpected<WalkOrder::kPreOrder>(
+ root,
+ [&](const Add& add) -> Expected<WalkResult> {
+ ++num_adds;
+ return WalkResult::Advance();
+ });
+
+ Expected<Any> mapped = StructuralMapExpected<WalkOrder::kPostOrder>(
root,
- [&](const Add& add) -> Expected<WalkResult> {
- ++num_adds;
- return WalkResult::Advance();
- },
- [&](const Mul& mul) -> Expected<WalkResult> {
- return WalkResult::Skip();
+ [&](const Add& add) -> Expected<Any> {
+ return FoldAdd(add);
});
-C++ callbacks dispatch on their first argument, which may be ``AnyView``,
-``Any``, an ``ObjectRef`` subclass, an ``Object`` pointer type, or another
-FFI-convertible POD type. They may also take an optional second
-``TVMFFIDefRegionKind`` argument to distinguish definition sites from uses.
-Errors should be returned as ``Expected<WalkResult>``.
-
-Low-Level ``StructuralVisitor``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-``StructuralVisitor`` is the lower-level traversal object. It is mainly useful
-inside structural visit hooks or C++ integrations that need to participate in
-the same recursive traversal protocol.
-
-Python users normally call ``structural_walk`` instead. The low-level visitor
-API exposes:
-
-- ``visitor.visit(value)`` to recursively visit a child value.
-- ``visitor.def_region_kind()`` to inspect the current definition-region mode.
-- ``visitor.with_def_region_kind(kind, callback)`` to run a recursive visit
- under a temporary definition-region mode.
-
-Custom visit hooks are registered as the ``__s_visit__`` type attribute. They
-receive the active visitor and the current object, and are responsible for
-calling ``visitor.visit(child)`` on structural children.
+Walk callbacks return ``Expected<WalkResult>``. Map callbacks return
+``Expected<Any>`` and must obey the same non-in-place callback contract as the
+Python API. For ``Map`` and ``Dict``, both APIs process values and skip keys.
+``StructuralWalk`` and ``StructuralMap`` are the corresponding throwing
+convenience forms.
diff --git a/docs/guides/rust_lang_guide.md b/docs/guides/rust_lang_guide.md
index 226b5e31..717f1205 100644
--- a/docs/guides/rust_lang_guide.md
+++ b/docs/guides/rust_lang_guide.md
@@ -241,6 +241,10 @@ structural_walk(&values, &mut probe, WalkOrder::PreOrder)?;
assert_eq!(probe.total, 6);
```
+For `Map` and `Dict`, structural walk treats keys as structural anchors: it
+visits container values but does not pass keys to handlers. The map or dict
+object itself is still visited normally.
+
Lambdas also work — pass a single typed lambda, or a tuple of them (up to 8)
tried in order with the first matching argument type winning, like the
variadic C++ `StructuralWalk(root, callbacks...)` chain. Unmatched values
diff --git a/docs/reference/python/index.rst b/docs/reference/python/index.rst
index b5227f94..80a66725 100644
--- a/docs/reference/python/index.rst
+++ b/docs/reference/python/index.rst
@@ -83,13 +83,16 @@ Structural
StructuralKey
StructuralVisitor
+ StructuralMutator
VisitInterrupt
+ DefRegionKind
WalkOrder
WalkResult
get_first_structural_mismatch
structural_equal
structural_hash
structural_walk
+ structural_map
Global Registry
diff --git a/include/tvm/ffi/container/dict.h b/include/tvm/ffi/container/dict.h
index e75ea2cd..32751a09 100644
--- a/include/tvm/ffi/container/dict.h
+++ b/include/tvm/ffi/container/dict.h
@@ -41,6 +41,15 @@ namespace ffi {
/*! \brief Dict object — mutable map with shared reference semantics. */
class DictObj : public MapBaseObj {
public:
+ /*!
+ * \brief Create a shallow copy with the same iteration order.
+ * \param src The source dictionary object.
+ * \return The copied underlying dictionary storage.
+ */
+ static ObjectPtr<Object> ShallowCopy(const DictObj* src) {
+ return MapBaseObj::CopyFrom<DictObj>(const_cast<DictObj*>(src));
+ }
+
/// \cond Doxygen_Suppress
static constexpr const int32_t _type_index = TypeIndex::kTVMFFIDict;
static const constexpr bool _type_final = true;
diff --git a/include/tvm/ffi/container/map.h b/include/tvm/ffi/container/map.h
index 10013be9..5c0807ca 100644
--- a/include/tvm/ffi/container/map.h
+++ b/include/tvm/ffi/container/map.h
@@ -39,6 +39,15 @@ namespace ffi {
/*! \brief Map object */
class MapObj : public MapBaseObj {
public:
+ /*!
+ * \brief Create a shallow copy with the same iteration order.
+ * \param src The source map object.
+ * \return The copied underlying map storage.
+ */
+ static ObjectPtr<Object> ShallowCopy(const MapObj* src) {
+ return MapBaseObj::CopyFrom<MapObj>(const_cast<MapObj*>(src));
+ }
+
/// \cond Doxygen_Suppress
static constexpr const int32_t _type_index = TypeIndex::kTVMFFIMap;
static const constexpr bool _type_final = true;
diff --git a/include/tvm/ffi/extra/structural_mutate.h
b/include/tvm/ffi/extra/structural_mutate.h
new file mode 100644
index 00000000..908bb5e8
--- /dev/null
+++ b/include/tvm/ffi/extra/structural_mutate.h
@@ -0,0 +1,919 @@
+/*
+ * 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.
+ */
+/*!
+ * \file tvm/ffi/extra/structural_mutate.h
+ * \brief Structural mutation API with optional in-place optimization.
+ */
+#ifndef TVM_FFI_EXTRA_STRUCTURAL_MUTATE_H_
+#define TVM_FFI_EXTRA_STRUCTURAL_MUTATE_H_
+
+#include <tvm/ffi/any.h>
+#include <tvm/ffi/c_api.h>
+#include <tvm/ffi/cast.h>
+#include <tvm/ffi/container/array.h>
+#include <tvm/ffi/container/map.h>
+#include <tvm/ffi/container/tuple.h>
+#include <tvm/ffi/container/variant.h>
+#include <tvm/ffi/expected.h>
+#include <tvm/ffi/extra/structural_visit.h>
+#include <tvm/ffi/extra/visit_error_context.h>
+#include <tvm/ffi/function.h>
+#include <tvm/ffi/function_details.h>
+#include <tvm/ffi/optional.h>
+#include <tvm/ffi/reflection/accessor.h>
+
+#include <cstddef>
+#include <exception>
+#include <optional>
+#include <string>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+
+namespace tvm {
+namespace ffi {
+
+class StructuralMutatorObj;
+
+/*!
+ * \brief ABI callback type for structural mutation.
+ *
+ * \param mutator The active structural mutator.
+ * \param value The borrowed value to mutate.
+ * \return Raw ``TVMFFIAny`` containing the mutated value or an Error.
+ */
+using FStructuralMutate = TVMFFIAny (*)(StructuralMutatorObj* mutator, AnyView
value) noexcept;
+
+/*!
+ * \brief ABI callback type for looking up an identity substitution.
+ *
+ * \param mutator The active structural mutator.
+ * \param var The borrowed variable identity to look up.
+ * \return Raw ``TVMFFIAny`` containing the owning mapped value, FFI None when
no mapping exists,
+ * or an Error.
+ */
+using FStructuralVarRemapGet = TVMFFIAny (*)(StructuralMutatorObj* mutator,
AnyView var) noexcept;
+
+/*!
+ * \brief ABI callback type for recording an identity substitution.
+ *
+ * \param mutator The active structural mutator.
+ * \param var The borrowed variable identity to bind.
+ * \param mapped_value The borrowed replacement value.
+ * \return Raw ``TVMFFIAny`` containing FFI None on success or an Error.
+ */
+using FStructuralVarRemapSet = TVMFFIAny (*)(StructuralMutatorObj* mutator,
AnyView var,
+ AnyView mapped_value) noexcept;
+
+namespace details {
+
+// Copy and structurally mutate the reflected fields of an object-backed value.
+TVM_FFI_INLINE static Expected<Any>
MutateReflectedFieldsExpected(StructuralMutatorObj* mutator,
+ AnyView
value) noexcept;
+
+} // namespace details
+
+/*!
+ * \brief VTable ABI for \ref StructuralMutator dispatch.
+ */
+struct StructuralMutatorVTable {
+ /*!
+ * \brief Mutate a value without modifying the source in place.
+ *
+ * \param mutator The active structural mutator.
+ * \param value The borrowed value to mutate.
+ * \return Raw ``TVMFFIAny`` carrying the mutated value or Error.
+ */
+ FStructuralMutate mutate = nullptr;
+ /*!
+ * \brief Mutate a value, permitting an in-place implementation when it is
safe.
+ *
+ * \param mutator The active structural mutator.
+ * \param value The borrowed value to mutate.
+ * \return Raw ``TVMFFIAny`` carrying the mutated value or Error.
+ *
+ * The returned value may refer to the same object as \p value when the
implementation mutates
+ * that object in place.
+ */
+ FStructuralMutate maybe_inplace_mutate = nullptr;
+ /*!
+ * \brief Look up the replacement for a variable identity.
+ *
+ * \param mutator The active structural mutator.
+ * \param var The borrowed variable identity to look up.
+ * \return Raw ``TVMFFIAny`` carrying the owning replacement, FFI None on a
miss, or Error.
+ */
+ FStructuralVarRemapGet var_remap_get = nullptr;
+ /*!
+ * \brief Record the replacement for a variable identity.
+ *
+ * \param mutator The active structural mutator.
+ * \param var The borrowed variable identity to bind.
+ * \param mapped_value The borrowed replacement value.
+ * \return Raw ``TVMFFIAny`` carrying None or Error.
+ */
+ FStructuralVarRemapSet var_remap_set = nullptr;
+};
+
+/*!
+ * \brief Object node of a structural mutator.
+ */
+class StructuralMutatorObj : public Object {
+ public:
+ /*!
+ * \brief Mutate a value through the mutator vtable.
+ *
+ * \param value The value to mutate.
+ * \return The mutated owning value.
+ * \throws Error if mutation fails.
+ *
+ * This entry point never intentionally mutates \p value in place. Recursive
mutations
+ * also use \ref Mutate.
+ */
+ TVM_FFI_INLINE Any Mutate(AnyView value) { return
MutateExpected(value).value(); }
+
+ /*!
+ * \brief Exception-free form of \ref Mutate.
+ *
+ * \param value The value to mutate.
+ * \return The mutated owning value, or an Error if mutation failed.
+ */
+ TVM_FFI_INLINE Expected<Any> MutateExpected(AnyView value) noexcept {
+ return
details::ExpectedUnsafe::MoveFromTVMFFIAny<Any>((*vtable_->mutate)(this,
value));
+ }
+
+ /*!
+ * \brief Mutate a value, permitting an in-place implementation when it is
safe.
+ *
+ * \param value The borrowed value to mutate.
+ * \return The mutated owning value.
+ * \throws Error if mutation fails.
+ *
+ * The returned value may refer to the same object as \p value. Callers must
use the return value
+ * as the result of the mutation rather than assuming that the input object
was reused.
+ */
+ TVM_FFI_INLINE Any MaybeInplaceMutate(AnyView value) {
+ return MaybeInplaceMutateExpected(value).value();
+ }
+
+ /*!
+ * \brief Exception-free form of \ref MaybeInplaceMutate.
+ *
+ * \param value The borrowed value to mutate.
+ * \return The mutated owning value, or an Error if mutation failed.
+ */
+ TVM_FFI_INLINE Expected<Any> MaybeInplaceMutateExpected(AnyView value)
noexcept {
+ return details::ExpectedUnsafe::MoveFromTVMFFIAny<Any>(
+ (*vtable_->maybe_inplace_mutate)(this, value));
+ }
+
+ /*!
+ * \brief Mutate a value, using in-place mutation only for a uniquely owned
object.
+ *
+ * \param value The borrowed value to mutate.
+ * \return The mutated owning value, or an Error if mutation failed.
+ */
+ TVM_FFI_INLINE Expected<Any> MaybeInplaceMutateIfUniqueExpected(AnyView
value) noexcept {
+ const Object* obj = value.as<Object>();
+ if (obj != nullptr && obj->unique()) {
+ return MaybeInplaceMutateExpected(value);
+ }
+ return MutateExpected(value);
+ }
+
+ /*!
+ * \brief Apply the default structural mutation with copy-on-write behavior.
+ *
+ * \param value The value to mutate.
+ * \return The mutated value, or an Error if hook dispatch, copying, or
field mutation failed.
+ *
+ * \note A registered ``__s_mutate__`` hook is dispatched before the
reflected fallback and is
+ * responsible for variable-remap lookup and insertion when it
represents a FreeVar or DAG
+ * identity. Automatic remapping applies only to the reflected
fallback.
+ */
+ TVM_FFI_INLINE Expected<Any> DefaultMutateExpected(AnyView value) noexcept {
+ int32_t type_index = value.type_index();
+ static reflection::TypeAttrColumn
column(reflection::type_attr::kStructuralMutate);
+ AnyView attr = column[type_index];
+ if (attr.type_index() != TypeIndex::kTVMFFINone) {
+ if (attr.type_index() == TypeIndex::kTVMFFIOpaquePtr) {
+ auto* hook = reinterpret_cast<FStructuralMutate>(attr.cast<void*>());
+ return details::ExpectedUnsafe::MoveFromTVMFFIAny<Any>((*hook)(this,
value));
+ }
+ if (attr.type_index() == TypeIndex::kTVMFFIFunction) {
+ return attr.cast<Function>().CallExpected<Any>(this, value);
+ }
+ return Unexpected(Error(
+ "TypeError", "__s_mutate__ must be an opaque function pointer or
ffi.Function", ""));
+ }
+ if (type_index < TypeIndex::kTVMFFIStaticObjectBegin) {
+ return Any(value);
+ }
+ return MutateWithIdentityRemapExpected(value, [&]() -> Expected<Any> {
+ return details::MutateReflectedFieldsExpected(this, value);
+ });
+ }
+
+ /*!
+ * \brief Apply custom maybe-in-place mutation, or fall back to non-in-place
mutation.
+ *
+ * \param value The borrowed value to mutate.
+ * \return The mutated owning value, or an Error if mutation failed.
In-place changes
+ * completed before an Error are not rolled back.
+ *
+ * \note In-place mutation is explicitly opt-in. A registered
+ * ``__s_maybe_inplace_mutate__`` hook may rely on its input being
safe to mutate and owns
+ * any variable-remap handling. When the hook is absent, this method
calls
+ * \ref DefaultMutateExpected.
+ */
+ TVM_FFI_INLINE Expected<Any> DefaultMaybeInplaceMutateExpected(AnyView
value) noexcept {
+ int32_t type_index = value.type_index();
+ static reflection::TypeAttrColumn maybe_inplace_mutate_column(
+ reflection::type_attr::kStructuralMaybeInplaceMutate);
+ AnyView maybe_inplace_mutate_attr =
maybe_inplace_mutate_column[type_index];
+ if (maybe_inplace_mutate_attr.type_index() == TypeIndex::kTVMFFIOpaquePtr)
{
+ auto* hook =
reinterpret_cast<FStructuralMutate>(maybe_inplace_mutate_attr.cast<void*>());
+ return details::ExpectedUnsafe::MoveFromTVMFFIAny<Any>((*hook)(this,
value));
+ }
+ if (maybe_inplace_mutate_attr.type_index() == TypeIndex::kTVMFFIFunction) {
+ return
maybe_inplace_mutate_attr.cast<Function>().CallExpected<Any>(this, value);
+ }
+ return DefaultMutateExpected(value);
+ }
+
+ /*!
+ * \brief Look up the replacement recorded for a variable identity.
+ *
+ * \param var The borrowed variable identity to look up.
+ * \return The owning replacement, FFI None if no replacement exists, or an
Error if lookup
+ * fails.
+ *
+ * \note The identity must have ``kTVMFFISEqHashKindFreeVar`` or
+ * ``kTVMFFISEqHashKindDAGNode`` structural-equality metadata.
+ */
+ TVM_FFI_INLINE Expected<Any> VarRemapGetExpected(AnyView var) noexcept {
+ return
details::ExpectedUnsafe::MoveFromTVMFFIAny<Any>((*vtable_->var_remap_get)(this,
var));
+ }
+
+ /*!
+ * \brief Record the replacement for a variable identity.
+ *
+ * \param var The borrowed variable identity to bind.
+ * \param mapped_value The borrowed replacement value.
+ * \return Successful completion, or an Error if the binding is invalid or
cannot be stored.
+ *
+ * \note The identity must have ``kTVMFFISEqHashKindFreeVar`` or
+ * ``kTVMFFISEqHashKindDAGNode`` structural-equality metadata.
+ */
+ TVM_FFI_INLINE Expected<void> VarRemapSetExpected(AnyView var, AnyView
mapped_value) noexcept {
+ return details::ExpectedUnsafe::MoveFromTVMFFIAny<void>(
+ (*vtable_->var_remap_set)(this, var, mapped_value));
+ }
+
+ /*!
+ * \brief Return the current def-region context.
+ * \return The active def-region kind.
+ */
+ TVM_FFI_INLINE TVMFFIDefRegionKind def_region_kind() const { return
def_region_mode_; }
+
+ /*!
+ * \brief Temporarily switch the def-region context while invoking \p
callback.
+ *
+ * \param kind The def-region kind to set during the callback.
+ * \param callback A nullary callable that performs recursive mutation.
+ * \return The value returned by \p callback.
+ */
+ template <typename Callback>
+ TVM_FFI_INLINE auto WithDefRegionKind(TVMFFIDefRegionKind kind, Callback&&
callback)
+ -> decltype(std::forward<Callback>(callback)()) {
+ class Scope {
+ public:
+ Scope(StructuralMutatorObj* mutator, TVMFFIDefRegionKind kind)
+ : mutator_(mutator), old_kind_(mutator->def_region_mode_) {
+ mutator_->def_region_mode_ = kind;
+ }
+ ~Scope() { mutator_->def_region_mode_ = old_kind_; }
+ Scope(const Scope&) = delete;
+ Scope& operator=(const Scope&) = delete;
+
+ private:
+ StructuralMutatorObj* mutator_;
+ TVMFFIDefRegionKind old_kind_;
+ };
+ Scope scope(this, kind);
+ return std::forward<Callback>(callback)();
+ }
+
+ /// \cond Doxygen_Suppress
+ static constexpr const bool _type_mutable = true;
+ TVM_FFI_DECLARE_OBJECT_INFO("ffi.StructuralMutator", StructuralMutatorObj,
Object);
+ /// \endcond
+
+ protected:
+ /*!
+ * \brief Mutate a FreeVar or DAG identity once and reuse its final result.
+ *
+ * \tparam Mutation Nullary callable returning ``Expected<Any>``.
+ * \param value The borrowed value to mutate.
+ * \param mutation The complete mutation to apply on a cache miss.
+ * \return The cached or newly computed owning result, or an Error.
+ */
+ template <typename Mutation>
+ TVM_FFI_INLINE Expected<Any> MutateWithIdentityRemapExpected(AnyView value,
+ Mutation&&
mutation) noexcept {
+ int32_t type_index = value.type_index();
+ if (type_index < TypeIndex::kTVMFFIStaticObjectBegin) {
+ return mutation();
+ }
+
+ const TVMFFITypeInfo* type_info = TVMFFIGetTypeInfo(type_index);
+ bool is_remappable_identity =
+ type_info->metadata != nullptr &&
+ (type_info->metadata->structural_eq_hash_kind ==
kTVMFFISEqHashKindFreeVar ||
+ type_info->metadata->structural_eq_hash_kind ==
kTVMFFISEqHashKindDAGNode);
+ if (!is_remappable_identity) {
+ return mutation();
+ }
+
+ Expected<Any> mapped_value = VarRemapGetExpected(value);
+ if (TVM_FFI_PREDICT_FALSE(mapped_value.is_err())) {
+ return Unexpected(std::move(mapped_value).error());
+ }
+ if (details::ExpectedUnsafe::GetData(mapped_value).type_index() !=
TypeIndex::kTVMFFINone) {
+ return mapped_value;
+ }
+
+ Expected<Any> result = mutation();
+ if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
+ return result;
+ }
+ Expected<void> set_result =
+ VarRemapSetExpected(value, details::ExpectedUnsafe::GetData(result));
+ if (TVM_FFI_PREDICT_FALSE(set_result.is_err())) {
+ return Unexpected(std::move(set_result).error());
+ }
+ return result;
+ }
+
+ /*!
+ * \brief Construct a structural mutator from an immutable dispatch vtable.
+ * \param vtable The non-null dispatch table for this mutator. It must
outlive this object.
+ */
+ explicit StructuralMutatorObj(const StructuralMutatorVTable* vtable) :
vtable_(vtable) {}
+
+ /*!
+ * \brief Non-owning pointer to the required ABI dispatch table.
+ */
+ const StructuralMutatorVTable* vtable_ = nullptr;
+
+ /*!
+ * \brief Current def-region context for def-region-aware structural
mutation.
+ */
+ TVMFFIDefRegionKind def_region_mode_ = kTVMFFIDefRegionKindNone;
+};
+
+/*!
+ * \brief ObjectRef wrapper for \ref StructuralMutatorObj.
+ *
+ * \sa StructuralMutatorObj
+ */
+class StructuralMutator : public ObjectRef {
+ public:
+ /*!
+ * \brief Construct from an existing mutator object pointer.
+ * \param n The object pointer to wrap.
+ */
+ explicit StructuralMutator(ObjectPtr<StructuralMutatorObj> n) :
ObjectRef(std::move(n)) {}
+
+ /// \cond Doxygen_Suppress
+ TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(StructuralMutator, ObjectRef,
StructuralMutatorObj);
+ /// \endcond
+};
+
+namespace details {
+
+/*!
+ * \brief Mutate the reflected structural fields of an object-backed value.
+ *
+ * \param mutator The active structural mutator.
+ * \param value The object-backed value to mutate.
+ * \return The original value when no field changes, a mutated shallow copy
otherwise, or an
+ * Error if copying or mutation failed.
+ */
+TVM_FFI_INLINE static Expected<Any>
MutateReflectedFieldsExpected(StructuralMutatorObj* mutator,
+ AnyView
value) noexcept {
+ const Object* obj = value.as<Object>();
+ int32_t type_index = obj->type_index();
+
+ static reflection::TypeAttrColumn
column(reflection::type_attr::kShallowCopy);
+ AnyView attr = column[type_index];
+ if (TVM_FFI_PREDICT_FALSE(attr.type_index() != TypeIndex::kTVMFFIFunction)) {
+ return Unexpected(Error("TypeError", "__ffi_shallow_copy__ must be an
ffi.Function", ""));
+ }
+
+ Expected<Any> result = attr.cast<Function>().CallExpected<Any>(value);
+ if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
+ return result;
+ }
+
+ const Any& result_value = details::ExpectedUnsafe::GetData(result);
+ Object* new_obj = const_cast<Object*>(result_value.as<Object>());
+ // Copy-on-write mutation requires a distinct target so partial updates
cannot modify the source.
+ if (TVM_FFI_PREDICT_FALSE(new_obj == nullptr || result.type_index() !=
value.type_index() ||
+ new_obj == obj)) {
+ return Unexpected(Error(
+ "TypeError",
+ "Shallow copy callback must return a distinct object with the same
type as its input", ""));
+ }
+
+ const TVMFFITypeInfo* type_info = TVMFFIGetTypeInfo(new_obj->type_index());
+ bool field_changed = false;
+ auto mutate_fields = [&]() {
+ reflection::ForEachFieldInfoWithEarlyStop(
+ type_info, [&](const TVMFFIFieldInfo* field_info) -> bool {
+ if (field_info->flags & kTVMFFIFieldFlagBitMaskSEqHashIgnore) {
+ return false;
+ }
+
+ Any field_value;
+ void* field_addr = reinterpret_cast<char*>(new_obj) +
field_info->offset;
+ int ret_code = field_info->getter(field_addr,
reinterpret_cast<TVMFFIAny*>(&field_value));
+ if (TVM_FFI_PREDICT_FALSE(ret_code != 0)) {
+ result = Unexpected(details::MoveFromSafeCallRaised());
+ return true;
+ }
+
+ Expected<Any> mutated_field = [&]() -> Expected<Any> {
+ if (field_info->flags &
kTVMFFIFieldFlagBitMaskSEqHashDefNonRecursive) {
+ return
mutator->WithDefRegionKind(kTVMFFIDefRegionKindNonRecursive, [&]() {
+ return mutator->MutateExpected(field_value);
+ });
+ } else if (field_info->flags &
kTVMFFIFieldFlagBitMaskSEqHashDefRecursive) {
+ return mutator->WithDefRegionKind(kTVMFFIDefRegionKindRecursive,
[&]() {
+ return mutator->MutateExpected(field_value);
+ });
+ } else {
+ return mutator->MutateExpected(field_value);
+ }
+ }();
+ if (TVM_FFI_PREDICT_FALSE(mutated_field.is_err())) {
+ result = Unexpected(std::move(mutated_field).error());
+ return true;
+ }
+ const Any& new_field =
details::ExpectedUnsafe::GetData(mutated_field);
+ if (field_value.same_as(new_field)) {
+ return false;
+ }
+
+ if (TVM_FFI_PREDICT_FALSE(field_info->setter == nullptr)) {
+ result = Unexpected(Error(
+ "TypeError",
+ "Cannot structurally mutate field `" +
+ std::string(field_info->name.data, field_info->name.size)
+ "` of type `" +
+ std::string(type_info->type_key.data,
type_info->type_key.size) +
+ "` because it does not define a setter",
+ ""));
+ return true;
+ }
+
+ ret_code = reflection::CallFieldSetter(field_info, field_addr,
+ reinterpret_cast<const
TVMFFIAny*>(&new_field));
+ if (TVM_FFI_PREDICT_FALSE(ret_code != 0)) {
+ result = Unexpected(details::MoveFromSafeCallRaised());
+ return true;
+ }
+ field_changed = true;
+ return false;
+ });
+ };
+
+ // A non-recursive definition applies to the FreeVar itself, but its fields
are uses. The
+ // complete field traversal are clamped to None, then the definition region
is restored.
+ if (mutator->def_region_kind() == kTVMFFIDefRegionKindNonRecursive &&
+ type_info->metadata != nullptr &&
+ type_info->metadata->structural_eq_hash_kind ==
kTVMFFISEqHashKindFreeVar) {
+ mutator->WithDefRegionKind(kTVMFFIDefRegionKindNone, mutate_fields);
+ } else {
+ mutate_fields();
+ }
+
+ if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
+ return result;
+ }
+ if (!field_changed) {
+ return Any(value);
+ }
+ return result;
+}
+
+} // namespace details
+
+// ---------------------------------------------------------------------------
+// Structural Map API.
+// ---------------------------------------------------------------------------
+
+namespace details {
+
+/// \cond Doxygen_Suppress
+// Return from the current mutation function if Result is an Error.
+// Append Node to the mutate error context before returning.
+#define TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(Result, Node)
\
+ do {
\
+ auto&& tvm_ffi_res_ = (Result);
\
+ if (TVM_FFI_PREDICT_FALSE(tvm_ffi_res_.type_index() ==
::tvm::ffi::TypeIndex::kTVMFFIError)) { \
+ if ((Node).type_index() >=
::tvm::ffi::TypeIndex::kTVMFFIStaticObjectBegin) { \
+ ::tvm::ffi::Error tvm_ffi_mutate_err_ = tvm_ffi_res_.error();
\
+ ::tvm::ffi::details::UpdateVisitErrorContext(tvm_ffi_mutate_err_,
\
+
(Node).cast<::tvm::ffi::ObjectRef>()); \
+ }
\
+ return ::std::move(tvm_ffi_res_);
\
+ }
\
+ } while (0)
+/// \endcond
+
+/*!
+ * \brief Structural mutator that invokes typed callbacks during recursive
mapping.
+ *
+ * \tparam order Callback placement relative to child mapping.
+ * \tparam Dispatch Callback dispatcher with signature
+ * ``Expected<Any>(AnyView, TVMFFIDefRegionKind)``.
+ * \sa StructuralMapCallbackChain
+ */
+template <WalkOrder order, typename Dispatch>
+class StructuralMapMutatorObj : public StructuralMutatorObj {
+ public:
+ /*!
+ * \brief Construct a callback-aware mutator.
+ * \param dispatch The composed callback dispatcher.
+ */
+ explicit StructuralMapMutatorObj(Dispatch dispatch)
+ : StructuralMutatorObj(VTable()), dispatch_(std::move(dispatch)) {}
+
+ private:
+ /*!
+ * \brief Return the shared callback-aware mutator vtable.
+ * \return Pointer to the immutable mutator vtable for this specialization.
+ */
+ static const StructuralMutatorVTable* VTable() {
+ static const StructuralMutatorVTable vtable{
+ &StructuralMapMutatorObj::DispatchMutate,
+ &StructuralMapMutatorObj::DispatchMaybeInplaceMutate,
+ &StructuralMapMutatorObj::DispatchVarRemapGet,
+ &StructuralMapMutatorObj::DispatchVarRemapSet,
+ };
+ return &vtable;
+ }
+
+ /*!
+ * \brief Dispatch variable-remap lookup through this mutator's vtable.
+ * \param mutator The erased callback-aware mutator.
+ * \param var The borrowed variable identity to look up.
+ * \return Raw ``TVMFFIAny`` containing the owning replacement, FFI None, or
Error.
+ */
+ static TVMFFIAny DispatchVarRemapGet(StructuralMutatorObj* mutator, AnyView
var) noexcept {
+ auto* self = static_cast<StructuralMapMutatorObj*>(mutator);
+ Expected<Any> result = self->VarRemapGetImpl(var);
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+ }
+
+ /*!
+ * \brief Dispatch variable-remap insertion through this mutator's vtable.
+ * \param mutator The erased callback-aware mutator.
+ * \param var The borrowed variable identity to bind.
+ * \param mapped_value The borrowed replacement value.
+ * \return Raw ``TVMFFIAny`` containing FFI None or Error.
+ */
+ static TVMFFIAny DispatchVarRemapSet(StructuralMutatorObj* mutator, AnyView
var,
+ AnyView mapped_value) noexcept {
+ auto* self = static_cast<StructuralMapMutatorObj*>(mutator);
+ Expected<void> result = self->VarRemapSetImpl(var, mapped_value);
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+ }
+
+ /*!
+ * \brief Look up a replacement in this mutator's identity-substitution
environment.
+ * \param var The borrowed variable identity to look up.
+ * \return The owning replacement, FFI None on a miss, or an Error.
+ */
+ Expected<Any> VarRemapGetImpl(AnyView var) noexcept {
+ if (var.type_index() < TypeIndex::kTVMFFIStaticObjectBegin) {
+ return Unexpected(
+ Error("TypeError", "Variable-remap key must be an object-backed
value", ""));
+ }
+ try {
+ ObjectRef var_ref = var.cast<ObjectRef>();
+ std::optional<Any> result = var_remap_.Get(var_ref);
+ if (!result.has_value()) {
+ return Any(nullptr);
+ }
+ return *std::move(result);
+ } catch (const Error& err) {
+ return Unexpected(err);
+ }
+ }
+
+ /*!
+ * \brief Record a replacement in this mutator's identity-substitution
environment.
+ * \param var The borrowed variable identity to bind.
+ * \param mapped_value The borrowed replacement value.
+ * \return Successful completion, or an Error if the binding cannot be
stored.
+ */
+ Expected<void> VarRemapSetImpl(AnyView var, AnyView mapped_value) noexcept {
+ if (var.type_index() < TypeIndex::kTVMFFIStaticObjectBegin) {
+ return Unexpected(
+ Error("TypeError", "Variable-remap key must be an object-backed
value", ""));
+ }
+ try {
+ ObjectRef var_ref = var.cast<ObjectRef>();
+ Any owned_mapped_value(mapped_value);
+ var_remap_.Set(var_ref, owned_mapped_value);
+ return Expected<void>();
+ } catch (const Error& err) {
+ return Unexpected(err);
+ }
+ }
+
+ /*!
+ * \brief Dispatch callback-aware optional in-place mutation through the ABI
vtable.
+ * \param mutator The erased callback-aware mutator.
+ * \param value The borrowed value to mutate.
+ * \return Raw ``TVMFFIAny`` containing the mutated value or Error.
+ */
+ static TVMFFIAny DispatchMaybeInplaceMutate(StructuralMutatorObj* mutator,
+ AnyView value) noexcept {
+ auto* self = static_cast<StructuralMapMutatorObj*>(mutator);
+ return
ExpectedUnsafe::MoveToTVMFFIAny(self->MaybeInplaceMutateImpl(value));
+ }
+
+ /*!
+ * \brief Dispatch callback-aware mutation through the ABI vtable.
+ * \param mutator The erased callback-aware mutator.
+ * \param value The borrowed value to mutate.
+ * \return Raw ``TVMFFIAny`` containing the mutated value or Error.
+ */
+ static TVMFFIAny DispatchMutate(StructuralMutatorObj* mutator, AnyView
value) noexcept {
+ auto* self = static_cast<StructuralMapMutatorObj*>(mutator);
+ return ExpectedUnsafe::MoveToTVMFFIAny(self->MutateImpl(value));
+ }
+
+ /*!
+ * \brief Invoke the callback and select mutation according to its result
and walk order.
+ *
+ * \param value The borrowed value to mutate.
+ * \return The mutated value or an Error.
+ */
+ Expected<Any> MaybeInplaceMutateImpl(AnyView value) noexcept {
+ return MutateWithIdentityRemapExpected(value, [&]() -> Expected<Any> {
+ if constexpr (order == WalkOrder::kPreOrder) {
+ Expected<Any> callback_result = dispatch_(value, def_region_kind());
+
TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(callback_result, value);
+ // A pre-order result can be mutated in place if unchanged or uniquely
owned.
+ const Any& mapped_value = ExpectedUnsafe::GetData(callback_result);
+ const TVMFFIAny* mapped_data =
AnyUnsafe::TVMFFIAnyPtrFromAny(mapped_value);
+ const TVMFFIAny input_data = value.CopyToTVMFFIAny();
+ if (mapped_data->type_index != input_data.type_index ||
+ mapped_data->zero_padding != input_data.zero_padding ||
+ mapped_data->v_int64 != input_data.v_int64) {
+ const Object* mapped_obj = mapped_value.as<Object>();
+ bool can_mutate_mapped_value_inplace = mapped_obj != nullptr &&
mapped_obj->unique();
+ Expected<Any> result = can_mutate_mapped_value_inplace
+ ?
DefaultMaybeInplaceMutateExpected(mapped_value)
+ : DefaultMutateExpected(mapped_value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(result,
mapped_value);
+ return result;
+ }
+ Expected<Any> result = DefaultMaybeInplaceMutateExpected(value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(result, value);
+ return result;
+ } else {
+ Expected<Any> result = DefaultMaybeInplaceMutateExpected(value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(result, value);
+
+ const Any& mapped_value = ExpectedUnsafe::GetData(result);
+ Expected<Any> callback_result = dispatch_(mapped_value,
def_region_kind());
+
TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(callback_result,
mapped_value);
+ return callback_result;
+ }
+ });
+ }
+
+ /*!
+ * \brief Mutate one value and invoke its matching callback in the
configured order.
+ * \param value The borrowed input value.
+ * \return The mutated value or an Error.
+ */
+ Expected<Any> MutateImpl(AnyView value) noexcept {
+ return MutateWithIdentityRemapExpected(value, [&]() -> Expected<Any> {
+ if constexpr (order == WalkOrder::kPreOrder) {
+ Expected<Any> callback_result = dispatch_(value, def_region_kind());
+
TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(callback_result, value);
+
+ const Any& mapped_value = ExpectedUnsafe::GetData(callback_result);
+ Expected<Any> result = DefaultMutateExpected(mapped_value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(result,
mapped_value);
+ return result;
+ } else {
+ Expected<Any> result = DefaultMutateExpected(value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(result, value);
+
+ const Any& mapped_value = ExpectedUnsafe::GetData(result);
+ Expected<Any> callback_result = dispatch_(mapped_value,
def_region_kind());
+
TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(callback_result,
mapped_value);
+ return callback_result;
+ }
+ });
+ }
+
+ /*! \brief Composed callback dispatcher owned by this mutator. */
+ Dispatch dispatch_;
+
+ /*! \brief Identity-substitution table. */
+ Map<ObjectRef, Any> var_remap_;
+};
+
+/*!
+ * \brief Build a callback dispatcher from a typed callback chain.
+ */
+struct StructuralMapCallbackChain {
+ public:
+ /*!
+ * \brief Construct a dispatcher owning \p callbacks.
+ * \tparam Callbacks Callback types.
+ * \param callbacks Callbacks tested in declaration order.
+ * \return A callback-aware structural-map dispatcher.
+ */
+ template <typename... Callbacks>
+ static auto FromChain(Callbacks... callbacks) {
+ auto callback_tuple = std::make_tuple(std::move(callbacks)...);
+ return [callbacks = std::move(callback_tuple)](
+ AnyView value, TVMFFIDefRegionKind kind) mutable ->
Expected<Any> {
+ try {
+ std::optional<Expected<Any>> result;
+ // Fold expression: each TryCallLink returns empty std::optional on
no-match
+ // (falsy) or a result on match (truthy); || short-circuits on first
match.
+ std::apply(
+ [&](auto&... callback) { (... || (result = TryCallLink(callback,
value, kind))); },
+ callbacks);
+ if (result.has_value()) {
+ return *std::move(result);
+ }
+ return Any(value);
+ } catch (const Error& err) {
+ return Unexpected(err);
+ }
+ };
+ }
+
+ private:
+ /*!
+ * \brief Invoke \p callback when \p value matches its first argument.
+ * \tparam Callback Callback type whose first argument selects the value
type.
+ * \param callback The callback under test.
+ * \param value The value to match and pass to the callback.
+ * \param kind The active def-region kind.
+ * \return The callback result on match, or an empty ``std::optional``
otherwise.
+ */
+ template <typename Callback>
+ TVM_FFI_INLINE static std::optional<Expected<Any>> TryCallLink(Callback&
callback, AnyView value,
+
TVMFFIDefRegionKind kind) {
+ using FuncInfo = FunctionInfo<std::decay_t<Callback>>;
+ static_assert(FuncInfo::num_args == 1 || FuncInfo::num_args == 2,
+ "StructuralMap callbacks must take one argument (value) or
two arguments "
+ "(value, def-region kind)");
+ using FirstArg = std::tuple_element_t<0, typename FuncInfo::ArgType>;
+ using TSub = std::remove_cv_t<std::remove_reference_t<FirstArg>>;
+ if constexpr (std::is_same_v<TSub, AnyView>) {
+ return InvokeCallbackLink(callback, value, kind);
+ } else if constexpr (std::is_same_v<TSub, Any>) {
+ return InvokeCallbackLink(callback, Any(value), kind);
+ } else {
+ if (auto opt = value.template as<TSub>()) {
+ return InvokeCallbackLink(callback, *std::move(opt), kind);
+ }
+ }
+ return std::nullopt;
+ }
+
+ /*!
+ * \brief Invoke a matched callback with optional def-region context.
+ * \tparam Callback Callable whose result is convertible to
``Expected<Any>``.
+ * \tparam Value Type of the converted value passed to the callback.
+ * \param callback The matched callback.
+ * \param value The converted value passed to the callback.
+ * \param kind The active def-region kind.
+ * \return The callback result converted to ``Expected<Any>``.
+ */
+ template <typename Callback, typename Value>
+ TVM_FFI_INLINE static Expected<Any> InvokeCallbackLink(Callback& callback,
Value&& value,
+ TVMFFIDefRegionKind
kind) {
+ using FuncInfo = FunctionInfo<std::decay_t<Callback>>;
+ if constexpr (FuncInfo::num_args == 1) {
+ return callback(std::forward<Value>(value));
+ } else {
+ return callback(std::forward<Value>(value), kind);
+ }
+ }
+};
+
+} // namespace details
+
+/*!
+ * \brief Map a structured value graph and invoke typed replacement callbacks.
+ *
+ * Each callback is selected by the type of its first argument. The argument
may be ``AnyView``,
+ * ``Any``, an object reference type, an object pointer type, or another
FFI-convertible POD type. A
+ * callback may optionally take a second ``TVMFFIDefRegionKind`` argument.
Callbacks are tested in
+ * declaration order and only the first strict type match is invoked. An
``AnyView`` callback
+ * argument is borrowed and must not be retained after the callback returns.
+ *
+ * Each callback should follow map semantics: it must not mutate the input in
place and should
+ * return ``Expected<Any>`` containing either the unchanged input or its
replacement. An ``Error``
+ * stops the mapping. In pre-order, an unchanged input or uniquely owned
replacement may
+ * continue through ``MaybeInplaceMutate``; a shared replacement uses
``Mutate``. In post-order,
+ * the callback runs after the node's optional in-place mutation. In-place
mutation is available
+ * only through an explicit ``__s_maybe_inplace_mutate__`` hook.
+ *
+ * Objects marked ``kTVMFFISEqHashKindFreeVar`` or
``kTVMFFISEqHashKindDAGNode`` are
+ * identity-substituted. A callback is invoked only for the first occurrence
of each identity; its
+ * final result, including an unchanged result, is reused for every later
occurrence in the same
+ * structural-map invocation.
+ *
+ * \sa WalkOrder, StructuralMutator
+ *
+ * Example:
+ *
+ * \code{.cpp}
+ * Expected<Any> result = StructuralMapExpected<WalkOrder::kPostOrder>(
+ * root,
+ * [](const IntImm& value) -> Expected<Any> {
+ * if (value->value < 0) {
+ * return Unexpected(Error("ValueError", "negative constant", ""));
+ * }
+ * return Any(IntImm(value->value + 1));
+ * },
+ * [](const Add& add, TVMFFIDefRegionKind kind) -> Expected<Any> {
+ * // In post-order, add->lhs and add->rhs have already been mapped.
+ * return Any(add);
+ * });
+ * \endcode
+ *
+ * \tparam order Whether callbacks run before or after recursively mapping
children.
+ * \tparam Callbacks Callback types whose first parameters select matching
values.
+ * \param root The borrowed root value to map.
+ * \param callbacks Callbacks tested in declaration order. Each accepts
``(value)`` or
+ * ``(value, def_region_kind)`` and should return ``Expected<Any>``.
+ * \return The mapped owning value, or an Error if mapping or a callback fails.
+ *
+ * \note Return type of each callback should be ``Expected<Any>``.
+ */
+template <WalkOrder order, typename... Callbacks>
+Expected<Any> StructuralMapExpected(AnyView root, Callbacks&&... callbacks)
noexcept {
+ static_assert(sizeof...(Callbacks) != 0, "StructuralMap requires at least
one callback");
+ auto dispatch =
+
details::StructuralMapCallbackChain::FromChain(std::forward<Callbacks>(callbacks)...);
+ using Mutator = details::StructuralMapMutatorObj<order, decltype(dispatch)>;
+ StructuralMutator mutator(make_object<Mutator>(std::move(dispatch)));
+ return mutator->MaybeInplaceMutateIfUniqueExpected(root);
+}
+
+/*!
+ * \brief Throwing form of \ref tvm::ffi::StructuralMapExpected.
+ *
+ * See \ref tvm::ffi::StructuralMapExpected for callback dispatch, ordering,
and ownership
+ * semantics.
+ *
+ * \tparam order Whether callbacks run before or after recursively mapping
children.
+ * \tparam Callbacks Callback types whose first parameters select matching
values.
+ * \param root The borrowed root value to map.
+ * \param callbacks Callbacks tested in declaration order. Each accepts
``(value)`` or
+ * ``(value, def_region_kind)`` and should return ``Expected<Any>``.
+ * \return The mapped owning value.
+ * \throws Error if mapping or a callback fails.
+ *
+ * \note Return type of each callback should be ``Expected<Any>``.
+ */
+template <WalkOrder order, typename... Callbacks>
+Any StructuralMap(AnyView root, Callbacks&&... callbacks) {
+ return StructuralMapExpected<order>(root,
std::forward<Callbacks>(callbacks)...).value();
+}
+
+} // namespace ffi
+} // namespace tvm
+
+#endif // TVM_FFI_EXTRA_STRUCTURAL_MUTATE_H_
diff --git a/include/tvm/ffi/extra/structural_visit.h
b/include/tvm/ffi/extra/structural_visit.h
index e4ea5fad..463843b6 100644
--- a/include/tvm/ffi/extra/structural_visit.h
+++ b/include/tvm/ffi/extra/structural_visit.h
@@ -218,10 +218,8 @@ class StructuralVisitorObj : public Object {
}
if (TVM_FFI_PREDICT_FALSE(attr.type_index() != TypeIndex::kTVMFFINone)) {
- return Unexpected(Error("TypeError",
-
std::string(reflection::type_attr::kStructuralVisit) +
- " must be an opaque function pointer or
ffi.Function",
- ""));
+ return Unexpected(
+ Error("TypeError", "__s_visit__ must be an opaque function pointer
or ffi.Function", ""));
}
if (type_index < TypeIndex::kTVMFFIStaticObjectBegin) {
@@ -305,42 +303,47 @@ TVM_FFI_INLINE static Expected<Optional<VisitInterrupt>>
VisitReflectedFieldsExp
StructuralVisitorObj* visitor, const Object* obj) noexcept {
int32_t type_index = obj->type_index();
const TVMFFITypeInfo* type_info = TVMFFIGetTypeInfo(type_index);
- // A non-recursive definition applies to a FreeVar itself, but not to its
children. All other
- // inherited modes propagate until an explicit field annotation overrides
them.
- TVMFFIDefRegionKind inherited_kind = visitor->def_region_kind();
- if (inherited_kind == kTVMFFIDefRegionKindNonRecursive &&
type_info->metadata != nullptr &&
+ auto visit_fields = [&]() -> Expected<Optional<VisitInterrupt>> {
+ Expected<Optional<VisitInterrupt>> result =
Optional<VisitInterrupt>(std::nullopt);
+ reflection::ForEachFieldInfoWithEarlyStop(
+ type_info, [&](const TVMFFIFieldInfo* field_info) -> bool {
+ if (field_info->flags & kTVMFFIFieldFlagBitMaskSEqHashIgnore) {
+ return false;
+ }
+
+ Any field_value;
+ const void* field_addr = reinterpret_cast<const char*>(obj) +
field_info->offset;
+ int ret_code = field_info->getter(const_cast<void*>(field_addr),
+
reinterpret_cast<TVMFFIAny*>(&field_value));
+ if (TVM_FFI_PREDICT_FALSE(ret_code != 0)) {
+ result = Unexpected(details::MoveFromSafeCallRaised());
+ return true;
+ }
+
+ if (field_info->flags &
kTVMFFIFieldFlagBitMaskSEqHashDefNonRecursive) {
+ result =
visitor->WithDefRegionKind(kTVMFFIDefRegionKindNonRecursive, [&]() {
+ return visitor->VisitExpected(field_value);
+ });
+ } else if (field_info->flags &
kTVMFFIFieldFlagBitMaskSEqHashDefRecursive) {
+ result = visitor->WithDefRegionKind(kTVMFFIDefRegionKindRecursive,
[&]() {
+ return visitor->VisitExpected(field_value);
+ });
+ } else {
+ result = visitor->VisitExpected(field_value);
+ }
+ return StructuralVisitNeedEarlyReturn(result);
+ });
+ return result;
+ };
+
+ // A non-recursive definition applies to the FreeVar itself, but its fields
are uses. The
+ // complete field traversal are clamped to None, then the definition region
is restored.
+ if (visitor->def_region_kind() == kTVMFFIDefRegionKindNonRecursive &&
+ type_info->metadata != nullptr &&
type_info->metadata->structural_eq_hash_kind ==
kTVMFFISEqHashKindFreeVar) {
- inherited_kind = kTVMFFIDefRegionKindNone;
+ return visitor->WithDefRegionKind(kTVMFFIDefRegionKindNone, visit_fields);
}
-
- Expected<Optional<VisitInterrupt>> result =
Optional<VisitInterrupt>(std::nullopt);
- reflection::ForEachFieldInfoWithEarlyStop(
- type_info, [&](const TVMFFIFieldInfo* field_info) -> bool {
- if (field_info->flags & kTVMFFIFieldFlagBitMaskSEqHashIgnore) {
- return false;
- }
-
- Any field_value;
- const void* field_addr = reinterpret_cast<const char*>(obj) +
field_info->offset;
- int ret_code = field_info->getter(const_cast<void*>(field_addr),
-
reinterpret_cast<TVMFFIAny*>(&field_value));
- if (TVM_FFI_PREDICT_FALSE(ret_code != 0)) {
- result = Unexpected(details::MoveFromSafeCallRaised());
- return true;
- }
-
- TVMFFIDefRegionKind kind = inherited_kind;
- if (field_info->flags & kTVMFFIFieldFlagBitMaskSEqHashDefNonRecursive)
{
- kind = kTVMFFIDefRegionKindNonRecursive;
- } else if (field_info->flags &
kTVMFFIFieldFlagBitMaskSEqHashDefRecursive) {
- kind = kTVMFFIDefRegionKindRecursive;
- }
-
- result =
- visitor->WithDefRegionKind(kind, [&]() { return
visitor->VisitExpected(field_value); });
- return StructuralVisitNeedEarlyReturn(result);
- });
- return result;
+ return visit_fields();
}
} // namespace details
@@ -483,13 +486,13 @@ namespace details {
* accept either ``(value)`` or ``(value, def_region_kind)``.
*/
template <WalkOrder order, typename Dispatch>
-class StructuralWalkCallbackVisitorObj : public StructuralVisitorObj {
+class StructuralWalkVisitorObj : public StructuralVisitorObj {
public:
/*!
* \brief Construct a structural walk visitor.
* \param dispatch The composed dispatcher invoked on each visited node.
*/
- explicit StructuralWalkCallbackVisitorObj(Dispatch dispatch)
+ explicit StructuralWalkVisitorObj(Dispatch dispatch)
: StructuralVisitorObj(VTable()), dispatch_(std::move(dispatch)) {}
private:
@@ -499,7 +502,7 @@ class StructuralWalkCallbackVisitorObj : public
StructuralVisitorObj {
*/
static const StructuralVisitorVTable* VTable() {
static const StructuralVisitorVTable vtable{
- &StructuralWalkCallbackVisitorObj::DispatchVisit,
+ &StructuralWalkVisitorObj::DispatchVisit,
};
return &vtable;
}
@@ -511,7 +514,7 @@ class StructuralWalkCallbackVisitorObj : public
StructuralVisitorObj {
* \return Interrupt state, or an error if traversal failed.
*/
static TVMFFIAny DispatchVisit(StructuralVisitorObj* self, AnyView value)
noexcept {
- return
static_cast<StructuralWalkCallbackVisitorObj*>(self)->VisitImpl(value);
+ return static_cast<StructuralWalkVisitorObj*>(self)->VisitImpl(value);
}
/*!
@@ -698,7 +701,7 @@ Expected<Optional<VisitInterrupt>>
StructuralWalkExpected(AnyView root,
static_assert(sizeof...(Callbacks) != 0, "StructuralWalk requires at least
one callback");
auto dispatch =
details::StructuralWalkCallbackChain::FromChain(std::forward<Callbacks>(callbacks)...);
- using Visitor = details::StructuralWalkCallbackVisitorObj<order,
decltype(dispatch)>;
+ using Visitor = details::StructuralWalkVisitorObj<order, decltype(dispatch)>;
StructuralVisitor visitor(make_object<Visitor>(std::move(dispatch)));
return visitor->VisitExpected(root);
}
diff --git a/include/tvm/ffi/reflection/accessor.h
b/include/tvm/ffi/reflection/accessor.h
index daa0f26d..0f8337d9 100644
--- a/include/tvm/ffi/reflection/accessor.h
+++ b/include/tvm/ffi/reflection/accessor.h
@@ -497,6 +497,50 @@ inline constexpr const char* kSEqual = "__s_equal__";
* visit structural children.
*/
inline constexpr const char* kStructuralVisit = "__s_visit__";
+
+/*!
+ * \brief Custom structural mutation hook used by ``StructuralMutator``.
+ *
+ * The hook receives the active mutator and the input value. It should
recursively mutate structural
+ * children through the mutator, return the input unchanged if no fields are
changed, and avoid
+ * intentionally mutating the source object. The input is borrowed and remains
valid for the
+ * duration of the call.
+ *
+ * Value type: either an opaque function pointer to a C++ structural mutation
hook
+ *
+ * ``TVMFFIAny (*)(StructuralMutatorObj* mutator, AnyView value) noexcept``
+ *
+ * returning raw ``Expected<Any>`` storage, or an ``ffi::Function`` with
signature
+ *
+ * ``(StructuralMutator mutator, Any value) -> Any``.
+ *
+ * This is the canonical mutation hook. A type that defines
+ * ``kStructuralMaybeInplaceMutate`` must also define this attribute.
+ */
+inline constexpr const char* kStructuralMutate = "__s_mutate__";
+/*!
+ * \brief Optional custom mutation hook that may reuse its input object.
+ *
+ * The hook receives the active mutator and a borrowed input value that is
safe to mutate in place.
+ * Callers must route shared objects through non-in-place mutation instead.
The hook may mutate and
+ * return the source object, delegate to non-in-place mutation, or return
another replacement.
+ *
+ * Value type: either an opaque function pointer to a C++ structural mutation
hook
+ *
+ * ``TVMFFIAny (*)(StructuralMutatorObj* mutator, AnyView value) noexcept``
+ *
+ * returning raw ``Expected<Any>`` storage, or an
+ * ``ffi::Function`` with signature
+ *
+ * ``(StructuralMutator mutator, Any value) -> Any``.
+ *
+ * This hook is optional. When it is absent,
``DefaultMaybeInplaceMutateExpected`` falls back to
+ * non-in-place mutation through ``kStructuralMutate`` or reflected structural
fields. In-place
+ * mutation is therefore explicitly opt-in and is never inferred from
ownership by the reflected
+ * fallback.
+ */
+inline constexpr const char* kStructuralMaybeInplaceMutate =
"__s_maybe_inplace_mutate__";
+
/*!
* \brief Serialize object data to a JSON-compatible value.
*
diff --git a/python/tvm_ffi/__init__.py b/python/tvm_ffi/__init__.py
index 9ce96656..59d55d89 100644
--- a/python/tvm_ffi/__init__.py
+++ b/python/tvm_ffi/__init__.py
@@ -78,6 +78,7 @@ if TYPE_CHECKING or not _is_config_mode():
from .structural import (
DefRegionKind,
StructuralKey,
+ StructuralMutator,
StructuralVisitor,
VisitInterrupt,
WalkOrder,
@@ -85,6 +86,7 @@ if TYPE_CHECKING or not _is_config_mode():
get_first_structural_mismatch,
structural_equal,
structural_hash,
+ structural_map,
structural_walk,
)
from . import serialization
@@ -154,6 +156,7 @@ __all__ = [
"Shape",
"StreamContext",
"StructuralKey",
+ "StructuralMutator",
"StructuralVisitor",
"Tensor",
"VisitInterrupt",
@@ -184,6 +187,7 @@ __all__ = [
"structural",
"structural_equal",
"structural_hash",
+ "structural_map",
"structural_walk",
"system_lib",
"use_raw_stream",
diff --git a/python/tvm_ffi/_ffi_api.py b/python/tvm_ffi/_ffi_api.py
index 38a54fff..c19828eb 100644
--- a/python/tvm_ffi/_ffi_api.py
+++ b/python/tvm_ffi/_ffi_api.py
@@ -25,7 +25,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Mapping, MutableMapping, MutableSequence,
Sequence
from ctypes import c_void_p
- from tvm_ffi import Device, Module, Object, StructuralKey as
_StructuralKey, StructuralVisitor as _StructuralVisitor, VisitInterrupt as
_VisitInterrupt
+ from tvm_ffi import Device, Module, Object, StructuralKey as
_StructuralKey, StructuralMutator as _StructuralMutator, StructuralVisitor as
_StructuralVisitor, VisitInterrupt as _VisitInterrupt
from tvm_ffi.access_path import AccessPath
from typing import Any, Callable
# isort: on
@@ -111,6 +111,13 @@ if TYPE_CHECKING:
def StructuralHash(_0: Any, _1: bool, _2: bool, /) -> int: ...
def StructuralKey(_0: Any, /) -> _StructuralKey: ...
def StructuralKeyEqual(_0: Any, _1: Any, /) -> bool: ...
+ def StructuralMap(_0: Any, _1: Sequence[tuple[int, Callable[..., Any]]],
_2: Sequence[tuple[int, Callable[..., Any]]], _3: int, /) -> Any: ...
+ def StructuralMutatorDefRegionKind(_0: _StructuralMutator, /) -> int: ...
+ def StructuralMutatorMaybeInplaceMutate(_0: _StructuralMutator, _1: Any,
/) -> Any: ...
+ def StructuralMutatorMutate(_0: _StructuralMutator, _1: Any, /) -> Any: ...
+ def StructuralMutatorVarRemapGet(_0: _StructuralMutator, _1: Any, /) ->
Any: ...
+ def StructuralMutatorVarRemapSet(_0: _StructuralMutator, _1: Any, _2: Any,
/) -> None: ...
+ def StructuralMutatorWithDefRegionKind(_0: _StructuralMutator, _1: int,
_2: Callable[..., Any], /) -> Any: ...
def StructuralVisitorDefRegionKind(_0: _StructuralVisitor, /) -> int: ...
def StructuralVisitorVisit(_0: _StructuralVisitor, _1: Any, /) ->
_VisitInterrupt | None: ...
def StructuralVisitorWithDefRegionKind(_0: _StructuralVisitor, _1: int,
_2: Callable[..., Any], /) -> Any: ...
@@ -202,6 +209,13 @@ __all__ = [
"StructuralHash",
"StructuralKey",
"StructuralKeyEqual",
+ "StructuralMap",
+ "StructuralMutatorDefRegionKind",
+ "StructuralMutatorMaybeInplaceMutate",
+ "StructuralMutatorMutate",
+ "StructuralMutatorVarRemapGet",
+ "StructuralMutatorVarRemapSet",
+ "StructuralMutatorWithDefRegionKind",
"StructuralVisitorDefRegionKind",
"StructuralVisitorVisit",
"StructuralVisitorWithDefRegionKind",
diff --git a/python/tvm_ffi/dataclasses/py_class.py
b/python/tvm_ffi/dataclasses/py_class.py
index 75e7fc39..1109cbff 100644
--- a/python/tvm_ffi/dataclasses/py_class.py
+++ b/python/tvm_ffi/dataclasses/py_class.py
@@ -66,6 +66,8 @@ _FFI_TYPE_ATTR_NAMES: frozenset[str] = frozenset(
"__s_equal__",
"__s_hash__",
"__s_visit__",
+ "__s_mutate__",
+ "__s_maybe_inplace_mutate__",
"__data_to_json__",
"__data_from_json__",
}
diff --git a/python/tvm_ffi/structural.py b/python/tvm_ffi/structural.py
index ed87dc7d..7a91ef6d 100644
--- a/python/tvm_ffi/structural.py
+++ b/python/tvm_ffi/structural.py
@@ -33,6 +33,7 @@ from .registry import register_object
__all__ = [
"DefRegionKind",
"StructuralKey",
+ "StructuralMutator",
"StructuralVisitor",
"VisitInterrupt",
"WalkOrder",
@@ -40,17 +41,20 @@ __all__ = [
"get_first_structural_mismatch",
"structural_equal",
"structural_hash",
+ "structural_map",
"structural_walk",
]
class WalkOrder(IntEnum):
- """Callback placement before or after visiting children for structural
walks.
+ """Callback placement before or after recursively traversing children.
See Also
--------
:py:func:`tvm_ffi.structural_walk`
Walk an object graph, invoke matching callbacks.
+ :py:func:`tvm_ffi.structural_map`
+ Structurally map an object graph with replacement callbacks.
"""
@@ -87,6 +91,8 @@ class DefRegionKind(IntEnum):
:py:class:`tvm_ffi.StructuralVisitor`
Structural traversal visitor that carries object dispatch and
def-region
state across recursive visits.
+ :py:class:`tvm_ffi.StructuralMutator`
+ Structural mutator that recursively mutates values.
"""
@@ -315,7 +321,7 @@ class VisitInterrupt(Object):
return None
- result = tvm_ffi.structural_walk(root, (object, on_node))
+ result = tvm_ffi.structural_walk(root, (object, on_node), order="pre")
if result is not None:
found = result.value
@@ -408,11 +414,124 @@ class StructuralVisitor(Object):
return _ffi_api.StructuralVisitorWithDefRegionKind(self, kind,
callback)
+@register_object("ffi.StructuralMutator")
+class StructuralMutator(Object):
+ """Low-level structural mutator.
+
+ This class exposes the mutator object used by structural-map and custom
+ mutation hooks.
+ """
+
+ def maybe_inplace_mutate(self, value: Any) -> Any:
+ """Mutate ``value``, permitting an in-place implementation when safe.
+
+ The caller must ensure that an object-backed ``value`` is safe to
mutate
+ in place and use :meth:`mutate` for a shared object.
+
+ Parameters
+ ----------
+ value
+ Value to mutate.
+
+ Returns
+ -------
+ result
+ The mutated owning value. It may refer to the same object as
``value``.
+
+ """
+ return _ffi_api.StructuralMutatorMaybeInplaceMutate(self, value)
+
+ def mutate(self, value: Any) -> Any:
+ """Mutate ``value`` without modifying it in place.
+
+ The original value is returned when none of its structural fields
+ change; otherwise, the result is a mutated copy.
+
+ Parameters
+ ----------
+ value
+ Value to mutate.
+
+ Returns
+ -------
+ result
+ The mutated owning value.
+
+ """
+ return _ffi_api.StructuralMutatorMutate(self, value)
+
+ def var_remap_get(self, var: Object) -> Any | None:
+ """Return the replacement recorded for a variable identity.
+
+ Parameters
+ ----------
+ var
+ Variable identity to look up. It must be an object-backed value
+ with free-variable or DAG-node structural metadata.
+
+ Returns
+ -------
+ result
+ The recorded replacement, or ``None`` if ``var`` has no
replacement.
+
+ """
+ return _ffi_api.StructuralMutatorVarRemapGet(self, var)
+
+ def var_remap_set(self, var: Object, mapped_value: Any) -> None:
+ """Record or replace the substitution for a variable identity.
+
+ Parameters
+ ----------
+ var
+ Variable identity to bind. It must be an object-backed value with
+ free-variable or DAG-node structural metadata.
+
+ mapped_value
+ Replacement returned for subsequent lookups of ``var``.
+
+ """
+ _ffi_api.StructuralMutatorVarRemapSet(self, var, mapped_value)
+
+ def def_region_kind(self) -> DefRegionKind:
+ """Return the currently active structural def-region kind.
+
+ Returns
+ -------
+ kind
+ The active :class:`DefRegionKind`.
+
+ """
+ return DefRegionKind(_ffi_api.StructuralMutatorDefRegionKind(self))
+
+ def with_def_region_kind(
+ self,
+ kind: int,
+ callback: Callable[[], Any],
+ ) -> Any:
+ """Run ``callback`` with a temporarily active def-region kind.
+
+ Parameters
+ ----------
+ kind
+ Def-region kind to use while running ``callback``.
+
+ callback
+ Nullary callable to execute inside the scoped region.
+
+ Returns
+ -------
+ result
+ The value returned by ``callback``.
+
+ """
+ return _ffi_api.StructuralMutatorWithDefRegionKind(self, kind,
callback)
+
+
def structural_walk(
root: Any,
callbacks: tuple | Sequence | Callable = (),
with_def_region_kind: tuple | Sequence | Callable = (),
- order: str | WalkOrder = "pre",
+ order: str | WalkOrder = "post",
) -> VisitInterrupt | None:
"""Walk a value structurally and invoke the first matching typed callback.
@@ -441,8 +560,9 @@ def structural_walk(
as ``callbacks``.
order
- ``"pre"``/``WalkOrder.PREORDER`` to invoke callbacks before children,
or
- ``"post"``/``WalkOrder.POSTORDER`` to invoke callbacks after children.
+ ``"post"``/``WalkOrder.POSTORDER`` (the default) to invoke callbacks
+ after children, or ``"pre"``/``WalkOrder.PREORDER`` to invoke callbacks
+ before children.
Returns
-------
@@ -477,68 +597,163 @@ def structural_walk(
else:
raise ValueError(f"Unknown structural walk order: {order!r}")
- def normalize_callbacks(
- callbacks: tuple | Sequence | Callable,
- ) -> list[tuple[object, Callable]]:
- callback_entries = []
-
- def add_callback_entry(callback_entry: tuple) -> None:
- callback_type, fn = callback_entry
- callback_types = callback_type if isinstance(callback_type, tuple)
else (callback_type,)
- callback_entries.extend((t, fn) for t in callback_types)
-
- if callable(callbacks):
- callback_entries.append((Any, callbacks))
- elif isinstance(callbacks, tuple) and len(callbacks) == 2 and
callable(callbacks[1]):
- add_callback_entry(callbacks)
- elif isinstance(callbacks, Sequence) and not isinstance(callbacks,
(str, bytes)):
- for callback in callbacks:
- if (
- not isinstance(callback, tuple)
- or len(callback) != 2
- or not callable(callback[1])
- ):
- raise TypeError(
- "structural_walk callbacks within a sequence must be "
- "(type, callback) tuples"
- )
- add_callback_entry(callback)
- else:
- raise TypeError(
- "structural_walk callbacks must be callbacks, (type, callback)
entries, "
- "((type1, type2, ...), callback) entries, or sequences of
tuple entries"
- )
- return callback_entries
-
def wrap_callback_with_def_region_kind(fn: Callable[..., Any]) ->
Callable[[Any, int], Any]:
return lambda value, kind: fn(value, DefRegionKind(kind))
- callback_entries = normalize_callbacks(callbacks)
- callback_entries_with_def_region_kind =
normalize_callbacks(with_def_region_kind)
+ callback_entries = _normalize_callbacks(callbacks,
api_name="structural_walk")
+ callback_entries_with_def_region_kind = _normalize_callbacks(
+ with_def_region_kind, api_name="structural_walk"
+ )
entries: list[tuple[int, Callable[[Any], Any]]] = [
- (_callback_type_to_type_index(t), fn) for t, fn in callback_entries
+ (_callback_type_to_type_index(t, api_name="structural_walk"), fn)
+ for t, fn in callback_entries
]
entries_with_def_region_kind: list[tuple[int, Callable[[Any, int], Any]]]
= [
- (_callback_type_to_type_index(t),
wrap_callback_with_def_region_kind(fn))
+ (
+ _callback_type_to_type_index(t, api_name="structural_walk"),
+ wrap_callback_with_def_region_kind(fn),
+ )
for t, fn in callback_entries_with_def_region_kind
]
return _ffi_api.StructuralWalk(root, entries,
entries_with_def_region_kind, order_int)
-def _callback_type_to_type_index(callback_type: type[Any] | Any) -> int:
+def structural_map(
+ root: Any,
+ callbacks: tuple | Sequence | Callable = (),
+ with_def_region_kind: tuple | Sequence | Callable = (),
+ order: str | WalkOrder = "post",
+) -> Any:
+ """Structurally map a value using typed replacement callbacks.
+
+ Each callback must follow map semantics: it returns the unchanged input or
+ a replacement value and must not mutate its input in place.
+
+ Parameters
+ ----------
+ root
+ Root value to map.
+
+ callbacks
+ Normal callbacks. These callbacks receive one argument, ``value``, and
+ return its mapped value. Callback entries are tried in order.
+
+ May be one of:
+
+ - A single callback, used as a ``typing.Any`` catch-all.
+ - A ``(type, callback)`` entry.
+ - A grouped ``((type1, type2, ...), callback)`` entry.
+ - A sequence of entries.
+
+ Types may be builtins, registered FFI object classes, or
+ ``typing.Any``/``object`` as a catch-all.
+
+ with_def_region_kind
+ Def-region-aware callbacks. These callbacks receive
+ ``(value, def_region_kind)`` and return the mapped value. They accept
+ the same callback entry forms as ``callbacks``.
+
+ order
+ ``"post"``/``WalkOrder.POSTORDER`` (the default) to invoke callbacks
+ after children, or ``"pre"``/``WalkOrder.PREORDER`` to invoke callbacks
+ before children.
+
+ Returns
+ -------
+ result
+ The mapped owning value.
+
+ Examples
+ --------
+ .. code-block:: python
+
+ def fold_add(expr):
+ if isinstance(expr.lhs, IntImm) and isinstance(expr.rhs, IntImm):
+ return IntImm(expr.lhs.value + expr.rhs.value)
+ return expr
+
+
+ optimized = tvm_ffi.structural_map(
+ function,
+ (Add, fold_add),
+ )
+
+ """
+ if isinstance(order, WalkOrder):
+ order_int = int(order)
+ elif order in ("pre", "post"):
+ order_int = int(WalkOrder.PREORDER if order == "pre" else
WalkOrder.POSTORDER)
+ else:
+ raise ValueError(f"Unknown structural map order: {order!r}")
+
+ def wrap_callback_with_def_region_kind(fn: Callable[..., Any]) ->
Callable[[Any, int], Any]:
+ return lambda value, kind: fn(value, DefRegionKind(kind))
+
+ callback_entries = _normalize_callbacks(callbacks,
api_name="structural_map")
+ callback_entries_with_def_region_kind = _normalize_callbacks(
+ with_def_region_kind, api_name="structural_map"
+ )
+
+ entries: list[tuple[int, Callable[[Any], Any]]] = [
+ (_callback_type_to_type_index(t, api_name="structural_map"), fn)
+ for t, fn in callback_entries
+ ]
+ entries_with_def_region_kind: list[tuple[int, Callable[[Any, int], Any]]]
= [
+ (
+ _callback_type_to_type_index(t, api_name="structural_map"),
+ wrap_callback_with_def_region_kind(fn),
+ )
+ for t, fn in callback_entries_with_def_region_kind
+ ]
+ return _ffi_api.StructuralMap(root, entries, entries_with_def_region_kind,
order_int)
+
+
+def _normalize_callbacks(
+ callbacks: tuple | Sequence | Callable,
+ *,
+ api_name: str,
+) -> list[tuple[object, Callable]]:
+ """Normalize typed callback shorthand into individual callback entries."""
+ callback_entries = []
+
+ def add_callback_entry(callback_entry: tuple) -> None:
+ callback_type, fn = callback_entry
+ callback_types = callback_type if isinstance(callback_type, tuple)
else (callback_type,)
+ callback_entries.extend((t, fn) for t in callback_types)
+
+ if callable(callbacks):
+ callback_entries.append((Any, callbacks))
+ elif isinstance(callbacks, tuple) and len(callbacks) == 2 and
callable(callbacks[1]):
+ add_callback_entry(callbacks)
+ elif isinstance(callbacks, Sequence) and not isinstance(callbacks, (str,
bytes)):
+ for callback in callbacks:
+ if not isinstance(callback, tuple) or len(callback) != 2 or not
callable(callback[1]):
+ raise TypeError(
+ f"{api_name} callbacks within a sequence must be (type,
callback) tuples"
+ )
+ add_callback_entry(callback)
+ else:
+ raise TypeError(
+ f"{api_name} callbacks must be callbacks, (type, callback)
entries, "
+ "((type1, type2, ...), callback) entries, or sequences of tuple
entries"
+ )
+ return callback_entries
+
+
+def _callback_type_to_type_index(callback_type: type[Any] | Any, *, api_name:
str) -> int:
"""Convert a callback arg type to a type index."""
annotation = Any if callback_type is object else callback_type
try:
type_index =
core.TypeSchema.from_annotation(annotation).origin_type_index
except TypeError as err:
raise TypeError(
- "structural_walk callback type must be a supported builtin, "
+ f"{api_name} callback type must be a supported builtin, "
"typing.Any/object, or an FFI-registered object class"
) from err
if type_index < 0 and annotation is not Any:
raise TypeError(
- "structural_walk callback type_index is negative, the only"
+ f"{api_name} callback type_index is negative, the only "
"acceptable negative type_index is -1 for Any"
)
return type_index
diff --git a/rust/tvm-ffi/src/extra/structural_visit.rs
b/rust/tvm-ffi/src/extra/structural_visit.rs
index 1d15efd8..6d5f9f76 100644
--- a/rust/tvm-ffi/src/extra/structural_visit.rs
+++ b/rust/tvm-ffi/src/extra/structural_visit.rs
@@ -953,7 +953,7 @@ fn visit_children_raw<C: ChildVisit>(
{
// Fast path: read the MapBaseObj storage layout directly, like
// the SeqPrefix path for arrays — zero FFI calls per entry.
- // Dict entries are snapshotted first to keep the re-entrant
+ // Dict values are snapshotted first to keep the re-entrant
// mutation guard. If the one-time layout validation fails
// (e.g. an ABI-debug build), fall back to the packed-functor
// iteration protocol.
@@ -1025,9 +1025,10 @@ fn visit_sequence<C: ChildVisit>(
Ok(())
}
-/// Walk map/dict entries by reading the `MapBaseObj` storage directly —
-/// the map analog of the `SeqPrefix` array fast path. `snapshot` first
-/// takes owned copies of all entries (Dict re-entrant mutation guard).
+/// Walk map/dict values by reading the `MapBaseObj` storage directly —
+/// the map analog of the `SeqPrefix` array fast path. Keys are structural
+/// anchors and are skipped. `snapshot` first takes owned copies of all values
+/// (Dict re-entrant mutation guard).
#[inline(never)]
fn visit_map_layout<C: ChildVisit>(
value: TVMFFIAny,
@@ -1043,22 +1044,16 @@ fn visit_map_layout<C: ChildVisit>(
let mut cursor = unsafe { MapCursor::new(map) };
if snapshot {
- let mut entries: Vec<(Any, Any)> = Vec::with_capacity(size);
+ let mut values: Vec<Any> = Vec::with_capacity(size);
for _ in 0..size {
- let Some((key, val)) = (unsafe { cursor.next() }) else {
+ let Some((_, value)) = (unsafe { cursor.next() }) else {
return Err(runtime_error("native visitor: map iteration ended
early").into());
};
- entries.push((
- Any::from(unsafe { view_of(&key) }),
- Any::from(unsafe { view_of(&val) }),
- ));
+ values.push(Any::from(unsafe { view_of(&value) }));
}
- for (index, (key, val)) in entries.into_iter().enumerate() {
+ for (index, value) in values.into_iter().enumerate() {
visitor
- .visit_child(raw_of_owned(&key), def_region_kind)
- .map_err(|halt| with_error_context(halt, &format!("dict key
[{index}]")))?;
- visitor
- .visit_child(raw_of_owned(&val), def_region_kind)
+ .visit_child(raw_of_owned(&value), def_region_kind)
.map_err(|halt| with_error_context(halt, &format!("dict value
[{index}]")))?;
}
return Ok(());
@@ -1068,14 +1063,11 @@ fn visit_map_layout<C: ChildVisit>(
// callbacks, so visit them in place. The `size` bound also guards the
// dense iteration list against corruption-induced cycles.
for index in 0..size {
- let Some((key, val)) = (unsafe { cursor.next() }) else {
+ let Some((_, value)) = (unsafe { cursor.next() }) else {
return Err(runtime_error("native visitor: map iteration ended
early").into());
};
visitor
- .visit_child(key, def_region_kind)
- .map_err(|halt| with_error_context(halt, &format!("map key
[{index}]")))?;
- visitor
- .visit_child(val, def_region_kind)
+ .visit_child(value, def_region_kind)
.map_err(|halt| with_error_context(halt, &format!("map value
[{index}]")))?;
}
Ok(())
@@ -1084,10 +1076,10 @@ fn visit_map_layout<C: ChildVisit>(
/// Cold fallback used when the mirrored layout fails validation (e.g. an
/// ABI-debug build): iterate through the public packed functors. Map storage
/// is private C++; the Rust binding itself uses these iterator functors, so
-/// no structural visiting or traversal control leaves Rust. Entries are
-/// snapshotted before user callbacks run — required for Dict, whose mutation
-/// invalidates the iterator, and harmless for immutable Map on this
-/// non-performance path.
+/// no structural visiting or traversal control leaves Rust. Keys are
structural
+/// anchors and are skipped. Values are snapshotted before user callbacks run —
+/// required for Dict, whose mutation invalidates the iterator, and harmless
for
+/// immutable Map on this non-performance path.
fn visit_map<C: ChildVisit>(
value: TVMFFIAny,
visitor: &mut C,
@@ -1114,20 +1106,16 @@ fn visit_map<C: ChildVisit>(
let iter_any = Function::get_global(iter_name)?.call_packed(&[unsafe {
view_of(&value) }])?;
let iter = Function::try_from(iter_any)?;
- let mut entries = Vec::with_capacity(size);
+ let mut values = Vec::with_capacity(size);
for index in 0..size {
- let key = iter.call_packed(&[AnyView::from(&0i64)])?;
let map_value = iter.call_packed(&[AnyView::from(&1i64)])?;
- entries.push((key, map_value));
+ values.push(map_value);
if index + 1 != size {
iter.call_packed(&[AnyView::from(&2i64)])?;
}
}
- for (index, (key, map_value)) in entries.into_iter().enumerate() {
- visitor
- .visit_child(raw_of_owned(&key), def_region_kind)
- .map_err(|halt| with_error_context(halt, &format!("{kind} key
[{index}]")))?;
+ for (index, map_value) in values.into_iter().enumerate() {
visitor
.visit_child(raw_of_owned(&map_value), def_region_kind)
.map_err(|halt| with_error_context(halt, &format!("{kind} value
[{index}]")))?;
diff --git a/rust/tvm-ffi/tests/test_structural_visit.rs
b/rust/tvm-ffi/tests/test_structural_visit.rs
index 723de807..7fa04585 100644
--- a/rust/tvm-ffi/tests/test_structural_visit.rs
+++ b/rust/tvm-ffi/tests/test_structural_visit.rs
@@ -48,16 +48,19 @@ fn plain_walk_uses_native_sequence_fallback() {
}
#[test]
-fn plain_walk_uses_native_map_fallback() {
+fn plain_walk_visits_map_values_without_visiting_keys() {
let root: Map<FfiString, i64> = [(FfiString::from("a"), 1i64),
(FfiString::from("b"), 2i64)]
.into_iter()
.collect();
let mut integers = 0;
+ let mut strings = 0;
assert!(structural_walk(
&root,
|value: &VisitValue| {
if value.cast::<i64>().is_some() {
integers += 1;
+ } else if value.cast::<FfiString>().is_some() {
+ strings += 1;
}
WalkResult::Advance
},
@@ -66,6 +69,7 @@ fn plain_walk_uses_native_map_fallback() {
.unwrap()
.is_none());
assert_eq!(integers, 2);
+ assert_eq!(strings, 0);
}
#[derive(Default)]
@@ -229,7 +233,7 @@ fn mutable_dict_is_snapshotted_before_callbacks() {
}
#[test]
-fn dense_map_layout_is_traversed_completely() {
+fn dense_map_layout_visits_all_values_without_visiting_keys() {
// More than 4 entries forces the dense (block + iteration list) layout.
let root: Map<FfiString, i64> = (0..9)
.map(|i| (FfiString::from(format!("k{i}")), i as i64))
@@ -251,7 +255,7 @@ fn dense_map_layout_is_traversed_completely() {
.unwrap()
.is_none());
assert_eq!(sum, (0..9).sum::<i64>());
- assert_eq!(strings, 9);
+ assert_eq!(strings, 0);
}
#[test]
diff --git a/src/ffi/extra/structural_mutate.cc
b/src/ffi/extra/structural_mutate.cc
new file mode 100644
index 00000000..c240392c
--- /dev/null
+++ b/src/ffi/extra/structural_mutate.cc
@@ -0,0 +1,378 @@
+/*
+ * 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.
+ */
+/*!
+ * \file src/ffi/extra/structural_mutate.cc
+ * \brief Structural mutator and structural map registration.
+ */
+#include <tvm/ffi/container/array.h>
+#include <tvm/ffi/container/dict.h>
+#include <tvm/ffi/container/list.h>
+#include <tvm/ffi/container/map.h>
+#include <tvm/ffi/extra/structural_mutate.h>
+#include <tvm/ffi/function.h>
+#include <tvm/ffi/reflection/accessor.h>
+#include <tvm/ffi/reflection/registry.h>
+
+#include <utility>
+
+namespace tvm {
+namespace ffi {
+
+namespace details {
+
+/*!
+ * \brief Runtime structural map for callback arrays.
+ *
+ * \param root The root value to map.
+ * \param callbacks Runtime callback entries of ``(type_index,
ffi::Function)`` invoked as
+ * ``callback(value)``.
+ * \param callbacks_with_def_region_kind Runtime callback entries of
``(type_index, ffi::Function)``
+ * invoked as ``callback(value,
def_region_kind)``.
+ * \param order Integer value of \ref WalkOrder.
+ * \return The mapped owning value, or an Error.
+ */
+Expected<Any> StructuralMapExpected(
+ AnyView root, const Array<Tuple<int32_t, Function>>& callbacks,
+ const Array<Tuple<int32_t, Function>>& callbacks_with_def_region_kind, int
order) noexcept {
+ auto dispatch = [callbacks, callbacks_with_def_region_kind](
+ AnyView x, TVMFFIDefRegionKind kind) -> Expected<Any> {
+ for (const auto& entry : callbacks) {
+ int32_t type_index = entry.template get<0>();
+ if (!RuntimeTypeIndexMatch(x.type_index(), type_index)) {
+ continue;
+ }
+ Function fn = entry.template get<1>();
+ return fn.CallExpected<Any>(x);
+ }
+ for (const auto& entry : callbacks_with_def_region_kind) {
+ int32_t type_index = entry.template get<0>();
+ if (!RuntimeTypeIndexMatch(x.type_index(), type_index)) {
+ continue;
+ }
+ Function fn = entry.template get<1>();
+ return fn.CallExpected<Any>(x, kind);
+ }
+ return Any(x);
+ };
+
+ if (order == static_cast<int>(WalkOrder::kPreOrder)) {
+ using Mutator = StructuralMapMutatorObj<WalkOrder::kPreOrder,
decltype(dispatch)>;
+ StructuralMutator mutator(make_object<Mutator>(std::move(dispatch)));
+ return mutator->MaybeInplaceMutateIfUniqueExpected(root);
+ } else {
+ using Mutator = StructuralMapMutatorObj<WalkOrder::kPostOrder,
decltype(dispatch)>;
+ StructuralMutator mutator(make_object<Mutator>(std::move(dispatch)));
+ return mutator->MaybeInplaceMutateIfUniqueExpected(root);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Built-in container structural mutation.
+// ---------------------------------------------------------------------------
+
+/*!
+ * \brief Structurally mutate the elements of a sequence container.
+ *
+ * \tparam SeqObj The underlying sequence object type.
+ * \param mutator The active structural mutator.
+ * \param value The borrowed sequence container.
+ * \param source The sequence object stored in \p value.
+ * \return The mutated sequence, or an Error.
+ */
+template <typename SeqObj>
+Expected<Any> MutateSeqContainerExpected(StructuralMutatorObj* mutator,
AnyView value,
+ const SeqObj* source) noexcept {
+ try {
+ int64_t size = static_cast<int64_t>(source->size());
+ ObjectPtr<SeqObj> output = nullptr;
+
+ for (int64_t i = 0; i < size; ++i) {
+ const Any& item = source->at(i);
+ Expected<Any> mapped_item = mutator->MutateExpected(item);
+ if (TVM_FFI_PREDICT_FALSE(mapped_item.is_err())) {
+ return Unexpected(std::move(mapped_item).error());
+ }
+ const Any& mapped_value = details::ExpectedUnsafe::GetData(mapped_item);
+
+ if (output == nullptr) {
+ if (item.same_as(mapped_value)) {
+ continue;
+ }
+ output = SeqObj::CreateRepeated(size, Any());
+ for (int64_t j = 0; j < i; ++j) {
+ output->SetItem(j, source->at(j));
+ }
+ }
+ output->SetItem(i, mapped_value);
+ }
+
+ if (output == nullptr) {
+ return Any(value);
+ }
+ return Any(ObjectRef(std::move(output)));
+ } catch (const Error& err) {
+ return Unexpected(err);
+ }
+}
+
+/*!
+ * \brief Structurally mutate the elements of a sequence container in place
when safe.
+ *
+ * \tparam SeqObj The underlying sequence object type.
+ * \param mutator The active structural mutator.
+ * \param value The borrowed sequence container, which must be safe to mutate
in place.
+ * \param target The sequence object stored in \p value.
+ * \return The mutated sequence, or an Error.
+ */
+template <typename SeqObj>
+Expected<Any> MaybeInplaceMutateSeqContainerExpected(StructuralMutatorObj*
mutator, AnyView value,
+ SeqObj* target) noexcept {
+ try {
+ for (int64_t i = 0; i < static_cast<int64_t>(target->size()); ++i) {
+ const Any& item = target->at(i);
+ Expected<Any> mapped_item =
mutator->MaybeInplaceMutateIfUniqueExpected(item);
+ if (TVM_FFI_PREDICT_FALSE(mapped_item.is_err())) {
+ return Unexpected(std::move(mapped_item).error());
+ }
+ const Any& mapped_value = details::ExpectedUnsafe::GetData(mapped_item);
+
+ if (!item.same_as(mapped_value)) {
+ target->SetItem(i, mapped_value);
+ }
+ }
+ return Any(value);
+ } catch (const Error& err) {
+ return Unexpected(err);
+ }
+}
+
+/*!
+ * \brief Structurally mutate the values of a map container.
+ *
+ * \tparam MapObjType The underlying map object type.
+ * \param mutator The active structural mutator.
+ * \param value The borrowed map container.
+ * \param source The map object stored in \p value.
+ * \return The mutated map, or an Error.
+ */
+template <typename MapObjType>
+Expected<Any> MutateMapValuesExpected(StructuralMutatorObj* mutator, AnyView
value,
+ const MapObjType* source) noexcept {
+ try {
+ ObjectPtr<Object> output = nullptr;
+ MapBaseObj::iterator output_it;
+ size_t index = 0;
+
+ for (auto source_it = source->begin(); source_it != source->end();
++source_it, ++index) {
+ const Any& old_value = source_it->second;
+ Expected<Any> mapped_value = mutator->MutateExpected(old_value);
+ if (TVM_FFI_PREDICT_FALSE(mapped_value.is_err())) {
+ return Unexpected(std::move(mapped_value).error());
+ }
+
+ const Any& new_value = details::ExpectedUnsafe::GetData(mapped_value);
+ bool changed = !old_value.same_as(new_value);
+ if (output == nullptr) {
+ if (!changed) {
+ continue;
+ }
+ output = MapObjType::ShallowCopy(source);
+ output_it = static_cast<MapBaseObj*>(output.get())->begin();
+ for (size_t i = 0; i < index; ++i) {
+ ++output_it;
+ }
+ }
+ if (changed) {
+ output_it->second = new_value;
+ }
+ ++output_it;
+ }
+
+ if (output == nullptr) {
+ return Any(value);
+ }
+ return Any(ObjectRef(std::move(output)));
+ } catch (const Error& err) {
+ return Unexpected(err);
+ }
+}
+
+/*!
+ * \brief Structurally mutate the values of a map container in place when safe.
+ *
+ * \tparam MapObjType The underlying map object type.
+ * \param mutator The active structural mutator.
+ * \param value The borrowed map container, which must be safe to mutate in
place.
+ * \param target The map object stored in \p value.
+ * \return The mutated map, or an Error.
+ */
+template <typename MapObjType>
+Expected<Any> MaybeInplaceMutateMapValuesExpected(StructuralMutatorObj*
mutator, AnyView value,
+ MapObjType* target) noexcept
{
+ try {
+ for (auto it = target->begin(); it != target->end(); ++it) {
+ const Any& old_value = it->second;
+ Expected<Any> mapped_value =
mutator->MaybeInplaceMutateIfUniqueExpected(old_value);
+ if (TVM_FFI_PREDICT_FALSE(mapped_value.is_err())) {
+ return Unexpected(std::move(mapped_value).error());
+ }
+ const Any& new_value = details::ExpectedUnsafe::GetData(mapped_value);
+
+ if (!old_value.same_as(new_value)) {
+ it->second = new_value;
+ }
+ }
+ return Any(value);
+ } catch (const Error& err) {
+ return Unexpected(err);
+ }
+}
+
+/*! \brief Identity structural mutation hook for immutable String and Bytes
leaves. */
+TVMFFIAny MutateImmutableLeaf(StructuralMutatorObj*, AnyView value) noexcept {
+ Expected<Any> result = Any(value);
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+}
+
+/*! \brief Structural mutation hook for ArrayObj. */
+TVMFFIAny MutateArray(StructuralMutatorObj* mutator, AnyView value) noexcept {
+ Expected<Any> result = MutateSeqContainerExpected(mutator, value,
value.cast<const ArrayObj*>());
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+}
+
+/*! \brief Maybe-in-place structural mutation hook for ArrayObj. */
+TVMFFIAny MaybeInplaceMutateArray(StructuralMutatorObj* mutator, AnyView
value) noexcept {
+ Expected<Any> result = MaybeInplaceMutateSeqContainerExpected(
+ mutator, value, const_cast<ArrayObj*>(value.cast<const ArrayObj*>()));
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+}
+
+/*! \brief Structural mutation hook for ListObj. */
+TVMFFIAny MutateList(StructuralMutatorObj* mutator, AnyView value) noexcept {
+ Expected<Any> result = MutateSeqContainerExpected(mutator, value,
value.cast<const ListObj*>());
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+}
+
+/*! \brief Maybe-in-place structural mutation hook for ListObj. */
+TVMFFIAny MaybeInplaceMutateList(StructuralMutatorObj* mutator, AnyView value)
noexcept {
+ Expected<Any> result = MaybeInplaceMutateSeqContainerExpected(
+ mutator, value, const_cast<ListObj*>(value.cast<const ListObj*>()));
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+}
+
+/*! \brief Structural mutation hook for MapObj. */
+TVMFFIAny MutateMap(StructuralMutatorObj* mutator, AnyView value) noexcept {
+ Expected<Any> result = MutateMapValuesExpected(mutator, value,
value.cast<const MapObj*>());
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+}
+
+/*! \brief Maybe-in-place structural mutation hook for MapObj. */
+TVMFFIAny MaybeInplaceMutateMap(StructuralMutatorObj* mutator, AnyView value)
noexcept {
+ Expected<Any> result = MaybeInplaceMutateMapValuesExpected(
+ mutator, value, const_cast<MapObj*>(value.cast<const MapObj*>()));
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+}
+
+/*! \brief Structural mutation hook for DictObj. */
+TVMFFIAny MutateDict(StructuralMutatorObj* mutator, AnyView value) noexcept {
+ Expected<Any> result = MutateMapValuesExpected(mutator, value,
value.cast<const DictObj*>());
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+}
+
+/*! \brief Maybe-in-place structural mutation hook for DictObj. */
+TVMFFIAny MaybeInplaceMutateDict(StructuralMutatorObj* mutator, AnyView value)
noexcept {
+ Expected<Any> result = MaybeInplaceMutateMapValuesExpected(
+ mutator, value, const_cast<DictObj*>(value.cast<const DictObj*>()));
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+}
+} // namespace details
+
+// ---------------------------------------------------------------------------
+// Static registration.
+// ---------------------------------------------------------------------------
+
+TVM_FFI_STATIC_INIT_BLOCK() {
+ namespace refl = tvm::ffi::reflection;
+ refl::ObjectDef<StructuralMutatorObj>(); // NOLINT(bugprone-unused-raii)
+ refl::GlobalDef()
+ .def_method("ffi.StructuralMutatorMaybeInplaceMutate",
+ &StructuralMutatorObj::MaybeInplaceMutate)
+ .def_method("ffi.StructuralMutatorMutate", &StructuralMutatorObj::Mutate)
+ .def_method("ffi.StructuralMutatorVarRemapGet",
+ [](const StructuralMutator& mutator, AnyView var) {
+ return mutator->VarRemapGetExpected(var).value();
+ })
+ .def_method("ffi.StructuralMutatorVarRemapSet",
+ [](const StructuralMutator& mutator, AnyView var, AnyView
mapped_value) {
+ mutator->VarRemapSetExpected(var, mapped_value).value();
+ })
+ .def_method("ffi.StructuralMutatorDefRegionKind",
&StructuralMutatorObj::def_region_kind)
+ .def_method(
+ "ffi.StructuralMutatorWithDefRegionKind",
+ [](const StructuralMutator& mutator, TVMFFIDefRegionKind kind, const
Function& callback) {
+ return mutator->WithDefRegionKind(kind, callback);
+ })
+ .def("ffi.StructuralMap",
+ [](AnyView root, const Array<Tuple<int32_t, Function>>& callbacks,
+ const Array<Tuple<int32_t, Function>>&
callbacks_with_def_region_kind,
+ int32_t order) -> Any {
+ return details::StructuralMapExpected(root, callbacks,
callbacks_with_def_region_kind,
+ order)
+ .value();
+ });
+ refl::EnsureTypeAttrColumn(refl::type_attr::kStructuralMutate);
+ refl::EnsureTypeAttrColumn(refl::type_attr::kStructuralMaybeInplaceMutate);
+ refl::TypeAttrDef<details::StringObj>()
+ .attr(refl::type_attr::kStructuralMutate,
+
reinterpret_cast<void*>(static_cast<FStructuralMutate>(&details::MutateImmutableLeaf)))
+ .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+
reinterpret_cast<void*>(static_cast<FStructuralMutate>(&details::MutateImmutableLeaf)));
+ refl::TypeAttrDef<details::BytesObj>()
+ .attr(refl::type_attr::kStructuralMutate,
+
reinterpret_cast<void*>(static_cast<FStructuralMutate>(&details::MutateImmutableLeaf)))
+ .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+
reinterpret_cast<void*>(static_cast<FStructuralMutate>(&details::MutateImmutableLeaf)));
+ refl::TypeAttrDef<ArrayObj>()
+ .attr(refl::type_attr::kStructuralMutate,
+
reinterpret_cast<void*>(static_cast<FStructuralMutate>(&details::MutateArray)))
+ .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+ reinterpret_cast<void*>(
+
static_cast<FStructuralMutate>(&details::MaybeInplaceMutateArray)));
+ refl::TypeAttrDef<ListObj>()
+ .attr(refl::type_attr::kStructuralMutate,
+
reinterpret_cast<void*>(static_cast<FStructuralMutate>(&details::MutateList)))
+ .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+ reinterpret_cast<void*>(
+
static_cast<FStructuralMutate>(&details::MaybeInplaceMutateList)));
+ refl::TypeAttrDef<MapObj>()
+ .attr(refl::type_attr::kStructuralMutate,
+
reinterpret_cast<void*>(static_cast<FStructuralMutate>(&details::MutateMap)))
+ .attr(
+ refl::type_attr::kStructuralMaybeInplaceMutate,
+
reinterpret_cast<void*>(static_cast<FStructuralMutate>(&details::MaybeInplaceMutateMap)));
+ refl::TypeAttrDef<DictObj>()
+ .attr(refl::type_attr::kStructuralMutate,
+
reinterpret_cast<void*>(static_cast<FStructuralMutate>(&details::MutateDict)))
+ .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+ reinterpret_cast<void*>(
+
static_cast<FStructuralMutate>(&details::MaybeInplaceMutateDict)));
+}
+
+} // namespace ffi
+} // namespace tvm
diff --git a/src/ffi/extra/structural_visit.cc
b/src/ffi/extra/structural_visit.cc
index 913bb4e4..bcd4ce41 100644
--- a/src/ffi/extra/structural_visit.cc
+++ b/src/ffi/extra/structural_visit.cc
@@ -74,11 +74,11 @@ Expected<Optional<VisitInterrupt>> StructuralWalkExpected(
};
if (order == static_cast<int>(WalkOrder::kPreOrder)) {
- using Visitor = StructuralWalkCallbackVisitorObj<WalkOrder::kPreOrder,
decltype(dispatch)>;
+ using Visitor = StructuralWalkVisitorObj<WalkOrder::kPreOrder,
decltype(dispatch)>;
StructuralVisitor visitor(make_object<Visitor>(std::move(dispatch)));
return visitor->VisitExpected(root);
} else {
- using Visitor = StructuralWalkCallbackVisitorObj<WalkOrder::kPostOrder,
decltype(dispatch)>;
+ using Visitor = StructuralWalkVisitorObj<WalkOrder::kPostOrder,
decltype(dispatch)>;
StructuralVisitor visitor(make_object<Visitor>(std::move(dispatch)));
return visitor->VisitExpected(root);
}
@@ -92,10 +92,9 @@ TVMFFIAny VisitSeqContainer(StructuralVisitorObj* visitor,
const SeqBaseObj* seq
return
ExpectedUnsafe::MoveToTVMFFIAny(Expected<Optional<VisitInterrupt>>(std::nullopt));
}
-/*! \brief Visit keys and values in a map container. */
+/*! \brief Visit values in a map container while treating keys as structural
anchors. */
TVMFFIAny VisitMapContainer(StructuralVisitorObj* visitor, const MapBaseObj*
map) noexcept {
for (const auto& kv : *map) {
- TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(kv.first));
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(kv.second));
}
return
ExpectedUnsafe::MoveToTVMFFIAny(Expected<Optional<VisitInterrupt>>(std::nullopt));
diff --git a/tests/cpp/extra/test_structural_mutate.cc
b/tests/cpp/extra/test_structural_mutate.cc
new file mode 100644
index 00000000..2066d074
--- /dev/null
+++ b/tests/cpp/extra/test_structural_mutate.cc
@@ -0,0 +1,259 @@
+/*
+ * 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.
+ */
+#include <gtest/gtest.h>
+#include <tvm/ffi/container/array.h>
+#include <tvm/ffi/container/map.h>
+#include <tvm/ffi/extra/structural_mutate.h>
+#include <tvm/ffi/string.h>
+
+#include <cstdint>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "../testing_object.h"
+
+namespace {
+
+using namespace tvm::ffi;
+using namespace tvm::ffi::testing;
+
+using AnyArray = Array<Any>;
+using StringMap = Map<String, Any>;
+
+Expected<Any> Increment(int64_t value) { return Any(value + 1); }
+
+template <WalkOrder order>
+void CheckNestedArrayMapOrder(const std::vector<std::string>& expected_trace) {
+ AnyArray inner_array{int64_t{1}};
+ const Object* inner_array_address = inner_array.get();
+ StringMap map{{"value", Any(std::move(inner_array))}};
+ const Object* map_address = map.get();
+ AnyArray root{Any(std::move(map))};
+ const Object* root_address = root.get();
+ std::vector<std::string> trace;
+
+ AnyArray mapped =
+ StructuralMap<order>(
+ root,
+ [&](const AnyArray& array) -> Expected<Any> {
+ trace.emplace_back(array.get() == root_address ? "outer-array" :
"inner-array");
+ return Any(array);
+ },
+ [&](const StringMap& value) -> Expected<Any> {
+ trace.emplace_back("map");
+ return Any(value);
+ },
+ [&](const String&) -> Expected<Any> {
+ trace.emplace_back("map-key");
+ return Any(String("renamed"));
+ },
+ [&](int64_t value) -> Expected<Any> {
+ trace.emplace_back("int");
+ return Any(value + 1);
+ })
+ .template cast<AnyArray>();
+
+ StringMap mapped_map = mapped[0].cast<StringMap>();
+ AnyArray mapped_inner_array = mapped_map["value"].cast<AnyArray>();
+
+ EXPECT_EQ(trace, expected_trace);
+ EXPECT_EQ(mapped.get(), root_address);
+ EXPECT_EQ(mapped_map.get(), map_address);
+ EXPECT_EQ(mapped_inner_array.get(), inner_array_address);
+ EXPECT_EQ(mapped_inner_array[0].cast<int64_t>(), 2);
+ EXPECT_EQ(mapped_map.count("value"), 1U);
+ EXPECT_EQ(mapped_map.count("renamed"), 0U);
+}
+
+TEST(StructuralMap, MapsNestedArrayAndMapInConfiguredOrder) {
+ CheckNestedArrayMapOrder<WalkOrder::kPreOrder>({"outer-array", "map",
"inner-array", "int"});
+ CheckNestedArrayMapOrder<WalkOrder::kPostOrder>({"int", "inner-array",
"map", "outer-array"});
+}
+
+TEST(StructuralMap, PreservesSharedArrayAndMapInputs) {
+ // A shared Array is copied when one of its elements changes.
+ {
+ AnyArray child{int64_t{1}};
+ const Object* child_address = child.get();
+ AnyArray root{Any(std::move(child))};
+ AnyArray owner = root; //
NOLINT(performance-unnecessary-copy-initialization)
+ const Object* root_address = root.get();
+
+ AnyArray mapped = StructuralMap<WalkOrder::kPostOrder>(root,
Increment).cast<AnyArray>();
+ AnyArray original_child = root[0].cast<AnyArray>();
+ AnyArray mapped_child = mapped[0].cast<AnyArray>();
+
+ EXPECT_NE(mapped.get(), root_address);
+ EXPECT_EQ(owner.get(), root_address);
+ EXPECT_NE(mapped_child.get(), child_address);
+ EXPECT_EQ(original_child[0].cast<int64_t>(), 1);
+ EXPECT_EQ(mapped_child[0].cast<int64_t>(), 2);
+ }
+
+ // A shared Map and its changed value path are also copied.
+ {
+ AnyArray value{int64_t{1}};
+ const Object* value_address = value.get();
+ StringMap root{{"value", Any(std::move(value))}};
+ StringMap owner = root; //
NOLINT(performance-unnecessary-copy-initialization)
+ const Object* root_address = root.get();
+
+ StringMap mapped = StructuralMap<WalkOrder::kPostOrder>(root,
Increment).cast<StringMap>();
+ AnyArray original_value = root["value"].cast<AnyArray>();
+ AnyArray mapped_value = mapped["value"].cast<AnyArray>();
+
+ EXPECT_NE(mapped.get(), root_address);
+ EXPECT_EQ(owner.get(), root_address);
+ EXPECT_NE(mapped_value.get(), value_address);
+ EXPECT_EQ(original_value[0].cast<int64_t>(), 1);
+ EXPECT_EQ(mapped_value[0].cast<int64_t>(), 2);
+ }
+
+ // Copy-on-write remains lazy: an unchanged shared Map is returned directly.
+ {
+ StringMap root{{"value", AnyArray{int64_t{1}}}};
+ StringMap owner = root; //
NOLINT(performance-unnecessary-copy-initialization)
+
+ StringMap mapped =
+ StructuralMap<WalkOrder::kPostOrder>(root, [](int64_t value) ->
Expected<Any> {
+ return Any(value);
+ }).cast<StringMap>();
+
+ EXPECT_TRUE(mapped.same_as(root));
+ EXPECT_TRUE(owner.same_as(root));
+ EXPECT_TRUE(mapped["value"].same_as(root["value"]));
+ }
+}
+
+TEST(StructuralMap, PreOrderRecursivelyMapsCallbackResult) {
+ StringMap root{{"value", AnyArray{int64_t{1}}}};
+ AnyArray replacement{int64_t{10}};
+
+ StringMap mapped =
+ StructuralMap<WalkOrder::kPreOrder>(
+ root, [&](const AnyArray&) -> Expected<Any> { return
Any(replacement); }, Increment)
+ .cast<StringMap>();
+ AnyArray mapped_value = mapped["value"].cast<AnyArray>();
+
+ EXPECT_FALSE(mapped_value.same_as(replacement));
+ EXPECT_EQ(replacement[0].cast<int64_t>(), 10);
+ EXPECT_EQ(mapped_value[0].cast<int64_t>(), 11);
+}
+
+template <WalkOrder order>
+void CheckRepeatedVarRemap() {
+ TVar var("n");
+ StringMap use{{"use", var}};
+ AnyArray root{var, Any(std::move(use))};
+ int callback_count = 0;
+
+ AnyArray mapped = StructuralMap<order>(root, [&](const TVarObj* value) ->
Expected<Any> {
+ ++callback_count;
+ return Any(TVar(value->name + "-mapped"));
+ }).template cast<AnyArray>();
+ TVar mapped_var = mapped[0].cast<TVar>();
+ StringMap mapped_uses = mapped[1].cast<StringMap>();
+ TVar mapped_use = mapped_uses["use"].cast<TVar>();
+
+ EXPECT_EQ(callback_count, 1);
+ EXPECT_TRUE(mapped_var.same_as(mapped_use));
+ EXPECT_EQ(mapped_var->name, "n-mapped");
+ EXPECT_EQ(var->name, "n");
+}
+
+TEST(StructuralMap, ReusesFinalCallbackResultForRepeatedVar) {
+ CheckRepeatedVarRemap<WalkOrder::kPreOrder>();
+ CheckRepeatedVarRemap<WalkOrder::kPostOrder>();
+}
+
+AnyArray MakeStringAndBytesLeaves() {
+ return AnyArray{int64_t{1}, String("1234567"), String("12345678"),
Bytes("1234567", 7),
+ Bytes("12345678", 8)};
+}
+
+template <WalkOrder order>
+void CheckStringAndBytesLeaves() {
+ // An unmatched callback leaves inline and heap-backed values untouched.
+ {
+ AnyArray root = MakeStringAndBytesLeaves();
+ EXPECT_EQ(root[1].type_index(), TypeIndex::kTVMFFISmallStr);
+ EXPECT_EQ(root[2].type_index(), TypeIndex::kTVMFFIStr);
+ EXPECT_EQ(root[3].type_index(), TypeIndex::kTVMFFISmallBytes);
+ EXPECT_EQ(root[4].type_index(), TypeIndex::kTVMFFIBytes);
+
+ AnyArray unmatched = StructuralMap<order>(root, [](int64_t value) ->
Expected<Any> {
+ return Any(value);
+ }).template cast<AnyArray>();
+
+ EXPECT_TRUE(unmatched.same_as(root));
+ }
+
+ // Identity callbacks return the original shared Array for both
representations.
+ {
+ AnyArray root = MakeStringAndBytesLeaves();
+ AnyArray owner = root; //
NOLINT(performance-unnecessary-copy-initialization)
+ int string_callback_count = 0;
+ int bytes_callback_count = 0;
+
+ AnyArray identity = StructuralMap<order>(
+ root,
+ [&](const String& value) -> Expected<Any> {
+ ++string_callback_count;
+ return Any(value);
+ },
+ [&](const Bytes& value) -> Expected<Any> {
+ ++bytes_callback_count;
+ return Any(value);
+ })
+ .template cast<AnyArray>();
+
+ EXPECT_TRUE(identity.same_as(root));
+ EXPECT_TRUE(owner.same_as(root));
+ EXPECT_EQ(string_callback_count, 2);
+ EXPECT_EQ(bytes_callback_count, 2);
+ }
+
+ // Matching callbacks can replace both representations without traversing
into them.
+ {
+ AnyArray root = MakeStringAndBytesLeaves();
+ AnyArray replaced = StructuralMap<order>(
+ root,
+ [](const String& value) -> Expected<Any> {
+ return Any(static_cast<int64_t>(value.size()));
+ },
+ [](const Bytes& value) -> Expected<Any> {
+ return Any(static_cast<int64_t>(value.size()));
+ })
+ .template cast<AnyArray>();
+
+ EXPECT_EQ(replaced[0].cast<int64_t>(), 1);
+ EXPECT_EQ(replaced[1].cast<int64_t>(), 7);
+ EXPECT_EQ(replaced[2].cast<int64_t>(), 8);
+ EXPECT_EQ(replaced[3].cast<int64_t>(), 7);
+ EXPECT_EQ(replaced[4].cast<int64_t>(), 8);
+ }
+}
+
+TEST(StructuralMap, HandlesInlineAndHeapStringAndBytesLeaves) {
+ CheckStringAndBytesLeaves<WalkOrder::kPreOrder>();
+ CheckStringAndBytesLeaves<WalkOrder::kPostOrder>();
+}
+
+} // namespace
diff --git a/tests/cpp/extra/test_structural_visit.cc
b/tests/cpp/extra/test_structural_visit.cc
index 6bff25ad..6ac34a43 100644
--- a/tests/cpp/extra/test_structural_visit.cc
+++ b/tests/cpp/extra/test_structural_visit.cc
@@ -192,7 +192,7 @@ TEST(StructuralVisitor, TraversesArray) {
EXPECT_TRUE(AsTestVisitor(visitor)->visited[1].same_as(rhs));
}
-TEST(StructuralVisitor, TraversesMap) {
+TEST(StructuralVisitor, TraversesMapValuesWithoutVisitingKeys) {
ObjectRef key = TVar("key");
ObjectRef value = TVar("value");
Map<Any, Any> root{{key, value}};
@@ -202,9 +202,8 @@ TEST(StructuralVisitor, TraversesMap) {
ASSERT_TRUE(result.is_ok());
EXPECT_FALSE(result.value().has_value());
- ASSERT_EQ(AsTestVisitor(visitor)->visited.size(), 2U);
- EXPECT_TRUE(AsTestVisitor(visitor)->visited[0].same_as(key));
- EXPECT_TRUE(AsTestVisitor(visitor)->visited[1].same_as(value));
+ ASSERT_EQ(AsTestVisitor(visitor)->visited.size(), 1U);
+ EXPECT_TRUE(AsTestVisitor(visitor)->visited[0].same_as(value));
}
TEST(StructuralVisitor, UsesFuncHook) {
diff --git a/tests/python/test_structural.py b/tests/python/test_structural.py
index 68779788..51d4e6f7 100644
--- a/tests/python/test_structural.py
+++ b/tests/python/test_structural.py
@@ -20,6 +20,7 @@ from __future__ import annotations
from typing import Any
import numpy as np
+import pytest
import tvm_ffi
import tvm_ffi.testing
from tvm_ffi.dataclasses import Object, field, py_class
@@ -184,6 +185,7 @@ def test_structural_walk_typed_callbacks() -> None:
((int, float), lambda value: trace.append(f"number:{value}")),
(str, lambda value: trace.append(f"str:{value}")),
],
+ order=tvm_ffi.WalkOrder.PREORDER,
)
assert result is None
@@ -232,6 +234,7 @@ def test_structural_walk_first_match_and_skip() -> None:
),
(object, lambda value: trace.append(type(value).__name__)),
],
+ order=tvm_ffi.WalkOrder.PREORDER,
)
assert result is None
@@ -246,13 +249,17 @@ def test_structural_walk_interrupt() -> None:
return tvm_ffi.VisitInterrupt({"found": value})
return None
- result = tvm_ffi.structural_walk(root, (int, on_int))
+ result = tvm_ffi.structural_walk(
+ root,
+ (int, on_int),
+ order=tvm_ffi.WalkOrder.PREORDER,
+ )
assert isinstance(result, tvm_ffi.VisitInterrupt)
assert tvm_ffi.structural_equal(result.value, {"found": 2})
-def test_structural_walk_nested_containers() -> None:
+def test_structural_walk_nested_containers_and_skips_map_keys() -> None:
root = tvm_ffi.Array(
[
tvm_ffi.Map(
@@ -284,7 +291,7 @@ def test_structural_walk_nested_containers() -> None:
assert ("map", 2) in containers
assert ("dict", 1) in containers
assert sorted(scalars) == [1, 1, 2, 3]
- assert set(strings) == {"numbers", "meta", "flag"}
+ assert strings == []
def test_structural_walk_object_and_any_callbacks() -> None:
@@ -297,6 +304,7 @@ def test_structural_walk_object_and_any_callbacks() -> None:
(tvm_ffi.Object, lambda value:
trace.append(f"object:{type(value).__name__}")),
(Any, lambda value: trace.append(f"any:{value}")),
],
+ order=tvm_ffi.WalkOrder.PREORDER,
)
assert result is None
@@ -306,27 +314,51 @@ def test_structural_walk_object_and_any_callbacks() ->
None:
result = tvm_ffi.structural_walk(
tvm_ffi.Array([1]),
(object, lambda value: alias_trace.append(type(value).__name__)),
+ order=tvm_ffi.WalkOrder.PREORDER,
)
assert result is None
assert alias_trace == ["Array", "int"]
-def test_structural_walk_post_order_enum() -> None:
[email protected](
+ ("order", "expected_trace"),
+ [
+ pytest.param(
+ None,
+ ["int:1", "array:1", "int:2", "array:2"],
+ id="default-postorder",
+ ),
+ pytest.param(
+ tvm_ffi.WalkOrder.PREORDER,
+ ["array:2", "array:1", "int:1", "int:2"],
+ id="preorder",
+ ),
+ pytest.param(
+ tvm_ffi.WalkOrder.POSTORDER,
+ ["int:1", "array:1", "int:2", "array:2"],
+ id="postorder",
+ ),
+ ],
+)
+def test_structural_walk_pre_and_post_order(
+ order: tvm_ffi.WalkOrder | None,
+ expected_trace: list[str],
+) -> None:
root = tvm_ffi.Array([tvm_ffi.Array([1]), 2])
trace: list[str] = []
- result = tvm_ffi.structural_walk(
- root,
- [
- (tvm_ffi.Array, lambda value: trace.append(f"array:{len(value)}")),
- (int, lambda value: trace.append(f"int:{value}")),
- ],
- order=tvm_ffi.WalkOrder.POSTORDER,
- )
+ callbacks = [
+ (tvm_ffi.Array, lambda value: trace.append(f"array:{len(value)}")),
+ (int, lambda value: trace.append(f"int:{value}")),
+ ]
+ if order is None:
+ result = tvm_ffi.structural_walk(root, callbacks)
+ else:
+ result = tvm_ffi.structural_walk(root, callbacks, order=order)
assert result is None
- assert trace == ["int:1", "array:1", "int:2", "array:2"]
+ assert trace == expected_trace
def test_structural_walk_mixed_callback_forms() -> None:
@@ -360,7 +392,172 @@ def test_structural_walk_mixed_callback_forms() -> None:
),
),
],
+ order=tvm_ffi.WalkOrder.PREORDER,
)
assert result is None
assert trace == ["array:2", "array:1", "array:2", "use:x", "use:y",
"str:tag"]
+
+
+def test_structural_map_nested_array_map_order_and_keys() -> None:
+ def run(order: tvm_ffi.WalkOrder | None) -> list[str]:
+ root = tvm_ffi.Array([tvm_ffi.Map({"value": tvm_ffi.Array([1])})])
+ root_handle = root.__chandle__()
+ map_handle = root[0].__chandle__()
+ inner_array_handle = root[0]["value"].__chandle__()
+ trace: list[str] = []
+
+ def map_array(value: tvm_ffi.Array) -> tvm_ffi.Array:
+ trace.append("outer-array" if value.same_as(root) else
"inner-array")
+ return value
+
+ def map_map(value: tvm_ffi.Map) -> tvm_ffi.Map:
+ trace.append("map")
+ return value
+
+ def map_string(_: str) -> str:
+ trace.append("map-key")
+ return "renamed"
+
+ def map_int(value: int) -> int:
+ trace.append("int")
+ return value + 1
+
+ callbacks = [
+ (tvm_ffi.Array, map_array),
+ (tvm_ffi.Map, map_map),
+ (str, map_string),
+ (int, map_int),
+ ]
+ if order is None:
+ mapped = tvm_ffi.structural_map(root, callbacks)
+ else:
+ mapped = tvm_ffi.structural_map(root, callbacks, order=order)
+
+ assert mapped.__chandle__() == root_handle
+ assert mapped[0].__chandle__() == map_handle
+ assert mapped[0]["value"].__chandle__() == inner_array_handle
+ assert list(mapped[0]["value"]) == [2]
+ assert "value" in mapped[0]
+ assert "renamed" not in mapped[0]
+ return trace
+
+ assert run(tvm_ffi.WalkOrder.PREORDER) == [
+ "outer-array",
+ "map",
+ "inner-array",
+ "int",
+ ]
+ assert run(None) == ["int", "inner-array", "map", "outer-array"]
+
+
+def test_structural_map_array_ownership() -> None:
+ # A unique outer Array is reused, but its externally shared child is
copied.
+ shared_child = tvm_ffi.Array([1])
+ root = tvm_ffi.Array([shared_child])
+ root_handle = root.__chandle__()
+ mapped = tvm_ffi.structural_map(root, (int, lambda value: value + 1))
+
+ assert mapped.__chandle__() == root_handle
+ assert not mapped[0].same_as(shared_child)
+ assert list(shared_child) == [1]
+ assert list(mapped[0]) == [2]
+
+ # Sharing the outer Array preserves its complete original path.
+ shared_root = tvm_ffi.Array([tvm_ffi.Array([1])])
+ owner = tvm_ffi.Array([shared_root])
+ child_handle = shared_root[0].__chandle__()
+ mapped = tvm_ffi.structural_map(shared_root, (int, lambda value: value +
1))
+
+ assert not mapped.same_as(shared_root)
+ assert owner[0].same_as(shared_root)
+ assert mapped[0].__chandle__() != child_handle
+ assert list(shared_root[0]) == [1]
+ assert list(mapped[0]) == [2]
+
+
+def test_structural_map_map_value_ownership() -> None:
+ # A unique Map is reused, but its externally shared value is copied.
+ shared_value = tvm_ffi.Array([1])
+ root = tvm_ffi.Map({"value": shared_value})
+ root_handle = root.__chandle__()
+ mapped = tvm_ffi.structural_map(root, (int, lambda value: value + 1))
+
+ assert mapped.__chandle__() == root_handle
+ assert not mapped["value"].same_as(shared_value)
+ assert list(shared_value) == [1]
+ assert list(mapped["value"]) == [2]
+
+ # Sharing the Map copies both the Map and its changed value path.
+ shared_root = tvm_ffi.Map({"value": tvm_ffi.Array([1])})
+ owner = tvm_ffi.Array([shared_root])
+ value_handle = shared_root["value"].__chandle__()
+ mapped = tvm_ffi.structural_map(shared_root, (int, lambda value: value +
1))
+
+ assert not mapped.same_as(shared_root)
+ assert owner[0].same_as(shared_root)
+ assert mapped["value"].__chandle__() != value_handle
+ assert list(shared_root["value"]) == [1]
+ assert list(mapped["value"]) == [2]
+
+ # A shared Map is returned directly when no value changes.
+ shared_root = tvm_ffi.Map({"value": tvm_ffi.Array([1])})
+ owner = tvm_ffi.Array([shared_root])
+ mapped = tvm_ffi.structural_map(shared_root, (int, lambda value: value))
+
+ assert mapped.same_as(shared_root)
+ assert owner[0].same_as(shared_root)
+ assert mapped["value"].same_as(shared_root["value"])
+
+
+def test_structural_map_reuses_var_and_dag_callback_results() -> None:
+ @py_class(structural_eq="var")
+ class PyMapVar(Object):
+ value: int = field(structural_eq="ignore")
+
+ @py_class(structural_eq="dag")
+ class PyMapDAG(Object):
+ value: int
+
+ for order in (tvm_ffi.WalkOrder.PREORDER, tvm_ffi.WalkOrder.POSTORDER):
+ for node_type in (PyMapVar, PyMapDAG):
+ node = node_type(1)
+ root = tvm_ffi.Array([node, tvm_ffi.Map({"use": node})])
+ callback_count = 0
+
+ def replace(value: Any) -> Any:
+ nonlocal callback_count
+ callback_count += 1
+ return node_type(value.value + 1)
+
+ mapped = tvm_ffi.structural_map(root, (node_type, replace),
order=order)
+
+ assert callback_count == 1
+ assert mapped[0].same_as(mapped[1]["use"])
+ assert not mapped[0].same_as(node)
+ assert mapped[0].value == 2
+
+
+def test_structural_map_handles_inline_and_heap_strings_and_bytes() -> None:
+ values = [1, "1234567", "12345678", b"1234567", b"12345678"]
+
+ for order in (tvm_ffi.WalkOrder.PREORDER, tvm_ffi.WalkOrder.POSTORDER):
+ root = tvm_ffi.Array(values)
+ unmatched = tvm_ffi.structural_map(root, (int, lambda value: value),
order=order)
+ assert unmatched.same_as(root)
+ assert list(unmatched) == values
+
+ root = tvm_ffi.Array(values)
+ owner = tvm_ffi.Array([root])
+ identity = tvm_ffi.structural_map(
+ root,
+ [(str, lambda value: value), (bytes, lambda value: value)],
+ order=order,
+ )
+ assert identity.same_as(root)
+ assert owner[0].same_as(root)
+ assert list(identity) == values
+
+ root = tvm_ffi.Array(values)
+ replaced = tvm_ffi.structural_map(root, [(str, len), (bytes, len)],
order=order)
+ assert list(replaced) == [1, 7, 8, 7, 8]