Skip to content

feat: a cased expression prints once, as a definition - #37

Merged
FBumann merged 2 commits into
claude/expression-casesfrom
claude/cases-definitions
Aug 23, 2026
Merged

FBumann merged 2 commits into
claude/expression-casesfrom
claude/cases-definitions

Conversation

@FBumann

@FBumann FBumann commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #36 — base is claude/expression-cases, so the diff here is the rendering alone. Retarget down the stack as each lands.

Expansion is untouched: the AST still inlines a cased expression where its name stood, exactly as #36 landed it. This changes only what the typesetter does with the node.

The defect

A CasesNode rendered as an atom, so the block printed where the name was. A three-arm block is three rows tall, and whatever follows it in the equation sits beside the middle arm:

\text{ramp\_up} && p_{t,g} - p_{t \boxminus_{0} 1,g} & \le \mathit{ramp\_limit}_{g} \cdot \begin{cases} 1 & \text{if } \neg \mathit{committable}_{g} \\ \mathit{status}^{\mathrm{initial}}_{g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) = 0 \\ \mathit{status}_{t - 1,g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) > 0 \end{cases} + \mathit{start\_up\_limit}_{g} \cdot \left( 1 - \begin{cases} 1 & \text{if } \neg \mathit{committable}_{g} \\ \mathit{status}^{\mathrm{initial}}_{g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) = 0 \\ \mathit{status}_{t - 1,g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) > 0 \end{cases} \right) && \forall\, t \in \mathcal{T},\ g \in \mathcal{G}

That is one row of examples/commitment.yaml, which #36 adds — not a contrived model, and visible on its gallery page as the page stands before this change. The reader's eye lands on \wedge \mathrm{pos}(t) = 0 \cdot \mathit{start\_up\_limit}_g, and the same three arms are printed twice because ramp_up names the quantity twice. A quantity written once in the file was written once per use on the page, which is the opposite of what naming it was for.

What it prints now

A use prints the symbol:

$$p_{t,g} - p_{t \boxminus_{0} 1,g} \le \mathit{ramp_limit}_{g} \cdot \mathit{previous_status}_{t,g} + \mathit{start_up_limit}_{g} \cdot \left( 1 - \mathit{previous_status}_{t,g} \right) \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

and the block prints once, under a new Definitions section between Subject to and Variable domains — which is where a paper states a quantity defined by region:

$$\mathit{previous_status}_{t,g} = \begin{cases} 1 & \text{if } \neg \mathit{committable}_{g} \cr \mathit{status}^{\mathrm{initial}}_{g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) = 0 \cr \mathit{status}_{t - 1,g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) > 0 \end{cases} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

The section is named Definitions rather than the conventional where, which in this repo is a keyword and would read as the wrong thing.

How

CasesNode already carries name and foreach, so the walk needs nothing new from the AST: rendering one records it and returns the indexed symbol. definitions() then prints what the other sections reached — so it runs after them, and to a fixpoint, since an arm may name another cased expression. An expression nobody names prints nothing.

Symbols. Cased expressions join the symbol pool, deriving a symbol like any other name, and --symbols can rename one. Uncased expressions stay out: they print nothing under their own name, so a table entry for one would never apply — the silent-typo failure the table is strict about.

The asymmetry is deliberate and is the one thing worth arguing with. test_macros_and_named_expressions_are_expanded_away still holds for every other named expression; a cased one is the exception. The defence: it is the only kind that cannot inline legibly, and the only kind whose declaration a reader has to see to check the regions.

Scope

