Skip to content

feat(program): a where mask answers its own questions — conjuncts, names, atoms and the dims it is read at - #319

Closed
FBumann wants to merge 4 commits into
mainfrom
feat/mask-stateful
Closed

feat(program): a where mask answers its own questions — conjuncts, names, atoms and the dims it is read at#319
FBumann wants to merge 4 commits into
mainfrom
feat/mask-stateful

Conversation

@FBumann

@FBumann FBumann commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Prompt: "Can we make the masking fully statefull using dataclasses like the other operators and declarations in Program? Changing what lives in .root?" / "I think we should do both."

Note

The following content was generated by AI.

A where mask reaches a consumer as a Mask that answers every question the language owns about it: .conjuncts, .names_read, .atoms as properties of .root, and .dims — the dimensions the mask is read at — stored, resolved at lowering, the one moment that holds both the predicate and every name's dims. Region.when arrives in the same carrier, so one helper covers every mask in a program. The free conjuncts() is retired to _conjuncts behind the property — one public door.

Built from main per instruction (the stack was abandoned): the two commits of #317 and #318 are cherry-picked here, so this PR supersedes both.

Why dims stops being a question the consumer answers (corrected after the consumer's review, superseded in shape by #324): dims_read(name_dims) required a name→dims mapping that the first consumer builds by hand today from public Program fields, and calls at three sites — a parameters+variables-only copy of the read-through rule that would silently narrow the answer the day an atom kind consulted the mapping for anything more. This PR stores the answer on Mask at lowering; #324, three PRs up the stack and on the same reviewer's proposal, moves it further — onto the leaves at resolution, making Mask.dims derived and this PR's stored field an intermediate shape that never survives to the end of the stack. Read this PR for Mask as the carrier and Region.when joining it; read #324 for where dims finally lives.

The trade, named: Mask is the program's first denormalized node — a hand-built Mask(root, wrong_dims) disagrees with itself and nothing validates it. In production only lowering builds one, so it cannot bite there; a test fixture that hand-builds declarations now supplies dims by hand, and a wrong set is a silent behavioural switch in whatever reads it. Free dims_read stays available for synthetic predicates (and enters program.__all__ in the stacked #321).

Verified

pixi run ci exit 0 in this worktree: lint clean, 930 passed, mkdocs build --strict builds, 27 TeX documents compile. schema/ and tests/typesetting/golden/ untouched — the typesetter runs off the Spec side and never reads program.Region.when.

Failing-first: with src/ stashed back to the unreworked tree, the new tests cannot even collect — TypeError: Mask.__init__() takes 2 positional arguments but 3 were given. The pins that pass on both trees (unmasked where is None, constraint where is a Mask, atoms-vs-conjuncts on a and (b or c)) close gaps mutation testing found in #318, where dropping _mask's None fold or collapsing atoms into conjuncts left the suite green.

Post-review: _Lowering carries name_dims as a cached_property, so a cases-heavy walk resolves the mapping once rather than once per cases: node — machinery #324 then deletes with the mapping itself. Do not land this PR without the rest of the stack: alone, it ships an intermediate shape.

Deliberately not done: relocating the resolved where-node vocabulary from where_parser into program — that is the stacked follow-up (#321), kept out so this diff stays the API reshape alone. conjuncts/names_read/atoms stay properties rather than stored fields: they are pure functions of .root and cannot disagree with it; only dims needs outside knowledge. One departure from the house dims: tuple[str, ...] style: Mask.dims is a frozenset because a mask restricts an unordered set of axes — declaration tuples carry axis order, a mask has none.

FBumann and others added 3 commits September 1, 2026 15:40
…to give, not each consumer's to re-derive

`names_read(where)` returns the parameters, lookups and variables a resolved
predicate's leaves test — the name rule beside `dims_read`'s dim rule, and its
complement: a leaf is read at a dimension and of a declaration, and where
`dims_read` gives the first this gives the second. A comparison on a dimension
names no declaration, so it drops out here and stays `dims_read`'s; a lookup
pair names both maps it compares.

Built on `atoms`, closed by `assert_never` the same way `_atom_dims` is, so a
predicate node added without a reading is a type error at the one branch rather
than a name silently missed. A consumer asking which parameters a mask gates on
— whether the corpus ever varies one — asks it here rather than walking the
leaves itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jk5LAMCoiD39q4Xz4AMVw
…er than a bare node each consumer re-walks

A declaration's `where` is now a `Mask`, not a bare `WhereNode`. The mask holds
the resolved predicate as `.root` — the node an engine still dispatches on with
`isinstance` — and carries the questions asked of it beside it: `.conjuncts`,
`.names_read`, `.atoms`, `.dims_read`. This is the shape every other declaration
already has (`DimensionDeclaration.maps`, `Program.footprint`): the question
lives on the thing that holds the data, so two consumers cannot answer it
differently, rather than each reaching for a free function with the raw node.

The free functions stay as the one-home implementation the Mask delegates to and
that internal frame checks call on raw nodes. Lowering wraps the resolved where;
an always-true mask folds to `None` as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jk5LAMCoiD39q4Xz4AMVw
…mask arrives in the same carrier

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D59sEp5d7CecJAwrzACbRh
@read-the-docs-community

read-the-docs-community Bot commented Sep 1, 2026

Copy link
Copy Markdown

…ng once per walk rather than once per cases node

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D59sEp5d7CecJAwrzACbRh
@FBumann

FBumann commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Note

The following content was generated by AI.

Superseded by #327, which combines the reviewed stack into a single diff on the maintainer's call. The review history — two consumer reviews and one consolidated agent review, with mutation tables — lives in this stack's bodies; the tree at #327 is byte-identical to the stack's top.

@FBumann FBumann closed this Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant