Skip to content

fix(language): a long chain of named expressions loads, a typo beside a formal is refused, and a fault in an entry hides no other - #643

Merged
FBumann merged 4 commits into
claude/mathspec-packages-review-urd4yafrom
claude/peaceful-tesla-eumvcy
Sep 23, 2026
Merged

FBumann merged 4 commits into
claude/mathspec-packages-review-urd4yafrom
claude/peaceful-tesla-eumvcy

Conversation

@FBumann

@FBumann FBumann commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: Please adress this review

Note

The following content was generated by AI.

Every should-fix and nit of the stack review of #626 through #638 that is a code change, fixed on top of #638, each with the test that fails without it. Stacked on #638. The three #628 findings are fixed in #628 itself, and the stack above it is rebased.

What this changes

Finding Was Is
#628 blocking: a formal along= beside by=/within= refused as "no key column over" fixed silently at #638 fixed in #628's second commit, and a call-site test here asserts the node the call builds
#628: sum(x, by=nope, over=a, into=b) loaded with formal columns the formals sent the call back before the relation was read the relation is read first, in #628's second commit
#628 nit: over=/by= refusals lost "or a formal of this macro" — the words are back, in #628's second commit
#632: a fault in a macro or an entry hid every other fault raise after the entries every declaration is read; a use of a refused entry says it does not load, and the refusal is listed with it
#632: a cycle through a macro read a -> a expansion knew the macro, the resolver did not expansion asks the namespace, so the chain is a -> m -> a
#632: a long chain was a RecursionError or the parser's "nests deeper" each entry recursed into the next entries resolve from a worklist in dependency order; the resolved tree is held to MAX_RESOLVED_DEPTH (300), and the refusal names that depth
#632 rule: a cycle through a case's when refused, untested pinned
#638: unused templates and entries now refused for an amount, an edge or a bare sum unclaimed pinned, and claimed in #638's body
#638: a dim fault under a bare sum() is a SchemaError tests loosened to LanguageError the class is parametrized per rule; resolution's faults are SchemaError, the dim rules' DimensionError
#638: sum(sum(p, into=g)) added "already a scalar" the refused inner call was built a refused call builds nothing
#638: 123456789 < p rewrote to p > 1.23457e+08 :g the number as the file wrote it
#638: an undeclared piecewise over: added per-link lines — one line
#638: "the expansion cannot fail" a str/bool breakpoint parameter and an lp x-link with no variable were refused under cost_curve_increasing / cost_curve_domain_lo refused on the block's own link at load
#638 nit: offset=-tag lost the dtype hint the dtype rule was the dim checker's a named amount is held to dtype: int where it is read, before its sign
#638 nit: six guards with no test — a mutation sweep found nine; three were redundant with the check beside them and are gone, the rest are pinned (below)
#638 nits +p untested; fan_in crashed on Named; _operands repeated children; the otherwise test at _arms always true; UNREACHABLE named a line that runs each fixed

