Skip to content

Commit b80041e

Browse files
Haofeiclaude
andcommitted
test(testgen): T3 — collections (List / Map / Set / Deque)
Generate populated collections as mutable locals (`C.new<..>()` + 1..=3 inserts) and observe them by length (a deterministic Int; element iteration order is not backend-stable). Map keys / Set elements are restricted to hashable scalars (Int/Bool/String). Collections are intentionally kept out of arbitrary binding/param/return positions (see the finding below). Smoke: >=95% accept rate, vm-interpreter == vm-jit. Full N-way (incl. compiled) green at RSS_GENERATIVE_CASES=30. Note (framework's second find): an empty `C.new<T>()` whose only use is `len()` lowers to a Rust `C::new()` with an un-inferable element type (`E0282: type annotations needed for VecDeque<_>`) — RSScript accepts it (even with a `let v: Deque<Float> = ...` annotation), the VM runs it, the compiled backend won't build. Same "valid RSScript -> invalid Rust" class as the earlier `Ok(x)` finding (lost generic type info on lowering). Worked around by always populating generated collections; the lowering gap is filed separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 26e732a commit b80041e

2 files changed

Lines changed: 158 additions & 6 deletions

File tree

crates/rss-testgen/src/generator.rs

Lines changed: 144 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,20 @@ impl<'a> Generator<'a> {
5858
SCALARS[self.seed.choice(SCALARS.len())].clone()
5959
}
6060

61+
/// A scalar usable as a `Map` key / `Set` element (`Hashable`): no `Float`.
62+
fn pick_hashable_scalar(&mut self) -> Ty {
63+
const HASHABLE: &[Ty] = &[Ty::Int, Ty::Bool, Ty::String];
64+
HASHABLE[self.seed.choice(HASHABLE.len())].clone()
65+
}
66+
6167
/// A type for a binding/param/return: scalars dominate; compounds appear when
6268
/// available. Compound payloads are restricted to scalars to keep the surface
6369
/// finite and generation simple.
6470
fn pick_type(&mut self) -> Ty {
71+
// Collections are deliberately *not* pickable here: an empty `C.new<T>()`
72+
// in an inferred position lowers to a Rust `C::new()` whose element type
73+
// can't be inferred (E0282) when only `len()` is observed. Collections are
74+
// instead emitted, always populated, by `gen_collection_stmt`.
6575
match self.seed.weighted(&[10, 2, 2, 3, 3]) {
6676
0 => self.pick_scalar(),
6777
1 => Ty::Option(Box::new(self.pick_scalar())),
@@ -294,6 +304,24 @@ impl<'a> Generator<'a> {
294304
.join("\n");
295305
format!("{pad}match {access} {{\n{arms}\n{pad}}}\n")
296306
}
307+
// Collections are observed by their length (a deterministic Int);
308+
// element iteration order isn't guaranteed identical across backends.
309+
Ty::List(_) => {
310+
let len = format!("List.len(list: read {access})");
311+
format!("{pad}{}\n", self.log_scalar(&len, &Ty::Int))
312+
}
313+
Ty::Map(_, _) => {
314+
let len = format!("Map.len(map: read {access})");
315+
format!("{pad}{}\n", self.log_scalar(&len, &Ty::Int))
316+
}
317+
Ty::Set(_) => {
318+
let len = format!("Set.len(set: read {access})");
319+
format!("{pad}{}\n", self.log_scalar(&len, &Ty::Int))
320+
}
321+
Ty::Deque(_) => {
322+
let len = format!("Deque.len(deque: read {access})");
323+
format!("{pad}{}\n", self.log_scalar(&len, &Ty::Int))
324+
}
297325
}
298326
}
299327

@@ -328,19 +356,116 @@ impl<'a> Generator<'a> {
328356
// -- statements ------------------------------------------------------
329357

330358
fn gen_stmt(&mut self, scope: &mut Scope) -> String {
331-
// Bias toward lets; sometimes assign; sometimes a `?`-unwrap.
332-
match self.seed.weighted(&[6, 2, 2]) {
359+
// Bias toward lets; sometimes assign, a `?`-unwrap, or a populated
360+
// collection.
361+
match self.seed.weighted(&[6, 2, 2, 3]) {
333362
0 => self.gen_let(scope),
334363
1 => self
335364
.gen_assign(scope)
336365
.unwrap_or_else(|| self.gen_let(scope)),
337-
_ => self
366+
2 => self
338367
.gen_try(scope)
339368
.or_else(|| self.gen_assign(scope))
340369
.unwrap_or_else(|| self.gen_let(scope)),
370+
_ => self.gen_collection_stmt(scope),
341371
}
342372
}
343373

374+
/// Build a populated collection: `let mut v: C = C.new<..>()` followed by a
375+
/// few inserts. Returned as a single (possibly multi-line) statement whose
376+
/// continuation lines are pre-indented to match the caller's block.
377+
fn gen_collection_stmt(&mut self, scope: &mut Scope) -> String {
378+
let name = self.fresh_var();
379+
// Always at least one element: a non-empty collection lets the Rust
380+
// backend infer the element type from the pushed literal (an empty
381+
// `C.new<T>()` observed only via `len()` is un-inferable — E0282).
382+
let count = 1 + self.seed.choice(3); // 1..=3 elements
383+
let (ty, new_expr, mut inserts, len_expr): (Ty, String, Vec<String>, String) =
384+
match self.seed.choice(4) {
385+
0 => {
386+
let elem = self.pick_scalar();
387+
let ty = Ty::List(Box::new(elem.clone()));
388+
let inserts = (0..count)
389+
.map(|_| {
390+
let v = self.gen_literal(&elem);
391+
format!("List.push(list: mut {name}, value: read {v})")
392+
})
393+
.collect();
394+
(
395+
ty,
396+
format!("List.new<{}>()", elem.render()),
397+
inserts,
398+
format!("List.len(list: read {name})"),
399+
)
400+
}
401+
1 => {
402+
let elem = self.pick_scalar();
403+
let ty = Ty::Deque(Box::new(elem.clone()));
404+
let inserts = (0..count)
405+
.map(|_| {
406+
let v = self.gen_literal(&elem);
407+
format!("Deque.push_back(deque: mut {name}, value: read {v})")
408+
})
409+
.collect();
410+
(
411+
ty,
412+
format!("Deque.new<{}>()", elem.render()),
413+
inserts,
414+
format!("Deque.len(deque: read {name})"),
415+
)
416+
}
417+
2 => {
418+
let elem = self.pick_hashable_scalar();
419+
let ty = Ty::Set(Box::new(elem.clone()));
420+
let inserts = (0..count)
421+
.map(|_| {
422+
let v = self.gen_literal(&elem);
423+
format!("Set.insert(set: mut {name}, value: read {v})")
424+
})
425+
.collect();
426+
(
427+
ty,
428+
format!("Set.new<{}>()", elem.render()),
429+
inserts,
430+
format!("Set.len(set: read {name})"),
431+
)
432+
}
433+
_ => {
434+
let key = self.pick_hashable_scalar();
435+
let value = self.pick_scalar();
436+
let ty = Ty::Map(Box::new(key.clone()), Box::new(value.clone()));
437+
let inserts = (0..count)
438+
.map(|_| {
439+
let k = self.gen_literal(&key);
440+
let v = self.gen_literal(&value);
441+
format!("Map.insert(map: mut {name}, key: read {k}, value: read {v})")
442+
})
443+
.collect();
444+
(
445+
ty,
446+
format!("Map.new<{}, {}>()", key.render(), value.render()),
447+
inserts,
448+
format!("Map.len(map: read {name})"),
449+
)
450+
}
451+
};
452+
let ty_render = ty.render();
453+
scope.bindings.push(Binding {
454+
name: name.clone(),
455+
ty,
456+
mutable: true,
457+
});
458+
let mut lines = vec![format!("let mut {name}: {ty_render} = {new_expr}")];
459+
lines.append(&mut inserts);
460+
// Observe the length so the collection contributes to stdout (deterministic
461+
// across backends, unlike element iteration order).
462+
lines.push(format!(
463+
"Log.write(message: read String.from_int(value: {len_expr}))"
464+
));
465+
// Caller prefixes the first line with one indent; pre-indent the rest.
466+
lines.join("\n ")
467+
}
468+
344469
fn gen_let(&mut self, scope: &mut Scope) -> String {
345470
let ty = self.pick_type();
346471
// Occasionally exercise the None/Err constructors (they need a known type,
@@ -496,6 +621,14 @@ impl<'a> Generator<'a> {
496621
let def = self.sum_def(name);
497622
def.variants[self.seed.choice(def.variants.len())].clone()
498623
}
624+
// Collections are constructed empty here; populated instances are
625+
// built by `gen_collection_stmt` (new + a few inserts).
626+
Ty::List(inner) => format!("List.new<{}>()", inner.render()),
627+
Ty::Map(key, value) => {
628+
format!("Map.new<{}, {}>()", key.render(), value.render())
629+
}
630+
Ty::Set(inner) => format!("Set.new<{}>()", inner.render()),
631+
Ty::Deque(inner) => format!("Deque.new<{}>()", inner.render()),
499632
}
500633
}
501634

@@ -597,9 +730,14 @@ impl<'a> Generator<'a> {
597730
}
598731
},
599732
// Compounds have no operator forms: just construct.
600-
Ty::Option(_) | Ty::Result(_, _) | Ty::Struct(_) | Ty::Sum(_) => {
601-
self.gen_construct(ty, scope)
602-
}
733+
Ty::Option(_)
734+
| Ty::Result(_, _)
735+
| Ty::Struct(_)
736+
| Ty::Sum(_)
737+
| Ty::List(_)
738+
| Ty::Map(_, _)
739+
| Ty::Set(_)
740+
| Ty::Deque(_) => self.gen_construct(ty, scope),
603741
}
604742
}
605743

