eudsl-py (removed in #650) bound MLIR in Python by auto-generating nanobind bindings from MLIR C++ headers via eudsl-nbgen (a source-to-source translator driven mostly off TableGen-generated *.h.inc files). That approach turned out to be brittle and only translated a narrow slice of the API correctly.
This issue tracks doing MLIR Python bindings more correctly, applying the eudsl-llvmpy approach where it fits: hand-written nanobind wrappers over the core C++ API.
Note this is not an all-hand-written vs. all-generated choice. MLIR dialect ops are themselves auto-generated in C++ from TableGen (mlir-tblgen Op Definitions), so binding the dialects will necessarily involve some form of autogeneration too. The goal is to split the work correctly:
- Core IR API — hand-written, the way
eudsl-llvmpy does LLVM IR / Machine IR (Context/Module/Operation/Block/Region/Value/Type/Attribute, etc.). This stable, hand-authored surface is what eudsl-py's generated approach got wrong.
- Dialect ops — generated, since the C++ for them is already generated from TableGen. The open question is the right generation strategy (drive off the ODS/TableGen records or the generated headers, emit nanobind directly, etc.), not whether to generate at all.
The eudsl-llvmpy conventions we'd want to carry over to the hand-written core (see projects/eudsl-llvmpy/CLAUDE.md), and ideally have the generated dialect layer respect as well:
- Mirror the C++ API. Bind names as MLIR spells them; the Python class hierarchy mirrors the C++ one. Don't rename/merge methods because they read as equivalent.
- Bind the full API; don't hardcode assumptions. Surface the C++ parameters (defaults are fine) rather than baking one choice into C++.
- Prefer specific nanobind types over
nb::object/nb::handle, so casts and identity checks are precise and is identity holds via nanobind's instance registry.
- Breaking changes are fine — no stability contract; evolve the API when it improves things and update tests/docs to match.
- Tests + coverage discipline as in eudsl-llvmpy (leak checks, per-PR C++ coverage over the binding sources).
Scope / open questions:
- Which MLIR surfaces to bind first (IR core listed above), and the boundary between the hand-written core and the generated dialect layer.
- The dialect-op generation strategy (source of truth: ODS/TableGen records vs. generated
*.h.inc; what the emitted bindings look like) and how much can be shared with / learned from eudsl-nbgen.
- Relationship to the upstream MLIR Python bindings (
mlir-python-bindings) — where this complements vs. overlaps.
Follow-up to #650.
eudsl-py(removed in #650) bound MLIR in Python by auto-generating nanobind bindings from MLIR C++ headers viaeudsl-nbgen(a source-to-source translator driven mostly off TableGen-generated*.h.incfiles). That approach turned out to be brittle and only translated a narrow slice of the API correctly.This issue tracks doing MLIR Python bindings more correctly, applying the
eudsl-llvmpyapproach where it fits: hand-writtennanobindwrappers over the core C++ API.Note this is not an all-hand-written vs. all-generated choice. MLIR dialect ops are themselves auto-generated in C++ from TableGen (
mlir-tblgenOp Definitions), so binding the dialects will necessarily involve some form of autogeneration too. The goal is to split the work correctly:eudsl-llvmpydoes LLVM IR / Machine IR (Context/Module/Operation/Block/Region/Value/Type/Attribute, etc.). This stable, hand-authored surface is whateudsl-py's generated approach got wrong.The
eudsl-llvmpyconventions we'd want to carry over to the hand-written core (seeprojects/eudsl-llvmpy/CLAUDE.md), and ideally have the generated dialect layer respect as well:nb::object/nb::handle, so casts and identity checks are precise andisidentity holds via nanobind's instance registry.Scope / open questions:
*.h.inc; what the emitted bindings look like) and how much can be shared with / learned fromeudsl-nbgen.mlir-python-bindings) — where this complements vs. overlaps.Follow-up to #650.