Prototype: demand-driven generics as the only mechanism - #237
Prototype: demand-driven generics as the only mechanism#237SimonHeybrock wants to merge 16 commits into
Conversation
Generic providers whose type variables lack constraints are no longer rejected at insertion. They are kept as templates and instantiated by unifying their type patterns with the concrete keys appearing in the pipeline: backward from requested targets, forward from present concrete keys when mapping or listing output keys. This makes PEP 695 generics (scoped type variables with empty __constraints__) work without declaring constraints anywhere, see #235. Providers with constrained type variables are expanded eagerly as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Setting a value for a generic key (e.g. params={Raw: Raw(1.2)}) with
unconstrained type variables now stores the value as a template,
matched on demand against requested specializations, symmetric to
generic providers. This restores the generic-parameter feature for
PEP 695 generics, as raised in review of the prototype.
Forward instantiation at map() time is now restricted to bindings that
consume a mapped key or a key derived from one. Unrestricted forward
expansion instantiated providers for unrelated concrete keys, creating
extra sink nodes that broke cyclebane's unique-sink requirement in
reduce().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove the eager constraints-based expansion and the 'constraints' Pipeline argument. All generic providers and generic-key params are now templates, instantiated on demand; constraints declared on type variables act as filters during unification instead of driving eager enumeration. Closes the gaps found when running the test suite against the demand-driven path: - Bare generic classes (e.g. 'def foo() -> A' for generic A) are normalized to subscripted patterns. - Pydantic generic models, whose metaclass hides type parameters from typing introspection, are unified via their generic metadata. - Generic providers and generic-key values share one insertion-ordered template list, so last-write-wins holds among generics. Semantic change: a specialized provider now shadows a generic one regardless of insertion order (previously a generic provider inserted later replaced specialized providers). This restores the specialization priority sciline had before the cyclebane rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No description provided. |
Living document for the discussion around issue 235 and the two prototypes: problem statement, mechanism, measured compatibility evidence, cost accounting vs main, the three open design questions (forward chaining vs deferred map, coherence among rules, rule-graph inspection), and a decision log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolution among generic providers/values matching a demanded key is now order-independent: - Templates with equivalent patterns (equal up to renaming of type variables, across provider/value kinds) replace each other at registration, preserving the explicit-override idiom. - Among matches, a strictly more specific pattern wins, determined by one-sided subsumption. Constrained type variables are more specific than unconstrained ones. - Incomparable overlapping matches raise AmbiguousProvider (new, exported) instead of silently picking the latest registration. test_multiple_matching_partial_providers_uses_latest documented the order-dependent behavior and now asserts the error instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reading cyclebane shows map() does not duplicate dependents: it stores values on the side and symbolically relabels descendants as MappedNode; per-index duplication happens only in to_networkx(), i.e. at build time. The only eager part is the labeling, which is derivable at compile time from reachability and the stored node values. Replace the record-and-replay option with a concrete proposal: derive mapped labeling in to_networkx(), making map() order-independent and deleting forward chaining from sciline. List open points (reduce sink resolution, mapped roots as satisfied keys, index-order compatibility, groupby) and note the coupling with Q3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With cyclebane deriving mapped-node labeling at task-graph build time (scipp/cyclebane#32), dependents of mapped nodes no longer need to exist when map() is called: providers instantiated on demand after mapping receive their indices from the derivation. Remove the forward-instantiation hook from map() and the seed-restriction machinery; forward chaining remains only behind output_keys(). Mapped roots count as satisfied keys, so backward instantiation never provides them. reduce() with an explicit key treats that key as a demand and instantiates providers for it; without a key, only nodes present in the graph determine the reduced sink, so reducing a rule-derived sink requires an explicit key (tests updated accordingly). get_mapped_node_names uses the new cyclebane named_indices accessor instead of scanning for MappedNode labels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Q3 decision: output_keys() returns concrete graph sinks plus the return-type patterns of generic providers not consumed by other generic providers (approximated by pattern origin); no-argument visualize() shows the concrete part of the graph, since patterns cannot be demanded. This removes _instantiate_forward and forward_bindings; the engine is backward-only. With cyclebane forbidding reduce results and grafted branches from shadowing mapped nodes, get_mapped_node_names loses the multiple-candidates disambiguation; index_names= is now pure validation. Tests using the shadowing idioms are rewritten with distinct names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An independent review found three cases where making eager semantics lazy changed results silently, plus hardening gaps. All fixed with regression tests: - reduce() without an explicit key raised no error when the sink could come from a not-yet-instantiated generic provider, silently reducing the mapped root instead. It now raises unless the unique sink is not matched by any generic provider argument; an explicit key is treated as a demand as before. - TypeVar bounds (def f[T: Base]) were silently ignored during unification, matching any key; they now act as filters like declared constraints. - A rule whose argument pattern is structurally larger than its return pattern (e.g. P[list[T]] -> P[T]) recursed forever; demanded-key nesting depth is now capped with a clear error. - CycleError was raised without a message; it now carries the cycle. - get_mapped_node_names conflated "not mapped" with an index_names mismatch; the messages are split. Cleanups from the same review: instantiation hooks unified behind a _demanded helper, index-based dominance check in template resolution, pattern_origin_and_args made module-public, reduce key hoisted into the signature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
An independent fresh-eyes review of both branches found three silent-wrong-answer holes, all where eager semantics were made lazy without being pinned by tests. Fixed in d0b184e (sciline) and scipp/cyclebane@3727304, each with regression tests: |
Consolidated end-state proposal for #235, now targeting
main. Draft for discussion — depends on scipp/cyclebane#32; CI is red until that lands and is released. #238 has been merged into this PR; #236 remains open as the coexistence alternative. Full context, derivation, measured evidence, and a decision log are indocs/developer/architecture-and-design/demand-driven-generics.md(part of this PR); the commit history preserves one decision per commit.What this is
Generic providers are resolved by demand-driven instantiation (structural unification of type patterns with requested keys), replacing eager constraint-based expansion:
type X[T] = ...aliases) work with zero ceremony; the example from Support for Python 3.12 generics #235 runs verbatim. Old-style unconstrainedTypeVars work the same way. Fixes the blocker behind Scope does not work with Python 3.12 generics #233.params={Raw: value}) are templates applied on demand to all requested specializations.map()needs no special handling: Derive mapped-node labeling at compile time cyclebane#32 keeps plain node names and derives mapped-node labeling at task-graph build time, so providers instantiated after mapping receive their indices automatically.Semantic changes vs
mainconstraints=argument is removed. Constraints declared on type variables (TypeVar('T', int, float),class Raw[Run: (A, B)]) still work, as filters during unification.reduce()requires an explicitkeywhen the reduced sink would come from a generic provider; fully concrete pipelines are unaffected.reduce(name=<name of the reduced node>)andpipeline[C] = pipeline[C].map(...).reduce(func=merge)now raise clear errors; use a distinct name for the mapped node vs. its reduction. This also turns a silent self-loop bug on cyclebanemaininto an error.output_keys()returns concrete sinks plus return-type patterns of generic providers not consumed by other generic providers; no-argumentvisualize()shows the concrete part of the graph. Inspection surfaces show demanded/derived state, not the constraint cross-product.Evidence
Of the 248 tests before this change, 242 pass unchanged; six tested the removed
constraints=argument. All 143 cyclebane tests pass with identicalto_networkx()output. mypy clean (sciline), one pre-existing error fewer (cyclebane). New tests cover PEP 695 classes and aliases, chains, target-only instantiation, shadowing, multi-TypeVar providers, coherence errors, mapped pipelines with post-map instantiation, and generic params.Before this could land
generic-providers.ipynbstill documentsconstraints=, and the parameter-table docs use the now-forbidden reduce-graft idiom; the docs build fails on this branch.sl.Scope(Scope does not work with Python 3.12 generics #233) and a migration/deprecation story forconstraints=(this branch hard-removes it to show the end state).🤖 Generated with Claude Code