Skip to content

feat(program): a sum through a relation is a sum over the axes its join opens - #641

Closed
FBumann wants to merge 21 commits into
mainfrom
claude/zen-gates-8rh0oo
Closed

FBumann wants to merge 21 commits into
mainfrom
claude/zen-gates-8rh0oo

Conversation

@FBumann

@FBumann FBumann commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: Look into 638. Will the conceptual change in 605 work with it ? Or even improve it? … Do a new branch from 605 and do a stacked PR onto 638. … Join, then mean. THats conceptually the best we can have. Can we get there again? … Do it

Note

The following content was generated by AI.

#605's join and group-by, on #638's resolver. A Join opens an axis named relation.column for each column it drops. sum(by=) is a Sum over those axes, and at() is the bare Join. Each node's frame is its own. #605 is unchanged.

What this changes

The branch starts at #605's head (53ba4a2) and merges #638's head (3270e48). The diff against #638 is #605's change, carried onto the resolver that builds program nodes. It keeps #605's two nodes and adds one thing: the axes a join opens.

  • Direction, GroupSum and Pullback are gone. Join(operand, columns) is the one relation node. JoinColumns holds the joined and grouped roles, with dropped, added, kept, one_row_per_group and axes derived.
  • A join's frame is the operand's dims, less the dims joined on, plus the dims grouped by, plus one axis per column joined on and not grouped by. The axis is named for the relation's column, for example zone_of.generator, and not for its dimension. A dimension name holds no dot, so an axis never meets one. The spelling is the one a where: already reads a column with.
  • A lookup opens no axis. Where the grouped columns hold the key, they determine every other column. at(x, by=R, over=a, into=b) lowers to the bare Join.
  • A grouped sum is the group-by after the join. sum(x, by=R, over=a, into=b) lowers to Sum(Join(x, cols), over=('R.a',)). Sum.over names dimensions, or the axes of the Join under it.
  • The node type again says what happens. fan_in(Join) is one-to-one and Sum is many-to-one, and degree counts Sum and WindowSum as reductions. Separability reports a Sum over a join's axes as a grouping, and a bare Join as a coordinate read. The walk prints a Sum over a join's axes with the domain condition, and a bare Join at the leaves.
  • PulledBackPredicate.direction is .columns. dimensions.pulled_back_dims is join_dims, one frame rule for an expression and a predicate. Partition.group is Partition.grouped, as in feat(language): a relation call is a join on the columns it names, and a sum through a relation is a sum over that join #605.
  • feat(language): a relation call is a join on the columns it names, and a sum through a relation is a sum over that join #605's refusal wording, relations page and linear-maps page come along. The linear-maps page gains two sentences on the axis. contributing.md names the axes in its node table, and reading.md names JoinColumns on PulledBackPredicate.

Why

#638 makes dims_of, the walk and separability read program nodes, one node at a time. #605's Sum(Join(x), over=dropped_dims) names the join's dropped columns by their dimension, and two cases break:

  1. A map into its own dimension. Golden representative sums through rep_of: { key: snapshot, values: { rep: snapshot } }. The join drops snapshot and adds it again, so the two columns share one name. The Sum over snapshot then takes away the dim the row keeps. With the axis, the join gives {snapshot, rep_of.snapshot} and the sum gives {snapshot}.
  2. A sum over a lookup. sum(at(q, by=zone_of, over=zone, into=u), over=u) has feat(language): a relation call is a join on the columns it names, and a sum through a relation is a sum over that join #605's grouped-sum shape. Separability then reports "groups u into u" and drops the lookup's read of zone. Now it is a Sum over the dim u, and a grouped sum is a Sum over zone_of.u, so the names tell them apart.

With the join and the reduction separate again, another reduction can follow the same join. A Mean over a join's axes is one new node. It is not in this PR.

Guards, tests, gates, what was not done

Guards and the tests that fail without them

Each guard was deleted in turn, the suite run, and the tree restored.

guard caught by
the axes in join_dims' frame test_dimensions.py frame cases, and the golden and gallery pages
a lookup opens no axis test_dim_inference[its-lookup-keeps-the-joined-key-too], [a-from-list-reads-two-value-columns-at-once]
the walk's grouped-sum dispatch the gallery pages in test_docs.py, and the golden files
separability's grouping arm test_a_grouping_that_sums_the_axis_away_couples_it
separability counting only an axis-free Join as a lookup test_a_grouping_that_sums_the_axis_away_couples_it, after it gained the coupled and undecided asserts; before, this guard survived
the frame of a map into its own dimension new test_a_join_opens_an_axis_for_the_column_it_drops_and_the_sum_over_it_closes_it

I checked two things against #605's code in a separate worktree:

Degree has two new cases: two lookups multiplied are one term each, and a grouped sum is a reduction.

No guard refuses a join axis in a declaration's frame. Resolution always builds the Sum over a grouping Join, so a file cannot reach one.

Coverage that moved

Gates

Pixi is not reachable here (pixi.sh refused), so the gates ran from a uv venv on Python 3.12 with the pinned ruff and pyrefly.

