Skip to content

chore(piecewise): a block is checked once, and its rows are written from what the check settled - #623

Merged
FBumann merged 2 commits into
claude/mathspec-piecewise-api-7j0wptfrom
chore/piecewise-simplify
Sep 22, 2026
Merged

FBumann merged 2 commits into
claude/mathspec-piecewise-api-7j0wptfrom
chore/piecewise-simplify

Conversation

@FBumann

@FBumann FBumann commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Prompt: "Do a stacked PR trying to simplify the picewise feature as much as possible, removing all indirection. Also centralize the checks of whats allowed and what not. Most methods to a lot of assertions currently. We want focused methods, and narrow things down beforehand"

Note

The following content was generated by AI.

What this changes

Stacked on #569. Every check on a piecewise: block has one home, and the emitters decide nothing: they write rows from facts a single check settled. No model loads or refuses differently, and every emitted row is byte-identical.

Three layers, in the order a file passes them:

layer where decides
the block alone PiecewiseBlock's validators, as before arity, signs, which methods take a refinement or a gate
the block against the file's keys Spec._piecewise_shapes and Spec._piecewise_emitted_names, beside the sos: checks in _validate_references along, dims:, each link's values parameter, the relation and columns a walk names, the dimension a split spans, the gate; and that no name the expansion writes is one the file declares
the block against the typed model piecewise.check, returning a Curve the frame, each link's row, that each link's expression and values fit its row, that the where: fits the frame

What went. _Expansion, a class of thirty methods that interleaved emitting and checking, computed each link's row frame three times, and parsed every link expression and the gate itself. The link expressions and the where are now read off Spec.resolved, which types them with the rest of the model, so _expr_dims, _dims_of, its cache, the second parse of the gate, and the module's imports of the parser, the resolver and the degree check are gone. CurveMask takes the resolved mask rather than resolving text, so it holds no namespace. The emitters are four functions over a Curve.

Load order. Spec._validate_expressions resolves the file before expanding it, where it expanded first. A fault in a link is still named against the link the file wrote, by resolution's own context (piecewise 'x' link 0), and the guard tests assert that.

Error classes, on the alpha stream. A refusal about what a block names by key, or a collision, is now a SchemaError with the file's other cross-declaration refusals, and a fault in a link expression is the SchemaError resolution raises, with the same wording. PiecewiseExpansionError is left for what only the typed model can decide: a link that does not fit its row, or a where outside the frame. Six tests that pinned PiecewiseExpansionError on those cases pin LanguageError, the base of both, so their message assertions stand.

What each moved refusal is still covered by

Every message is preserved verbatim, and each has a test matching on it:

refusal test
undeclared parameter 'nope' test_a_malformed_block_is_refused[undeclared-parameter]
must be binary, is not a declared variable …[activity-must-be-binary], test_a_gate_that_is_not_a_variable_is_refused
emitted sos 'cost_curve' collides, emitted assumption … collides test_an_emitted_set_may_not_collide_with_a_declared_one, test_a_condition_a_method_states_is_a_name_the_block_emits
references undeclared relation, has no column for, over and into both reach …[a-walk-through-an-undeclared-relation], …[a-walk-that-does-not-name-both-ends] and the refinement table
dims carries, dims repeats, into names '…', the breakpoint dim …[splitting-along-the-breakpoint-dim] and the frame table
link expression faults (over variables, the divisor contains variables, which is degree 2, a dual exists only after a solve) the four link tests, now on LanguageError, still asserting piecewise 'cost_curve' link 0 in the message

One new test: test_every_assumption_a_block_may_derive_is_a_name_it_reserves. The collision check runs before the mask that decides which assumptions are written is typed, so Names reserves all five suffixes in ASSUMED, and the test holds assumptions_of to that list for every method.

Gates, and what could not be run
gate result
pytest -q -n auto 1619 passed, 1 skipped
ruff check, ruff format --check clean
pyrefly check 12 errors, all missing-import for pydantic and yaml stubs from the venv layout, the same count as the base
the schema, the golden output, the notation and expansion pages regenerated; no drift
prettier on the one edited page clean
typos, reuse lint, taplo, zizmor, compile-tex, mkdocs build --strict not run here, for want of the tools or of the proxied Python inventory

Run from a uv environment on Python 3.12 with the pinned ruff==0.16.1 and pyrefly==1.2.0, because this environment's proxy refuses pixi.sh.

piecewise.py is 722 lines, from 786. The diff as a whole adds 53 lines, because the key checks that moved into model.py carry their own docstrings there; what fell is the concept count, not the character count, and that is the trade this PR makes.

Deliberately not done
  • The block's own validators stay on PiecewiseBlock. They need no schema, and pydantic runs them before any cross-declaration rule, which is the narrowing-beforehand asked for.
  • sos:'s checks are not touched. They already sit where the piecewise ones now do.
  • No mutation table. No guard was added or reworded; each moved one keeps the test that fails without it, listed above.

Why

The expansion had grown into one class where every emitter re-derived facts and every fact carried a check. Reading a row meant reading the checks around it, and a check could not be found without knowing which emitter needed it. Deciding everything first, in one place per layer, is what lets each emitter be read on its own.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GNZN57CR8oGTVpRmpXF8YZ


Generated by Claude Code

…rom what the check settled

What a block names by key, and the names it writes, are checked as the
file loads with every other cross-declaration rule. The link expressions
and the where are typed with the rest of the model, so the expansion no
longer parses them itself. What needs those typed forms is decided in one
function, check, which returns the facts the rows read; the emitters are
plain functions over them and decide nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNZN57CR8oGTVpRmpXF8YZ
@FBumann
FBumann merged commit e4962e6 into claude/mathspec-piecewise-api-7j0wpt Sep 22, 2026
5 checks passed
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.

2 participants