This is an automated email from the ASF dual-hosted git repository.
junrushao 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 41d38fa3 refactor(python)!: rename `structure=` to `structural_eq=`
(#509)
41d38fa3 is described below
commit 41d38fa32f361fd219463455e5ef061e43a85945
Author: Junru Shao <[email protected]>
AuthorDate: Mon Mar 23 07:38:54 2026 -0700
refactor(python)!: rename `structure=` to `structural_eq=` (#509)
## Summary
- Rename the `structure=` parameter to `structural_eq=` in `@py_class()`
and `field()` to better convey that the parameter controls structural
equality/hashing behavior
- Update Cython layer (`type_info.pxi`) to read the new attribute name
- Update all tests and documentation to use the new parameter name
## Breaking Change
The `structure=` parameter on `@py_class()` and `field()` has been
renamed to `structural_eq=`.
**Migration**: replace all occurrences of `structure=` with
`structural_eq=` in your `@py_class()` and `field()` calls:
```python
# Before
@py_class(structure="tree")
class MyNode(Object):
span: Object = field(structure="ignore")
# After
@py_class(structural_eq="tree")
class MyNode(Object):
span: Object = field(structural_eq="ignore")
```
## Test plan
- [x] `uv pip install --force-reinstall --verbose -e .` (full C++
rebuild required)
- [x] `uv run pytest -vvs tests/python/test_structural_py_class.py`
- [ ] Verify all pre-commit hooks pass (confirmed locally: all passed)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
docs/concepts/structural_eq_hash.rst | 74 ++++++++++++++++----------------
python/tvm_ffi/cython/type_info.pxi | 2 +-
python/tvm_ffi/dataclasses/field.py | 35 ++++++++-------
python/tvm_ffi/dataclasses/py_class.py | 18 ++++----
tests/python/test_structural_py_class.py | 52 +++++++++++-----------
5 files changed, 92 insertions(+), 89 deletions(-)
diff --git a/docs/concepts/structural_eq_hash.rst
b/docs/concepts/structural_eq_hash.rst
index 006114b0..63dbd4c1 100644
--- a/docs/concepts/structural_eq_hash.rst
+++ b/docs/concepts/structural_eq_hash.rst
@@ -25,9 +25,9 @@ fields — rather than by pointer identity.
The behavior is controlled by two layers of annotation on
:func:`~tvm_ffi.dataclasses.py_class`:
-1. **Type-level** ``structure=`` — what *role* does this type play in the
+1. **Type-level** ``structural_eq=`` — what *role* does this type play in the
IR graph?
-2. **Field-level** ``structure=`` on :func:`~tvm_ffi.dataclasses.field` —
+2. **Field-level** ``structural_eq=`` on :func:`~tvm_ffi.dataclasses.field` —
should this field be skipped, or does it introduce new variable bindings?
This document explains what each annotation means, when to use it, and how
@@ -37,12 +37,12 @@ they compose.
Type-Level Annotation
---------------------
-The ``structure`` parameter on ``@py_class`` declares how instances of the
+The ``structural_eq`` parameter on ``@py_class`` declares how instances of the
type participate in structural equality and hashing:
.. code-block:: python
- @py_class(structure="tree")
+ @py_class(structural_eq="tree")
class Expr(Object):
...
@@ -53,7 +53,7 @@ Quick reference
:header-rows: 1
:widths: 18 37 45
- * - ``structure=``
+ * - ``structural_eq=``
- Meaning
- Use when...
* - ``"tree"``
@@ -81,7 +81,7 @@ Quick reference
.. code-block:: python
- @py_class(structure="tree")
+ @py_class(structural_eq="tree")
class Add(Object):
lhs: Expr
rhs: Expr
@@ -149,7 +149,7 @@ If sharing needs to matter, use ``"dag"`` instead.
.. code-block:: python
- @py_class(structure="const-tree")
+ @py_class(structural_eq="const-tree")
class DeviceMesh(Object):
shape: list[int]
device_ids: list[int]
@@ -222,7 +222,7 @@ is recorded:
.. code-block:: python
- @py_class(structure="dag")
+ @py_class(structural_eq="dag")
class Binding(Object):
var: Var
value: Expr
@@ -335,7 +335,7 @@ Full comparison: ``"tree"`` vs ``"dag"``
.. code-block:: python
- @py_class(structure="var")
+ @py_class(structural_eq="var")
class Var(Object):
name: str
@@ -358,8 +358,8 @@ binding position and are used in the same way.
How it works: definition regions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-``"var"`` works together with ``field(structure="def")`` (see
-:ref:`field-annotations`). A field marked ``structure="def"`` is a
+``"var"`` works together with ``field(structural_eq="def")`` (see
+:ref:`field-annotations`). A field marked ``structural_eq="def"`` is a
**definition region** — it's where new variable bindings are introduced.
- **Inside a definition region**: encountering two different variables
@@ -376,7 +376,7 @@ The following diagram traces the comparison of two
alpha-equivalent functions:
participant L as lhs: fun x → x + 1
participant R as rhs: fun y → y + 1
- Note over C: Field "params" has structure="def"
+ Note over C: Field "params" has structural_eq="def"
C->>L: get params → [x]
C->>R: get params → [y]
Note over C: Enter definition region
@@ -475,7 +475,7 @@ enclosing function:
.. code-block:: python
- @py_class(structure="singleton")
+ @py_class(structural_eq="singleton")
class Op(Object):
name: str
@@ -499,18 +499,18 @@ unequal; same pointer is always equal.
Field-Level Annotations
-----------------------
-The ``structure`` parameter on :func:`~tvm_ffi.dataclasses.field` controls
+The ``structural_eq`` parameter on :func:`~tvm_ffi.dataclasses.field` controls
how structural equality/hashing treats that specific field.
-``structure="ignore"`` — Exclude a field
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+``structural_eq="ignore"`` — Exclude a field
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
- @py_class(structure="tree")
+ @py_class(structural_eq="tree")
class MyNode(Object):
value: int
- span: str = field(structure="ignore")
+ span: str = field(structural_eq="ignore")
**Meaning**: "This field is not part of the node's structural identity.
Skip it during comparison and hashing."
@@ -523,14 +523,14 @@ Use for:
redundant to compare.
- **Debug annotations** — names, comments, metadata for human consumption.
-``structure="def"`` — Definition region
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+``structural_eq="def"`` — Definition region
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
- @py_class(structure="tree")
+ @py_class(structural_eq="tree")
class Lambda(Object):
- params: list[Var] = field(structure="def")
+ params: list[Var] = field(structural_eq="def")
body: Expr
**Meaning**: "This field introduces new variable bindings. When comparing
@@ -538,7 +538,7 @@ or hashing this field, allow new variable correspondences
to be
established."
This is the counterpart to ``"var"``. A ``"var"`` type says "I am a
-variable"; ``structure="def"`` says "this field is where variables are
+variable"; ``structural_eq="def"`` says "this field is where variables are
defined." Together they enable alpha-equivalence: comparing functions up
to consistent variable renaming.
@@ -636,14 +636,14 @@ When defining a new type:
graph TD
Start["New @py_class type"] --> Q1{"Singleton?<br/>(one instance
per<br/>logical identity)"}
- Q1 -->|Yes| UI["structure="singleton""]
+ Q1 -->|Yes| UI["structural_eq="singleton""]
Q1 -->|No| Q2{"Represents a<br/>variable binding?"}
- Q2 -->|Yes| FV["structure="var""]
+ Q2 -->|Yes| FV["structural_eq="var""]
Q2 -->|No| Q3{"Pointer sharing<br/>semantically<br/>meaningful?"}
- Q3 -->|Yes| DN["structure="dag""]
+ Q3 -->|Yes| DN["structural_eq="dag""]
Q3 -->|No| Q4{"Immutable AND<br/>no transitive<br/>var children?"}
- Q4 -->|Yes| CTN["structure="const-tree""]
- Q4 -->|No| TN["structure="tree""]
+ Q4 -->|Yes| CTN["structural_eq="const-tree""]
+ Q4 -->|No| TN["structural_eq="tree""]
style UI fill:#e2e3e5
style FV fill:#fff3cd
@@ -657,9 +657,9 @@ For fields:
graph TD
Start["field() parameter"] --> Q1{"Irrelevant to<br/>structural
identity?<br/>(span, cache, debug)"}
- Q1 -->|Yes| IGN["structure="ignore""]
+ Q1 -->|Yes| IGN["structural_eq="ignore""]
Q1 -->|No| Q2{"Introduces new<br/>variable bindings?"}
- Q2 -->|Yes| DEF["structure="def""]
+ Q2 -->|Yes| DEF["structural_eq="def""]
Q2 -->|No| NONE["No flag needed"]
style IGN fill:#f8d7da
@@ -675,17 +675,17 @@ source location:
.. code-block:: python
- @py_class(structure="tree")
+ @py_class(structural_eq="tree")
class Lambda(Object):
- params: list[Var] = field(structure="def")
+ params: list[Var] = field(structural_eq="def")
body: Expr
- span: str = field(structure="ignore", default="")
+ span: str = field(structural_eq="ignore", default="")
- @py_class(structure="var")
+ @py_class(structural_eq="var")
class Var(Object):
name: str
- @py_class(structure="singleton")
+ @py_class(structural_eq="singleton")
class Op(Object):
name: str
@@ -697,9 +697,9 @@ With these annotations, alpha-equivalent functions are
structurally equal:
fun [x] → x + 1 (span="a.py:1")
fun [y] → y + 1 (span="b.py:5")
- # - params has structure="def" → x maps to y
+ # - params has structural_eq="def" → x maps to y
# - body uses that mapping → (x + 1) ≅ (y + 1)
- # - span has structure="ignore" → locations don't matter
+ # - span has structural_eq="ignore" → locations don't matter
And in Python:
diff --git a/python/tvm_ffi/cython/type_info.pxi
b/python/tvm_ffi/cython/type_info.pxi
index 128efa70..7a195903 100644
--- a/python/tvm_ffi/cython/type_info.pxi
+++ b/python/tvm_ffi/cython/type_info.pxi
@@ -838,7 +838,7 @@ cdef _register_one_field(
if py_field.kw_only:
flags |= kTVMFFIFieldFlagBitMaskKwOnly
# Structural equality/hashing field annotations
- cdef object field_structure = getattr(py_field, "structure", None)
+ cdef object field_structure = getattr(py_field, "structural_eq", None)
if field_structure == "ignore":
flags |= kTVMFFIFieldFlagBitMaskSEqHashIgnore
elif field_structure == "def":
diff --git a/python/tvm_ffi/dataclasses/field.py
b/python/tvm_ffi/dataclasses/field.py
index 97e8864e..3f367613 100644
--- a/python/tvm_ffi/dataclasses/field.py
+++ b/python/tvm_ffi/dataclasses/field.py
@@ -70,7 +70,7 @@ class Field:
kw_only : bool | None
Whether this field is keyword-only in ``__init__``.
``None`` means "inherit from the decorator-level *kw_only* flag".
- structure : str | None
+ structural_eq : str | None
Structural equality/hashing annotation for this field. Valid
values are:
@@ -96,7 +96,7 @@ class Field:
"kw_only",
"name",
"repr",
- "structure",
+ "structural_eq",
"ty",
)
name: str | None
@@ -108,11 +108,13 @@ class Field:
hash: bool | None
compare: bool
kw_only: bool | None
- structure: str | None
+ structural_eq: str | None
doc: str | None
- #: Valid values for the *structure* parameter.
- _VALID_STRUCTURE_VALUES: ClassVar[frozenset[str | None]] =
frozenset({None, "ignore", "def"})
+ #: Valid values for the *structural_eq* parameter.
+ _VALID_STRUCTURAL_EQ_VALUES: ClassVar[frozenset[str | None]] = frozenset(
+ {None, "ignore", "def"}
+ )
def __init__( # noqa: PLR0913
self,
@@ -126,7 +128,7 @@ class Field:
hash: bool | None = True,
compare: bool = False,
kw_only: bool | None = False,
- structure: str | None = None,
+ structural_eq: str | None = None,
doc: str | None = None,
) -> None:
# MISSING means "parameter not provided".
@@ -139,10 +141,11 @@ class Field:
raise TypeError(
f"default_factory must be a callable, got
{type(default_factory).__name__}"
)
- if structure not in Field._VALID_STRUCTURE_VALUES:
+ if structural_eq not in Field._VALID_STRUCTURAL_EQ_VALUES:
raise ValueError(
- f"structure must be one of
{sorted(Field._VALID_STRUCTURE_VALUES, key=str)}, "
- f"got {structure!r}"
+ f"structural_eq must be one of "
+ f"{sorted(Field._VALID_STRUCTURAL_EQ_VALUES, key=str)}, "
+ f"got {structural_eq!r}"
)
self.name = name
self.ty = ty
@@ -153,7 +156,7 @@ class Field:
self.hash = hash
self.compare = compare
self.kw_only = kw_only
- self.structure = structure
+ self.structural_eq = structural_eq
self.doc = doc
@@ -166,7 +169,7 @@ def field(
hash: bool | None = None,
compare: bool = True,
kw_only: bool | None = None,
- structure: str | None = None,
+ structural_eq: str | None = None,
doc: str | None = None,
) -> Any:
"""Customize a field in a ``@py_class``-decorated class.
@@ -198,7 +201,7 @@ def field(
kw_only
Whether this field is keyword-only in ``__init__``.
``None`` means "inherit from the decorator-level ``kw_only`` flag".
- structure
+ structural_eq
Structural equality/hashing annotation. ``None`` (default) means
the field participates normally. ``"ignore"`` excludes the field
from structural comparison and hashing. ``"def"`` marks the field
@@ -221,11 +224,11 @@ def field(
y: float = field(default=0.0, repr=False)
- @py_class(structure="tree")
+ @py_class(structural_eq="tree")
class MyFunc(Object):
- params: Array = field(structure="def")
+ params: Array = field(structural_eq="def")
body: Expr
- span: Object = field(structure="ignore")
+ span: Object = field(structural_eq="ignore")
"""
return Field(
@@ -236,6 +239,6 @@ def field(
hash=hash,
compare=compare,
kw_only=kw_only,
- structure=structure,
+ structural_eq=structural_eq,
doc=doc,
)
diff --git a/python/tvm_ffi/dataclasses/py_class.py
b/python/tvm_ffi/dataclasses/py_class.py
index 07b6aa08..2d3d2872 100644
--- a/python/tvm_ffi/dataclasses/py_class.py
+++ b/python/tvm_ffi/dataclasses/py_class.py
@@ -251,7 +251,7 @@ def _phase2_register_fields(
py_methods = _collect_py_methods(cls)
# Register fields and type-level structural eq/hash kind with the C layer.
- structure_kind = _STRUCTURE_KIND_MAP.get(params.get("structure"))
+ structure_kind = _STRUCTURE_KIND_MAP.get(params.get("structural_eq"))
type_info._register_fields(own_fields, structure_kind)
# Register user-defined dunder methods and read back system-generated ones.
type_info._register_py_methods(py_methods)
@@ -416,7 +416,7 @@ def py_class(
order: bool = False,
unsafe_hash: bool = False,
kw_only: bool = False,
- structure: str | None = None,
+ structural_eq: str | None = None,
slots: bool = True,
) -> Callable[[_T], _T] | _T:
"""Register a Python-defined FFI class with dataclass-style semantics.
@@ -443,10 +443,10 @@ def py_class(
class Point(Object): ...
- @py_class(structure="tree") # structural eq/hash kind
+ @py_class(structural_eq="tree") # structural eq/hash kind
class MyNode(Object):
value: int
- span: Object = field(structure="ignore")
+ span: Object = field(structural_eq="ignore")
Parameters
----------
@@ -469,7 +469,7 @@ def py_class(
If True, generate ``__hash__`` (unsafe for mutable objects).
kw_only
If True, all fields are keyword-only in ``__init__`` by default.
- structure
+ structural_eq
Structural equality/hashing kind for this type. Controls how
instances participate in ``StructuralEqual`` and ``StructuralHash``.
Valid values are:
@@ -496,11 +496,11 @@ def py_class(
"""
if order and not eq:
raise ValueError("order=True requires eq=True")
- if structure not in _STRUCTURE_KIND_MAP:
+ if structural_eq not in _STRUCTURE_KIND_MAP:
raise ValueError(
- f"structure must be one of "
+ f"structural_eq must be one of "
f"{sorted(k for k in _STRUCTURE_KIND_MAP if k is not None)}"
- f" or None, got {structure!r}"
+ f" or None, got {structural_eq!r}"
)
effective_type_key = type_key
@@ -511,7 +511,7 @@ def py_class(
"order": order,
"unsafe_hash": unsafe_hash,
"kw_only": kw_only,
- "structure": structure,
+ "structural_eq": structural_eq,
}
def decorator(cls: _T) -> _T:
diff --git a/tests/python/test_structural_py_class.py
b/tests/python/test_structural_py_class.py
index f280980d..d6b4585f 100644
--- a/tests/python/test_structural_py_class.py
+++ b/tests/python/test_structural_py_class.py
@@ -18,7 +18,7 @@
Mirrors the C++ tests in tests/cpp/extra/test_structural_equal_hash.cc,
porting the object-level tests (FreeVar, FuncDefAndIgnoreField, etc.)
-to Python using ``@py_class(structure=...)`` and ``field(structure=...)``.
+to Python using ``@py_class(structural_eq=...)`` and
``field(structural_eq=...)``.
"""
from __future__ import annotations
@@ -33,7 +33,7 @@ from tvm_ffi.dataclasses import field, py_class
# ---------------------------------------------------------------------------
-@py_class("testing.py.Var", structure="var")
+@py_class("testing.py.Var", structural_eq="var")
class TVar(tvm_ffi.Object):
"""Variable node — compared by binding position, not by name.
@@ -42,17 +42,17 @@ class TVar(tvm_ffi.Object):
name field has SEqHashIgnore
"""
- name: str = field(structure="ignore")
+ name: str = field(structural_eq="ignore")
-@py_class("testing.py.Int", structure="tree")
+@py_class("testing.py.Int", structural_eq="tree")
class TInt(tvm_ffi.Object):
"""Simple integer literal node."""
value: int
-@py_class("testing.py.Func", structure="tree")
+@py_class("testing.py.Func", structural_eq="tree")
class TFunc(tvm_ffi.Object):
"""Function node with definition region and ignored comment.
@@ -61,19 +61,19 @@ class TFunc(tvm_ffi.Object):
comment has SEqHashIgnore
"""
- params: list = field(structure="def")
+ params: list = field(structural_eq="def")
body: list
- comment: str = field(structure="ignore", default="")
+ comment: str = field(structural_eq="ignore", default="")
-@py_class("testing.py.Expr", structure="tree")
+@py_class("testing.py.Expr", structural_eq="tree")
class TExpr(tvm_ffi.Object):
"""A simple expression node for tree-comparison tests."""
value: int
-@py_class("testing.py.Metadata", structure="const-tree")
+@py_class("testing.py.Metadata", structural_eq="const-tree")
class TMetadata(tvm_ffi.Object):
"""Immutable metadata node — pointer shortcut is safe (no var children)."""
@@ -81,7 +81,7 @@ class TMetadata(tvm_ffi.Object):
version: int
-@py_class("testing.py.Binding", structure="dag")
+@py_class("testing.py.Binding", structural_eq="dag")
class TBinding(tvm_ffi.Object):
"""Binding node — sharing structure is semantically meaningful."""
@@ -95,7 +95,7 @@ class TBinding(tvm_ffi.Object):
class TestFreeVar:
- """Test structure="var" kind (C++ kTVMFFISEqHashKindFreeVar)."""
+ """Test structural_eq="var" kind (C++ kTVMFFISEqHashKindFreeVar)."""
def test_free_var_equal_with_mapping(self) -> None:
"""Two different vars are equal when map_free_vars=True."""
@@ -127,7 +127,7 @@ class TestFreeVar:
assert structural_equal(x, x)
def test_free_var_name_ignored(self) -> None:
- """The name field is structure="ignore", so it doesn't affect
comparison."""
+ """The name field is structural_eq="ignore", so it doesn't affect
comparison."""
a = TVar("different_name_a")
b = TVar("different_name_b")
# Names differ, but with mapping they are still equal
@@ -140,7 +140,7 @@ class TestFreeVar:
class TestFuncDefAndIgnore:
- """Test structure="def" and structure="ignore" on fields."""
+ """Test structural_eq="def" and structural_eq="ignore" on fields."""
def test_alpha_equivalent_functions(self) -> None:
"""fun(x){1, x} with comment_a == fun(y){1, y} with comment_b."""
@@ -217,7 +217,7 @@ class TestFuncDefAndIgnore:
class TestTreeNode:
- """Test structure="tree" kind."""
+ """Test structural_eq="tree" kind."""
def test_equal_content(self) -> None:
"""Two tree nodes with identical content are structurally equal."""
@@ -249,7 +249,7 @@ class TestTreeNode:
class TestConstTreeNode:
- """Test structure="const-tree" kind."""
+ """Test structural_eq="const-tree" kind."""
def test_equal_content(self) -> None:
"""Two const-tree nodes with identical content are structurally
equal."""
@@ -276,7 +276,7 @@ class TestConstTreeNode:
class TestDAGNode:
- """Test structure="dag" kind."""
+ """Test structural_eq="dag" kind."""
def test_same_dag_shape(self) -> None:
"""Two DAGs with the same sharing shape are equal."""
@@ -319,10 +319,10 @@ class TestDAGNode:
class TestUnsupported:
- """Test that types without structure= raise on structural comparison."""
+ """Test that types without structural_eq= raise on structural
comparison."""
def test_unsupported_raises_on_hash(self) -> None:
- """Structural hashing raises TypeError for types without structure=."""
+ """Structural hashing raises TypeError for types without
structural_eq=."""
@py_class("testing.py.Plain")
class Plain(tvm_ffi.Object):
@@ -332,7 +332,7 @@ class TestUnsupported:
structural_hash(Plain(x=1))
def test_unsupported_raises_on_equal(self) -> None:
- """Structural equality raises TypeError for types without
structure=."""
+ """Structural equality raises TypeError for types without
structural_eq=."""
@py_class("testing.py.Plain2")
class Plain2(tvm_ffi.Object):
@@ -348,14 +348,14 @@ class TestUnsupported:
class TestValidation:
- """Test that invalid structure= values are rejected."""
+ """Test that invalid structural_eq= values are rejected."""
def test_invalid_type_structure(self) -> None:
- """Invalid type-level structure= value raises ValueError."""
- with pytest.raises(ValueError, match="structure"):
- py_class(structure="invalid")
+ """Invalid type-level structural_eq= value raises ValueError."""
+ with pytest.raises(ValueError, match="structural_eq"):
+ py_class(structural_eq="invalid")
def test_invalid_field_structure(self) -> None:
- """Invalid field-level structure= value raises ValueError."""
- with pytest.raises(ValueError, match="structure"):
- field(structure="bad_value")
+ """Invalid field-level structural_eq= value raises ValueError."""
+ with pytest.raises(ValueError, match="structural_eq"):
+ field(structural_eq="bad_value")