typeset/walk.py (the symbol at the use site, definitions()), typeset/__init__.py (the section, and the ordering the collection needs), typeset/symbols.py (printed_expressions, the pool and the table's accepted names), typeset/format.py (a docstring that licensed an arm order the walk does not take), tools/notation.py (expressions: becomes a section with rows, replacing the block #36 showed above the legend), expressions.md, notation.md, the three goldens, the regenerated commitment gallery page, and tests.

The gallery page is regenerated rather than hand-edited, and tests/test_docs.py is what caught it going stale under the new rendering — which is what that test is for.

Checks

Gates reproduced with a 3.13 venv on the versions pixi.toml pins plus prettier@3.9.3: ruff format and check clean, pyrefly 0 errors, reuse lint compliant, typos clean, prettier clean, mkdocs build --strict clean, 410 passed — including the Typst compile and the walk's line-coverage guard, which the bare install skips. Every equation quoted above is the renderer's own output, not written by hand.


Generated by Claude Code

@read-the-docs-community

read-the-docs-community Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

@FBumann
FBumann force-pushed the claude/cases-definitions branch from f6bac42 to 85eddf4 Compare August 22, 2026 21:13
@FBumann
FBumann force-pushed the claude/cases-definitions branch from 85eddf4 to f059a6b Compare August 23, 2026 06:19
@FBumann
FBumann force-pushed the claude/cases-definitions branch 2 times, most recently from 7ca1f5a to 6dd5a1d Compare August 23, 2026 07:57
@FBumann
FBumann force-pushed the claude/cases-definitions branch from 6dd5a1d to a2ce521 Compare August 23, 2026 08:07
claude added 2 commits August 23, 2026 08:27
Inlining the block where its name stood is what the AST does, and it is the
wrong thing to print. A three-arm block is three rows tall, so whatever follows
it in the equation sits beside the middle arm and reads as part of that arm's
condition. `examples/commitment.yaml` is the worst case and it is not
contrived: `ramp_up` names the quantity twice, so one row carried the same
three arms twice over. And a quantity written once in the file was written once
per use on the page, which is the opposite of what naming it was for.

So a use prints the symbol and the block prints under a `Definitions` section,
which is how a paper states a quantity defined by region. Nothing about
expansion changes — the AST still inlines, and `CasesNode` already carries the
name and the frame the walk needs.

Cased expressions join the symbol pool, so they derive a symbol like any other
name and `--symbols` can rename one. Uncased ones stay out: they print nothing
under their own name, and a table entry that never applies is the silent typo
the table is strict to avoid.

`definitions()` runs after the sections that use it, since what lands there is
what they reached; an arm may name another cased expression, so it runs to a
fixpoint. An expression nobody names prints nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADtfZf4V6W9XcLRSwSgHzE
`walk.definitions()` prints what the other sections reached, so it has to run
after them. It sat inside the section list, where that dependency held only
because Python evaluates the tuple assignment above it first — invisible to
anyone tidying the list back into inline calls, and silent when broken: the
section would simply come out empty.

It is a statement of its own now, after the three it depends on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADtfZf4V6W9XcLRSwSgHzE
@FBumann
FBumann force-pushed the claude/cases-definitions branch from a2ce521 to ea759fb Compare August 23, 2026 08:27
@FBumann
FBumann merged commit ea759fb into main Aug 23, 2026
5 checks passed

FBumann commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Folded into #36 rather than reviewed as a stack. The two are one change, not two layers: this PR rewrote nine of the thirty files #36 writes, and in two of them it undid what #36 had just done — tools/notation.py lost nineteen lines where #36 added nineteen, and the LaTeX golden lost the single line #36 added. Both goldens, the commitment gallery page, notation.md and expressions.md churned twice.

The stronger reason is what #36 alone would publish: the inline rendering it calls "reads badly" ships on docs/examples/commitment.md, so the docs site would carry a three-arm block printed twice in one row — with \wedge \mathrm{pos}(t) = 0 \cdot \mathit{start\_up\_limit}_g under a reader's eye — until this landed. And the asymmetry this PR flags as the one thing worth arguing with (a cased expression prints under its name; an uncased one does not) is an argument about #36's cases: surface, so it belongs beside it.

Both commits are on claude/expression-cases unchanged (ea759fb, a fast-forward from 454cf1f — no rebase, no rewrite). This PR's body has been merged into #36's description, the defect and the Definitions section included. Verified on the folded head: 425 passed, ruff and pyrefly clean.

Closing — nothing here is abandoned.


Generated by Claude Code

@FBumann
FBumann deleted the claude/cases-definitions branch September 9, 2026 06:45
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.

2 participants