feat: a cased expression prints once, as a definition - #37
Conversation
Documentation build overview
42 files changed ·
|
f6bac42 to
85eddf4
Compare
85eddf4 to
f059a6b
Compare
7ca1f5a to
6dd5a1d
Compare
6dd5a1d to
a2ce521
Compare
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
a2ce521 to
ea759fb
Compare
|
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 — The stronger reason is what #36 alone would publish: the inline rendering it calls "reads badly" ships on Both commits are on Closing — nothing here is abandoned. Generated by Claude Code |
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
CasesNoderendered 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: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 becauseramp_upnames 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:
and the block prints once, under a new Definitions section between
Subject toandVariable domains— which is where a paper states a quantity defined by region: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
CasesNodealready carriesnameandforeach, 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
--symbolscan 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_awaystill 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.pyis 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.tomlpins plusprettier@3.9.3:ruffformat and check clean,pyrefly0 errors,reuse lintcompliant,typosclean, prettier clean,mkdocs build --strictclean, 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