Skip to content

feat(language): a relation is summed with over=/by= and indexed with x[rel] - #509

Closed
coroa wants to merge 5 commits into
feat/relation-dot-syntaxfrom
feat/relation-dot-syntax-impl
Closed

coroa wants to merge 5 commits into
feat/relation-dot-syntaxfrom
feat/relation-dot-syntax-impl

Conversation

@coroa

@coroa coroa commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Prompt: I'd like to change how relations work. [...] sum(ex, over=rel.k1) [...] sum(ex, by=rel.v1) [...] at is in principle an indexing operation, so i'd like to write it as such [...] price[zone_of]. [...] Then implement it. [...] commit early [and on a] new branch for the new implementations PR.

Note

The following content was generated by AI.

Implements the dot syntax PR #506 documents. Stacked on that docs PR. Every relation operator joins the operand on shared dims, then reduces; the retired at()/into=/partition-by=/"walk" are gone.

The design, the diff, and how it was verified

The surface

  • sum(x, over=rel.k) sums key column k away; the value and other keys ride in.
  • sum(x, by=rel.v) groups onto value column v; the whole key is summed away.
  • x[rel] indexes: reads the value at the key, replacing at(x, by=rel). The operand is a name or (expr); x[a, b] indexes through several relations at once.
  • shift/sum_back/position take along=rel.k / position(rel.k); within= stays; position has no bare form.
  • The dot picks columns; a bare rel is all of them.

Three commits

  1. feat(parser) — the grammar reads x[rel] and rel.col as unresolved nodes.
  2. feat(language) — resolution rewrites the index to an internal index operator (at renamed); both sum forms and the index build one Join whose dim rule is uniformly (inner - consumed) | landed. The dead is_function_read guard on the key-join is dropped — no sum can be degenerate now that indexing is its own operator. Examples, the golden model, PyPSA models, and every fixture asserting the retired spelling migrated; the generated tables/pages regenerate from them.
  3. chore — the internal Walk structure renamed to Join (you asked for the "walk" vocabulary gone); no behaviour change. The AST-traversal walk() is a different concept and stays.

Verification

  • pixi run pytest — 1280 passed.
  • pixi run pyrefly check src/ — 0 errors.
  • pixi run ruff check — clean.
  • pixi run compile-tex — 31 documents.
  • pixi run docs-build — --strict, clean.
  • All doc generators current; the schema regenerated with one corrected description.

Deliberately not done

  • The program node for indexing is still named At internally (not Index) — it is not "walk" vocabulary, so renaming it was out of scope; say the word and it is a one-commit follow-up.
  • Multi-relation index members are bare (mirroring sum(by=[a, b])); a dotted member in a list is not wired.

Departure from a Part 2 default: the branch lives in the primary checkout, not the ../wt/ worktree AGENTS.md prescribes, because the sandbox permits writes only inside the repo directory.

🤖 Generated with Claude Code

coroa and others added 3 commits September 16, 2026 23:29
…lection

Add the surface forms the relation redesign needs, as unresolved AST nodes:
- IndexNode for x[rel] and x[rel.col] — an atom, a bare name indexed.
- ColumnRefNode for a dotted column selection in a kwarg value, over=rel.k,
  by=rel.v, along=rel.d, and the bracketed list rel.[a, b].

Both are unresolved: resolution rewrites them in a later commit. Until then no
declaration reaches them, so the loader still refuses the new syntax downstream.
The nodes join UNRESOLVED in the golden node-census test, beside NameNode.

Verified: pixi run pytest (1285 passed), pyrefly clean, ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…x[rel]

Replace at(), into=, partition by=, and the "walk" concept with a dot syntax.
Every relation operator joins the operand to the relation on shared dims, then
reduces; three forms differ in what they drop:

- sum(x, over=rel.k) sums key column k away; the value and other keys ride in.
- sum(x, by=rel.v) groups onto value column v; the whole key is summed away.
- x[rel] indexes: reads the value at the key, replacing at(x, by=rel).
- shift/sum_back/position take along=rel.k (partition by=rel folded in);
  within= stays for group selection; position(rel.k) has no bare form.

The dot picks columns; a bare rel is all of them; a list mirrors sum(by=[a, b]).
The index operand is a name or a parenthesized expression, and x[a, b] indexes
through several relations at once.

Internals: the parser reads ColumnRefNode and IndexNode; resolution rewrites an
index to the internal `index` operator (at renamed), and both sum forms and the
index build one Join whose dim rule is uniformly (inner - consumed) | landed.
The dead is_function_read guard in the key-join is dropped — no sum can be
degenerate now that indexing is its own operator.

Migrated: the operator example set, the golden completeness model, the PyPSA
examples, and every test fixture asserting the retired spelling; the
operator-math table, notation page and gallery pages regenerate from them.

Verified: pixi run pytest (1280 passed), pyrefly clean, ruff clean,
compile-tex (31 documents), docs generators current. The Walk class and internal
"walk" identifiers are not yet renamed to join/reduce vocabulary — a follow-up
churn commit with no behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The language redesign dropped the "walk" metaphor: a relation operator joins the
operand to the relation, then reduces. Rename the internal structure to match —
program.Walk -> program.Join, the walks fields on GroupSum/At/RelationNode/
Translate/Window -> joins, and the "walk" wording in the docstrings and the one
schema description that named the retired over=/into= spelling.

No behaviour change. The AST-traversal walk() and its Walk visitor in
typesetting are a different concept and stay. The schema regenerates with the
corrected description only.

Verified: pixi run pytest (1280 passed), pyrefly clean, ruff clean, schema
current.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@read-the-docs-community

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

Copy link
Copy Markdown

@FBumann

FBumann commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

@coroa I have a few opinions:

  1. I still dont like the new at() syntax. Could we stay with at() for now for the sake of only worrying about trelations? var[relation.key] is an easy drop in for at(var, single_kw) in a follow up. This reduces the surface we need to argue about, and the diff.

  2. This notation has a shortcoming when summing over a relation with multiple values:

main:  sum(p, by=slot_of, over=generator, into=bus) <= cap

509: sum(p, over=slot_of.generator) <= cap
# REFUSED: the expression carries dims ['technology'] that are not in its dims: ['bus', 'period']
#   over= lands on every value column, so technology comes along

#509:  sum(p, by=slot_of.bus) <= cap

Assumes:

dimensions:
  generator:  { dtype: str }
  period:     { dtype: int }
  bus:        { dtype: str }
  technology: { dtype: str }

relations:
  slot_of: { key: [generator, period], value: [bus, technology] }

variables:
  p: { dims: [generator, period], bounds: { lower: 0 } }

As a side note for now:

I found a few bugs. Can you confirm they are bugs? Or am i misunderstanding a design choice as a bug?

  • Two value columns over one dimension. cap[ends.[bus0, bus1]] and bare cap[ends] read bus1 only. _relation_node dedups the consumed dims, so the collision is gone before any check. Mirror on the sum side: sum(f, over=ends.line) lands on both bus columns unrefused. Main refuses both.
  • Two relations on different keys. price[zone_of, tech_of] loads and the math has an unbound s. The index path in _relation_ref returns early with the first join's key only, and skips the repeated-landing check, so pz[zone_of, zone2] passes too.
  • Dotted member in an index list. u[ends.bus0, gen_bus] drops the dot and reads both bus columns. _index keeps only r.name. The PR says "not wired", but it is accepted, not refused.
  • Macro formal as a relation. sum(x, by=r) works, but x[r] and sum(x, by=r.bus1) are refused at the call site, and sum(x, over=r.line) crashes with KeyError: 'r'. The template check accepts the formal, but expansion never substitutes it: IndexNode.relations and ColumnRefNode.name are not children.

@FBumann FBumann added the area: relations relations and dimensions: the relation design label Sep 17, 2026
@FBumann FBumann added the proposal: dot label Sep 17, 2026 — with Claude
@FBumann FBumann added proposal: dot + x[rel] A column is named after the relation, and the read is an index, as price[zone_of] (#506, #509) and removed proposal: dot labels Sep 17, 2026 — with Claude
@coroa

coroa commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

I really don't like issues, too much scattered information.

Re 1: I'm fine with separating at/indexing out. It does not change much. I do like indexing better.

Re 2: I guess the implementation is buggy then.

dimensions:
  generator:  { dtype: str }
  period:     { dtype: int }
  bus:        { dtype: str }
  technology: { dtype: str }

relations:
  slot_of: { key: [generator, period], value: [bus, technology] }

parameters:
  cap: { dims: [period, bus] }   # was that your assumption, as well?

variables:
  p: { dims: [generator, period], bounds: { lower: 0 } }
sum(p, by=slot_of.[period, bus]) <= cap

is what i imagine you should be saying for this case, and then my assumption that by can only name value columns was wrong. and it should be able to include key columns.

coroa and others added 2 commits September 18, 2026 00:15
…join

Replace the one generic RelationNode and its Join structure with three
per-operation resolved nodes — SumRelationNode, IndexRelationNode and
PartitionRelationNode. Each holds the columns the author named and derives its
own frame, so a consumer reads a relation in the operation's own terms rather
than decoding a shared consumed/produced/joined triple. over= is desugared to
by= at resolution, and the frame-dependent checks stay one node-level call.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…elation list takes dotted members

Four bugs in the relation dot syntax, each a load that should refuse or a
crash that should be a message:

- An index reading two value columns over one dimension —
  `cap[ends.[bus0, bus1]]`, bare `cap[ends]`, or two relations each reading
  it — loaded and read one column, deduping the collision away. It now refuses,
  as the sum side already did. Two relations keyed over one dimension still
  loads: they join at a shared key, which is what `tech_cap[gen_bus, gen_tech]`
  does.
- A dotted member in an index or `by=` list dropped its dot. A relation list
  now takes dotted members uniformly, so `by=[a.x, b.y]` and `x[a.x, b.y]`
  read the named columns, and `by=rel.[x, y]` is the same grouping written
  from one relation.
- A macro formal standing where a relation does — bare in `x[rel]`, or before
  a dot in `by=rel.v` — was never substituted; expansion left the formal name
  in place. It now substitutes, and a formal bound to an expression where a
  relation belongs is refused rather than crashing.
- `sum(x, over=rel.k)` naming an undeclared relation crashed with a KeyError.
  The `over=`/partition path now guards the name like the `by=` path, so it is
  a "does not name a relation" refusal.

The grammar reads a bracketed relation list as relation references, so
`NameListNode` carries `NameNode | ColumnRefNode` members; `within=` reads its
members bare through `.names` and refuses a dotted one.

pixi run pytest — 1293 passed. pyrefly, ruff, docs-build --strict,
compile-tex (31) all clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@FBumann

FBumann commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Closed in favor of #540

Will revisit for syntax discussions

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 proposal: dot + x[rel] A column is named after the relation, and the read is an index, as price[zone_of] (#506, #509)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants