Skip to content

Commit 9f9b929

Browse files
Haofeiclaude
andcommitted
rsscript: list slice patterns in match
Add `MatchPattern::List` and lower it on both backends at parity: `[]`, fixed-length `[a, b]`, head/rest `[first, ..rest]`, tail `[..init, last]`, and middle-rest `[a, ..mid, z]`. - Parser: `parse_list_pattern` (and `..`/`..name` rest segment parsing) wired into `parse_match_pattern` ahead of paren/tuple grouping. - Checker: scrutinee type `List<T>` accepted; element patterns checked against `T`; structured-pattern scrutinee-effect requirement extended to lists; exhaustiveness tracks covered lengths so `[]` + `[x, ..rest]` is exhaustive. - reg VM: length test (`>=`/`<=` bracketing, no `Equal` opcode) plus `ListGet` for fixed elements and `List.slice` for the bound rest. - rust_lower: native Rust slice patterns with the scrutinee viewed via `.as_slice()`; owned rebindings (`*x` / `x.clone()` / `rest.to_vec()`) so arms see owned `T` / `List<T>` rather than slice references. - Formatter round-trips list patterns; symbol index records rest bindings. Tests: `parity_list_match` VM/compiled parity, `fixtures/pass/list_patterns.rss`, and a formatter rendering test. Full `cargo test -p rsscript` green; clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e30e2b0 commit 9f9b929

13 files changed

Lines changed: 609 additions & 17 deletions

File tree

crates/rsscript/src/analyzer.rs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,6 +2034,36 @@ impl Analyzer<'_> {
20342034
return true;
20352035
}
20362036
let root = self.resolve_type_alias(type_root_name(type_name));
2037+
if root == "List" {
2038+
// A rest pattern `[a.., ..rest, ..z]` covers every length `>= a+z`; a
2039+
// fixed pattern covers exactly its element count. The match is
2040+
// exhaustive iff some rest pattern caps the open tail and every shorter
2041+
// length is covered by a fixed pattern. With no rest pattern, infinitely
2042+
// many lengths are uncovered, so an explicit `_` is required (handled
2043+
// above).
2044+
let mut min_rest: Option<usize> = None;
2045+
let mut fixed_lengths = HashSet::new();
2046+
for pattern in patterns {
2047+
if let MatchPattern::List {
2048+
prefix,
2049+
rest,
2050+
suffix,
2051+
..
2052+
} = pattern
2053+
{
2054+
let count = prefix.len() + suffix.len();
2055+
if rest.is_some() {
2056+
min_rest = Some(min_rest.map_or(count, |m: usize| m.min(count)));
2057+
} else {
2058+
fixed_lengths.insert(count);
2059+
}
2060+
}
2061+
}
2062+
let Some(min_rest) = min_rest else {
2063+
return false;
2064+
};
2065+
return (0..min_rest).all(|length| fixed_lengths.contains(&length));
2066+
}
20372067
if root == "Bool" {
20382068
let bool_literals = patterns
20392069
.iter()
@@ -2300,7 +2330,7 @@ impl Analyzer<'_> {
23002330
value: crate::syntax::ast::MatchLiteral::Bool(value),
23012331
..
23022332
} => matches!(witness, PatternWitness::Bool(candidate) if candidate == value),
2303-
MatchPattern::Literal { .. } => false,
2333+
MatchPattern::Literal { .. } | MatchPattern::List { .. } => false,
23042334
MatchPattern::Variant { name, binding, .. } => {
23052335
let PatternWitness::Constructor {
23062336
name: witness_name,
@@ -5960,6 +5990,14 @@ fn constructor_pattern_is_irrefutable(pattern: &MatchPattern) -> bool {
59605990
)
59615991
})
59625992
}),
5993+
// `[..]` / `[..rest]` matches any list; every other list pattern adds a
5994+
// length or element constraint and so is refutable.
5995+
MatchPattern::List {
5996+
prefix,
5997+
rest,
5998+
suffix,
5999+
..
6000+
} => prefix.is_empty() && suffix.is_empty() && rest.is_some(),
59636001
MatchPattern::Literal { .. } => false,
59646002
}
59656003
}
@@ -5991,9 +6029,10 @@ fn constrained_field_patterns(pattern: &MatchPattern) -> Vec<(String, &MatchPatt
59916029
}
59926030
})
59936031
.collect(),
5994-
MatchPattern::Binding { .. } | MatchPattern::Literal { .. } | MatchPattern::Wildcard(_) => {
5995-
Vec::new()
5996-
}
6032+
MatchPattern::Binding { .. }
6033+
| MatchPattern::Literal { .. }
6034+
| MatchPattern::List { .. }
6035+
| MatchPattern::Wildcard(_) => Vec::new(),
59976036
}
59986037
}
59996038

