refactor(program): a program names its nodes by the naming rule and its groups as the file does - #585
Merged
Merged
Conversation
…ts groups as the file does Every public name in `math_spec.program` follows the naming rule in `docs/contributing.md`, that rule now states what it actually governs, and a relation is read from the program the way the file declares it. Nothing the language accepts, refuses or prints changes. The where vocabulary drops the `Node` suffix, which the naming table reserves for the core AST. `At` is `Pullback` and `Window` is `WindowSum`, because both this tree and lpspec already call the one a pullback and the other did not say it sums. A translation and a window call their axis `along`, as the file, the surface keyword and `Partition.along` do. `Program.relations` is a field keyed by name rather than a property undoing a per-dimension nesting, so a `Direction` and a `Partition` carry the relation's name and the declaration no longer holds a second home for it. The file's `expressions:` section arrives as `Program.expressions`, and the derived traversal that held that name is `Program.roots`. The `Expression` base class goes: the nodes share none, `Expression` is the union, and a consumer builds `Add(a, b)` rather than `a + b`, which the reading page already said it never does. The three singular accessors go with it — `program.dimensions[name]` is the mapping lookup they wrapped. Coverage moved: - `test_a_relation_names_the_dimension_its_values_label` is replaced by `test_a_relation_is_declared_as_the_file_declares_it`, which lowers a file rather than hand-building a program. - `test_an_unknown_dimension_is_a_near_miss_rather_than_an_empty_declaration` is deleted with the accessor it tested. It asserted a `KeyError` message for a mistyped literal in consumer code, which is the wrong layer. - `test_expressions_are_the_ones_a_row_is_built_from` is `test_roots_are_the_trees_a_row_is_built_from`. - `test_a_program_seals_its_declaration_groups` is parametrized over `relations` too. - Three assertions that built nodes with `*` and `+` call `Multiply` and `Add`. - The golden test's carrier set gains `Direction`, `Partition` and `RelationDeclaration`, which its dataclass walk now reaches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPqtQuarRGDmX2WrNVuKDP
FBumann
added a commit
that referenced
this pull request
Sep 20, 2026
… states #585 renamed the program's where vocabulary and expression nodes on main. This branch was written against the old names, and its own two comparison nodes arrived with the suffix the rule reserves for the core AST. Applying the same rename here first is what lets the base merge that follows agree line for line rather than conflict on every one of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPqtQuarRGDmX2WrNVuKDP
FBumann
added a commit
that referenced
this pull request
Sep 20, 2026
#585 landed the program naming rule on main, and #584 cut alpha.107. The branch's own two comparison nodes arrive under that rule as `ExpressionComparison` and `ArithmeticComparison`. Resolved by hand where the two sides changed the same code: - `_check_where_dims` keeps this branch's `leaf` form. main's `noun` form cannot name a comparison of expressions, which carries no single name. - `_declared_rhs_error` keeps this branch's three kinds. A parameter on the right-hand side is arithmetic here, not a refusal, so main's `parameter` branch has nothing left to fire on. - `Namespace` keeps its schema-carrying form and builds a `RelationDeclaration` without the name main dropped from it. - `expression_of` and `where_of` stay in `tests/fixtures.py`, where this branch moved them; main's copies in `resolution.py` go with the merge. - `Translate.along` and `WindowSum.along`, `Program.roots`, `Footprint.kinds` and the relation records are main's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPqtQuarRGDmX2WrNVuKDP
FBumann
added a commit
that referenced
this pull request
Sep 20, 2026
… states #585 landed the program naming rule on main. This branch carries the arithmetic where work under the old names, and applying the same rename here first is what lets the merge that follows agree line for line rather than conflict on every one of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPqtQuarRGDmX2WrNVuKDP
FBumann
added a commit
that referenced
this pull request
Sep 20, 2026
…anch The base branch carries #585's program naming rule and alpha.107 with it. Resolved by hand where the two sides changed the same code: - `Resolved` keeps this branch's `piecewise` field, and `lower_program` keeps the `piecewise` it builds above rather than the inline one below. - `Program` is built with `relations=` and `expressions=`, the names main now gives those groups. - `PiecewiseExpansion` keeps its per-expression dim cache. - The relation records, `Translate.along`, `WindowSum.along`, `Program.roots` and `Footprint.kinds` are the base branch's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPqtQuarRGDmX2WrNVuKDP
FBumann
added a commit
that referenced
this pull request
Sep 20, 2026
#585 landed this branch's renames on main, and the two branches below carried them up. What is left here is the paragraph saying lowering rewrites every `ArithmeticComparison` into an `ExpressionComparison`, which needs #566's two nodes to be true, and two docstrings. Both conflicts were wording the base branch already carries: the row-building trees are `roots`, and the golden test's carrier note names a direction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPqtQuarRGDmX2WrNVuKDP
This was referenced Sep 20, 2026
FBumann
added a commit
that referenced
this pull request
Sep 22, 2026
* chore(language): a where comparison is one grammar rule over the expression grammar's arithmetic The where grammar's three comparison rules — a name against a literal, two relation columns, and position() against an integer — are one rule, side <op> side, where a side is the expression grammar's ARITHMETIC. What a side is, resolution decides with the schema in hand, and it hands back the same typed nodes with the same messages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * test: the parser test names the validation class that holds the refusals it points at Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * chore(parser): the where depth measurement names the nodes it walks rather than Any `explicit-any` is an error on `src/math_spec` since #572, and that PR took only the fold's return type from #567, leaving `_nested` on this branch with a signature whose `Any` no longer even imports. `_ParsedWhere` is the union the measurement walks, which is #567's own answer for this file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V2SFmoxEF3SnaPKp7HbZTk * feat(language): a where may compare arithmetic over parameters Either side of a where comparison may be an expression over parameters: arithmetic, a reduction, a pullback, a translation with its edge, a macro, a named expression. A side is expanded, typed, degree-checked and dim-checked as an expression is, and refused where it names a variable or a dual. Two parameters compare the same way. The resolved tree holds ArithmeticComparisonNode over the core syntax tree, and lowering rebuilds every mask with ExpressionComparisonNode over program expressions. A case when: comparing expressions is refused as undecidable before the data arrives. expressions.md sentence lengths: n 68, avg 14.0, median 13, over 25 words 3 (base: n 56, avg 13.0, median 13, over 25 words 2). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * refactor(language): a comparison of expressions answers what it reads once, on the program's form The resolved form of the comparison had a second walk collecting the parameters and relations its sides read, and nothing asks a resolved mask that question: lowering rebuilds every mask before one reaches a consumer. The arm is the assertion the typesetter already carries for the lowered node. The one-line mask wrapper in lowering is inlined, and the two validation classes for what a where side may be are one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * refactor(language): a namespace is built from its schema and carries it, so nothing passes both Namespace(schema) reads every declaration at construction; the eight argument constructor and the classmethod that was its only caller are gone. expression_of, _check_expression and _named took the schema and a namespace built from that same schema; they take the namespace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * refactor(language): the single-text doors live with the tests that are their only callers expression_of and where_of had no caller in the package, and the two assertions that named expression_of as the door now name resolve_expression, which is the one the package walks through. The plain form of a where comparison is read by one method: a name that is a value against another is arithmetic, which used to be a second question asked after the first. The "prefix a context once" rule has one home in errors.py. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * refactor(program): the branch's nodes follow the naming rule main now states #585 renamed the program's where vocabulary and expression nodes on main. This branch was written against the old names, and its own two comparison nodes arrived with the suffix the rule reserves for the core AST. Applying the same rename here first is what lets the base merge that follows agree line for line rather than conflict on every one of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPqtQuarRGDmX2WrNVuKDP * docs(reading): the predicate union says which member a program never carries The `Predicate` union said it was what a lowered mask's `root` is built of. That is false here: it also holds `ArithmeticComparison`, which lowering rewrites into an `ExpressionComparison`, so a consumer walking a program meets every other member and never that one. These lines were #580's, which is where they were written. They are only true where the two nodes exist, and this is the branch that adds them. docs/reference/reading.md: the two sentences added are 11 and 9 words. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPqtQuarRGDmX2WrNVuKDP * docs(reading): a comparison against a literal is not an expression comparison A consumer walking a lowered mask gets ParameterComparison for `p_max > 5` and ExpressionComparison for `1 * p_max > 5`, though both mask the same coordinates. The page named only the second form. Gates: pytest tests/test_docs.py tests/test_reading_page.py (34 passed), the full suite (1383 passed, 1 skipped), mkdocs build --strict (clean, with the docs.python.org inventory dropped for the run, which this environment's proxy refuses with a 403), prettier --check (clean). Not run: typos, reuse lint, taplo, zizmor, compile-tex, for want of the tools. docs/reference/reading.md, by the measurement in the docs-writing skill: n 55, avg 14.6, median 13, over25 7. Every long sentence predates this change; the three added are 10, 20 and 9 words. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJfEUsCDtwXuXV8CANCHuR * fix(language): a comparison with the literal first is refused as the order it is (#594) * fix(language): a where comparing two numbers, and a lone case comparing expressions, are refused at load A comparison with a number on each side is decided before any data arrives, and reached the program as an ExpressionComparison over two constants. A cases: block of one case escaped the undecidable refusal, which only looked at pairs. names_read on a mask over a cased entry dropped the data its regions are decided by. The relation refusal still said every other comparison tests a name against a literal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019tCWoetBmjF1LpbTbjQY29 --------- Co-authored-by: Claude <noreply@anthropic.com>
FBumann
pushed a commit
that referenced
this pull request
Sep 22, 2026
…ares `named_expressions` is `expressions` since #585, and a program now holds `relations` and `assumptions` as well. The sweep compares all nine, so a section the form drops or renames is caught rather than skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LCUHhoVyReQaBh2pj8CuGd
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.
Every public name in
math_spec.programfollows the naming rule indocs/contributing.md, that rule now states what it actually governs, and a relation is read from the program the way the file declares it. Nothing the language accepts, refuses or prints changes.This is #580 on
main, without the two renames that need #566. It is a break, described here because the alpha stream refuses the marker. Eighteen public names go and sixteen arrive.What this changes
Predicate nodes. The
Nodesuffix marks the core AST, and the program uses bare names. The where vocabulary carried the suffix.AndNodeAndOrNodeOrNotNodeNotBooleanLiteralNodeBooleanLiteralParameterDefinedNodeParameterDefinedVariableDefinedNodeVariableDefinedParameterComparisonNodeParameterComparisonDimensionComparisonNodeDimensionComparisonDimensionPositionNodeDimensionPositionRelationComparisonNodeRelationComparisonRelationPairComparisonNodeRelationPairComparisonRelationDefinedNodeRelationDefinedUnions
ExpressionNodeExpressionWhereNodePredicateTypedPredicateNodeTypedPredicateConnectiveWhereNodeConnectiveExpression nodes and their fields
AtPullbackWindowWindowSumTranslate.dimensionTranslate.along, the keywordshifttakesWindow.dimensionWindowSum.along, the keywordsum_backtakesProgram and declarations
Program.named_expressionsProgram.expressions, the file's ownexpressions:sectionProgram.expressions, the objective and both sides of every constraintProgram.rootsProgram.relations, a property built from the per-dimension nestingProgram.relations, a field: aMapping[str, RelationDeclaration]keyed by nameDimensionDeclaration.relationsDimensionDeclarationcarriesdtypealoneRelationDeclaration(name, columns, key), aNamedTupleRelationDeclaration(columns, key), a frozen dataclass; the name is its keyDirection, aNamedTuplewhosenameread off the relationnameas a fieldPartition, the sameDirection.is_function_readFootprint.shapesFootprint.kindsDeleted
Expressionbase class, with__add__and__mul__Expressionis the union. BuildAdd(a, b),Multiply(a, b)Program.dimension(name)program.dimensions[name]Program.parameter(name)program.parameters[name]Program.variable(name)program.variables[name]The naming rule itself.
docs/contributing.mdsaid a node names the coordinate map rather than the operator. That describes one node of five, and it madeTranslateread as a counter-example to the rule it was cited for. It now says a node names the operation, with the five verbs and their nodes beside each other, because one verb lowers to two nodes and the file's spelling cannot decide the name. Its layer table citedDimensionComparisonNodeas a core-AST name, which this change makes a program name; it citesUnresolvedComparisonNodeinstead.One lookup, one home.
RelationDeclaration.dimreaddict(self.columns)[role], rebuilding the mapping on every call. The typesetter calls it once per role inside comprehensions over roles. It now reads acached_propertymapping. No measurement was taken, so this is a shape change and no speed is claimed.Kept, deliberately.
Sum.over, because the file writesover=and the program follows the file. Also the public walk helpers with no caller in this tree, since whether lpspec reads them is not visible from here.Why
The naming table says
Nodemarks the core AST and the program uses bare names, yet the program's where vocabulary carried the suffix.Atis a preposition that says nothing, where both this tree and lpspec already call the operator a pullback in prose, in a refusal, in the typesetter and in the notation page's own examples.Windowdid not say it sums, whereGroupSumandSumdo. A translation and a window called their axisdimension, where the file, the surface keyword andPartition.alongall sayalong.The file's
expressions:arrived under a different name becauseProgram.expressionsmeant something else: a derived traversal sitting among six declaration groups and reading like a seventh.rootsmarks it as derived.Relations were nested under each dimension they touch, one relation under several, and a property undid the nesting. With the group keyed by name, the declaration's own
namefield was a second home for one fact.DirectionandPartitioncarry the name instead, because a value that travels without the program has to say which relation it came from. lpspec'sread_columntakes a node and no program, which is what makes that necessary.is_function_readnamed one fact twice: the refusal printed two lines below it says "is not single-valued". "Function" also reads as callable to a Python reader. It has two callers in one resolution function and no consumer asks it, so it is a local there rather than a member of the public record.Footprint.kindsholdstype[Expression], and the reading page already iterates it askind.The singular accessors covered three of five groups and one had no caller. Their near-miss message only fired when a Python consumer mistyped a literal, which is a bug in the consumer; every name a file can get wrong is already caught at load. The operator sugar contradicted the reading page's "you never build a node yourself", and existed for three assertions.
The break
lpspec reads
program.At,program.Window,program.ExpressionNode,program.WhereNodeand the suffixed predicate nodes. It pins math-spec by tag, so nothing breaks before this tags. It already owes the same follow-up to #494 and #559, and doing all three at once is one edit rather than three.The later renames cost it nothing. lpspec reads
footprint.domains,footprint.quadraticandfootprint.sos_types, nevershapes, and it does not readis_function_read.What this leaves to #580, and why it is not #580 retargeted
#580 sits on #569, which sits on #566, which sat on #565. Its diff does not retarget onto
main: replayed there it conflicts in eleven of its twenty-two files, so this branch re-applies the renames by script and takes the residue by hand, the same way #580 was itself re-derived from #475.Two of #580's predicate renames are not here.
ExpressionComparisonNodeandArithmeticComparisonNodearrive with #566, somainhas no such names to rename. When #566 lands it owes its two new nodes under the rule this change states, and the sentence #580 adds to the reading page about lowering rewriting one into the other comes with them. Nothing else of #580'sprogram.pywork is left out.#580 is not closed, its branch is not touched, and nothing is force-pushed. Whether it is rebased onto this or reduced to the two comparison nodes is the author's call.
Gates
pixi.shis refused by this environment's egress proxy with a 403, so the gates ran from auvenvironment on Python 3.12 with the pinnedruff==0.16.1andpyrefly==1.2.0. That is a departure from the "every command runs underpixi run" default.pytest -qruff check,ruff format --checkpyrefly checkmkdocs build --strictdocs.python.orginventory dropped for the run, which the proxy refuses with a 403prettier --checkon every Markdowntypos,reuse lintpython -m tools.schemapython -m tests.typesetting.goldentests/test_docs.pycovers them and passestaplo,zizmor,compile-texNeither the schema nor the golden output moves, because the schema is built from
model.pyand this diff does not reach it. No TOML and no workflow file is touched, which is whattaploandzizmorread.The
cached_propertyon a frozen dataclass was checked against the three things that could break:Directionstill hashes as a dict key, a declaration still compares equal after its cache is warm, and a lowered spec still pickles, whichtest_a_lowered_spec_still_pickles_and_lowers_to_the_same_programcovers.Coverage moved:
test_a_relation_names_the_dimension_its_values_labelis replaced bytest_a_relation_is_declared_as_the_file_declares_it, which lowers a file rather than hand-building a program.test_an_unknown_dimension_is_a_near_miss_rather_than_an_empty_declarationis deleted with the accessor it tested. It asserted aKeyErrormessage for a mistyped literal in consumer code, which is the wrong layer.test_expressions_are_the_ones_a_row_is_built_fromistest_roots_are_the_trees_a_row_is_built_from.test_a_program_seals_its_declaration_groupsis parametrized overrelationstoo.*and+callMultiplyandAdd.Direction,PartitionandRelationDeclaration, which its dataclass walk now reaches.What this deliberately does not do
DirectionandPartitionshare five members and differ in three each. No shared base or protocol is introduced. A base for two small records buys a detour for every reader, and the two are different concepts by refactor(language): a partition is a class of its own rather than a direction #559's argument.node.direction.consumed_dimskeeps its extra hop. That arrived with refactor(language): a relation is read in a direction rather than walked #494 and feat(language): a call walks one relation, so by= names one rather than a list #533, which took the accessors off the node so the direction is the one home for the fact.program.pyis over a thousand lines across four vocabularies. Splitting it is separable work and belongs on its own branch, not in a rename.Sum.overis not renamed. The file writesover=.Translateis notShift. This tree says "translation" for the operator inseparability.py,dimensions.py,lowering.py, the typesetter README and a section heading on the operators page.shiftis the file's verb andTranslateis the program's noun, which is the split the new rule states.<type>/<topic>from a worktree.🤖 Generated with Claude Code
https://claude.ai/code/session_01JPqtQuarRGDmX2WrNVuKDP
Generated by Claude Code