Skip to content

Commit e30e2b0

Browse files
Haofeiclaude
andcommitted
rsscript: add tuples, destructuring, and generic match/field inference
Tuples desugar at parse time to synthetic `__TupleN` generic structs, so the checker, HIR, both backends, and the formatter handle them via existing machinery: - Literals `(a, b)`, types `(Int, String)`, and `.itemN` field access. - Tuple patterns in `match` (binding, wildcard, and literal element patterns), lowered to `__TupleN { itemK: .. }` struct patterns. - `let (a, b) = expr` destructuring (`_` skips an element), expanded to a temporary plus per-element field reads. - `read (a, b)` and `manage (a, b)` operands now parse as tuple literals. - The formatter renders the synthetic `__TupleN` forms back as tuple syntax for literals, types, patterns, and destructuring (idempotent round-trip). Fixing tuple parity surfaced several pre-existing generic-struct gaps, fixed here so both backends agree (they benefit all generic structs, not just tuples): - rust_lower: by-ref match field bindings are rebound to owned values (deref `Copy`, clone others) so the arm body sees owned fields — previously only `Some`/`Ok` payloads were handled, so `match read s { S { x } => f(x) }` failed to compile. - reg_vm: plain (non-variant) struct patterns are now supported in the general refutable matcher, including literal element patterns. - checker + HIR: generic field-access, match-binding, and literal-pattern type checks substitute the scrutinee's concrete arguments for the type's parameters (`item0` on `__Tuple2<Int, String>` resolves to `Int`, not `A`); generic construction also infers parameters from scalar literal arguments. Covered by `tests/fixtures/pass/tuples.rss`, the `parity_tuple_*` and `parity_generic_struct_match_and_field_inference` VM/compiled parity tests, and a formatter round-trip test. Full suite green (1032 tests); clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 75c47bd commit e30e2b0

12 files changed

Lines changed: 1295 additions & 52 deletions

File tree

crates/rsscript/src/checks/body.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::text_util::{
2-
split_top_level_type_args, strip_fresh_type, type_arg_names, type_root_name,
2+
split_top_level_type_args, strip_fresh_type, substitute_type_args, type_arg_names,
3+
type_root_name,
34
};
45
use std::collections::{HashMap, HashSet};
56

@@ -2041,13 +2042,23 @@ fn check_match_pattern_matches_type(
20412042
push_variant_or_struct_cannot_match(analyzer, name, type_name, span);
20422043
return;
20432044
};
2045+
// Map the type's declared parameters (`A`, `B`) to the scrutinee's
2046+
// concrete arguments so a field declared `A` is checked as `Int`.
2047+
let type_params = analyzer
2048+
.hir
2049+
.type_info(root)
2050+
.map(|info| info.type_params.to_vec())
2051+
.unwrap_or_default();
2052+
let substitutions = generic_substitutions(&type_params, type_name);
20442053
for field in fields {
20452054
if let Some(pattern) = &field.pattern
20462055
&& let Some(field_info) = declared
20472056
.iter()
20482057
.find(|candidate| candidate.name == field.name)
20492058
{
2050-
check_match_pattern_matches_type(analyzer, pattern, &field_info.type_name);
2059+
let field_type =
2060+
substitute_type_args(&field_info.type_name, &substitutions);
2061+
check_match_pattern_matches_type(analyzer, pattern, &field_type);
20512062
}
20522063
}
20532064
}
@@ -2120,6 +2131,20 @@ fn allowed_sum_variant_names(analyzer: &Analyzer<'_>, root: &str) -> Vec<String>
21202131
.unwrap_or_default()
21212132
}
21222133

2134+
/// Build a map from a type's declared generic parameters to the concrete
2135+
/// arguments named in `type_name`, e.g. params `[A, B]` against
2136+
/// `__Tuple2<Int, String>` → `{A: Int, B: String}`.
2137+
fn generic_substitutions(type_params: &[String], type_name: &str) -> HashMap<String, String> {
2138+
let Some(args) = type_arg_names(type_name) else {
2139+
return HashMap::new();
2140+
};
2141+
type_params
2142+
.iter()
2143+
.zip(args)
2144+
.map(|(param, arg)| (param.clone(), arg.to_string()))
2145+
.collect()
2146+
}
2147+
21232148
fn pattern_sum_variant_fields(
21242149
analyzer: &Analyzer<'_>,
21252150
root: &str,

crates/rsscript/src/formatter.rs

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,13 @@ impl Formatter {
336336
if stmt.is_mut {
337337
self.out.push_str("mut ");
338338
}
339-
self.out.push_str(&stmt.name);
339+
if let Some(names) = &stmt.destructure {
340+
self.out.push('(');
341+
self.out.push_str(&names.join(", "));
342+
self.out.push(')');
343+
} else {
344+
self.out.push_str(&stmt.name);
345+
}
340346
if let Some(type_annotation) = &stmt.type_annotation {
341347
self.out.push_str(": ");
342348
self.type_ref(type_annotation);
@@ -511,6 +517,9 @@ impl Formatter {
511517
self.out.push(']');
512518
}
513519
Expr::Call { callee, args, .. } => {
520+
if self.tuple_literal(callee, args, indent) {
521+
return;
522+
}
514523
if self.receiver_call_chain(expr, indent) {
515524
return;
516525
}
@@ -681,6 +690,35 @@ impl Formatter {
681690
self.out.push(']');
682691
}
683692

693+
/// Render a synthetic tuple constructor `__TupleN(item0: .., item1: ..)` back
694+
/// as `(.., ..)`. Returns `false` if the call is not a well-formed tuple
695+
/// literal, leaving it to the ordinary call renderer.
696+
fn tuple_literal(&mut self, callee: &Callee, args: &[CallArg], indent: usize) -> bool {
697+
let Callee::Name(name) = callee else {
698+
return false;
699+
};
700+
let Some(arity) = tuple_arity_of(name) else {
701+
return false;
702+
};
703+
if args.len() != arity
704+
|| !args
705+
.iter()
706+
.enumerate()
707+
.all(|(index, arg)| arg.name.as_deref() == Some(format!("item{index}").as_str()))
708+
{
709+
return false;
710+
}
711+
self.out.push('(');
712+
for (index, arg) in args.iter().enumerate() {
713+
if index > 0 {
714+
self.out.push_str(", ");
715+
}
716+
self.expr_at(&arg.value, 0, indent);
717+
}
718+
self.out.push(')');
719+
true
720+
}
721+
684722
fn call_expr(&mut self, callee: &Callee, args: &[CallArg], indent: usize) {
685723
if let Some(inline) = inline_call_expr(callee, args) {
686724
if inline.len() <= MAX_INLINE_SIGNATURE_LEN {
@@ -790,6 +828,33 @@ impl Formatter {
790828
self.out.push(')');
791829
}
792830
MatchPattern::Variant { name, .. } => self.out.push_str(name),
831+
MatchPattern::Struct {
832+
name,
833+
fields,
834+
has_rest,
835+
..
836+
} if tuple_arity_of(name) == Some(fields.len())
837+
&& !has_rest
838+
&& fields.iter().enumerate().all(|(index, field)| {
839+
field.name == format!("item{index}") && field.effect.is_none()
840+
}) =>
841+
{
842+
// Render a synthetic tuple struct pattern back as `(p0, p1, ..)`.
843+
self.out.push('(');
844+
for (index, field) in fields.iter().enumerate() {
845+
if index > 0 {
846+
self.out.push_str(", ");
847+
}
848+
if field.ignored {
849+
self.out.push('_');
850+
} else if let Some(pattern) = &field.pattern {
851+
self.match_pattern(pattern);
852+
} else if let Some(binding) = &field.binding {
853+
self.out.push_str(binding);
854+
}
855+
}
856+
self.out.push(')');
857+
}
793858
MatchPattern::Struct {
794859
name,
795860
fields,
@@ -1025,6 +1090,9 @@ fn inline_expr(expr: &Expr) -> Option<String> {
10251090
Some(format!("{}[{}]", inline_expr(base)?, inline_expr(index)?))
10261091
}
10271092
Expr::Call { callee, args, .. } => {
1093+
if let Some(tuple) = inline_tuple_literal(callee, args) {
1094+
return Some(tuple);
1095+
}
10281096
let args = args
10291097
.iter()
10301098
.map(inline_call_arg)
@@ -1045,6 +1113,29 @@ fn inline_expr(expr: &Expr) -> Option<String> {
10451113
}
10461114
}
10471115

1116+
/// Render a synthetic tuple constructor `__TupleN(item0: .., ..)` inline as
1117+
/// `(.., ..)`, or `None` if the call is not a well-formed tuple literal.
1118+
fn inline_tuple_literal(callee: &Callee, args: &[CallArg]) -> Option<String> {
1119+
let Callee::Name(name) = callee else {
1120+
return None;
1121+
};
1122+
let arity = tuple_arity_of(name)?;
1123+
if args.len() != arity
1124+
|| !args
1125+
.iter()
1126+
.enumerate()
1127+
.all(|(index, arg)| arg.name.as_deref() == Some(format!("item{index}").as_str()))
1128+
{
1129+
return None;
1130+
}
1131+
let inner = args
1132+
.iter()
1133+
.map(|arg| inline_expr(&arg.value))
1134+
.collect::<Option<Vec<_>>>()?
1135+
.join(", ");
1136+
Some(format!("({inner})"))
1137+
}
1138+
10481139
fn inline_call_arg(arg: &CallArg) -> Option<String> {
10491140
let value = inline_expr(&arg.value)?;
10501141
Some(match &arg.name {
@@ -1175,7 +1266,31 @@ fn format_effect(effect: &EffectDecl) -> String {
11751266
}
11761267
}
11771268

1269+
/// The tuple arity encoded in a synthetic `__TupleN` name (`N >= 2`), if any.
1270+
fn tuple_arity_of(name: &str) -> Option<usize> {
1271+
name.strip_prefix("__Tuple")
1272+
.and_then(|n| n.parse::<usize>().ok())
1273+
.filter(|arity| *arity >= 2)
1274+
}
1275+
11781276
fn type_ref_text(ty: &TypeRef) -> String {
1277+
// Render synthetic tuple types `__TupleN<...>` back as `(A, B, ...)`.
1278+
if let Some(arity) = tuple_arity_of(&ty.name)
1279+
&& ty.args.len() == arity
1280+
{
1281+
let inner = ty
1282+
.args
1283+
.iter()
1284+
.map(type_ref_text)
1285+
.collect::<Vec<_>>()
1286+
.join(", ");
1287+
let tuple = format!("({inner})");
1288+
return if ty.is_fresh {
1289+
format!("fresh {tuple}")
1290+
} else {
1291+
tuple
1292+
};
1293+
}
11791294
let text = if ty.name == "Fn" {
11801295
let params = ty
11811296
.fn_params
@@ -1739,6 +1854,46 @@ fn write_line<W: Writer>(writer: mut W, message: read String) -> Unit {
17391854
impl Writer for BufferWriter {
17401855
write = BufferWriter.write
17411856
}
1857+
"#
1858+
);
1859+
}
1860+
1861+
#[test]
1862+
fn renders_tuple_literals_types_patterns_and_destructuring() {
1863+
let source = r#"fn first(p:read (Int,String))->Int{
1864+
match read p{
1865+
(0,_)=>{return 0}
1866+
(n,_)=>{return n}
1867+
}
1868+
}
1869+
1870+
fn main()->Unit{
1871+
let (a,b)=(5,"x")
1872+
Log.write(message:read b)
1873+
Log.write(message:read String.from_int(value:first(p:read (a,b))))
1874+
return Unit
1875+
}
1876+
"#;
1877+
1878+
assert_eq!(
1879+
format_source("tuples.rss", source),
1880+
r#"fn first(p: read (Int, String)) -> Int {
1881+
match read p {
1882+
(0, _) => {
1883+
return 0
1884+
}
1885+
(n, _) => {
1886+
return n
1887+
}
1888+
}
1889+
}
1890+
1891+
fn main() -> Unit {
1892+
let (a, b) = (5, "x")
1893+
Log.write(message: read b)
1894+
Log.write(message: read String.from_int(value: first(p: read (a, b))))
1895+
return Unit
1896+
}
17421897
"#
17431898
);
17441899
}

0 commit comments

Comments
 (0)