Skip to content

[eudsl-llvmpy] Design: a canonical live-value registry (dedup + identity + invalidation) for Value/Instruction/BasicBlock wrappers #615

Description

@makslevental

Context

eudsl-llvmpy currently binds Value and its subclasses (Instruction, BasicBlock, Argument, ...) as thin, non-owning, un-deduped pointer views: EUDSL_CAST_CTOR wraps a raw llvm::Value* with rv_policy::reference + keep_alive<0,1>, and there is no per-value registry. Only Context and Module have real lifetime tracking (what assert_no_leaks() counts). Everything below rides on keep_alive tying a value's lifetime to its parent.

This is a deliberate, cheap design that matches the "mirror the LLVM C++ API 1:1" goal. But it means the Python layer has no notion of which wrapper corresponds to a given C++ entity, so it cannot dedup wrappers, cannot offer stable identity, and cannot invalidate a wrapper when the underlying object goes away — except the one narrow case we've already patched.

This issue is a design note to decide whether to build a canonical live-value registry, analogous to MLIR's PyOperation + liveOperations system (mlir/lib/Bindings/Python/IRCore.cpp: PyOperation::forOperation, the valid/checkValid()/setInvalid() flag, and transitive invalidation on erase).

What we already fixed (for reference)

Instruction.erase_from_parent / BasicBlock.erase_from_parent now poison the called wrapper via nb::inst_set_state(self, ready=false, destruct=false) after freeing the C++ object, so a use-after-erase on that handle raises TypeError ("attempted to access an uninitialized instance") instead of dereferencing freed memory (PR #608). This handles the common footgun (erase then touch the same handle).

Gaps that remain (what a registry would close)

Gap Severity Needs a registry?
erase-then-touch the same handle common already fixed (poison)
erase-then-touch an aliased handle (a second wrapper for the same value, e.g. via maybe_downcast re-wrap) rare yes
stable identity (inst is inst across two lookups) quality-of-life yes
transitive invalidation (erase a BasicBlock -> wrappers for its instructions dangle) real but narrow yes

Only the bottom three need the machinery, and all three are lower-frequency than the one already handled.

What MLIR does (the model to consider)

  • Canonical dedup: one PyOperation per C++ operation, via a liveOperations map keyed by raw pointer (PyOperation::forOperation). Guarantees identity and gives a single place to invalidate that reaches every alias.
  • Validity flag: PyOperation::valid + checkValid() throws on any access after invalidation; erase() does checkValid(); setInvalid(); mlirOperationDestroy(...).
  • Transitive invalidation: erasing/attaching invalidates dependent wrappers (subtree, symbol-table erase, etc.).

MLIR pays for this with a map lookup+insert on every wrapper creation and per-wrapper context-ref + validity + attachment state.

Options

  1. Do nothing more. Keep the documented caveat: poison covers the common case; aliasing/transitive are rare and documented. Lowest cost; matches the thin-mirror design.
  2. Add a scoped-down liveValues registry keyed by llvm::Value*, giving dedup + is identity + a single poison point on erase (and a hook for transitive invalidation). This is the "do it properly" option.
  3. Full MLIR-style system with attachment tracking and transitive subtree invalidation. Largest change.

Cost / risks of a registry (options 2-3)

Recommendation (for discussion)

Lean option 1 now; pursue option 2 only if real usage surfaces identity/aliasing/transitive bugs. If we do option 2, co-design it with #614 so the registry is synchronized from the start.

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions