Skip to content

feat(language): one shift(over=, by=, edge=), replacing roll and shift - #359

Merged
FBumann merged 1 commit into
mainfrom
worktree-unify-shift
Jul 31, 2026
Merged

feat(language): one shift(over=, by=, edge=), replacing roll and shift#359
FBumann merged 1 commit into
mainfrom
worktree-unify-shift

Conversation

@FBumann

@FBumann FBumann commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Implements rename 1 from #332.

Breaking: roll() is gone and shift() changes shape. No ! marker — the alpha stream refuses one (RELEASING.md), so the break is described here.

The surface

shift(soc, over=snapshot, by=1)             # vacated positions are absent — the row drops
shift(soc, over=snapshot, by=1, edge=wrap)  # cyclic
shift(seg, over=bp,       by=1, edge=0)     # vacated positions contribute 0

Why one keyword rather than wrap= + fill=

#332 proposed shift(…, by=1, wrap=true) alongside the existing fill=. That re-creates a contradiction the two names used to prevent: "cyclic, and fill what it vacates" was nearly unwritable as roll(x, d=1, fill=0), and got a refusal that could say why. As two keywords on one call it reads plausible and needs a run-time check.

edge= collapses them: one keyword, three legal values, no illegal combination to refuse. A cyclic map vacates nothing, and the surface now says so by construction.

Why unify at all

roll and shift were already one IR node — plan.Translate, whose docstring says it is "named for the coordinate map rather than for either spelling". Only the surface was doubled, and the corpus picked the wrong member six times across four ports, needing four lines of comment in pypsa_ramp.yaml to justify the choice. That is two names for one node causing real mistakes in the most carefully-reviewed files in the repo.

It also moves the dimension out of the kwarg key. roll(x, snapshot=1) named its dim in the key, which is why a macro could not parameterise it — ARCHITECTURE.md cited exactly this as the language's own counterexample on macro-friendliness. over= is an ordinary value, so a formal can carry it, and helpers.py loses dimension_is_key and split_dimension_key.

What checks this is a surface change and nothing else

The typeset goldens are byte-identical. Only the source line in tests/golden/model.yaml changed; all three rendered outputs are unchanged, because the math is unchanged. All 11 ports still match their published optima.

EdgeNode

A closed keyword joins the resolved AST beside DimensionNode and CoordinateNode — it names neither data nor a coordinate. Adding it to the ArithmeticNode union made four exhaustive walkers incomplete, which is the type system doing its job: expansion, validation, the eager evaluator and the typesetter each now say what an edge policy is not.

Migration

roll(x, d=n)shift(x, over=d, by=n, edge=wrap) · shift(x, d=n)shift(x, over=d, by=n) · shift(x, d=n, fill=v)shift(x, over=d, by=n, edge=v)

699 tests pass; ruff and pyrefly are at parity with main.

🤖 Generated with Claude Code

**Breaking: `roll()` is gone and `shift()` changes shape.** No `!` marker
— the alpha stream refuses one (RELEASING.md), and the break is described
here instead.

roll and shift were always one IR node — plan.Translate, named for the
coordinate map rather than either spelling. The surface kept two names,
and the corpus picked the wrong one six times across four ports, needing
a four-line comment in pypsa_ramp to justify the choice. Two names for
one node cause real mistakes in the most carefully-reviewed files here.

  shift(soc, over=snapshot, by=1)             vacated positions absent
  shift(soc, over=snapshot, by=1, edge=wrap)  cyclic
  shift(seg, over=bp, by=1, edge=0)           vacated positions contribute 0

`edge` carries all three policies in one keyword instead of `wrap` and
`fill` as two. That is the part worth the diff: the pair could contradict
each other — cyclic *and* fill what it vacates — and the old surface had
to refuse that combination at run time. Now it has no spelling. One
keyword, three legal values, nothing to check.

The dimension also moves out of the kwarg *key*. `roll(x, snapshot=1)`
named its dim in the key, which is why a macro could not parameterise it
and why ARCHITECTURE cited it as the language's own counterexample on
macro-friendliness. `over=` is an ordinary value, so a formal can carry
it, and helpers.py loses dimension_is_key and split_dimension_key with it.

