refactor(language): an expression resolves straight into the program's own nodes - #638
Merged
FBumann merged 6 commits intoSep 23, 2026
Conversation
Documentation build overview
24 files changed ·
|
FBumann
added this pull request to stack #637
September 23, 2026 06:52
FBumann
force-pushed
the
claude/mathspec-packages-review-urd4ya
branch
from
September 23, 2026 08:16
540d412 to
3270e48
Compare
This was referenced Sep 23, 2026
FBumann
pushed a commit
that referenced
this pull request
Sep 23, 2026
…e test that fails without it A mutation sweep over the guards #638 moved and added left nine green. Two were redundant with the check beside them and are gone: a formal amount is caught under its sign, and a role that named no column is caught where the roles are counted. The other seven are pinned. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F
…yped tree `resolve_expression_text` in resolution.py is what validation.py's `_check_expression` was, and resolution's `_value` was the same function with `comparison=False, ceiling=None`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DwZZXWXoJfSMabvXTUndtn
…e expansion only writes rows `Spec` computes `resolved` before it expands, so the expansion reads each link's typed tree instead of parsing the text again. The names a block references and the names it emits are checked with the other `Spec` reference rules, and its frame in `curve_frame`, which the typesetter reads too. `PiecewiseExpansionError` is gone: a block is refused as a `SchemaError` or a `DimensionError` like every other declaration. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DwZZXWXoJfSMabvXTUndtn
`piecewise.assumptions_of` returns `AssumptionBlock`s, which is what it made from its `Assumed` tuples; `Resolved.assumptions` holds `program.Holds`, which `ResolvedAssumption` had the fields of; the `Assumption` alias of `Holds` and the `LinkSign` alias of `ComparisonOperator` are gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DwZZXWXoJfSMabvXTUndtn
…s own nodes Resolution builds `program.Expression` nodes from the syntax tree, so the dim rules, the degree rules, the exclusivity check, the typesetter and lowering read one vocabulary. The parser keeps its eight syntax nodes; the ten typed ones, the node groups and the assertion arms that policed them are gone, and lowering packages declarations with each `Named` use of an `expressions:` entry inlined. The form of an `offset=`, `window=` and `edge=` is decided where it is read, in resolution. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DwZZXWXoJfSMabvXTUndtn
… sentence Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DwZZXWXoJfSMabvXTUndtn
`program.Holds` is renamed `Assumption`: the file's section is `assumptions:`, every other declaration class is a noun, and the alias that carried the noun stood in for kinds that never came. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DwZZXWXoJfSMabvXTUndtn
FBumann
force-pushed
the
claude/mathspec-packages-review-urd4ya
branch
from
September 23, 2026 11:26
3270e48 to
f2675a2
Compare
FBumann
pushed a commit
that referenced
this pull request
Sep 23, 2026
… a formal is refused, and a fault in an entry hides no other The review of #626 through #638, addressed on top of #638. - A template's by= is checked before its formal columns send the call back, so sum(x, by=nope, over=a, into=b) is refused again; the over= and by= refusals say "or a formal of this macro" again; a formal along= beside a by= is pinned as building nothing. - Validation no longer raises after the macros and the expressions: entries, so a fault there hides no constraint's fault. A use of a refused entry says it does not load, and the refusal is listed with it. - A cycle closed through a macro names the macro in its chain, and one closed through a case's when is pinned. - Named expressions are resolved from a worklist in dependency order, so a chain of any length costs no stack, and the resolved tree is held to MAX_RESOLVED_DEPTH, three times what one text may nest, with a refusal that names the depth rather than the parser's message about a tree that was not deep. - A refused call builds nothing for the call around it, so sum(sum(p, into=g)) reports one fault. - A named offset or window is held to dtype: int where it is read, so offset=-tag says the dtype before the sign. - A piecewise block is refused on the link the file wrote for a str or bool breakpoint parameter and for an lp x-link with no variable; an undeclared over: is one line. - The exclusivity rewrite quotes a literal as the file wrote it. - fan_in reads through a Named; dimensions uses program.children; the walk prints "otherwise" for the last region without testing it; the census names no line that runs. - Tests pin the error class per rule, the unused template and entry refusals, +p as a named amount, and a link through a refused entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F
FBumann
pushed a commit
that referenced
this pull request
Sep 23, 2026
…e test that fails without it A mutation sweep over the guards #638 moved and added left nine green. Two were redundant with the check beside them and are gone: a formal amount is caught under its sign, and a role that named no column is caught where the roles are counted. The other seven are pinned. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F
FBumann
removed this pull request from stack #637
September 23, 2026 11:30
FBumann
added this pull request to stack #644
September 23, 2026 11:32
FBumann
added a commit
that referenced
this pull request
Sep 23, 2026
… a formal is refused, and a fault in an entry hides no other (#643) * fix(language): a long chain of named expressions loads, a typo beside a formal is refused, and a fault in an entry hides no other The review of #626 through #638, addressed on top of #638. - A template's by= is checked before its formal columns send the call back, so sum(x, by=nope, over=a, into=b) is refused again; the over= and by= refusals say "or a formal of this macro" again; a formal along= beside a by= is pinned as building nothing. - Validation no longer raises after the macros and the expressions: entries, so a fault there hides no constraint's fault. A use of a refused entry says it does not load, and the refusal is listed with it. - A cycle closed through a macro names the macro in its chain, and one closed through a case's when is pinned. - Named expressions are resolved from a worklist in dependency order, so a chain of any length costs no stack, and the resolved tree is held to MAX_RESOLVED_DEPTH, three times what one text may nest, with a refusal that names the depth rather than the parser's message about a tree that was not deep. - A refused call builds nothing for the call around it, so sum(sum(p, into=g)) reports one fault. - A named offset or window is held to dtype: int where it is read, so offset=-tag says the dtype before the sign. - A piecewise block is refused on the link the file wrote for a str or bool breakpoint parameter and for an lp x-link with no variable; an undeclared over: is one line. - The exclusivity rewrite quotes a literal as the file wrote it. - fan_in reads through a Named; dimensions uses program.children; the walk prints "otherwise" for the last region without testing it; the census names no line that runs. - Tests pin the error class per rule, the unused template and entry refusals, +p as a named amount, and a link through a refused entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F * chore(resolution): the resolved depth carries the number of the PR that measured it Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F * test(piecewise): every guard of a block's frame and references has the test that fails without it A mutation sweep over the guards #638 moved and added left nine green. Two were redundant with the check beside them and are gone: a formal amount is caught under its sign, and a role that named no column is caught where the roles are counted. The other seven are pinned. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F * chore(resolution): a formal amount is read once, where a bare name is Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F --------- Co-authored-by: Claude <noreply@anthropic.com>
FBumann
pushed a commit
that referenced
this pull request
Sep 23, 2026
Merged against the tree this branch was written on, the head of #638 before its rebase, so the conflicts were the four places #643 and the join touch the same lines: fan_in reads through a Named and then asks a Sum, the relation read builds a join, and the dim tests carry the error class and the join's wording. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F
FBumann
added a commit
to fluxopt/specsolve
that referenced
this pull request
Sep 23, 2026
…at() (#1718) > **Prompt:** Update lpspec to the latest mathspec release (119) > [!NOTE] > The following content was generated by AI. The pin moves from math-spec alpha.116 to alpha.119. Both lanes now build the new `at(<predicate>, by=, over=, into=)` in a `where:` (energy-models/mathspec#634), and they agree on the result. A coordinate with no relation row reads false. 70 insertions, 34 deletions. <details><summary>What moved</summary> * **`PulledBackPredicate`**: the relational lane walks the coordinates the operand admits through `walk_join`, as an expression's `at` does. The linopy lane reads the evaluated mask through `operator_at`. Before this change, both lanes hit `assert_never`. * **`PiecewiseExpansionError` is gone upstream** (energy-models/mathspec#638). A piecewise block now raises `DimensionError`, so `lps.PiecewiseExpansionError` goes too, with no alias. `test_api`, `test_architecture` and `test_piecewise` follow, and so does `docs/reference/api.md`. * **`ArithmeticComparison` is gone upstream** (energy-models/mathspec#631). The two lanes' branches for it were never reached, so they go. `NEVER_LOWERED` in `test_resolution_parity` goes with them. * **Tests**: `test_a_where_reads_a_relation` gains 2 cases and `test_a_relation_where_agrees_with_the_oracle` gains 3: a total relation, a partial one, a negation and a conjunction. `COVERED_ELSEWHERE` names the oracle test for `PulledBackPredicate`. Before the implementation, the coverage guard failed on it. * **Upstream now refuses `sum()` over a scalar**, so the carried-parameter probe in `test_strategy` reads `soc_initial` bare. It still asserts the refusal names "carried". * `uv.lock` is relocked. Only the math-spec entry changed. </details> <details><summary>Mutation table</summary> Run by hand on the committed tree. Each file was restored through `git checkout --` and `__pycache__` was dropped. The runs cover `test_label_coords.py` and `test_resolution_parity.py`. | Mutation | Result | | --- | --- | | linopy: a missing relation row reads true (`fillna(True)`) | caught, 1 failed | | polars: a missing row reads true (`fill_null(True)`) | caught, 5 failed | | polars: the operand's mask is ignored (`masked(..., None)`) | caught, 5 failed | </details> <details><summary>Gate</summary> ``` ruff check . clean ruff format --check . 327 files already formatted pyrefly check 0 errors (20 suppressed) pytest -q -n auto 3796 passed, 403 skipped, 1 xfailed, 11 failed ``` The run used `uv` with the `[linopy]` extra, not pixi. The xfail is the known `osemosys_utopia` / #894 case. Of the 11 failures, one was the `test_strategy` probe, which is fixed in the second commit. The other 10 are the gurobi and xpress parametrizations of `test_diagnostics` and `TestThePositionalHandoff`. They raise `ModuleNotFoundError` because neither package is installed here, and they fail the same way on `origin/main`. **Not run**: `docs-build`, `docs-test`, `test-floors`, `test-bench`, the gurobi and xpress sinks. I did not regenerate the gallery pages. The typesetter changed upstream (energy-models/mathspec#629), but the doc tests passed. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LzN7uYEvHW9dDGCmtAzbkD --- _Generated by [Claude Code](https://claude.ai/code/session_01LzN7uYEvHW9dDGCmtAzbkD)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 23, 2026
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.
Note
The following content was generated by AI.
Resolution builds
program.Expressionnodes, so every pass after it reads one vocabulary and lowering only packages declarations. Before that, a piecewise block is checked once at load, one function types an expression string, and four duplicate shapes are gone. Stacked on #632; the whole stack is rebased ontomainat #640.Warning
Found in review, fixed at #643: a dim fault under a bare
sum()is aSchemaErrorwhere the dim rules raiseDimensionError, and the tests were loosened rather than pinned;sum(sum(p, into=g))added a false "already a scalar"; the exclusivity rewrite rounded a literal to six digits; an undeclared piecewiseover:added per-link lines; astrorboolbreakpoint parameter and an lp x-link with no variable were refused only under an emitted row;offset=-taglost its dtype hint;fan_incrashed on aNamed;_operandsrepeatedchildren; theotherwisetest in the walk was always true; theUNREACHABLEentryif isinstance(node, Cases):names a line that runs. Unclaimed here and pinned there: an unused template or entry is now refused for the form of an amount, an edge or a bare sum.Concepts
program.Expression. The predicate side already worked this way; the arithmetic side now does too.Sum,GroupSum,Pullback,TranslateorWindowSumis built at the call, with theDirectionorPartitionresolution already computed.ResolvedholdsConstraintDeclarations and anObjectiveDeclaration. Lowering rebuilds each declaration with everyNameduse inlined and does nothing else to a tree._Resolver.arithreturnsNonewith an error for a refusal andNonewith no error for a node a macro formal stands under. A template is checked by resolving it and keeping the errors; the call site that binds the formals is where the node is built. NoFormalleaf exists. This is also what fixes fix(language): a macro template nothing calls is held to every rule a call site is #628's refusal of a formalalong=besideby=; fix(language): a long chain of named expressions loads, a typo beside a formal is refused, and a fault in an entry hides no other #643 pins it.otherwiseregion is built by resolution. Its mask is the remainder of the other regions (resolution.remainder), so a consumer adds regions. The typesetter prints "otherwise" for the last region.offset=orwindow=is whole and in range, a named one is a parameter, and ashiftwithout anedge=says what it means, all in resolution, soTranslate.offsetandWindowSum.widthare theint | strthey say. The dim rules keep only what needs the operand's dims.Specreference rules, the names it emits are collision rules the sos check shares, and its frame iscurve_frame, which the expansion and the typesetter both read. The expansion cannot fail for what load admits; two rules load did not decide here are decided at fix(language): a long chain of named expressions loads, a typo beside a formal is refused, and a fault in an entry hides no other #643.expressions:entry resolves to the oneNamednode built for the entry, and the memo, the cycle check and the "does not load" refusal stay where refactor(language): each named expression is resolved once, and every use reads that node #632 put them.Assumption. The file's section isassumptions:and every other declaration class is a noun; theHoldsclass and itsAssumptionalias, which stood in for kinds that never came, are one class.Renames
validation._check_expressionandresolution._valueresolution.resolve_expression_text, andresolve_constraint_textfor a constraintresolution.ResolvedConstraintprogram.ConstraintDeclarationresolution.ResolvedAssumptionprogram.Assumptionpiecewise.Assumedmodel.AssumptionBlockprogram.Holds, and theprogram.Assumptionalias of itprogram.Assumption, one classmodel.LinkSign_expression_parser.ComparisonOperatorprogram.ExpressionComparison[Side]program.ExpressionComparison, both sidesExpression_expression_parser.case_contexterrors.case_contextdimensions._AMOUNTSand_Amountoperators.AMOUNTSandAmountlowering._none_ofresolution.remainderlowering._Lowering.exprand.masklowering.inlineandinline_maskpiecewise._nominatedPiecewiseBlock.nominatedtypesetting.walk.Walk._curve_framepiecewise.curve_framedegree.carries_variableis removed as a duplicate ofprogram.carries_variable, which was already there.Removed
VariableNode,ParameterNode,DualNode,DimensionNode,DirectionNode,PartitionNode,EdgeNode,CaseArm,CasesNode,DefinitionNode, and the groupsKwargNode,UnresolvedNode,LeafNode. The parser keepsNumberNode,NameNode,NameListNode,KeywordNode,UnaryOperatorNode,BinaryOperatorNode,FunctionCallNodeandComparisonNode.math_spec.PiecewiseExpansionError,math_spec.program.Holds._Lowering,_CALLS,_amount,_partition_of._CALL_RULES,_dims_call,_cases_dims,_check_amount_form,_check_edge,_edge_fill,_vacates,_whole,_amount_of(the form checks moved to resolution; the rest is onedims_ofover program nodes)._REDUCTIONS, the reductions being the node kinds now.walk._amount,walk._step,Walk._curve_frame.Added
program.Named(name, body): the one node aSpec.resolvedtree holds that noProgramdoes. Exported, sinceprogram.__all__is checked against what the module defines; absent fromExpression, so a consumer's exhaustive match is unchanged.program.childrensteps through it.piecewise.Emittedandpiecewise.curve_frame,operators.Amount,lowering.inline,resolution.remainder,resolution.resolve_constraint_text.Method, gate output, alternatives
The commits
Six on one branch, because the session was held to one. That departs from one issue, one PR; split at the commits if wanted.
chore(resolution): one door for an expression string.resolve_expression_textin resolution.py replaces validation.py's_check_expressionand resolution's_value, which were the same five steps down to the message text.refactor(language): a piecewise block is checked once at load.Spec._validate_expressionscomputesresolvedbefore it expands, so the expansion reads each link's typed tree rather than parsing the text again.PiecewiseExpansionErroris gone: a block is refused as aSchemaErroror aDimensionErrorlike any other declaration.chore: a shape declared once.assumptions_ofreturnsAssumptionBlocks,Resolved.assumptionsholds the program's assumption class, and theLinkSignalias is gone.refactor(language): an expression resolves straight into the program's own nodes. The ledger above. feat(language): a where may read a predicate through a relation with at() #634'sat()over a predicate is carried in this commit:PulledBackPredicateis built from theDirectionthe relation read returns,pulled_back_dimsserves the expression and the predicate alike, and the walk's_pullbackreads through the helper feat(language): a where may read a predicate through a relation with at() #634 added.chore(schema): the published schema regenerated for a docstring the fourth commit changed.refactor(program): an assumption is the class namedAssumption. The rename ofHolds, in source, tests andreading.md.The rebase
The seven branches of the stack, #626 through #632 and this one, are rebased onto
mainat #640, one commit each, in that order. Two of the seven conflicted with #634: #626 on the shape of thePredicateunion, #632 on an import line, both resolved in the commit that meets them. This branch's fourth commit conflicted in the four modules #634 touched, and is resolved with the files of the merge commit that was pushed and validated here before (540d412); the rebased head's tree equals that merge's tree, release files aside, so the earlier gate output stands. Each branch was pushed with a lease against the head it was rebased from.Breaking for consumers
math_spec.PiecewiseExpansionErrorno longer exists. fluxopt/lpspec re-exports it inerrors.pyand__init__.py, lists it intests/test_architecture.pyandtests/test_api.py, and matches it intests/test_piecewise.py:293,298; each becomesSchemaErrororDimensionError. No alias, per the alpha-stream rule.math_spec.program.Holdsno longer exists; the class isAssumption, whichreading.mddocuments. lpspec names neither.math_spec.program.Namedis new in__all__and absent fromExpression. lpspec'stest_docs_site.py:291pinsget_args(program.Expression), which is unchanged.program.ConstraintSensestays: lpspec reads it in three places, and theLiteralcannot move into program.py without an import cycle through model.py.Behaviour a user sees
python -m tests.typesetting.goldenre-run onmain's model, no diff. The file language changes in one direction: a template nothing calls and anexpressions:entry nothing reads are now refused for a fractional or out-of-range amount, a bad edge, and a baresum()of a scalar, because those forms are decided in resolution. fix(language): a long chain of named expressions loads, a typo beside a formal is refused, and a fault in an entry hides no other #643 pins both.points:is refused with every other reference fault in the file, one per line.sum()of a scalar, a dim fault under a baresum(), and a negated named offset are collected with the other resolution faults as aSchemaError, where each raised aDimensionErrorfirst. fix(language): a long chain of named expressions loads, a typo beside a formal is refused, and a fault in an entry hides no other #643 pins the class per rule.+preads asp; only-pis refused as a negation.Tests
__str__, the unresolved-name test in test_degree, and two rows of the operator-table test went with the classes they tested.tests/test_lowering.py's "lowers to its node" cases assert the node resolution builds;tests/test_expansion.pyreads throughNamedwithlowering.inline; the golden census countsExpressionmembers plusNamedand the operators by node type;tests/test_piecewise.pyassertsSchemaErrorwhere it assertedPiecewiseExpansionError.test_the_golden_model_reaches_every_line_of_the_walk, needscoverage) passes; the one arm it cannot reach, aCasesoutside aNamed, is inUNREACHABLEwith its reason.curve_framecarries, three reference rules of a block, three kwargs naming nothing. Three of the nine were redundant with the check beside them. All are pinned or gone there.Numbers
Against #632's head,
src/is 16 files, 1281 added, 1745 removed. Parser node classes 18 to 8; program node classes 15 to 16, the one added beingNamed.Gates
ruff checkandruff format --check: green.pyrefly check --python-interpreter-path <venv>: 0 errors.pytest -q -n auto: 1565 passed.python -m tools.schemaand the golden generator re-run, no diff.prettieron the changed pages: applied.docs-buildandcompile-texdid not run: the session proxy refusesdocs.python.organdpixi.sh, andtypos,reuseandzizmorare not installed here.What was left out
Folding the dim rules into the resolver, so that each node is built knowing its dims, is the next cut the design invites and a separate one.
Namespacestill mirrorsSpecin five fields.🤖 Generated with Claude Code
https://claude.ai/code/session_01DwZZXWXoJfSMabvXTUndtn