Skip to content

feat(language): which dims a mask reads is the language's answer, so two consumers cannot restrict one model differently - #269

Merged
FBumann merged 1 commit into
mainfrom
feat/where-dims
Aug 31, 2026
Merged

FBumann merged 1 commit into
mainfrom
feat/where-dims

Conversation

@FBumann

@FBumann FBumann commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Prompt: "Please update to the latest mathspec and push as much work as possible to it."

Note

The following content was generated by AI.

What this changes

where_parser.dims_read(where, name_dims) — which dims a resolved predicate
reads. A parameter through its own dims, a variable through the frame it is
declared over, a comparison on a dimension through that dimension, and a lookup
through the dimension it maps out of.

dims_read(where_of('p_max > 0 AND snapshot == 0', ns, ctx), name_dims)
# frozenset({'generator', 'snapshot'})

Why it is the language's

lpspec answers this today, in
relational/engines/polars/predicates.py::predicate_dims — 37 lines walking
the same closed union and applying the same reading. Its own docstring says
what that costs:

a predicate this function does not know. One that forgot to answer here would
silently mis-restrict or mislabel a model

That is what-counts-as-language.md's test met exactly: two consumers deciding
this separately would mask the same model differently, with no error anywhere.
A Julia consumer would inherit none of it.

And this repository was already answering it — _check_where_dims walks the
same leaves with the same reading, to ask whether they fit the frame. So the
rule had two homes here and a third downstream. It now has one, and the
load-time check is a reader of it.

What lpspec removes

predicate_dims and its LanguageError fence, both call sites moved to
dims_read — 37 lines, and a rule it should never have owned. Reached by
module path, which where_parser's own docstring already grants and lpspec's
test_the_language_is_imported_as_one_package already allows, so the package
surface does not grow
.

It also drops a wrong branch lpspec had and this does not:

value = getattr(where, 'value', None)
if isinstance(value, str) and value in name_dims:   # a *literal* read as a name
    dims |= frozenset(name_dims[value])

ParameterComparisonNode.value is float | str and the str is a flag, never
a declaration. A model comparing a str parameter to a flag whose text
collides with another declared name would union that name's dims in. Not a
wrong answer downstream — it widens the set, which only costs lpspec a
join-strategy fallback — but it is a reading the language never had.

The messages do not move

_check_where_dims reports per leaf, and that is why it is a separate
reader rather than a dims_read(...) <= frame one-liner: a reader told only
that the predicate leaves the frame would still have to work out which half of
it did. Every one of the four sentences is byte-identical, and the existing
tests that assert them are untouched and green.

Verified

pixi run ci — lint, typecheck (0 errors), test (818 passed, 809
on main — nine new), docs-build --strict, render-tex and compile-tex
(26 documents).

Equivalence, measured rather than argued. dims_read was run against
lpspec's predicate_dims over every predicate in lpspec's corpus — all 46
models in examples/, 43 predicates, 0 mismatches — with this branch on
PYTHONPATH and lpspec unchanged.

Exhaustiveness, and where it moved

The old _check_where_dims ended in assert_never, so a predicate node added
without a branch was a type error. That fence is kept and improved: it now sits
on _atom_dims, the one function that has to grow a branch, instead of on a
checker that also had to grow a message. lpspec's version raised
LanguageError at runtime for the same case, which is a fence that fires at
the first model to use the node rather than at the commit that adds it.

atoms() is the walk both readers share — every leaf, connectives removed —
and is public for the same reason dims_read is: #253's Holds.dims is
exactly this question asked of a declared predicate, so it should be this
answer.

Not done: dims_read is not added to math_spec.__all__. where_parser
is deliberately out of it and reachable by module path, and a predicate walk
belongs to the module that defines predicates — by #246's test, this is a
capability that cost the package surface nothing, which is the shape to prefer.

…two consumers cannot restrict one model differently

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 math-spec | 🛠️ Build #34307542 | 📁 Comparing ba0df52 against latest (a9237ec)

  🔍 Preview build  

1 file changed
± reference/math_spec/where_parser/index.html

@FBumann
FBumann merged commit 350b1ed into main Aug 31, 2026
5 checks passed
@FBumann
FBumann deleted the feat/where-dims branch September 9, 2026 06:45
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