interpolated strings: align f-string and t-string diagnostics with CPython - #8601
interpolated strings: align f-string and t-string diagnostics with CPython#8601name-of-okja wants to merge 2 commits into
Conversation
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [ ] test: cpython/Lib/test/test_str.py (TODO: 5) dependencies: dependent tests: (no tests depend on str) [x] test: cpython/Lib/test/test_tstring.py dependencies: dependent tests: (no tests depend on tstring) Legend:
|
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueNo actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughThe compiler now produces CPython-aligned diagnostics for f-strings and t-strings, including nested replacement fields, literal mixing, comments, brackets, conversions, and unterminated fields. VM errors now use qualified type names for string and template concatenation. ChangesInterpolated-string diagnostics
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to The PR updates interpolated-string diagnostics to match CPython behavior and includes focused verification; no actionable merge-blocking risk remains beyond normal checks and review. Sequence Diagram(s)sequenceDiagram
participant SourceParser
participant CompilerDiagnostics
participant ReplacementFieldScanner
participant VMErrorAnalyzer
SourceParser->>CompilerDiagnostics: report f-string or t-string error
CompilerDiagnostics->>ReplacementFieldScanner: validate replacement fields
ReplacementFieldScanner-->>CompilerDiagnostics: return diagnostic
CompilerDiagnostics-->>VMErrorAnalyzer: provide compile error
VMErrorAnalyzer-->>SourceParser: format final error message
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Linked Issues checkExplanation The changes address the linked issue objectives [ Full details: Out of Scope Changes checkExplanation The changes remain within scope [ Full details: Docstring CoverageExplanation Docstring coverage is 51.35% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 37 functions across 4 files. (1 skipped: 1 unsupported.) ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
61cd516 to
cb40067
Compare
There was a problem hiding this comment.
Pull request overview
This PR improves RustPython’s CPython-compatibility for interpolated string (f-string and PEP 750 t-string) diagnostics by refining the post-parse-failure rescan logic and aligning several error messages and caret targets with CPython behavior. It also updates related stdlib tests by removing expectedFailure markers that should now pass.
Changes:
- Refactors the compiler’s interpolated-string diagnostic rescan to better model CPython replacement-field parsing (separators, comments, operators, nested fields) and avoid misattributing later syntax errors to valid literals.
- Aligns SyntaxError/TypeError wording with CPython for t-strings (unterminated literal messages; mixed-literal concatenation precedence; template/str concatenation operand type names).
- Removes
@unittest.expectedFailuremarkers inLib/test/test_tstring.pyandLib/test/test_fstring.pythat should now be passing.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| Lib/test/test_tstring.py | Removes expectedFailure markers for t-string behaviors now aligned with CPython. |
| Lib/test/test_fstring.py | Removes expectedFailure markers for f-string syntax/diagnostic cases now expected to pass. |
| crates/vm/src/vm/vm_new.rs | Adjusts SyntaxError message mapping for t-string unterminated literals and interpolated-string error formatting. |
| crates/vm/src/builtins/template.rs | Updates Template concatenation TypeError text to use module-qualified type names (slot_name). |
| crates/vm/src/builtins/str.rs | Updates str concatenation TypeError operand naming to use slot_name. |
| crates/compiler/src/lib.rs | Extends/adjusts CPython-style diagnostic override logic for interpolated strings, including new tests and performance fix for deeply nested format specs. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // The expression started fine but ran into a character that cannot continue it. CPython | ||
| // points at that character and lists the separators it wanted instead. | ||
| if let Some(stray) = replacement_expression_stray_character(bytes, expr_start, expr_end) { | ||
| return Some(( | ||
| format!("{prefix}: expecting '=', or '!', or ':', or '}}'"), | ||
| stray, | ||
| stray + 1, | ||
| )); | ||
| } | ||
|
|
||
| if let Some(operator) = dangling_operator(bytes, expr_start, expr_end) { | ||
| return Some(( | ||
| format!("{prefix}: expecting '=', or '!', or ':', or '}}'"), | ||
| operator, | ||
| operator + 1, | ||
| )); | ||
| } |
There was a problem hiding this comment.
Good catch — the divergence is real, and it was still present at the time of the review. Verified against CPython 3.14:
f'{a and' CPython: f-string: expecting '}' ← this branch: expecting '=', or '!', or ':', or '}'
f'{a+' f'{a==' f'{a.b.' f'{a~' f'{a is not' t'{a and' (same)
The suggested fix — guarding both checks on separator.is_some() — would have regressed a second set, though. CPython gives the separator message for a stray character even with no closing brace:
f'{a;' f'{a$' f'{a?' f'{a`' → f-string: expecting '=', or '!', or ':', or '}'
The line runs between the two, and it is drawn in the tokenizer rather than the grammar. A stray character is a finished token, so the parser rejects it on the lookahead in annotated_rhs !('='|'!'|':'|'}') without asking for another token. A dangling operator makes it ask for one more, and producing that token runs into the literal's closing quote, where Parser/lexer/lexer.c:1181 answers first:
if (INSIDE_FSTRING(tok)) {
/* ...this must be a missing '}' token so raise the proper error */
if (the_current_tok->quote == quote && the_current_tok->quote_size == quote_size) {
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok,
"%c-string: expecting '}'", TOK_GET_STRING_PREFIX(tok)));
}
}So the fix is to move only the dangling_operator check below the separator guard and leave replacement_expression_stray_character where it is — no new condition, just the reorder. Done in eb42e6e, with a regression test covering both sides of the boundary (11 cases) and the mechanism recorded in the commit message. All 16 inputs above now match CPython.
| interpolated_string_prefix(bytes, quote_start)?; | ||
| let mut index = content_start; | ||
| let mut open = None; | ||
| while index < content_end { | ||
| match bytes[index] { | ||
| b'{' if bytes.get(index + 1) == Some(&b'{') => index += 2, | ||
| b'}' if bytes.get(index + 1) == Some(&b'}') => index += 2, | ||
| b'{' => { | ||
| open = Some(index); | ||
| index += 1; | ||
| } | ||
| b'}' => { | ||
| open = None; | ||
| index += 1; | ||
| } | ||
| _ => index += 1, | ||
| } | ||
| } | ||
| let open = open?; | ||
| Some(("'{' was never closed".to_owned(), open, open + 1)) | ||
| } |
There was a problem hiding this comment.
Correct diagnosis, and this had already been rewritten by the time the review landed — the single open slot is now a full delimiter stack, for exactly the reason given here:
// One entry per unclosed delimiter: its position, its opening character, and — for a brace —
// whether that field has reached its format spec.
let mut open: Vec<(usize, u8, bool)> = Vec::new();The bool mirrors the tokenizer's in_format_spec: only the field's own : sets it, so a : in a slice, a dict display or a lambda no longer ends the expression, and running out of input inside a format spec is an ordinary unterminated literal rather than an unclosed brace. Brackets and quotes are tracked too, since CPython names the innermost unclosed delimiter.
Verified against CPython 3.14 — the display cases this comment names among them:
f'{ {1:2} '{' was never closed
f'{ {1,2} '{' was never closed
f'{d[1:2] '{' was never closed
f'{(lambda x: x) '{' was never closed
f'{a[ '[' was never closed
f'{(a '(' was never closed
f'{a:>5 unterminated f-string literal (detected at line 1)
18 inputs in that group, all matching, pinned by a unit test.
9640b75 to
183639c
Compare
…dd__
`str.__add__` and `Template.__add__` built their TypeError with
`PyType::name()`, which drops the module, where CPython formats `tp_name`:
>>> t"a" + "b"
TypeError: can only concatenate Template (not 'str') to Template
CPython 3.14 reports the qualified name and quotes the operand with double
quotes, as `Objects/unicodeobject.c` and `Objects/templateobject.c` do:
TypeError: can only concatenate string.templatelib.Template
(not "str") to string.templatelib.Template
`Template.__add__` also spelled its own name literally rather than taking it
from `PyClassDef::TP_NAME`, so the two halves of the message could drift apart.
Known gap: `slot_name()` still does not qualify a class defined in a module, so
`"a" + collections.OrderedDict()` names `OrderedDict` where CPython names
`collections.OrderedDict`. This affects both methods.
Unblocks test_template_concatenation in test_tstring.
Assisted-by: Claude Code:claude-opus-5
183639c to
062c3ad
Compare
…ython
RustPython re-derives CPython's syntax diagnostics by scanning the source rather
than by translating ruff's parse errors, and the scanner shared by f-strings and
t-strings misread several replacement field states:
- A field that ran off the end of the literal (`f'{'`) was reported as an
expression that failed to start, where CPython asks for the closing brace.
- A field whose expression cannot start (`f'{;'`) and one whose expression runs
into a stray character (`f'{a;'`) both fell through to the missing brace
message instead of CPython's two distinct ones.
- A conversion or format spec following `=` was never validated, and a format
spec following a valid conversion was skipped entirely.
- Format specs were scanned with the `{{` escape rule that only applies to
literal text, so nested replacement fields inside them went unchecked.
- Brackets opened inside a field were never matched, so `f'{a[4)}'` and
`f'{3)+(4}'` fell back to a bare "invalid syntax".
- Comments inside a field were not recognised, so a `#` that swallows the
closing brace was reported as an unterminated literal, and a comment in a
multi-line field hid the expression behind it.
- Unterminated literals were named "string" whatever their prefix, and an
interpolated literal left with a replacement field open reported the missing
quote rather than the brace.
A field is only "never closed" while the tokenizer is still reading its
expression. Past the field's own `:` it is emitting literal text again, so
`f'{a:>5` runs out of input as an ordinary unterminated literal while `f'{a`
reports the brace. The field's own `:` is also not the one in a slice, a display
or a lambda, and an unclosed bracket inside the expression is what CPython names
rather than the field around it, so this walk tracks the whole delimiter stack.
Two messages were worded from the wrong branch of CPython's tokenizer. The hint
"perhaps you escaped the end quote?" is raised only where lexer.c handles a
literal without an interpolation prefix; its `%c-string` branch has just the
triple-quoted and plain forms, so pairing the hint with a prefix produced
`unterminated f-string literal (...); perhaps you escaped the end quote?`, which
CPython never emits. And a bracket mismatch inside a field dropped the
` on line %d` clause that lexer.c adds whenever `parenlinenostack[level]` differs
from the current `lineno`; the general bracket scanner in this file already
computed that suffix, so `f"""{a[\n4)}"""` now names line 1 as CPython does.
The rules these now follow are the ones CPython spells out in
Grammar/python.gram (`invalid_fstring_replacement_field` and its t-string twin)
and in Parser/lexer/lexer.c, which likewise parameterises the literal's prefix
rather than duplicating the messages.
Only a field's expression is code. A format spec is text, so a `(` there opens
nothing and a `#` selects the alternate form instead of starting a comment;
inside the expression a `#` does start one. Scanning the whole field for either
therefore reported diagnostics against valid literals, and because these
scanners only run once the source has already failed to parse, that let a good
literal take the blame for an error further down the file. Expression-level
checks now stop at the field's top-level separator and skip comments, and the
literal is walked field by field rather than byte by byte so that a `{` inside a
comment no longer opens one. A pre-existing instance of the same bug in the
line-continuation check is fixed along with them.
Ruff reports a mixed literal concatenation only as a bytes/non-bytes mix, so
`t"x" b"y"` arrived as a bytes error where CPython names the t-string. CPython's
`invalid_string_tstring_concat` is an `invalid_` rule, reached only on the error
pass, and `strings` tries `(fstring|string)+` ahead of it: that alternative
consumes the concatenation's leading run of non-t-string literals, so a mix among
those raises from `_PyPegen_concatenate_strings` on the first pass and sets
`error_indicator`, which suppresses the t-string rule. The t-string message
therefore wins for `t"x" b"y"` and the bytes message keeps precedence for
`"a" b"b" t"c"`. Both are now settled here rather than left to whichever scanner
runs next, which had been answering the second case with an unrelated
"Is this intended to be part of the string?".
A leading doubled `=` or `!` was read as a marker with an empty expression before it,
so `f"{==a}"` reported `valid expression required before '='` where CPython has no
expression to report at all. The same lookahead now guards that branch, and a
doubled `=` or `!` counts as an expression that cannot start.
A top-level `=` or `!` was read as a debug or conversion marker without checking
whether it belonged to a longer operator, so `f"{a==b}"` and `f"{a!=b}"` were also
read as fields ending early. CPython tokenises `==`, `!=`, `<=` and the rest as
single tokens before it ever considers the debug marker, so the separator scan now
skips a `=` that follows one of `= ! < > + - * / % & | ^ @ :` or precedes another
`=`, and a `!` that precedes one.
An expression cannot end on an operator that still wants an operand, and CPython
points at that operator rather than at the brace. `f"{a==}"`, `f"{a and}"` and
`f"{a.b.}"` fell through to a plain `invalid syntax`; they now carry the same
message and column CPython gives, reusing the scan that already handled `;` and
`$` for exactly this shape. `is not` and `not in` are single operators, so the
first word is what gets pointed at, and `...` is consumed in whole triples so
that `f"{....}"` blames the fourth dot rather than the first. The region has to
be walked forwards: a comment's terminating newline is whitespace, so trimming
backwards from the end would step into the comment body and read a triple-quoted
field whose comment ends in `+` as an expression ending in `+`.
That check runs only once the field is known to close, which is where a stray
character and a dangling operator part ways. A stray character is a finished
token, so the parser rejects it on the lookahead in
`annotated_rhs !('='|'!'|':'|'}')` and `f"{a;"` gets the separator message even
with no closing brace. A dangling operator instead makes the parser ask for one
more token, and producing it runs into the literal's closing quote, where
lexer.c answers from its `INSIDE_FSTRING(tok)` branch with
`%c-string: expecting '}'` before any `invalid_` rule is reached. So `f"{a and"`
is a missing brace while `f"{a and}"` names the operator.
A leading `.` was read as a character that cannot start an expression, so the
Ellipsis literal `f"{...}"`, the float `f"{.5}"` and its signed form `f"{-.5}"`
were reported as broken whenever the file failed to parse somewhere else.
`pegen` joins the cpython spelling dictionary, for the `_PyPegen_*` names these
comments cite.
Known gaps, none covered by either test file. Needing the longest valid expression
prefix, which a character scanner cannot compute: a starred tuple (`f"{*a,}"`) and
a dict-unpacking display are read as unable to start an expression, and
`f"{a===b}"`, `f"{a b}"` and `f"{a,,}"` fall through to a bare `invalid syntax`.
The caret still differs from CPython's on `expecting a valid expression after '{'`,
`expecting '}'`, `expecting '}', or format specs` and `unterminated ... literal`,
and on the messages CPython points at a whole token for, such as
`f"{lambda x: x}"` and `f"{x! r}"`. A quote inside a format spec is still treated
as a string delimiter.
Two more are reachable only through a t-string concatenation that ruff does not
report as a bytes mix, so `mixed_tstring_literal_error` never runs: three or more
literals with no bytes literal among them (`t"a" t"b" "c"`, `"a" "b" t"c"`) answer
with `invalid syntax. Is this intended to be part of the string?`, and so does a
tokenizer-level nesting overflow where CPython has
`too many nested f-strings or t-strings`. Two-literal mixes are correct because
ruff's own error carries the wording.
Two pre-existing bugs outside this change are worth naming, since it touches
their neighbourhood. `unterminated triple-quoted ... literal` reports
`detected at line 2` for a one-line source where CPython reports line 1, for every
prefix including none, so it is in the shared line arithmetic rather than the
interpolated path. And `str.__add__`'s message is unreachable for an operand that
defines `__radd__`: `"a" + 1` falls through to the generic
`unsupported operand type(s)` from the binop dispatch, where CPython has
`can only concatenate str (not "int") to str`.
Unblocks test_syntax_errors and test_literal_concatenation in test_tstring, and
test_comments,
test_conversions, test_invalid_syntax_error_message, test_mismatched_braces,
test_mismatched_parens, test_parens_in_expressions and
test_syntax_error_after_debug in test_fstring.
Assisted-by: Claude Code:claude-opus-5
062c3ad to
eb42e6e
Compare
|
Both review comments are addressed; replies with the details are on the threads.
The branch is also split in two now: the runtime @copilot review |
One of checkbox below must be checked.
Summary
RustPython synthesizes CPython's syntax-error messages by re-scanning the source in
cpython_parse_diagnostic_overrideonce ruff's parse has already failed. The scannershared by f-strings and t-strings misread several replacement-field states, which is
what #8495 reports.
Fixing that surfaced a second and larger problem. Because these scanners run only
after a parse failure, a scanner that misreads a valid literal makes that literal
take the blame for an error elsewhere in the file. Several of the field checks treated
the whole field as code, so a format spec containing
(or a comment containing+was enough to move the reported error onto a perfectly good line.
Both are addressed against the rules CPython spells out in
Grammar/python.gram,Parser/lexer/lexer.candParser/pegen*. All CPython links below are permalinks to197fdd7.1. Replacement field states
invalid_fstring_replacement_fieldgives four distinct answers, and the order of the alternatives is the priority. The
t-string twin
is the same rule with the prefix swapped. "the field has no content" and "the expression
failed to start" were collapsed into one branch here; they are now separate, and
lexer.c:1181-1192is explicit that a field still open at end of input becomes
expecting '}'.f'{'expecting a valid expression after '{'expecting '}'f'{1=}{;'expecting '}'expecting a valid expression after '{'f'{1=}{1;'expecting '}'expecting '=', or '!', or ':', or '}'t'{x=!}'invalid conversion charactermissing conversion charactert'{x!s:'expecting `}`expecting '}', or format specsA conversion or format spec following
=was never validated, and a format specfollowing a valid conversion was skipped entirely; both now run the same checks they
would have run directly after the expression, as
invalid_fstring_conversion_characterbeing referenced after
'='?implies.2. Only the expression is code
fstring_format_specis a separate grammar rule from
annotated_rhs. So inside a format spec a(opensnothing and a
#selects the alternate form, while inside the expression a#starts acomment that runs to end of line. Scanning the whole field for either produced
diagnostics against valid literals:
The expression-level checks now stop at the field's top-level separator and skip
comments, and the literal is walked field by field rather than byte by byte so a
{inside a comment no longer opens one. A pre-existing instance of the same bug in the
line-continuation check is fixed with them.
The spec also has no
{{escape at that level, so nested fields inside a spec werenever checked at all. They are now, resuming past each nested field once checked —
recursing per
{without resuming wasO(2^depth)(a 110-byte source took 6.1s;there is a regression test at depth 200).
3. Brackets and comments inside a field
lexer.c:1342attributes a closer with nothing open to the literal (so it carries the prefix) while a
kind mismatch keeps the generic bracket wording (so it does not). Brackets opened in a
field were not matched at all before.
f'{a[4)}'invalid syntaxclosing parenthesis ')' does not match opening parenthesis '['f'{3)+(4}'f-string: expecting '}'f-string: unmatched ')'f'{1#}'unterminated string literal (detected at line 1)'{' was never closed4. The literal's kind belongs in the message
lexer.c:1500-1514parameterises the prefix with
%crather than duplicating the messages, and thisscanner shares the same structure — which is why the f-string cases move together with
the t-string ones here. Separating them would need an artificial branch.
t'unterminated string literal (detected at line 1)unterminated t-string literal (detected at line 1)t'''unterminated triple-quoted string literal …unterminated triple-quoted t-string literal …An interpolated literal that runs out of input with a field still open reports the brace
instead, per
pegen_errors.c:18— but only while the tokenizer is reading that field's expression. Past the field's own
:it is emitting FSTRING_MIDDLE again, so running out of input there is an ordinaryunterminated literal. And the field's own
:is not the one in a slice, a display or alambda, nor is the field the innermost unclosed delimiter when a bracket is open inside
its expression. So this walk carries the whole delimiter stack:
t'{,f'{a,f'{a!r,f'{a='{' was never closedunterminated string literal …f'{ {1:2},f'{d[1:2],f'{(lambda x: x)'{' was never closedunterminated f-string literal …f'{a:,f'{a:>5,f'{a!r:,f'{a}{b:unterminated f-string literal …'{' was never closedf'''{a:>5unterminated triple-quoted f-string literal …'{' was never closedf'{a['[' was never closed'{' was never closedf'{(a'(' was never closed'{' was never closedThe last four rows are cases an earlier revision of this branch got wrong;
mainanswersevery row above with
unterminated string literal.5. Mixed literal concatenation
Ruff reports a mixed concatenation only as a bytes/non-bytes mix, so
t"x" b"y"arrivedas a bytes error where CPython names the t-string.
invalid_string_tstring_concatis an
invalid_rule, reached only on the error pass(
pegen.c:964-974),and
stringstries
(fstring|string)+ahead of it. That alternative consumes the concatenation'sleading run of non-t-string literals, so a mix among those raises from
_PyPegen_concatenate_stringson the first pass and
suppresses
the t-string rule. So the precedence splits:
t"x" b"y"cannot mix t-string literals with string or bytes literalscannot mix bytes and nonbytes literalsb"x" t"y"cannot mix t-string literals with …cannot mix bytes and nonbytes literals"a" b"b" t"c"cannot mix bytes and nonbytes literalsinvalid syntax. Is this intended to be part of the string?f"x" b"y"cannot mix bytes and nonbytes literalsBoth messages are now decided in one place. The third row has to be emitted rather
than deferred to ruff's own error, because otherwise
invalid_string_expression_errorfurther down the chain claims the concatenation with an unrelated message.
All 14 cases from
test_tstring.test_literal_concatenationand both fromtest_fstring.test_compile_time_concat_errorsmatch.6. Type names in the operand message
CPython uses
tp_nameforstr,i.e. the module-qualified name;
PyType::name()strips the module,slot_name()doesnot. Builtin types have no dot in
TP_NAME, so this only changes types that declare amodule.
t"a" + "b"can only concatenate Template (not 'str') to Templatecan only concatenate string.templatelib.Template (not "str") to string.templatelib.Template"a" + t"b"… (not "Template") to str… (not "string.templatelib.Template") to str7. Operators the scanner had no tokenizer for
CPython consumes
==,!=,<=and the rest as single tokens atlexer.c:1280-1298,before the
=ever reaches thedebug-marker check.
The scanner has no such stage, so
f"{a==b}"— valid code — was read as a field endingearly. Token boundaries are now inferred from the surrounding characters; the character
set is exactly the operators that end in
=.An expression also cannot end on an operator that still wants an operand, and CPython
points at that operator rather than at the brace.
is not/not inare singleoperators so the first word is pointed at, and
...is consumed in whole triples.f"{a==b}"(valid)f-string: expecting '!', or ':', or '}'f'{==a}'valid expression required before '='expecting a valid expression after '{'f'{a==}'invalid syntax(col 7)expecting '=', or '!', or ':', or '}'(col 5)f'{a is not}'invalid syntax(col 9)f"{...}",f"{.5}",f"{-.5}"(valid)This region has to be walked forwards: a comment's terminating newline is whitespace, so
trimming backwards from the end steps into the comment body.
Verification
Ran on x86-64 Linux (WSL2, Ubuntu).
t-string reference behavior is taken from thevendored
Lib/test/*.pyand the pinned CPython source — note that a locally installedpython3.14may be a beta that predatesinvalid_string_tstring_concatentirely.Lib/test/test_tstring.pyexpectedFailuremarkers removedLib/test/test_fstring.pyLib/, 1728 files)cargo test -p rustpython-compilercargo clippy -p rustpython-compiler --all-targetstest_syntax,test_compile,test_exceptions,test_grammar,test_string,test_ast,test_tokenize,test_codeop,test_traceback,test_str,test_bytes,test_typesall passKnown gaps
All pre-existing, none covered by either test file, all noted in the commit message:
f"{*a,}") and a dict-unpacking display are still read as unable tostart an expression;
f"{a===b}",f"{a b}"andf"{a,,}"need the longest valid expression prefix, whicha character scanner cannot compute —
f"{a b}"is CPython's "forgot a comma" rule;49-case sweep of interpolated-string errors the messages agree 49/49 and the
offset/end_offsetpair differs on 42. It splits into four independent anchors:unterminated … literal(CPython points at column 1, the prefix letter; we point at thequote),
expecting '}'(CPython points past the last token, we point at the{),expecting a valid expression after '{'(CPython spans the offending token, we point atthe
{), and the operator messages (CPython spans the whole operator, ourend_offsetis one past the start).
assertRaisesRegexonly inspects the message, which is whyneither test file covers this. Left out to keep this reviewable;
Template.__add__names the other operand withtp_namewhere CPython uses%T(identical for static types, divergent for heap types) — RustPython has no
%Tequivalent outside the C-API shim, so this is left for a follow-up;
AI assistance disclosure
Per the AI policy: this patch was written with Claude Code (
claude-opus-5), alsorecorded as an
Assisted-bytrailer on the commit.Extent: Claude wrote the scanner changes, the added tests and the commit message, working
from the CPython sources linked above. I set the scope, reviewed the diff, and ran every
check in the Verification table locally on the platform above.
Summary by CodeRabbit