Repository navigation
refactor: no signature says Any, and a symbol table section that is not a mapping is refused - #572
Merged
Merged
Conversation
`explicit-any` is on for `src/math_spec`. The eleven files that still write `Any` name themselves in a sub-config list, and the rule is on everywhere else, so a file leaves that list and cannot come back. One line that must say `Any` says so with `# pyrefly: ignore[explicit-any]` and a reason; `unused-ignore` is already an error, so the pragma fails the gate the day the line stops needing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nY2qq2hQzyMFApWuD5zFW
…ot a mapping is refused `explicit-any` is an error for `src/math_spec` with nothing exempt, so the eleven-file list the gate landed with is gone and `src/` says `Any` nowhere. The 73 sites in the way are taken from #567, which did the work on top of the parser stack: the public doors take `Mapping[str, object]`, a raw value is `object` until pydantic has read it, the grammars hand back the node type their walk names, and the exclusivity proof compares a cell with a literal of its own kind. `_where_parser` keeps main's grammar and takes only the fold's return type. Two sites that PR leaves are narrowed rather than excused, so no pragma is needed: `ValidationInfo` is `Protocol[ContextT]` and takes its argument, and `JsonSchemaValue` is pydantic's `dict[str, Any]` where those signatures mean `dict[str, object]`. One refusal comes with the rewrite: a symbol table whose `dimensions:` or `names:` is a list or a string raised `AttributeError`, and now raises `SchemaError` naming the rewrite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nY2qq2hQzyMFApWuD5zFW
FBumann
pushed a commit
that referenced
this pull request
Sep 19, 2026
…ather than Any `explicit-any` is an error on `src/math_spec` since #572, and that PR took only the fold's return type from #567, leaving `_nested` on this branch with a signature whose `Any` no longer even imports. `_ParsedWhere` is the union the measurement walks, which is #567's own answer for this file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V2SFmoxEF3SnaPKp7HbZTk
FBumann
pushed a commit
that referenced
this pull request
Sep 19, 2026
Two sites conflicted in model.py, and main's narrowing wins both: #572 took `ValidationInfo[object]` and `dict[str, object]` in place of this branch's unparameterised `ValidationInfo` and pydantic's `JsonSchemaValue`, which is itself an alias for `dict[str, Any]`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V2SFmoxEF3SnaPKp7HbZTk
FBumann
pushed a commit
that referenced
this pull request
Sep 19, 2026
#567 is closed and this branch retargets onto #566. The typing work it carried in its history is on main as #572, so model.py takes main's narrowing: `ValidationInfo[object]` and `dict[str, object]` rather than this branch's unparameterised `ValidationInfo` and pydantic's `JsonSchemaValue`. The two piecewise conflicts are this branch's own feature against the code it renamed: the breakpoint dim is `along`, and a weight's where is the block's where and its mask together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V2SFmoxEF3SnaPKp7HbZTk
FBumann
added a commit
that referenced
this pull request
Sep 20, 2026
…ession grammar's arithmetic (#565) * chore(language): a where comparison is one grammar rule over the expression grammar's arithmetic The where grammar's three comparison rules — a name against a literal, two relation columns, and position() against an integer — are one rule, side <op> side, where a side is the expression grammar's ARITHMETIC. What a side is, resolution decides with the schema in hand, and it hands back the same typed nodes with the same messages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * test: the parser test names the validation class that holds the refusals it points at Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * chore(parser): the where depth measurement names the nodes it walks rather than Any `explicit-any` is an error on `src/math_spec` since #572, and that PR took only the fold's return type from #567, leaving `_nested` on this branch with a signature whose `Any` no longer even imports. `_ParsedWhere` is the union the measurement walks, which is #567's own answer for this file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V2SFmoxEF3SnaPKp7HbZTk --------- Co-authored-by: Claude <noreply@anthropic.com>
FBumann
added a commit
that referenced
this pull request
Sep 22, 2026
* chore(language): a where comparison is one grammar rule over the expression grammar's arithmetic The where grammar's three comparison rules — a name against a literal, two relation columns, and position() against an integer — are one rule, side <op> side, where a side is the expression grammar's ARITHMETIC. What a side is, resolution decides with the schema in hand, and it hands back the same typed nodes with the same messages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * test: the parser test names the validation class that holds the refusals it points at Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * chore(parser): the where depth measurement names the nodes it walks rather than Any `explicit-any` is an error on `src/math_spec` since #572, and that PR took only the fold's return type from #567, leaving `_nested` on this branch with a signature whose `Any` no longer even imports. `_ParsedWhere` is the union the measurement walks, which is #567's own answer for this file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V2SFmoxEF3SnaPKp7HbZTk * feat(language): a where may compare arithmetic over parameters Either side of a where comparison may be an expression over parameters: arithmetic, a reduction, a pullback, a translation with its edge, a macro, a named expression. A side is expanded, typed, degree-checked and dim-checked as an expression is, and refused where it names a variable or a dual. Two parameters compare the same way. The resolved tree holds ArithmeticComparisonNode over the core syntax tree, and lowering rebuilds every mask with ExpressionComparisonNode over program expressions. A case when: comparing expressions is refused as undecidable before the data arrives. expressions.md sentence lengths: n 68, avg 14.0, median 13, over 25 words 3 (base: n 56, avg 13.0, median 13, over 25 words 2). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * refactor(language): a comparison of expressions answers what it reads once, on the program's form The resolved form of the comparison had a second walk collecting the parameters and relations its sides read, and nothing asks a resolved mask that question: lowering rebuilds every mask before one reaches a consumer. The arm is the assertion the typesetter already carries for the lowered node. The one-line mask wrapper in lowering is inlined, and the two validation classes for what a where side may be are one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * refactor(language): a namespace is built from its schema and carries it, so nothing passes both Namespace(schema) reads every declaration at construction; the eight argument constructor and the classmethod that was its only caller are gone. expression_of, _check_expression and _named took the schema and a namespace built from that same schema; they take the namespace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * refactor(language): the single-text doors live with the tests that are their only callers expression_of and where_of had no caller in the package, and the two assertions that named expression_of as the door now name resolve_expression, which is the one the package walks through. The plain form of a where comparison is read by one method: a name that is a value against another is arithmetic, which used to be a second question asked after the first. The "prefix a context once" rule has one home in errors.py. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEeM2YoAk4Xr2uWB5qwMsH * refactor(program): the branch's nodes follow the naming rule main now states #585 renamed the program's where vocabulary and expression nodes on main. This branch was written against the old names, and its own two comparison nodes arrived with the suffix the rule reserves for the core AST. Applying the same rename here first is what lets the base merge that follows agree line for line rather than conflict on every one of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPqtQuarRGDmX2WrNVuKDP * docs(reading): the predicate union says which member a program never carries The `Predicate` union said it was what a lowered mask's `root` is built of. That is false here: it also holds `ArithmeticComparison`, which lowering rewrites into an `ExpressionComparison`, so a consumer walking a program meets every other member and never that one. These lines were #580's, which is where they were written. They are only true where the two nodes exist, and this is the branch that adds them. docs/reference/reading.md: the two sentences added are 11 and 9 words. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPqtQuarRGDmX2WrNVuKDP * docs(reading): a comparison against a literal is not an expression comparison A consumer walking a lowered mask gets ParameterComparison for `p_max > 5` and ExpressionComparison for `1 * p_max > 5`, though both mask the same coordinates. The page named only the second form. Gates: pytest tests/test_docs.py tests/test_reading_page.py (34 passed), the full suite (1383 passed, 1 skipped), mkdocs build --strict (clean, with the docs.python.org inventory dropped for the run, which this environment's proxy refuses with a 403), prettier --check (clean). Not run: typos, reuse lint, taplo, zizmor, compile-tex, for want of the tools. docs/reference/reading.md, by the measurement in the docs-writing skill: n 55, avg 14.6, median 13, over25 7. Every long sentence predates this change; the three added are 10, 20 and 9 words. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJfEUsCDtwXuXV8CANCHuR * fix(language): a comparison with the literal first is refused as the order it is (#594) * fix(language): a where comparing two numbers, and a lone case comparing expressions, are refused at load A comparison with a number on each side is decided before any data arrives, and reached the program as an ExpressionComparison over two constants. A cases: block of one case escaped the undecidable refusal, which only looked at pairs. names_read on a mask over a cased entry dropped the data its regions are decided by. The relation refusal still said every other comparison tests a name against a literal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019tCWoetBmjF1LpbTbjQY29 --------- Co-authored-by: Claude <noreply@anthropic.com>
FBumann
pushed a commit
that referenced
this pull request
Sep 22, 2026
#572 turned `explicit-any` on, and `canonical.py` was written before it. Plain data is `object` here as it is in `model.py`, the one string a cast names is the expression a link writes first, and the piecewise section is narrowed rather than indexed blind. The page says a predicate in the `where` grammar is left as written, rather than a `where` string: an assumption's `holds:` is the same grammar and the form does not touch it either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LCUHhoVyReQaBh2pj8CuGd
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.
Note
The following content was generated by AI.
explicit-any = "error"onsrc/math_spec, with nothing exempt.src/saysAnynowhere, and a signature that writes one fails the type check. The rewrite is #567's, ported onto main; the two sites it leaves are narrowed rather than excused.What this changes
explicit-any = "error"in[tool.pyrefly.errors], and nosub-configlist. It is the oneAnythe table did not already reach: an unannotated parameter isimplicit-any-parameter, which the strict preset refuses, and an unannotated return is inferred rather than widened. What was left is theAnya signature writes down — a parameter, a return, a variable, a type alias, or inside a generic such asdict[str, Any].Mapping[str, object]; a raw value isobjectuntil pydantic has read it; the grammars hand back the node type their walk names; the exclusivity proof compares a cell with a literal of its own kind._where_parser.pykeeps main's grammar and takes only the fold's return type, since the rest of that file's change belongs to the arithmetic-where feature.ValidationInfoisProtocol[ContextT]and was left unparameterised, so its argument wasAny;ValidationInfo[object]says what the validator accepts, which never reads.context.JsonSchemaValueis pydantic's alias fordict[str, Any], where those three signatures meandict[str, object]. Neither wants a pragma.dimensions:ornames:is a list or a string raisedAttributeError. It now raisesSchemaError:dimensions: must be a mapping of names to entries, got list.Why
Anycame back into a signature because nothing refused it. Now the type checker does, everywhere, with no file exempt and no pragma outstanding.Gates
pixi.shis refused by this environment's egress proxy, so the gates ran from auvenvironment on Python 3.12.3 with pydantic 2.13.5 against the lock's 2.13.4, on the pinned pyrefly 1.2.0 and ruff 0.16.1. That is a departure from the "everything runs in a pixi environment" default.pyrefly checkpytest -n autoruff check,ruff formattaplo format,typosmkdocs build --strictdocs.python.orginventory dropped for the run, which the proxy refuses with a 403compile-texgrep -rn '\bAny\b' src/The gate was checked in both directions, not only for staying green. Given
def widened(x: Any) -> Nonein any file of the package,pyrefly checkfails withExplicit Any is not allowed [explicit-any]; the same line with# pyrefly: ignore[explicit-any]passes, and the suppression count rises by one.What the narrowing bought, and what it cost — five guards deleted in turn
Replacing
Anywith the type a line actually holds turns some checks the annotation used to make into checks the code has to make. Each one deleted, suite re-run, tree restored bygit checkout --:_numbers' "a label or a date reached a magnitude" assert_compare's "the two are of different kinds"AssertionError_section's "is a mapping in a validated model" assert_mask_dims' nominated branch, left reading the mask's dims_section'ssetdefault, left indexing the raw dictThe last two are load-bearing and covered. The first three are guards no test reaches, so this PR owes each one a purpose-built probe before merge, per AGENTS. None of them wants
Anyback: each states an invariant the dtype rules or a validated model already guarantee, and the alternative is notAnybut an uncheckedcast. The cast count did not rise —src/has one fewer than the base,exclusivity.pyhaving lostcast('set[int]', values).What this means for #567
This carries #567's
src/diff, so that PR is left with the parser work and nothing else to say about typing. The port was a three-way apply of5e37736..d080058limited tosrc/andtests/, and only_where_parser.pyconflicted — that file's change is mostly the arithmetic-where feature's_nested, which does not exist on main, so main keeps its grammar and takes the_folderreturn type alone. Everything else applied clean, which is why the counts above match what that PR measured.Rebasing the stack on this should drop those twelve files from it. What is genuinely #567's and is not here: nothing — its
tests/typesetting/test_symbols.pycases came along with the refusal.Departed from, and deliberately not done
src/diff after it was opened, so it is no longer the one-lineci:change its first commit was. It is one topic —Anyis refused andAnyis gone — but it is two commits rather than one, and the first commit's message describes a backlog list that the second deletes. A squash merge reads correctly; the branch history does not, and I did not force-push to fix that.refactor:. It is in the title for that reason, rather than left for the body, because it is what a changelog reader can see. The alternative was to keepraw.get('names') or {}and itsAny, which is the thing this PR exists to remove.implicit-anyfamily andunannotated-*are already at zero, so promoting them would be a no-op;unknown-argument-typereports 44 and is a separate argument._yaml.pykeeps its oneunknown-variable-typepragma onconstruct_document, whose stub returnsAny. That is the third-party boundary the per-line opt-out exists for, and it is the only one left in the package.🤖 Generated with Claude Code
https://claude.ai/code/session_015nY2qq2hQzyMFApWuD5zFW