crates/rss-testgen/src/ty.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ pub enum Ty {
2020
Struct(String),
2121
/// A declared (nullary-variant) `sum` by name.
2222
Sum(String),
23+
List(Box<Ty>),
24+
Map(Box<Ty>, Box<Ty>),
25+
Set(Box<Ty>),
26+
Deque(Box<Ty>),
2327
}
2428

2529
impl Ty {
@@ -33,9 +37,19 @@ impl Ty {
3337
Ty::Option(inner) => format!("Option<{}>", inner.render()),
3438
Ty::Result(ok, err) => format!("Result<{}, {}>", ok.render(), err.render()),
3539
Ty::Struct(name) | Ty::Sum(name) => name.clone(),
40+
Ty::List(inner) => format!("List<{}>", inner.render()),
41+
Ty::Map(key, value) => format!("Map<{}, {}>", key.render(), value.render()),
42+
Ty::Set(inner) => format!("Set<{}>", inner.render()),
43+
Ty::Deque(inner) => format!("Deque<{}>", inner.render()),
3644
}
3745
}
3846

47+
/// A builtin scalar usable as a `Map` key / `Set` element (`Hashable`):
48+
/// `Int`/`Bool`/`String` (not `Float`).
49+
pub fn is_hashable_scalar(&self) -> bool {
50+
matches!(self, Ty::Int | Ty::Bool | Ty::String)
51+
}
52+
3953
/// Whether a *parameter* of this type is declared with a `read` effect prefix.
4054
/// Copy scalars (Int/Bool/Float) are declared bare (`a: Int`); every reference
4155
/// type (`String`, `Option`, `Result`, structs, sums) is declared `x: read T`.

0 commit comments

Comments
 (0)