EdgeNode joins the resolved AST beside DimensionNode and CoordinateNode:
a closed keyword that names neither data nor a coordinate. Adding it to
the union made four exhaustive walkers incomplete, which is the type
system doing its job — expansion, validation, the eager evaluator and the
typesetter each say what an edge policy is not.

The typeset goldens are byte-identical: the same math renders the same
way, which is the check that this is a surface change and nothing else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@FBumann, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 44 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2089383f-8c40-48d9-9f55-e995d84387df

📥 Commits

Reviewing files that changed from the base of the PR and between 720b726 and af84ffb.

⛔ Files ignored due to path filters (1)
  • examples/walkthrough.out is excluded by !**/*.out
📒 Files selected for processing (38)
  • README.md
  • docs/ARCHITECTURE.md
  • docs/SPEC.md
  • docs/design/ceiling.md
  • docs/guide.md
  • docs/models/pypsa_cyclic_storage.md
  • docs/models/pypsa_ramp.md
  • docs/models/pypsa_storage.md
  • docs/models/pypsa_unit_commitment.md
  • docs/models/storage.md
  • examples/ports/pypsa_cyclic_storage.yaml
  • examples/ports/pypsa_ramp.yaml
  • examples/ports/pypsa_storage.yaml
  • examples/ports/pypsa_unit_commitment.yaml
  • examples/storage.yaml
  • src/lpspec/dimensions.py
  • src/lpspec/expansion.py
  • src/lpspec/expression_parser.py
  • src/lpspec/helpers.py
  • src/lpspec/linopy/builder.py
  • src/lpspec/lowering.py
  • src/lpspec/piecewise.py
  • src/lpspec/relational/compiler.py
  • src/lpspec/relational/executor.py
  • src/lpspec/relational/plan.py
  • src/lpspec/resolution.py
  • src/lpspec/typeset/walk.py
  • src/lpspec/validation.py
  • tests/golden/model.yaml
  • tests/test_arithmetic_laws.py
  • tests/test_dimensions.py
  • tests/test_duals.py
  • tests/test_piecewise.py
  • tests/test_relational.py
  • tests/test_shift.py
  • tests/test_typeset.py
  • tests/test_validation.py
  • tools/constructs.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@read-the-docs-community

Copy link
Copy Markdown

@FBumann
FBumann merged commit 8473a24 into main Jul 31, 2026
4 checks passed
FBumann added a commit that referenced this pull request Jul 31, 2026
…heir line (#361)

What survived rebasing onto #359 — the engine-side half of this pass (the
label arithmetic, `plan.children`, `dataclasses.replace` over `TermFragment`,
the shared shift-edge filter) landed with that branch, and the duplicated
`_check_where` call it also fixed is upstream too.

DRY:

- The "Did you mean …?" clause was written three times — for an unknown
  declaration name, an unknown YAML key, and an unknown symbol-table entry.
  `errors.did_you_mean` is now the clause and each caller keeps its own
  framing sentence, which is the part that genuinely differs. `errors.py`'s
  own docstring had already noted there were two copies before it added a
  third. Drops the `difflib` import from `schema.py` and `typeset/symbols.py`.
- `_fold_and` and `_fold_or` differed only in the node class; `_folder` takes
  it as an argument. The fold is the grammar's associativity, which is one
  rule.
- `_resolve_where` tested `isinstance(value, str) and ns.kind(value) == ...`
  three times in sequence for the three things a where-comparison's RHS may
  not name. One walrus and a `_declared_rhs_error` dispatch; the three
  messages stay distinct, because only the dimension case is a silent wrong
  answer rather than an obvious one.
- `Namespace._unknown` built its two kind labels and their two value lists
  through parallel ternaries that had to stay in step; one list of pairs.

Comments: removed the ones that restate the line below them — the
per-`isinstance` branch labels in `_coerce_to_dataarray`, `# Step 2:` and
`# Step 5:` numbering a sequence that no longer exists, `# Atoms` / `# Unary`
/ `# Function calls` over the grammar they name, `# Referenced dimensions
must be declared`. Every comment carrying an argument stays, and where a
section label was doing real work it was rewritten into one instead
(`# at most one comparison, and only at the top`, `# NOT binds tightest, then
AND, then OR`).

Also drops `expression_parser.RESOLVED_REFERENCE_NODES`, unused, and whose
comment claimed both consumers asserted on it when neither did.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@FBumann
FBumann deleted the worktree-unify-shift branch July 31, 2026 10:52
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