gate result
pytest -q -n auto 1570 passed
ruff check, ruff format --check clean
pyrefly check 0 errors
tools.schema, tests.typesetting.golden re-run, no diff: the schema and every golden output are #638's
generated pages (tests/test_docs.py) current
typos, reuse lint clean
prettier --check on the changed pages clean
mkdocs build --strict clean, with the docs.python.org inventory dropped for the run (the proxy refuses it)
compile-tex, zizmor not run: tectonic and zizmor are not installed here

No check runs are on this PR. The workflows may not run on a base branch other than main.

History

Not done

🤖 Generated with Claude Code

https://claude.ai/code/session_013M6RG2cnsrFCEbaUuU8KyJ

claude and others added 19 commits September 22, 2026 10:15
…ntraction

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SWBcNGLjNH2i4AqRsfyxaN
…on and groups by

The program's `Direction` is now `Join`, with `joined` and `grouped` roles in place of `consumed`, `produced` and `joined`; `Pullback` is `Lookup`, a join with no group-by; a partition's `group` is `grouped`. Every refusal, docstring and page says join and group-by in place of consume, produce and land on. The YAML surface is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SWBcNGLjNH2i4AqRsfyxaN
Main's new uses of Direction, Pullback and a partition's group take the renamed names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SWBcNGLjNH2i4AqRsfyxaN
`GroupSum` and `Lookup` are one node, `Join`, and `sum(by=)` lowers to the existing `Sum` over it, its `over` the dims the join drops; `at` lowers to the bare `Join`. The columns a call names move to `JoinColumns`. The YAML surface is unchanged.


Claude-Session: https://claude.ai/code/session_01SWBcNGLjNH2i4AqRsfyxaN

Co-authored-by: Claude <noreply@anthropic.com>
The duplicate-keyword refusal, the name-or-list read, a translation's step
count, a raw-model section and the predicate union each have one home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XwKgY5wZXv1bKgkfCW2q1
… grammar's nodes

UnresolvedNameNode was NameNode and QuotedNode was KeywordNode under other
names. A predicate call's kwargs are now typed as what they hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XwKgY5wZXv1bKgkfCW2q1
… call site is

The template check was a second walker that looked at names only. It is now
the resolver, with the template's formals left bare, so a label parameter
used as a value or an unknown relation column is refused at load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XwKgY5wZXv1bKgkfCW2q1
…n is

A format's glossary() returns the rows and section() sets the title, so the
Glossary class and three copies of each heading go. cases_row leaves the
Format protocol, since only a format's own cases() read it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XwKgY5wZXv1bKgkfCW2q1
… after lowering

ArithmeticComparison was ExpressionComparison with its sides still in the
core syntax tree. ExpressionComparison now takes its side type as a
parameter, and program.ArithmeticComparison is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XwKgY5wZXv1bKgkfCW2q1
… use reads that node

Namespace.named resolves an expressions: entry the first time anything
reads it. Expansion inlines that node, and the resolver passes it through.
Expansion no longer parses named expressions, the resolver no longer
re-reads a cased expression's arms, and validation drops its deduplication
of repeated arm errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XwKgY5wZXv1bKgkfCW2q1
…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
…ther each group is one row

Merge #638's head into #605. Resolution builds program nodes, so every
pass after it computes a frame from one node at a time. A Sum over a Join
has no frame of its own where the join drops and adds one dimension, as
rep_of does, so both calls lower to one Join whose frame is
(operand - joined) | grouped. JoinColumns.one_row_per_group tells a
lookup from a sum, for fan_in, degree, separability and the typesetter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013M6RG2cnsrFCEbaUuU8KyJ
@read-the-docs-community

read-the-docs-community Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

@FBumann
FBumann added this pull request to stack #637 September 23, 2026 08:33
@FBumann
FBumann marked this pull request as draft September 23, 2026 08:33
…in opens

A Join keeps each column it joins on and does not group by as an axis of
its own, named relation.column, so a column dropped and a column added
over one dimension stay two axes. sum(by=) lowers to a Sum over those
axes, and at() to the bare Join, which opens none because its groups are
one row. Each node's frame is its own again, and a reduction other than
a sum can follow the same join.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013M6RG2cnsrFCEbaUuU8KyJ
@FBumann FBumann changed the title feat(program): sum(by=) and at() are one join node, told apart by whether each group is one row feat(program): a sum through a relation is a sum over the axes its join opens Sep 23, 2026
@FBumann
FBumann force-pushed the claude/mathspec-packages-review-urd4ya branch from 3270e48 to f2675a2 Compare September 23, 2026 11:26
@FBumann
FBumann removed this pull request from stack #637 September 23, 2026 11:30
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
FBumann changed the base branch from claude/mathspec-packages-review-urd4ya to main September 23, 2026 11:40
@FBumann FBumann added the area: relations relations and dimensions: the relation design label Sep 23, 2026

FBumann commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Prompt: Merge them and update the PR description. And close 641

Note

The following content was generated by AI.

Closed as superseded. #664 carries this PR's change, moved onto main, together with the relation[column] syntax. This PR's body keeps the design and the guard table, and #664 links to it. The branch is kept.


Generated by Claude Code

@FBumann FBumann closed this Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: relations relations and dimensions: the relation design

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants