Skip to content

Commit 2231d9b

Browse files
authored
feat: improve error messages for ambiguous 3.toDecmial syntax (#10488)
This PR changes the way that scientific numerals are parsed in order to give better error messages for (invalid) syntax like `32.succ`. Example: ```lean4 #check 32.succ ``` Before, the error message is: ``` unexpected identifier; expected command ``` This is because `32.` parses as a complete float, and `#check 32.` parses as a complete command, so `succ` is being read as the start of a new command. With this change, the error message will move from the `succ` token to the `32` token (which isn't totally ideal from my perspective) but gives a less misleading error message and corresponding suggestion: ``` unexpected identifier after decimal point; consider parenthesizing the number ```
1 parent e72bf59 commit 2231d9b

File tree

4 files changed

+120
-19
lines changed

4 files changed

+120
-19
lines changed

src/Lean/Parser/Basic.lean

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -845,8 +845,8 @@ def decimalNumberFn (startPos : String.Pos) (includeWhitespace := true)
845845
mkNodeToken numLitKind startPos includeWhitespace c s
846846
where
847847
parseScientific s :=
848-
let s := parseOptDot s
849-
let s := parseOptExp s
848+
let (s, hasBareDot) := parseOptDot s
849+
let s := parseOptExp s hasBareDot
850850
mkNodeToken scientificLitKind startPos includeWhitespace c s
851851

852852
parseOptDot s :=
@@ -856,25 +856,30 @@ where
856856
let i := c.next i
857857
let curr := c.get i
858858
if curr.isDigit then
859-
takeDigitsFn (fun c => c.isDigit) "decimal number" false c (s.setPos i)
859+
(takeDigitsFn (fun c => c.isDigit) "decimal number" false c (s.setPos i), false)
860860
else
861-
s.setPos i
861+
(s.setPos i, true)
862862
else
863-
s
863+
(s, false)
864864

865-
parseOptExp s :=
866-
let i := s.pos
867-
let curr := c.get i
868-
if curr == 'e' || curr == 'E' then
869-
let i := c.next i
870-
let i := if c.get i == '-' || c.get i == '+' then c.next i else i
871-
let curr := c.get i
872-
if curr.isDigit then
873-
takeDigitsFn (fun c => c.isDigit) "decimal number" false c (s.setPos i)
874-
else
875-
s.mkUnexpectedError "missing exponent digits in scientific literal"
876-
else
865+
parseOptExp s hasBareDot :=
866+
if h : c.atEnd s.pos then
877867
s
868+
else
869+
let i := s.pos
870+
let curr := c.get' i h
871+
if curr == 'e' || curr == 'E' then
872+
let i := c.next i
873+
let i := if c.get i == '-' || c.get i == '+' then c.next i else i
874+
let curr := c.get i
875+
if curr.isDigit then
876+
takeDigitsFn (fun c => c.isDigit) "decimal number" false c (s.setPos i)
877+
else
878+
s.mkUnexpectedError "missing exponent digits in scientific literal"
879+
else if hasBareDot && isIdFirst curr then
880+
(s.setPos startPos).mkUnexpectedError s!"unexpected identifier after decimal point; consider parenthesizing the number"
881+
else
882+
s
878883

879884
def binNumberFn (startPos : String.Pos) (includeWhitespace := true) : ParserFn := fun c s =>
880885
let s := takeDigitsFn (fun c => c == '0' || c == '1') "binary number" true c s

tests/lean/10488.lean

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#eval (1,(2,3)).2.fst
2+
3+
#check 31.
4+
#check 31.0
5+
#eval 31.
6+
#eval 31.0
7+
8+
#check 31.e
9+
#check 31.ee
10+
11+
#check 31.f
12+
#check 31.ff
13+
14+
#check 31.3e
15+
#check 31.3e2
16+
#check 31.3ee2
17+
18+
#check 31.3f
19+
#check 31.3f2
20+
#check 31.3ff2
21+
22+
#check 11.toDigits 13
23+
#check (11).toDigits 13
24+
#eval (11).toDigits 13
25+
#check (11).toDigits(13)
26+
#check (11).toDigits (13)
27+
28+
def succ (a: Nat) := a + 1
29+
def foo {A B} (_: A) (_: B) : Unit := ()
30+
#check foo 31.succ
31+
#check foo (31).succ
32+
#check foo 31(.succ)
33+
#check foo (31)(.succ)
34+
#check foo 31 .succ
35+
#check foo 31. succ
36+
37+
#check 11succ
38+
#check 11.succ
39+
#check 11.12succ
40+
#check (11.succ)
41+
#check (11.12succ)
42+
43+
44+
-- This example (adapted from structInst4.lean) exercises the difference betwee
45+
-- term parsing and LVal parsing; the latter fails if we allow `2.snd` to parse
46+
-- as a scientificLit followed by an error token, so this test captures
47+
-- that we have to throw the error token right away, positioned before, rather
48+
-- than after the `2.`
49+
structure Foo where
50+
(x : Nat × (Nat × Nat) := (2, (4, 5)))
51+
def bar : Foo := {}
52+
#check bar.x.2.snd
53+
#eval { bar with x.2.snd := 1 }

tests/lean/10488.lean.expected.out

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2
2+
31. : Float
3+
31.0 : Float
4+
31.000000
5+
31.000000
6+
10488.lean:8:10: error: missing exponent digits in scientific literal
7+
10488.lean:9:10: error: missing exponent digits in scientific literal
8+
10488.lean:11:7: error: unexpected identifier after decimal point; consider parenthesizing the number
9+
10488.lean:12:7: error: unexpected identifier after decimal point; consider parenthesizing the number
10+
10488.lean:14:11: error: missing exponent digits in scientific literal
11+
313e1 : Float
12+
10488.lean:16:11: error: missing exponent digits in scientific literal
13+
31.3 : Float
14+
10488.lean:18:11-18:12: error: unexpected identifier; expected command
15+
31.3 : Float
16+
10488.lean:19:11-19:13: error: unexpected identifier; expected command
17+
31.3 : Float
18+
10488.lean:20:11-20:14: error: unexpected identifier; expected command
19+
10488.lean:22:7: error: unexpected identifier after decimal point; consider parenthesizing the number
20+
Nat.toDigits 11 13 : List Char
21+
['1', '2']
22+
Nat.toDigits 11 : Nat → List Char
23+
10488.lean:25:20-25:21: error: unexpected token '('; expected command
24+
Nat.toDigits 11 13 : List Char
25+
foo.{u_1, u_2} {A : Sort u_1} {B : Sort u_2} : A → B → Unit
26+
10488.lean:30:11: error: unexpected identifier after decimal point; consider parenthesizing the number
27+
foo (Nat.succ 31) : ?m → Unit
28+
foo 31 : ?m → Unit
29+
10488.lean:32:13-32:14: error: unexpected token '('; expected command
30+
foo 31 : ?m → Unit
31+
10488.lean:33:15-33:16: error: unexpected token '('; expected command
32+
10488.lean:34:14-34:19: error(lean.invalidDottedIdent): Invalid dotted identifier notation: The expected type of `.succ` could not be determined
33+
foo 31 sorry : Unit
34+
foo 31. succ : Unit
35+
11 : Nat
36+
10488.lean:37:9-37:13: error: unexpected identifier; expected command
37+
10488.lean:38:7: error: unexpected identifier after decimal point; consider parenthesizing the number
38+
11.12 : Float
39+
10488.lean:39:12-39:16: error: unexpected identifier; expected command
40+
10488.lean:40:8: error: unexpected identifier after decimal point; consider parenthesizing the number
41+
10488.lean:41:13-41:17: error: unexpected identifier; expected ')', ',' or ':'
42+
bar.x.snd.snd : Nat
43+
{ x := (2, 4, 1) }

tests/lean/run/6199.lean

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ Scientific tests
4646
#guard_msgs in #term "100_000.0"
4747
/-- info: 0. : Float -/
4848
#guard_msgs in #term "0."
49-
-- The decimal parser requires a digit at the start, so the `_` is left over:
50-
/-- error: <input>:1:4: expected end of input -/
49+
-- The decimal parser requires a digit at the start, so the `_` is left over and read as a distinct identifier:
50+
/-- error: <input>:1:0: unexpected identifier after decimal point; consider parenthesizing the number -/
5151
#guard_msgs in #term "100._"
5252
/-- info: 100.111111 : Float -/
5353
#guard_msgs in #term "100.111_111"

0 commit comments

Comments
 (0)