Skip to content

fix: make stripProblemMarkers lexer-aware rather than line-oriented - #547

Merged
kim-em merged 7 commits into
mainfrom
issue-544
Aug 21, 2026
Merged

fix: make stripProblemMarkers lexer-aware rather than line-oriented#547
kim-em merged 7 commits into
mainfrom
issue-544

Conversation

@kim-em

@kim-em kim-em commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

This PR replaces the line-oriented marker stripper in EvalTools/Generate.lean with a scanner that walks the whole source, tracking line and block comments, string, raw-string and character literals, French-quoted identifiers, syntax quotations and bracket depth. The marker is now recognised in every position Lean accepts it: sharing a line with its declaration or with a prefix command such as include x in or set_option … in, spanning several lines, carrying trivia as in @[/- why -/ eval_problem], spelled «eval_problem», or carrying siblings whose arguments contain a ] or a ,. Surviving siblings are spliced out of the source rather than reprinted, so a comment among them keeps the line break that terminates it. Imports are parsed the same way, so one broken across lines or carrying a comment is recognised, while a doc comment trailing an import stays with the declaration it documents.

Lexing Lean without its parser cannot be exact: whether a { in a string opens an interpolation hole is settled by the grammar, and the grammar is extensible. Three things keep that inexactness from editing somebody's string. A marker is stripped only in command position and only in front of a declaration; and no span the two readings of a string literal disagree about is stripped from at all. The first two are tests on text, which a string can reproduce; the third is not a test on the marker but on how the scanner came to be looking at it, and reaching marker-shaped text inside a literal takes exactly the disagreement it declines to resolve. What is left over is a marker left in place, which the generated workspace reports as unknown attribute [eval_problem] when it is built.

Source.opaqueLexemeEnd? is the one place that knows how to step over a comment or a literal; Source.findTheoremBodyMarker now uses it in place of its own copy.

Nothing in the catalog exercises any of this today, so a differential check against the old implementation over every .lean file in LeanEval/, EvalTools/, tests/, generated/ and templates/ — in both shapes the generator passes in, with and without local-import stripping — reports no change in output.

Closes #544.

🤖 Prepared with Claude Code

@kim-em

kim-em commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Reviewed by OpenAI Codex over six rounds; every reproducible finding is fixed and tested. The ones worth recording here, because they changed the design rather than the code:

  • No lookback from a quote can decide whether a { opens an interpolation hole — Lean settles that in the parser, and the grammar is extensible. So the pass stopped depending on one being right: where the two readings of a literal disagree about where it ends, nothing in that span is stripped. That test is symmetric in the two readings, including when one of them never closes.
  • No test on the text after a marker can be sound either, since a string can spell out @[eval_problem] theorem in full. Command position and the declaration that follows are still checked, but what makes marker-shaped text in a literal unreachable is the disputed-span test, which is about how the scan came to be looking at it.
  • A doc comment after an import belongs to the declaration below it, not the import.

Measurements, since the corpus exercises none of this: a differential against the pre-#547 implementation over all 2015 .lean files in LeanEval/, EvalTools/, tests/, generated/ and templates/, in both shapes the generator passes in, reports no change in output; and appending a marker to each of Mathlib's 8312 files strips all 8312.

What is left needs a syntax declaration to reach and cannot be fixed by lexing, so it is #551 rather than a change here.

kim-em and others added 7 commits August 21, 2026 05:54
Walk the whole source tracking comments, string, raw-string and character
literals, French-quoted identifiers and bracket depth, instead of inspecting one
physical line at a time.

Closes #544.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwavhp4R8ptW6rP8hXERHE
Read a brace as an interpolation hole only in a string Lean parses as
interpolated, splice surviving attributes out of the source instead of
reprinting them, treat a syntax quotation as opaque, and recognise trivia inside
an attribute instance and after an import.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwavhp4R8ptW6rP8hXERHE
Strip `@[eval_problem]` only where Lean would apply it, in front of a
declaration. That bounds every way this file's lexer can be wrong — an
interpolation it did not expect, a syntax extension it cannot know about — to a
marker left in place, which the generated workspace reports as `unknown
attribute [eval_problem]`, rather than an unannounced edit to somebody's source.

Read a string plainly unless the token in front of it says otherwise, so an
ordinary `"{"` no longer runs the scan on to the next `}`; recognise the marker
spelled `«eval_problem»`; read a quoted identifier inside an interpolation hole;
read an import broken across lines or carrying a comment; and coalesce the spans
removing two markers from one attribute list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwavhp4R8ptW6rP8hXERHE
…port

An attribute is only an attribute at the head of a command: at the start of its
line, after the `in` of a prefix command, or after another attribute block.
Together with the declaration that must follow, that is what marker-shaped text
in a misread string has to reproduce to be stripped by mistake — spelling out
`@[eval_problem] theorem` inside a string no longer suffices.

Stop treating a doc comment after an import as the import's own: Lean gives it
to the declaration that follows, so the import goes and the comment stays. Let a
declaration carry any number of attribute blocks, and add the declaration
keywords `coinductive`, `register_option` and `irreducible_def`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwavhp4R8ptW6rP8hXERHE
Where the brace-blind and interpolated readings of a string literal disagree
about its extent, the scanner does not know whether what follows is code or the
contents of somebody's string, and now declines to strip there rather than
resolve it by guessing.

That is the precondition of every construction that got marker-shaped text
edited out of a literal: reaching it takes a hole read as plain text. The
corroborating tests on command position and on the declaration that follows are
tests on text, so a string can spell out whatever they look for — a line break,
an `in`, a sibling attribute, `theorem`. This one is a test on how the scanner
came to be looking at the marker instead.

An attribute block now leaves command position open only where it was itself at
the head of a command.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwavhp4R8ptW6rP8hXERHE
A string opened inside a hole carries holes of its own only if it is itself
interpolated. In `s!"{f "a{b"}"` the inner literal's brace stands for itself, and
reading it as a hole leaves the outer string looking unterminated, so the scan
resumes partway through it — which is how marker-shaped text there was still
being edited out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwavhp4R8ptW6rP8hXERHE
A reading that never closes disagrees about where the literal ends by more than
any other, so it says the literal runs to the end of the input and the span is
disputed accordingly. Taking it for agreement was the one place the test was not
symmetric in the two readings, and it was reachable with built-in syntax: a
`dbg_trace` the lookback cannot classify, holding a raw string, left the
interpolated reading unbalanced and the message exposed as code.

It costs nothing to say so. Appending a marker to each of Mathlib's 8312 files
strips all 8312, before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwavhp4R8ptW6rP8hXERHE
@kim-em
kim-em merged commit 3c17142 into main Aug 21, 2026
13 checks passed
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.

Make stripProblemMarkers lexer-aware rather than line-oriented

1 participant