crates/rsscript/src/checks/body.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1882,7 +1882,7 @@ fn check_match_scrutinee_type(analyzer: &mut Analyzer<'_>, expr: &HirExpr) {
18821882
let Some(type_name) = hir_expr_type_name(expr) else {
18831883
return;
18841884
};
1885-
if type_root_name(type_name) == "Option" || type_root_name(type_name) == "Result" {
1885+
if matches!(type_root_name(type_name), "Option" | "Result" | "List") {
18861886
return;
18871887
}
18881888
if matches!(type_name, "Int" | "String" | "Bool") {
@@ -1898,7 +1898,7 @@ fn check_match_scrutinee_type(analyzer: &mut Analyzer<'_>, expr: &HirExpr) {
18981898
analyzer.diagnostics.push(
18991899
Diagnostic::error(
19001900
code::CONTROL_FLOW_TYPE_MISMATCH,
1901-
format!("match scrutinee has type `{type_name}`, expected `Option<T>`, `Result<T, E>`, a declared sum/struct/class type, or an `Int`/`String`/`Bool` literal match."),
1901+
format!("match scrutinee has type `{type_name}`, expected `Option<T>`, `Result<T, E>`, `List<T>`, a declared sum/struct/class type, or an `Int`/`String`/`Bool` literal match."),
19021902
hir_expr_span(expr).clone(),
19031903
"control-flow type mismatch",
19041904
)
@@ -2062,6 +2062,26 @@ fn check_match_pattern_matches_type(
20622062
}
20632063
}
20642064
}
2065+
MatchPattern::List {
2066+
prefix,
2067+
suffix,
2068+
span,
2069+
..
2070+
} => {
2071+
if root != "List" {
2072+
push_variant_or_struct_cannot_match(analyzer, "[..]", type_name, span);
2073+
return;
2074+
}
2075+
// Each element pattern is checked against the list's element type `T`
2076+
// (`List<T>`); the rest binding (if any) is itself a `List<T>`.
2077+
if let Some(element_type) =
2078+
type_arg_names(type_name).and_then(|args| args.first().copied())
2079+
{
2080+
for pattern in prefix.iter().chain(suffix) {
2081+
check_match_pattern_matches_type(analyzer, pattern, element_type);
2082+
}
2083+
}
2084+
}
20652085
}
20662086
}
20672087

@@ -2191,7 +2211,11 @@ fn check_match_pattern_effects(
21912211
);
21922212
}
21932213
for arm in arms {
2194-
if matches!(arm.pattern, MatchPattern::Struct { .. }) && scrutinee_effect.is_none() {
2214+
if matches!(
2215+
arm.pattern,
2216+
MatchPattern::Struct { .. } | MatchPattern::List { .. }
2217+
) && scrutinee_effect.is_none()
2218+
{
21952219
analyzer.diagnostics.push(
21962220
Diagnostic::error(
21972221
code::MISSING_DATA_EFFECT,

crates/rsscript/src/formatter.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,40 @@ impl Formatter {
897897
}
898898
self.out.push_str(" }");
899899
}
900+
MatchPattern::List {
901+
prefix,
902+
rest,
903+
suffix,
904+
..
905+
} => {
906+
self.out.push('[');
907+
let mut first = true;
908+
for pattern in prefix {
909+
if !first {
910+
self.out.push_str(", ");
911+
}
912+
first = false;
913+
self.match_pattern(pattern);
914+
}
915+
if let Some(rest_binding) = rest {
916+
if !first {
917+
self.out.push_str(", ");
918+
}
919+
first = false;
920+
self.out.push_str("..");
921+
if let Some(name) = rest_binding {
922+
self.out.push_str(name);
923+
}
924+
}
925+
for pattern in suffix {
926+
if !first {
927+
self.out.push_str(", ");
928+
}
929+
first = false;
930+
self.match_pattern(pattern);
931+
}
932+
self.out.push(']');
933+
}
900934
}
901935
}
902936

@@ -1858,6 +1892,52 @@ impl Writer for BufferWriter {
18581892
);
18591893
}
18601894

1895+
#[test]
1896+
fn renders_list_slice_patterns() {
1897+
let source = r#"features: local
1898+
1899+
fn describe(xs:read List<Int>)->Int{
1900+
match read xs{
1901+
[]=>{return 0}
1902+
[only]=>{return only}
1903+
[first,..rest]=>{return first}
1904+
[..init,last]=>{return last}
1905+
[a,..mid,z]=>{return a}
1906+
_=>{return -1}
1907+
}
1908+
}
1909+
"#;
1910+
1911+
assert_eq!(
1912+
format_source("list.rss", source),
1913+
r#"features: local
1914+
1915+
fn describe(xs: read List<Int>) -> Int {
1916+
match read xs {
1917+
[] => {
1918+
return 0
1919+
}
1920+
[only] => {
1921+
return only
1922+
}
1923+
[first, ..rest] => {
1924+
return first
1925+
}
1926+
[..init, last] => {
1927+
return last
1928+
}
1929+
[a, ..mid, z] => {
1930+
return a
1931+
}
1932+
_ => {
1933+
return -1
1934+
}
1935+
}
1936+
}
1937+
"#
1938+
);
1939+
}
1940+
18611941
#[test]
18621942
fn renders_tuple_literals_types_patterns_and_destructuring() {
18631943
let source = r#"fn first(p:read (Int,String))->Int{

crates/rsscript/src/hir.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3109,6 +3109,32 @@ fn match_pattern_binding_types(
31093109
}
31103110
}
31113111

3112+
if let MatchPattern::List {
3113+
prefix,
3114+
rest,
3115+
suffix,
3116+
..
3117+
} = pattern
3118+
{
3119+
let Some(value_type) = value_type else {
3120+
return Vec::new();
3121+
};
3122+
// Element patterns bind at the list's element type `T` (`List<T>`); a
3123+
// bound rest segment is itself a `List<T>`.
3124+
let element_type = value_type
3125+
.strip_prefix("List<")
3126+
.and_then(|rest| rest.strip_suffix('>'))
3127+
.map(str::trim);
3128+
let mut bindings = Vec::new();
3129+
for element in prefix.iter().chain(suffix) {
3130+
bindings.extend(match_pattern_binding_types(hir, element, element_type));
3131+
}
3132+
if let Some(Some(rest_name)) = rest {
3133+
bindings.push((rest_name.clone(), value_type.to_string()));
3134+
}
3135+
return bindings;
3136+
}
3137+
31123138
let MatchPattern::Struct { name, fields, .. } = pattern else {
31133139
return Vec::new();
31143140
};

0 commit comments

Comments
 (0)