Skip to content

Commit 6e9836e

Browse files
committed
Add parser tests for named repeat ranges
The named repeat range feature adds two new AST variants -- `RepeatRange` with a `name` field and `RepeatRangeNamed`. We add a `named_repeat_range()` helper that extracts all four fields (name, min, max, limit) and nine tests covering: - Named binding with closed range (`{n:1..=255}`). - Named binding with half-open range (`{n:2..5}`). - Named binding with omitted min (`{n:..=5}`). - Named binding with omitted max (`{n:2..}`). - Named reference (`{n}`) producing `RepeatRangeNamed`. - Combined named binding and reference in one production. - Backtrack from name parsing to plain range (`{2..5}`). - Error: Name followed by colon with no range (`{n:}`). - Error: Empty braces (`{}`).
1 parent c19a49c commit 6e9836e

1 file changed

Lines changed: 141 additions & 0 deletions

File tree

tools/grammar/src/parser.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)