feat: position(dim) replaces index(dim, i), converting on the left - #31
Merged
Merged
Conversation
1 task done
FBumann
force-pushed
the
claude/new-session-divgg4
branch
2 times, most recently
from
August 22, 2026 09:04
363f9c5 to
3864ba8
Compare
FBumann
force-pushed
the
claude/new-session-divgg4
branch
from
August 22, 2026 10:12
3864ba8 to
c2d612f
Compare
This was referenced Aug 22, 2026
Merged
FBumann
force-pushed
the
claude/new-session-divgg4
branch
from
August 22, 2026 21:13
afb0e6a to
c103c48
Compare
FBumann
force-pushed
the
claude/new-session-divgg4
branch
from
August 23, 2026 06:19
c103c48 to
937b2ee
Compare
FBumann
force-pushed
the
claude/new-session-divgg4
branch
2 times, most recently
from
August 23, 2026 08:26
3afbcea to
d414e87
Compare
FBumann
force-pushed
the
claude/new-session-divgg4
branch
from
August 23, 2026 12:34
d414e87 to
c52b9a7
Compare
`snapshot > index(snapshot, 0)` had two readings and the language picked neither, so the file loaded and the model it meant was undetermined (#32). As "the coordinate at position 0" it compared values and depended on the axis arriving sorted; as "position 0" it compared positions. `==` and `!=` agreed, which is why it had not bitten. Renaming it would have fixed the label and not the shape: the token on the left changed what it denoted depending on the right-hand side — `snapshot > 5` the coordinate's value, `snapshot > index(snapshot, 0)` its position. So the conversion moves to the left instead, where what is being converted is visible: where: "position(snapshot) == 0" # first where: "position(snapshot) > 0" # every other one where: "position(snapshot) == -1" # last where: "position(snapshot, by=period_of) == 0" # first of each period Both sides are now integers, so every comparator reads one way and the wrong reading is unsayable rather than documented against — the same move `objective:` makes by holding one block. The grammar shrinks: POSITION leaves the `value` production, and resolution loses the same-dimension check that only existed because both sides named a dimension. A value comparison is still written against the dimension itself, `snapshot > '2030-01-01'`, where it always was. The typeset follows: `pos(t) = 0` rather than `t = index(T, 0)`, an application to the row, since that is what it converts. The ambiguity reached the rendered math too, so a reader of the paper could not recover the model either. Every model that used the old spelling now fails to parse, and "Expected end of text, found '('" is the wrong message for the most likely reader, so the failure names the rewrite. This is a breaking spelling change, described here rather than marked: a '!' moves the base version rather than the alpha counter, which the project refuses while the stream is pinned (.github/workflows/pr-title.yml). Closes #32. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADtfZf4V6W9XcLRSwSgHzE
`OPERATOR_NAMES` is the vocabulary a walk may emit and `format.py` states the contract — "A format spells; it never decides" — so an operator name written into the walk escapes all three guards built around that set: that every format spells exactly it, that every Typst spelling compiles, and that the golden model asks for every one. `index` had the same problem, but this commit is the notation change, so it is the moment to move it. The spelling is unchanged, so no golden moves. What changes is that a format can now disagree about it, and is held to having an opinion. Also fixes what that exposed: `docs/reference/notation.md` is generated between markers from the golden model, and the earlier edit rewrote the YAML snippets by hand without the rendered math beneath them, leaving four lines reading `index(𝒯, 0)` under a snippet saying `position(...)`. `tools/notation.py` cannot run — it reads two example files that are not in the repo, on main as well — so the four lines are corrected from output rendered through the same code path rather than by regenerating. Two cleanups alongside: the position parse action reads its tokens from one end rather than three, and the migration hint is an `if` above its use site rather than a helper below it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADtfZf4V6W9XcLRSwSgHzE
`position(dim)` prints `pos(t)`, and a coordinate comparison prints `t >= 3`.
Both are an index-ish thing against a small integer, and nothing on the page
said which way the unmarked one goes — while the reader's own convention says
the opposite of this language's: papers write their sets as {1, …, T}, where
the index *is* the ordinal and nothing needs marking. So #32's ambiguity
survived into the artifact a reader recovers the model from.
The mark stays on the position, since a coordinate is what a dimension denotes
everywhere else in the language and a position is derived from whichever index
resolution produced. What is added is the legend that makes the unmarked side
readable, and two corrections to what a position prints:
- `pos` is introduced where it is used, gated the way the translation notes
are: one note for the symbol, one for the grouped form, one for the size —
each only where that form printed.
- A group rides a **subscript**, `pos_{season_of(t)}(t)`. As a second
argument it sat where the first one's integer sits, saying nothing about
"within".
- A position counted from the end prints against the size, `|T| - 1` rather
than `-1`: the order runs `0` to `|T| - 1`, so `-1` was an equation
asserting a position the legend had just ruled out. Grouped, it counts back
from the group's size, `|T_{season_of(t)}| - 1`.
- A dimension compared against a *number* names its coordinates in the set
legend (`snapshot` (`int` coordinates)) — the one comparison that can be
read as a position, since every other coordinate prints as prose.
Positions stay 0-based on the page, as in the file, so a clause reads off one
and writes into the other.
`|·|` is a `Format` method rather than an operator: `OPERATOR_NAMES` is a
vocabulary of infix spellings and the Typst gate compiles every one of them
between two operands.
The golden model gains `last`, the from-the-end pair the fixture had no case
for, which is what holds the new arms of the walk to output someone has read.
FBumann
force-pushed
the
claude/new-session-divgg4
branch
from
August 23, 2026 12:41
c52b9a7 to
f1a9632
Compare
FBumann
marked this pull request as ready for review
August 23, 2026 18:35
This was referenced Aug 25, 2026
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.
Closes #32. The partition/
cases:prototype that was on this branch has moved to #33, stacked on top of this. #40 has been folded back in, so this PR is the language fix and the notation that prints it.What this changes
snapshot > index(snapshot, 0)had two readings and the language picked neither, so a file loaded and the model it meant was undetermined (#32). As "the coordinate at position 0" it compared values and depended on the axis arriving sorted; as "position 0" it compared positions.==and!=agreed, which is why it had not bitten.Renaming the operator would have fixed the label and not the shape: the token on the left changed what it denoted depending on the right-hand side —
snapshot > 5the coordinate's value,snapshot > index(snapshot, 0)its position. So the conversion moves to the left, where what is being converted is visible:Both sides are integers now, so every comparator reads one way and the wrong reading is unsayable rather than documented against — the same move
objective:makes by holding one block.The grammar shrinks:
POSITIONleaves thevalueproduction, and resolution loses the same-dimension check that only existed because both sides named a dimension. A value comparison is still written against the dimension itself,snapshot > '2030-01-01', where it always was.The page, not just the file
The ambiguity reached the rendered math too, so a reader of the paper could not recover the model either — and half-fixing it there left
pos(t) = 0besidet \ge 3, both an index-ish thing against a small integer, with nothing saying which way the unmarked one goes. The reader's own convention says the opposite of this language's: papers write their sets as{1, …, T}, where the index is the ordinal and nothing needs marking.The mark stays on the position. A coordinate is what a dimension denotes everywhere else in the language, and a position is derived from whichever index resolution produced, so the derived thing is what gets converted in view. What is added is the legend that makes the unmarked side readable, plus two corrections to what a position prints.
posis introduced where it is used, gated the way the translation notes are — one note for the symbol, one for the grouped form, one for the size, each only where that form printed:A group rides a subscript,$\mathrm{pos}_{\mathrm{season_of}(t)}(t)$ . As a second argument it sat exactly where the first one's integer sits and said nothing about "within" — the shape an edge fill already has, riding its translation as a subscript.
A position counted from the end prints against the size:$\mathrm{pos}(t) = \lvert\mathcal{T}\rvert - 1$ , and $\lvert\mathcal{T}_{\mathrm{season_of}(t)}\rvert - 1$ where the count is grouped. The order runs
0to|T| - 1, so-1printed as itself was an equation asserting a position the legend had just ruled out — Python's index sugar, read as math.A dimension compared against a number names its coordinates in the set legend:
index t — snapshot (int coordinates). That is the one comparison a reader can take for a position; every other coordinate prints as prose and could not be.Positions stay 0-based on the page, as in the file, so a clause reads off one and writes into the other.
Why not the alternatives
coord(t) >= 3besidepos(t) = 0) taxes the common case, and contradicts the reason the conversion moved left: only one side should ever be marked.\hat tfor ordinals) is invisible at body-text size, does not survive plain text, and all three formats would have to agree on it.trange over{1..|T|}per the paper convention and printing values as\tau_t— is the only coherent inversion, and nearly free everywhere except a value comparison. It costs the symbol correspondence between page and file, which is what this PR is protecting.t \succ t_1, with the set enumerated in the legend) are more conventional thanpos(t) > 1, but they are a second surface for one construct. The order relation now appears only in the legend line, where it says whatposcounts along.Why not extend it further
Asked and answered while writing this, so it is on the record rather than rediscovered later.
position()takes a dimension and nothing else:position(gen)already says that, and the dim must be in the frame anyway), and undefined the moment a parameter has two dims. For "rank by its value" it is a window over unbounded rows —ceiling.md's reject row — and precomputable in data prep regardless, as anintparameter compared normally:where: "cost_rank == 0".position(dim, by=lookup)already covers positions within its groups, the only positional thing a lookup has.Keeping it in coordinate space is what makes it free: it reads the small dim table, never the data.
Breaking
Every model using the old spelling now fails to parse. "Expected end of text, found
(" is the wrong message for the reader most likely to hit it, so the failure names the rewrite:A test pins that unrelated parse failures stay silent about positions.
Described rather than marked with
!, which would move the base version off the alpha stream.Scope
Grammar, resolution, typeset, the two masks
piecewise.pygenerates, four docs pages, the golden model and its three regenerated outputs. New tests cover the grammar surface (both frames, every comparator, the grouped form), the resolution error paths (a parameter, an undeclared name, aby=that is not a lookup, aby=over another dimension), and the rewrite hint.The notation half adds
|·|as aFormatmethod rather than anOPERATOR_NAMESentry — that vocabulary is infix spellings, and the Typst gate compiles every one of them between two operands — spelled by all three formats. The golden model gainslast, the from-the-end pair it had no case for, which is what holds the new arms of the walk to output someone has read under the fixture's three censuses.The notation page, now that #41 has landed
Writing this turned up that
tools/notation.pycould not run in this repository at all — itsPIECEWISEmap named four models left behind influxopt/lpspecat the extraction (#17) — and that nothing ran--check, so the page had been drifting from the models it claims to be generated from. That is fixed onmainby #41, which restored the four models, put the page in.prettierignoreunder the ruledocs/examples/already follows, and added the guards.Two consequences here, both good:
test_the_notation_page_is_currentproves it, which it did not when this branch first carried a hand-spliced page.b \neq \mathrm{index}(\mathcal{B}, 0)to\mathrm{pos}(b) \neq 0on its own, because the tool now renders that model rather than a person copying the row across.Checks
pixi.shis unreachable from the environment this was written in, so the gates were reproduced with a 3.13 venv on the versionspixi.tomlpins, plusprettier@3.9.3andtyposfetched directly:ruffformat and check clean,pyrefly0 errors,reuse lintcompliant,typosclean, prettier clean, 381 passed — the three Typst compile tests included, which coverabs(cal(T))and the subscriptedposand skip without the bindings. Goldens regenerated withpython -m tests.typeset.golden, the page withpython -m tools.notation. CI is green on this head.