11import unittest
22
33from test import test_tools
4- from typing import Dict , Set
54
65test_tools .skip_if_missing ("peg_generator" )
76with test_tools .imports_under_tool ("peg_generator" ):
1211
1312
1413class TestFirstSets (unittest .TestCase ):
15- def calculate_first_sets (self , grammar_source : str ) -> Dict [str , Set [str ]]:
14+ def calculate_first_sets (self , grammar_source : str ) -> dict [str , set [str | None ]]:
1615 grammar : Grammar = parse_string (grammar_source , GrammarParser )
1716 return FirstSetCalculator (grammar .rules ).calculate ()
1817
@@ -164,8 +163,8 @@ def test_positive_lookahead(self) -> None:
164163 self .assertEqual (
165164 self .calculate_first_sets (grammar ),
166165 {
167- "expr" : {"'a'" },
168- "start" : {"'a'" },
166+ "expr" : {None , "'a'" , "'b'" , "'c '" },
167+ "start" : {None , "'a'" , "'b'" , "'c '" },
169168 "opt" : {"'b'" , "'c'" , "'a'" },
170169 },
171170 )
@@ -180,8 +179,8 @@ def test_negative_lookahead(self) -> None:
180179 self .calculate_first_sets (grammar ),
181180 {
182181 "opt" : {"'b'" , "'a'" , "'c'" },
183- "expr" : {"'b'" , "'c'" },
184- "start" : {"'b'" , "'c'" },
182+ "expr" : {None , "'a'" , "'b'" , "'c'" },
183+ "start" : {None , "'a'" , "'b'" , "'c'" },
185184 },
186185 )
187186
@@ -226,21 +225,20 @@ def test_mutual_left_recursion(self) -> None:
226225 self .calculate_first_sets (grammar ),
227226 {
228227 "foo" : {"'D'" , "'B'" },
229- "bar" : {"'D'" },
228+ "bar" : {"'B'" , "' D'" },
230229 "start" : {"'D'" , "'B'" },
231230 },
232231 )
233232
234233 def test_nasty_left_recursion (self ) -> None :
235- # TODO: Validate this
236234 grammar = """
237235 start: target '='
238236 target: maybe '+' | NAME
239237 maybe: maybe '-' | target
240238 """
241239 self .assertEqual (
242240 self .calculate_first_sets (grammar ),
243- {"maybe" : set () , "target" : {"NAME" }, "start" : {"NAME" }},
241+ {"maybe" : { "NAME" } , "target" : {"NAME" }, "start" : {"NAME" }},
244242 )
245243
246244 def test_nullable_rule (self ) -> None :
@@ -284,3 +282,43 @@ def test_multiple_nullable_rules(self) -> None:
284282 "another" : {"'/'" },
285283 },
286284 )
285+
286+ def test_compound_negative_lookahead (self ) -> None :
287+ sets = self .calculate_first_sets ("""
288+ start: choice NEWLINE ENDMARKER
289+ choice: !('a' 'b') ('a' | 'c') | 'd'
290+ """ )
291+ self .assertEqual (sets ['choice' ], {None , "'a'" , "'c'" , "'d'" })
292+
293+ def test_nullable_recursive_rules (self ) -> None :
294+ sets = self .calculate_first_sets ("""
295+ start: a NUMBER ENDMARKER
296+ a: b | NAME
297+ b: a | ['+']
298+ """ )
299+ self .assertEqual (sets ['a' ], {'' , 'NAME' , "'+'" })
300+ self .assertEqual (sets ['b' ], {'' , 'NAME' , "'+'" })
301+ self .assertEqual (sets ['start' ], {'NUMBER' , 'NAME' , "'+'" })
302+
303+ def test_control_flow_before_first_token (self ) -> None :
304+ sets = self .calculate_first_sets ("""
305+ start: NAME ENDMARKER
306+ cut: ~ NAME
307+ forced: &&'+'
308+ guarded: &forced NUMBER
309+ action: [NAME] { _PyPegen_dummy_name(p) }
310+ """ )
311+ self .assertEqual (sets ['cut' ], {None , 'NAME' })
312+ self .assertEqual (sets ['forced' ], {None , "'+'" })
313+ self .assertEqual (sets ['guarded' ], {None , 'NUMBER' })
314+ self .assertEqual (sets ['action' ], {None , '' , 'NAME' })
315+
316+ def test_nullable_repeat_and_gather (self ) -> None :
317+ sets = self .calculate_first_sets ("""
318+ start: NAME ENDMARKER
319+ optional: [NAME]
320+ repeat: optional+ NUMBER
321+ gather: ','.optional+ NUMBER
322+ """ )
323+ self .assertEqual (sets ['repeat' ], {'NAME' , 'NUMBER' })
324+ self .assertEqual (sets ['gather' ], {'NAME' , "','" , 'NUMBER' })
0 commit comments