Not changed: Mask.names_read (#631 nit) is moot at #638, where both sides of a comparison are Expression; _check_expression's ceiling=None path (#632 nit) is live at #638, where _named passes it.

Why

The review found them, and confirmed each by loading the same model on both branches. I reproduced every one at #638's head before changing anything.

Method, gate output, alternatives

The rebase

#628 gained one commit with the three fixes and their five tests, which fail on its first commit. #629, #631, #632, #638 and this branch were rebased onto it in that order, one lease each. Two conflicts: #632's commit and this one both append to tests/test_expansion.py, both kept; #638's fourth commit rewrote the two functions #628's fix touches, resolved with #638's version and the fix carried into it, so the fix holds at every rung. The suite ran at every rung.

The chain of named expressions

Measured before the change, load then lower then typeset, on a chain e{i} = e{i-1} + 1 read by one constraint:

Declared #632 head #638 head here
deepest last, n=100 ok ok ok
deepest first, n=100 RecursionError ok ok
n=200 ok / RecursionError ok / RecursionError ok
n=250 RecursionError RecursionError refused at e150, "nests 301 deep … past the 300 levels"

So the base branch loads 200 only when the deepest entry is declared last. The worklist makes loading order-free; what remains is every later pass recursing over the resolved tree. MAX_RESOLVED_DEPTH = 3 * MAX_DEPTH is where the passes were measured to survive: with the cap off, a chain of 200 survives every pass and 250 does not. docs/reference/language/expressions.md states the limit. The 80-entry chain test_a_call_expands_to_core_ast pins still loads.

Error class

The rule already on main: a dim rule the resolver needs to build a node (a where side's dims, a pullback's landing) is collected with the resolver's faults as a SchemaError. #638 added the bare sum to that set. tests/test_dimensions.py now pins the class per case, and tests/fixtures.py's helpers raise SchemaError as to_spec does.

Guards

A mutation sweep replaced each if of _call, _built, _bare_sum, _edge_fits, _amount, _edge, _dim_ref, _relation_ref, _known_roles, _partition, build, cycle, curve_frame, _piecewise_references and _domain_decides_nothing with if False: in turn and ran the suite: 78 guards, 69 red on the first run. Of the nine green:

  • three were redundant, and are gone: a formal amount is caught under its sign and again where a bare name resolves to nothing; a role that named no column is caught where the roles are counted;
  • over=1, by='lk' and over=1 as a column had no test: test_a_kwarg_that_names_nothing_is_refused;
  • the three frame refusals of curve_frame, and the values-carries-over, points-undeclared and points-carries-over rules of _piecewise_references, had none: seven cases on test_a_malformed_block_is_refused.

The sweep re-run on the touched functions is all red.

Gates

  • ruff check, ruff format --check: green. pyrefly check: 0 errors.
  • pytest -q -n auto: 1598 passed, 5 skipped (the typst compiles), the walk line census included.
  • python -m tools.schema and python -m tests.typesetting.golden: no diff.
  • prettier --check on the two changed pages: green. The Read the Docs build passed.
  • docs-build did not run here: the session proxy refuses docs.python.org. compile-tex did not run: no tectonic here.

Not done

The bodies of #626, #628, #632 and #638 carried the review's claim findings; each is edited to say what is true now.

🤖 Generated with Claude Code

https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F

@read-the-docs-community

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

Copy link
Copy Markdown

… a formal is refused, and a fault in an entry hides no other

The review of #626 through #638, addressed on top of #638.

- A template's by= is checked before its formal columns send the call back,
  so sum(x, by=nope, over=a, into=b) is refused again; the over= and by=
  refusals say "or a formal of this macro" again; a formal along= beside a
  by= is pinned as building nothing.
- Validation no longer raises after the macros and the expressions: entries,
  so a fault there hides no constraint's fault. A use of a refused entry says
  it does not load, and the refusal is listed with it.
- A cycle closed through a macro names the macro in its chain, and one closed
  through a case's when is pinned.
- Named expressions are resolved from a worklist in dependency order, so a
  chain of any length costs no stack, and the resolved tree is held to
  MAX_RESOLVED_DEPTH, three times what one text may nest, with a refusal that
  names the depth rather than the parser's message about a tree that was not
  deep.
- A refused call builds nothing for the call around it, so sum(sum(p, into=g))
  reports one fault.
- A named offset or window is held to dtype: int where it is read, so
  offset=-tag says the dtype before the sign.
- A piecewise block is refused on the link the file wrote for a str or bool
  breakpoint parameter and for an lp x-link with no variable; an undeclared
  over: is one line.
- The exclusivity rewrite quotes a literal as the file wrote it.
- fan_in reads through a Named; dimensions uses program.children; the walk
  prints "otherwise" for the last region without testing it; the census names
  no line that runs.
- Tests pin the error class per rule, the unused template and entry refusals,
  +p as a named amount, and a link through a refused entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F
…at measured it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F
…e test that fails without it

A mutation sweep over the guards #638 moved and added left nine green.
Two were redundant with the check beside them and are gone: a formal
amount is caught under its sign, and a role that named no column is
caught where the roles are counted. The other seven are pinned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F
@FBumann
FBumann force-pushed the claude/peaceful-tesla-eumvcy branch from ab87f55 to ce399f8 Compare September 23, 2026 11:27
@FBumann
FBumann added this pull request to stack #644 September 23, 2026 11:32
@FBumann
FBumann merged commit 1e010ca into main Sep 23, 2026
6 checks passed
FBumann pushed a commit that referenced this pull request Sep 23, 2026
Merged against the tree this branch was written on, the head of #638 before
its rebase, so the conflicts were the four places #643 and the join touch the
same lines: fan_in reads through a Named and then asks a Sum, the relation
read builds a join, and the dim tests carry the error class and the join's
wording.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F
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