Extensible program lowering through injected operator handlers - #119
Open
AlekseiChirkovVention wants to merge 5 commits into
Open
Extensible program lowering through injected operator handlers#119AlekseiChirkovVention wants to merge 5 commits into
AlekseiChirkovVention wants to merge 5 commits into
Conversation
…ndlers Assert that the framework owns reachability, dependency order, and selected-output slicing while consumer handlers emit opaque target values. Handlers dispatch by concrete operator type: an operator whose route name merely looks like a supported one must not reach that handler. Cover the fusion hook as optional, explicit, and bounded: candidates are limited by the declared look-ahead, a declined fusion falls back to per-operation handlers, fused operations keep their source provenance, and a fusion is rejected if it claims an operation twice, claims one outside the offered window, omits the operation it was offered, or would drop a value used elsewhere. Cover failing closed before any handler emits a value: an operation with no registered handler, a handler returning nothing or raising an uncategorized error, duplicate registrations, cycles, unknown selected outputs, duplicate producers, and missing dependencies. Add the handler contract violation category to the exhaustive category assertion.
The look-ahead window test reused a fixture built to make fusion illegal, so lowering raised before the test reached any of its assertions and the bound it names was never exercised. Point it at a new three-operation fixture whose fusion is legal, leaving its assertions unchanged. Three operations against a window of two also means the cap now genuinely applies, which the two-operation fixture could not demonstrate. The fixture with a live intermediate is left as it was, for the tests that assert a fusion stranding it is rejected.
…lers Add a framework-owned program construction seam. The framework consumes the structured dependency analysis for reachability, dependency order, and selected-output slicing, then dispatches each operation to a handler registered against its concrete operator type. Handlers return values the framework treats as opaque, so a consumer emits its own target representation without the framework depending on it. Enforce exactly-once lowering structurally rather than by checks along the way: a single claim is the only path by which an operation becomes lowered and the only path by which a produced value enters the environment, so a repeated claim fails identically whether it comes from a handler or from a fusion. One set comparison over the whole reachable region at the end catches the operations no claim covered, which a per-operation guard cannot see. Offer an optional fusion hook a bounded window of unclaimed operations. A fusion must claim the operation it was offered, may claim only operations inside that window, may not claim one twice, and must leave exactly one value escaping the fused region. That liveness rule is kept separate from the exactly-once rule, since conflating them would cover neither. Every reachable operation must be individually lowerable, without exception when a hook is present: fusion collapses operations that are already supported rather than substituting for support. An operation with no handler therefore fails before any handler emits a value. Add a handler contract violation category for a handler that returns no value or raises an uncategorized error; a categorized error raised by a handler propagates unchanged.
Assert that a fusion claiming an operation whose operand it never produces and which is not already bound is rejected, so a fused instruction cannot be emitted before an operation it depends on. A fusion may still draw operands from values bound earlier, which the accompanying test pins so the rule is not tightened into requiring every operand to come from inside the claimed set. Assert that a handler failing with an ordinary exception, including one of the consumer's own classes, is reported as a handler contract violation rather than escaping uncategorized, while an interrupt raised inside a handler still propagates. Two of these pin behaviour that already holds and passed before the change; they guard the permissive side of both rules against being tightened too far.
A fusion could claim operations whose operands neither it produced nor the framework had bound yet, because the claim checks covered membership and the liveness check covered only values escaping the fused region. The framework then emitted the fused instruction ahead of an operation it depended on and reported nothing, leaving the caller a program in an order it could not execute. Require every operand of every claimed operation to be already bound or produced within the claim, naming the operation and the value when it is not. This restores the guarantee the single-operation path already had when it resolved its operands. The rule is availability rather than adjacency by intent: a hook sees the operations it is offered and never their positions, so an adjacency check would refuse a legal claim for a reason the consumer cannot observe, and would change meaning if the traversal order were revised. Also normalize any ordinary exception a consumer callback raises into a handler contract violation, rather than a fixed list of types that a consumer's own exception classes fall outside of. A categorized error keeps its own category, and an interrupt or an interpreter exit still propagates.
code-tc
approved these changes
Aug 20, 2026
AlekseiChirkovVention
marked this pull request as ready for review
August 21, 2026 09:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Goal
Add a framework-owned program-construction seam that performs graph selection, validation, and traversal while delegating concrete operation emission and optional fusion to consumer-provided handlers. A consumer supplies only handlers keyed by concrete operator type; it never reimplements reachability, ordering, or dependency validation.
What changed
tinychain.autodiff.loweringis a new, additive module. It consumes the structured dependency analysis for reachability, dependency order, and selected-output slicing, then dispatches each operation to a handler registered against its concrete operator type. Handler return values are treated as fully opaque — the framework never compares, hashes, iterates, truth-tests, or stringifies them — so a consumer emits its own target representation with no framework dependency on it.An optional fusion hook is offered a bounded window of unclaimed operations and may collapse several into one instruction.
Three named invariants
The seam's correctness rests on three properties, each enforced in exactly one place rather than by checks spread along the lowering path:
Every reachable operation must be individually lowerable, with no exception when a hook is present: fusion collapses operations that are already supported rather than substituting for support. An operation with no handler therefore fails before any handler emits a value.
Error handling
Adds one category,
handler_contract_violation, for a handler that returns no value or fails with an uncategorized exception. Any ordinary exception a consumer callback raises is normalized to it, including a consumer's own exception classes, which the framework cannot enumerate ahead of time. A categorized error raised by a consumer keeps its own category. Interrupts and interpreter exits propagate untouched.Acceptance criteria and their tests
44 unit tests in
py/tests/test_autodiff_lowering.py.The derivative program path is exercised through the identical seam, including seed and forward-capture bindings.
Test evidence
Test level: unit. The seam is an in-process graph transformation whose only external actor is a consumer-supplied handler, for which a fake consumer is the natural double.
Tests were written first and committed before their implementation:
adc27efandb2aedfcare test-only commits, each observed failing for the right reason beforehand.f8f9cf0corrects one test whose fixture made its assertions unreachable; all four assertions are verbatim and the replaced fixture is left in place for the test that depends on it.Focused: 48 passed. Full non-integration suite: 717 passed, 1 skipped, with the 4 failures that pre-date this work unchanged — none vanished, no new one. Deterministic across varied hash seeds.
Stack position
Stack position 2 of 4 — based on
feat/derivative-dependency-analysis.Cannot merge into
mainbeforefeat/derivative-dependency-analysis.Notes for review
Public exports are deliberately not added here — that belongs to the public-contract branch at the top of the stack, and the tests import the module directly.
Under the current depth-first traversal, a fusion claim that skips an operation always turns out to read an unavailable operand, so operand availability and adjacency coincide in practice; extensive randomized search found no claim separating them. The guard is written as availability regardless, because that rule stays correct if the traversal order ever changes.