@@ -1179,4 +1179,145 @@ mod tests {
11791179 assert_eq ! ( * max, Some ( 5 ) ) ;
11801180 assert ! ( matches!( limit, RangeLimit :: HalfOpen ) ) ;
11811181 }
1182+
1183+ // --- Named repeat range tests ---
1184+
1185+ /// Extract full `RepeatRange` fields including the name.
1186+ fn named_repeat_range ( input : & str ) -> ( Option < String > , Option < u32 > , Option < u32 > , RangeLimit ) {
1187+ let grammar = parse ( input) . unwrap ( ) ;
1188+ let rule = grammar. productions . get ( "A" ) . unwrap ( ) ;
1189+ let ExpressionKind :: RepeatRange {
1190+ name,
1191+ min,
1192+ max,
1193+ limit,
1194+ ..
1195+ } = & rule. expression . kind
1196+ else {
1197+ panic ! ( "expected RepeatRange, got {:?}" , rule. expression. kind) ;
1198+ } ;
1199+ ( name. clone ( ) , * min, * max, * limit)
1200+ }
1201+
1202+ #[ test]
1203+ fn named_range_closed ( ) {
1204+ let ( name, min, max, limit) = named_repeat_range ( "A -> x{n:1..=255}" ) ;
1205+ assert_eq ! ( name. as_deref( ) , Some ( "n" ) ) ;
1206+ assert_eq ! ( min, Some ( 1 ) ) ;
1207+ assert_eq ! ( max, Some ( 255 ) ) ;
1208+ assert ! ( matches!( limit, RangeLimit :: Closed ) ) ;
1209+ }
1210+
1211+ #[ test]
1212+ fn named_range_half_open ( ) {
1213+ let ( name, min, max, limit) = named_repeat_range ( "A -> x{n:2..5}" ) ;
1214+ assert_eq ! ( name. as_deref( ) , Some ( "n" ) ) ;
1215+ assert_eq ! ( min, Some ( 2 ) ) ;
1216+ assert_eq ! ( max, Some ( 5 ) ) ;
1217+ assert ! ( matches!( limit, RangeLimit :: HalfOpen ) ) ;
1218+ }
1219+
1220+ #[ test]
1221+ fn named_range_omitted_min ( ) {
1222+ let ( name, min, max, limit) = named_repeat_range ( "A -> x{n:..=5}" ) ;
1223+ assert_eq ! ( name. as_deref( ) , Some ( "n" ) ) ;
1224+ assert_eq ! ( min, None ) ;
1225+ assert_eq ! ( max, Some ( 5 ) ) ;
1226+ assert ! ( matches!( limit, RangeLimit :: Closed ) ) ;
1227+ }
1228+
1229+ #[ test]
1230+ fn named_range_omitted_max ( ) {
1231+ let ( name, min, max, limit) = named_repeat_range ( "A -> x{n:2..}" ) ;
1232+ assert_eq ! ( name. as_deref( ) , Some ( "n" ) ) ;
1233+ assert_eq ! ( min, Some ( 2 ) ) ;
1234+ assert_eq ! ( max, None ) ;
1235+ assert ! ( matches!( limit, RangeLimit :: HalfOpen ) ) ;
1236+ }
1237+
1238+ #[ test]
1239+ fn named_reference ( ) {
1240+ // `{n}` without a colon or range produces a
1241+ // RepeatRangeNamed variant.
1242+ let grammar = parse ( "A -> x{n}" ) . unwrap ( ) ;
1243+ let rule = grammar. productions . get ( "A" ) . unwrap ( ) ;
1244+ let ExpressionKind :: RepeatRangeNamed ( _, name) = & rule. expression . kind else {
1245+ panic ! ( "expected RepeatRangeNamed, got {:?}" , rule. expression. kind) ;
1246+ } ;
1247+ assert_eq ! ( name, "n" ) ;
1248+ }
1249+
1250+ #[ test]
1251+ fn named_binding_and_reference_in_sequence ( ) {
1252+ // A production with a named binding and a named reference.
1253+ let grammar = parse ( "A -> x{n:1..=255} y{n}" ) . unwrap ( ) ;
1254+ let rule = grammar. productions . get ( "A" ) . unwrap ( ) ;
1255+ let ExpressionKind :: Sequence ( seq) = & rule. expression . kind else {
1256+ panic ! ( "expected Sequence, got {:?}" , rule. expression. kind) ;
1257+ } ;
1258+ assert_eq ! ( seq. len( ) , 2 ) ;
1259+
1260+ // First element: x{n:1..=255}
1261+ let ExpressionKind :: RepeatRange {
1262+ name,
1263+ min,
1264+ max,
1265+ limit,
1266+ ..
1267+ } = & seq[ 0 ] . kind
1268+ else {
1269+ panic ! ( "expected RepeatRange, got {:?}" , seq[ 0 ] . kind) ;
1270+ } ;
1271+ assert_eq ! ( name. as_deref( ) , Some ( "n" ) ) ;
1272+ assert_eq ! ( * min, Some ( 1 ) ) ;
1273+ assert_eq ! ( * max, Some ( 255 ) ) ;
1274+ assert ! ( matches!( limit, RangeLimit :: Closed ) ) ;
1275+
1276+ // Second element: y{n}
1277+ let ExpressionKind :: RepeatRangeNamed ( _, ref_name) = & seq[ 1 ] . kind else {
1278+ panic ! ( "expected RepeatRangeNamed, got {:?}" , seq[ 1 ] . kind) ;
1279+ } ;
1280+ assert_eq ! ( ref_name, "n" ) ;
1281+ }
1282+
1283+ #[ test]
1284+ fn named_range_backtrack_to_plain_range ( ) {
1285+ // When parse_name() succeeds but the next byte is
1286+ // neither `:` nor `}`, the parser backtracks and
1287+ // falls through to plain range parsing. `{2..5}` is
1288+ // such a case after the parse_name fix (digits are
1289+ // rejected), but let's test a scenario where a name is
1290+ // parsed and then backtracked.
1291+ //
1292+ // There is no single-character token after a name that
1293+ // triggers backtrack in valid grammar (the match arms
1294+ // cover `:` and `}`), but the fallback resets the index
1295+ // and tries plain range parsing. We verify that
1296+ // `{2..5}` parses correctly as a plain range even
1297+ // though it starts with a digit.
1298+ let ( min, max, limit) = repeat_range ( "A -> x{2..5}" ) ;
1299+ assert_eq ! ( min, Some ( 2 ) ) ;
1300+ assert_eq ! ( max, Some ( 5 ) ) ;
1301+ assert ! ( matches!( limit, RangeLimit :: HalfOpen ) ) ;
1302+ }
1303+
1304+ #[ test]
1305+ fn named_range_err_colon_missing_dots ( ) {
1306+ // `{n:}` — name followed by colon, then no `..`.
1307+ let err = parse ( "A -> x{n:}" ) . unwrap_err ( ) ;
1308+ assert ! (
1309+ err. contains( "expected `..`" ) ,
1310+ "expected `..` error for {{n:}}, got: {err}"
1311+ ) ;
1312+ }
1313+
1314+ #[ test]
1315+ fn named_range_err_empty_braces ( ) {
1316+ // `{}` — empty braces contain no name and no range.
1317+ let err = parse ( "A -> x{}" ) . unwrap_err ( ) ;
1318+ assert ! (
1319+ err. contains( "expected `..`" ) ,
1320+ "expected `..` error for {{}}, got: {err}"
1321+ ) ;
1322+ }
11821323}
0 commit comments