Skip to content

Commit 3a940f5

Browse files
Unicode RL1.1 support: Add \u{...} Hex Notation for Full Unicode Range (#149)
* Add support for \u{...} variable-length Unicode escape syntax * Refactor Unicode escape parser for better code clarity * Remove unused error variants from Parser.Error * Fix formatting of hexNumberVariable parser --------- Co-authored-by: pandaman <kointosudesuyo@infoseek.jp>
1 parent 153ed60 commit 3a940f5

3 files changed

Lines changed: 77 additions & 10 deletions

File tree

regex/Regex/Syntax/Parser/Basic.lean

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def specialCharacters := "[](){*+?|^$.\\"
4242

4343
def escapedChar : Parser.LT s Error (Char ⊕ PerlClass) :=
4444
charOrError '\\' *>
45-
((Sum.inl <$> (simple <|> hex2 <|> hex4)) <|> (Sum.inr <$> perlClass)).commit
45+
((Sum.inl <$> (simple <|> hexEscape)) <|> (Sum.inr <$> perlClass)).commit
4646
where
4747
simple : Parser.LT s Error Char :=
4848
anyCharOrError
@@ -62,11 +62,22 @@ where
6262
pure c
6363
else
6464
throw (.unexpectedEscapedChar c)
65-
hex2 : Parser.LT s Error Char :=
66-
charOrError 'x' *> (Char.ofNat <$> hexNumberN 2).commit
67-
-- TODO: support "\u{XXXX}" and "\u{XXXXX}"
68-
hex4 : Parser.LT s Error Char :=
69-
charOrError 'u' *> (Char.ofNat <$> hexNumberN 4).commit
65+
hexEscape : Parser.LT s Error Char :=
66+
(charOrError 'x' *> (Char.ofNat <$> hexNumberN 2).commit)
67+
<|> (charOrError 'u' *> unicodeEscape.commit)
68+
unicodeEscape : Parser.LT s Error Char :=
69+
hexNumberVariable.guard fun n =>
70+
if n.isValidChar then .ok (Char.ofNat n)
71+
else .error (.invalidCodePoint n)
72+
hexNumberVariable : Parser.LT s Error Nat :=
73+
betweenOr (charOrError '{') (charOrError '}').commit (.commit do
74+
let digits ← (many1 hexDigit).weaken
75+
if digits.size > 6 then
76+
throw (.tooManyHexDigits digits.size)
77+
else
78+
pure (digits.foldl (fun n d => 16 * n + d) 0)
79+
)
80+
<|> hexNumberN 4
7081
perlClass : Parser.LT s Error PerlClass :=
7182
anyCharOrError
7283
|>.guard fun c =>

regex/Regex/Syntax/Parser/Error.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ inductive Error where
1414
| invalidRange (c₁ : Char) (c₂ : Char)
1515
| invalidRepetition (min : Nat) (max : Nat)
1616
| expectedEof
17+
| invalidCodePoint (n : Nat)
18+
| tooManyHexDigits (n : Nat)
1719
deriving Repr, Inhabited, DecidableEq
1820

1921
instance : ToString Error where
@@ -26,5 +28,7 @@ instance : ToString Error where
2628
| .invalidRange c₁ c₂ => s!"invalid range: {c₁}..{c₂}"
2729
| .invalidRepetition min max => s!"invalid repetition: {min}..{max}"
2830
| .expectedEof => "expected EOF"
31+
| .invalidCodePoint n => s!"invalid code point: 0x{n.toDigits 16}"
32+
| .tooManyHexDigits n => s!"too many hex digits: {n}"
2933

3034
end Regex.Syntax.Parser

regex/Regex/Syntax/Parser/Test.lean

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,6 @@ private def test (input : String) (expected : Ast) : Bool :=
127127
#guard parseAst "\\x" = .error .unexpectedEof
128128
#guard parseAst "\\u" = .error .unexpectedEof
129129
#guard parseAst "\\u12" = .error .unexpectedEof
130-
#guard parseAst "\\u{}" = .error (.unexpectedChar '{')
131-
#guard parseAst "\\u{xyz}" = .error (.unexpectedChar '{')
132130
#guard parseAst "[]" = .error (.unexpectedChar ']')
133131
#guard parseAst "[^]" = .error (.unexpectedChar ']')
134132
#guard parseAst "a{1" = .error .unexpectedEof
@@ -140,7 +138,61 @@ private def test (input : String) (expected : Ast) : Bool :=
140138
#guard parseAst "(?:a" = .error .unexpectedEof
141139
#guard parseAst "(?:" = .error .unexpectedEof
142140

143-
-- We do not support \u{...} yet.
144-
#guard parseAst "\\u{1234}" = .error (.unexpectedChar '{')
141+
-- Minimum
142+
#guard parseAst "\\u{0}" = .ok (.char '\x00')
143+
144+
-- ASCII range
145+
#guard parseAst "\\u{7F}" = .ok (.char '\x7F')
146+
#guard parseAst "\\u{41}" = .ok (.char 'A')
147+
148+
-- Latin-1 Supplement
149+
#guard parseAst "\\u{80}" = .ok (.char (Char.ofNat 0x80))
150+
#guard parseAst "\\u{FF}" = .ok (.char 'ÿ')
151+
152+
-- BMP (Basic Multilingual Plane)
153+
#guard parseAst "\\u{1234}" = .ok (.char (Char.ofNat 0x1234))
154+
#guard parseAst "\\u{FFFF}" = .ok (.char (Char.ofNat 0xFFFF))
155+
156+
-- Supplementary planes (emoji and beyond)
157+
#guard parseAst "\\u{10000}" = .ok (.char (Char.ofNat 0x10000))
158+
#guard parseAst "\\u{1F600}" = .ok (.char '😀') -- GRINNING FACE
159+
#guard parseAst "\\u{1F4A9}" = .ok (.char '💩') -- PILE OF POO
160+
#guard parseAst "\\u{1F47D}" = .ok (.char '👽') -- EXTRATERRESTRIAL ALIEN
161+
162+
-- Maximum valid code point
163+
#guard parseAst "\\u{10FFFF}" = .ok (.char (Char.ofNat 0x10FFFF))
164+
165+
-- Lowercase hex digits
166+
#guard parseAst "\\u{1f600}" = .ok (.char '😀')
167+
#guard parseAst "\\u{abcd}" = .ok (.char (Char.ofNat 0xABCD))
168+
169+
-- Variable length (1-6 digits)
170+
#guard parseAst "\\u{a}" = .ok (.char (Char.ofNat 0xA))
171+
#guard parseAst "\\u{AB}" = .ok (.char (Char.ofNat 0xAB))
172+
#guard parseAst "\\u{ABC}" = .ok (.char (Char.ofNat 0xABC))
173+
#guard parseAst "\\u{ABCD}" = .ok (.char (Char.ofNat 0xABCD))
174+
#guard parseAst "\\u{ABCDE}" = .ok (.char (Char.ofNat 0xABCDE))
175+
176+
-- Empty braces
177+
#guard parseAst "\\u{}" = .error (.unexpectedChar '}')
178+
179+
-- Too large (beyond Unicode)
180+
#guard parseAst "\\u{110000}" = .error (.invalidCodePoint 0x110000)
181+
#guard parseAst "\\u{FFFFFF}" = .error (.invalidCodePoint 0xFFFFFF)
182+
183+
-- Too many digits (>6)
184+
#guard parseAst "\\u{1234567}" = .error (.tooManyHexDigits 7)
185+
186+
-- Invalid hex characters
187+
#guard parseAst "\\u{GHIJ}" = .error (.unexpectedChar 'G')
188+
#guard parseAst "\\u{12.34}" = .error (.unexpectedChar '.')
189+
190+
-- Missing closing brace
191+
#guard parseAst "\\u{1234" = .error (.unexpectedEof)
192+
193+
-- Surrogate range (optional, depending on Lean's Char behavior)
194+
-- If Lean's Char.ofNat rejects surrogates, these should error:
195+
#guard parseAst "\\u{D800}" = .error (.invalidCodePoint 0xD800)
196+
#guard parseAst "\\u{DFFF}" = .error (.invalidCodePoint 0xDFFF)
145197

146198
end Regex.Syntax.Parser.Test

0 commit comments

Comments
 (0)