Skip to content

refactor(program): a program names its nodes by the naming rule and its groups as the file does - #585

Merged
FBumann merged 1 commit into
mainfrom
claude/bold-brahmagupta-gkwr35
Sep 20, 2026
Merged

FBumann merged 1 commit into
mainfrom
claude/bold-brahmagupta-gkwr35

Conversation

@FBumann

@FBumann FBumann commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Prompt: "I landed 565

Lets land as much of 580 on main now as possible!! In adifferent branch and PR!"

Note

The following content was generated by AI.

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.

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 Node suffix marks the core AST, and the program uses bare names. The where vocabulary carried the suffix.

Was Is
AndNode And
OrNode Or
NotNode Not
BooleanLiteralNode BooleanLiteral
ParameterDefinedNode ParameterDefined
VariableDefinedNode VariableDefined
ParameterComparisonNode ParameterComparison
DimensionComparisonNode DimensionComparison
DimensionPositionNode DimensionPosition
RelationComparisonNode RelationComparison
RelationPairComparisonNode RelationPairComparison
RelationDefinedNode RelationDefined

Unions

Was Is
ExpressionNode Expression
WhereNode Predicate
TypedPredicateNode TypedPredicate
ConnectiveWhereNode Connective

Expression nodes and their fields

Was Is
At Pullback
Window WindowSum
Translate.dimension Translate.along, the keyword shift takes
Window.dimension WindowSum.along, the keyword sum_back takes

Program and declarations

Was Is
Program.named_expressions Program.expressions, the file's own expressions: section
Program.expressions, the objective and both sides of every constraint Program.roots
Program.relations, a property built from the per-dimension nesting Program.relations, a field: a Mapping[str, RelationDeclaration] keyed by name
DimensionDeclaration.relations gone; DimensionDeclaration carries dtype alone
RelationDeclaration(name, columns, key), a NamedTuple RelationDeclaration(columns, key), a frozen dataclass; the name is its key
Direction, a NamedTuple whose name read off the relation a frozen dataclass carrying name as a field
Partition, the same the same
Direction.is_function_read gone; a local of the one resolution call that asks it
Footprint.shapes Footprint.kinds

Deleted

Was Now
Expression base class, with __add__ and __mul__ the nodes share no base; Expression is the union. Build Add(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.md said a node names the coordinate map rather than the operator. That describes one node of five, and it made Translate read 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 cited DimensionComparisonNode as a core-AST name, which this change makes a program name; it cites UnresolvedComparisonNode instead.

One lookup, one home. RelationDeclaration.dim read dict(self.columns)[role], rebuilding the mapping on every call. The typesetter calls it once per role inside comprehensions over roles. It now reads a cached_property mapping. No measurement was taken, so this is a shape change and no speed is claimed.

Kept, deliberately. Sum.over, because the file writes over= 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 Node marks the core AST and the program uses bare names, yet the program's where vocabulary carried the suffix. At is 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. Window did not say it sums, where GroupSum and Sum do. A translation and a window called their axis dimension, where the file, the surface keyword and Partition.along all say along.

The file's expressions: arrived under a different name because Program.expressions meant something else: a derived traversal sitting among six declaration groups and reading like a seventh. roots marks 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 name field was a second home for one fact. Direction and Partition carry the name instead, because a value that travels without the program has to say which relation it came from. lpspec's read_column takes a node and no program, which is what makes that necessary.

is_function_read named 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.kinds holds type[Expression], and the reading page already iterates it as kind.

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.WhereNode and 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.quadratic and footprint.sos_types, never shapes, and it does not read is_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. ExpressionComparisonNode and ArithmeticComparisonNode arrive with #566, so main has 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's program.py work 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.sh is refused by this environment's egress proxy with a 403, so the gates ran from a uv environment on Python 3.12 with the pinned ruff==0.16.1 and pyrefly==1.2.0. That is a departure from the "every command runs under pixi run" default.

gate result
pytest -q 1357 passed
ruff check, ruff format --check clean
pyrefly check 0 errors, 9 suppressed, as on the base
mkdocs build --strict clean, with the docs.python.org inventory dropped for the run, which the proxy refuses with a 403
prettier --check on every Markdown clean
typos, reuse lint clean
python -m tools.schema no diff
python -m tests.typesetting.golden no diff
the four page generators no diff; tests/test_docs.py covers them and passes
taplo, zizmor, compile-tex not run, for want of the tools

Neither the schema nor the golden output moves, because the schema is built from model.py and this diff does not reach it. No TOML and no workflow file is touched, which is what taplo and zizmor read.

The cached_property on a frozen dataclass was checked against the three things that could break: Direction still hashes as a dict key, a declaration still compares equal after its cache is warm, and a lowered spec still pickles, which test_a_lowered_spec_still_pickles_and_lowers_to_the_same_program covers.

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.
What this deliberately does not do

🤖 Generated with Claude Code

https://claude.ai/code/session_01JPqtQuarRGDmX2WrNVuKDP


Generated by Claude Code

…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
FBumann merged commit 3848821 into main Sep 20, 2026
5 checks passed
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
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
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