Skip to content

Commit aacd612

Browse files
authored
feat: forbid non-reserved identifiers via withForbidden (#14432)
This PR makes `withForbidden` stop a term at a non-reserved word used as an identifier, not just at a registered token. A parser like `withForbidden "invariant" termParser` now ends the term when it reaches a bare `invariant`, while `invariant` remains usable as an identifier everywhere else. `identFn` rejects an identifier whose text is in `forbiddenTks`, mirroring the forbidden-token check in `mkTokenAndFixPos`. The check is guarded by `forbiddenTks.isEmpty`, so the common identifier path is unchanged. We considered putting a unified check in `tokenFn` or `mkIdResult`, but decided against that. The former would move the check outside the cache and regress some benchmarks by ~1%. The latter forces a flag on `mkIdResult` and `identFnAux` because `nameLitAux` and `rawLitFn` must not forbid any tokens.
1 parent 49ac957 commit aacd612

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

src/Lean/Parser/Basic.lean

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1295,7 +1295,20 @@ def nameLitNoAntiquot : Parser := {
12951295
info := mkAtomicInfo "name"
12961296
}
12971297

1298-
def identFn : ParserFn := expectTokenFn identKind "identifier"
1298+
def identFn : ParserFn := fun c s =>
1299+
if c.forbiddenTks.isEmpty then
1300+
expectTokenFn identKind "identifier" c s
1301+
else
1302+
-- A forbidden token used as an identifier (e.g. a non-reserved clause keyword like
1303+
-- `invariant`) stops the enclosing term, mirroring `mkTokenAndFixPos`.
1304+
let iniSz := s.stackSize
1305+
let iniPos := s.pos
1306+
let s := expectTokenFn identKind "identifier" c s
1307+
if s.hasError then s
1308+
else match s.stxStack.back with
1309+
| .ident _ rawVal _ _ =>
1310+
if c.forbiddenTks.contains rawVal.toString then s.mkErrorAt "forbidden token" iniPos iniSz else s
1311+
| _ => s
12991312

13001313
def identNoAntiquot : Parser := {
13011314
fn := identFn
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Lean
2+
/-!
3+
`withForbidden` stops a term at a non-reserved word used as a bare identifier, while the word
4+
stays usable as an identifier (escaped, projected, or a plain declaration name).
5+
-/
6+
open Lean Elab Command Parser
7+
8+
namespace ForbiddenIdentTest
9+
10+
-- `#collect e sep e'` parses `e` with the non-reserved word `sep` forbidden, so a bare `sep`
11+
-- ends `e` without `sep` being a reserved keyword.
12+
@[command_parser]
13+
def collectCmd : Parser := leading_parser
14+
"#collect " >> withForbidden "sep" termParser >> nonReservedSymbol "sep" >> ppSpace >> termParser
15+
16+
@[command_elab collectCmd]
17+
def elabCollect : CommandElab := fun _ => pure ()
18+
19+
-- A bare `sep` ends the first term (requires the forbidding; else `a sep` is an application).
20+
#collect a sep b
21+
-- A multi-token first term still stops at the bare `sep`.
22+
#collect f a sep b
23+
-- Escaped `«sep»` is not forbidden: it is consumed as the first term.
24+
#collect «sep» sep b
25+
-- A projection `.sep` (rawIdent path) is not forbidden.
26+
#collect (id x).sep sep b
27+
-- The word remains usable as an ordinary identifier.
28+
def sep : Nat := 0
29+
30+
end ForbiddenIdentTest

0 commit comments

Comments
 (0)