Skip to content

Commit d20dd60

Browse files
Haofeiclaude
andcommitted
test: cover Hashable Map/Set keys and a UOp interning example
Add pass fixtures for a multi-field struct Map key (insert/lookup/ overwrite/dedup), a Set of that struct, an enum-with-payload key type, and the canonical tinygrad-style UOp used as a Map<Uop, Uop> intern key and a dedup Set<Uop>. Add fail fixtures asserting RS0032 for non-hashable Map keys and Set elements. Add dedicated checker tests (positive, negative, builtin-key non-regression, and end-to-end UOp lowering) and a runnable examples/scripts/core/uop_interning.rss. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9039350 commit d20dd60

8 files changed

Lines changed: 325 additions & 0 deletions

File tree

crates/rsscript/tests/checker_frontend.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,135 @@ fn check_label(actual: read String, expected: read String) -> Unit {
398398
assert!(codes.contains(&"RS0204".to_string()));
399399
}
400400

401+
#[test]
402+
fn hashable_struct_is_accepted_as_map_key_and_set_element() {
403+
let source = r#"
404+
struct Coord derives(Clone, Eq, Hash) {
405+
x: Int
406+
y: Int
407+
}
408+
409+
fn main() -> Unit {
410+
let m = Map.new<Coord, Int>()
411+
let c = Coord(x: 1, y: 2)
412+
Map.insert(map: mut m, key: read c, value: read 10)
413+
let here = Map.contains_key(map: read m, key: read c)
414+
let s = Set.new<Coord>()
415+
let added = Set.insert(set: mut s, value: read c)
416+
return Unit
417+
}
418+
"#;
419+
assert_eq!(
420+
analyze_source_with_core("hashable-key.rss", source),
421+
Vec::new()
422+
);
423+
}
424+
425+
#[test]
426+
fn non_hashable_struct_map_key_is_rejected_in_rsscript() {
427+
// A struct without `derives(Hash)` is not `Hashable`, so it must be rejected
428+
// with RS0032 in RSScript rather than leaking a rustc trait-bound error.
429+
let source = r#"
430+
struct Coord derives(Clone) {
431+
x: Int
432+
y: Int
433+
}
434+
435+
fn main() -> Unit {
436+
let m = Map.new<Coord, Int>()
437+
let c = Coord(x: 1, y: 2)
438+
Map.insert(map: mut m, key: read c, value: read 10)
439+
return Unit
440+
}
441+
"#;
442+
let codes = analyze_source_with_core("non-hashable-key.rss", source)
443+
.into_iter()
444+
.map(|diagnostic| diagnostic.code)
445+
.collect::<Vec<_>>();
446+
assert!(
447+
codes.contains(&"RS0032".to_string()),
448+
"expected RS0032, got {codes:?}"
449+
);
450+
}
451+
452+
#[test]
453+
fn non_hashable_set_element_is_rejected_in_rsscript() {
454+
let source = r#"
455+
struct Coord derives(Clone) {
456+
x: Int
457+
y: Int
458+
}
459+
460+
fn main() -> Unit {
461+
let s = Set.new<Coord>()
462+
let c = Coord(x: 1, y: 2)
463+
let added = Set.insert(set: mut s, value: read c)
464+
return Unit
465+
}
466+
"#;
467+
let codes = analyze_source_with_core("non-hashable-set.rss", source)
468+
.into_iter()
469+
.map(|diagnostic| diagnostic.code)
470+
.collect::<Vec<_>>();
471+
assert!(
472+
codes.contains(&"RS0032".to_string()),
473+
"expected RS0032, got {codes:?}"
474+
);
475+
}
476+
477+
#[test]
478+
fn builtin_scalar_map_keys_remain_accepted() {
479+
// Widening Map/Set keys to `K: Hashable` must not regress the common
480+
// String/Int key cases.
481+
let source = r#"
482+
fn main() -> Unit {
483+
let by_name = Map.new<String, Int>()
484+
Map.insert(map: mut by_name, key: read "a", value: read 1)
485+
let by_id = Map.new<Int, String>()
486+
Map.insert(map: mut by_id, key: read 7, value: read "seven")
487+
let ids = Set.new<Int>()
488+
let added = Set.insert(set: mut ids, value: read 7)
489+
return Unit
490+
}
491+
"#;
492+
assert_eq!(
493+
analyze_source_with_core("builtin-keys.rss", source),
494+
Vec::new()
495+
);
496+
}
497+
498+
#[test]
499+
fn interned_uop_struct_lowers_to_runnable_package() {
500+
// Canonical target case: a tinygrad-style UOp struct used as a Map<Uop, Uop>
501+
// intern key and a dedup Set<Uop>, end-to-end through Rust lowering.
502+
let source = r#"
503+
struct Uop derives(Clone, Eq, Hash) {
504+
op: Int
505+
children: List<Int>
506+
arg: Option<Int>
507+
}
508+
509+
fn main() -> Unit {
510+
let intern = Map.new<Uop, Uop>()
511+
let a = Uop(op: 1, children: List.new<Int>(), arg: Some(7))
512+
Map.insert(map: mut intern, key: read a, value: read a)
513+
let here = Map.contains_key(map: read intern, key: read a)
514+
let seen = Set.new<Uop>()
515+
let added = Set.insert(set: mut seen, value: read a)
516+
return Unit
517+
}
518+
"#;
519+
assert_eq!(analyze_source_with_core("uop.rss", source), Vec::new());
520+
let package =
521+
lower_source_to_rust_package("uop.rss", source, "uop", &common::runtime_path())
522+
.unwrap_or_else(|diagnostics| panic!("uop.rss: {diagnostics:?}"));
523+
assert!(package.main_rs.is_some());
524+
let main_rs = package.main_rs.unwrap();
525+
assert!(!main_rs.contains("todo!"));
526+
// The derived struct hash/eq lower to Rust derives that make it a HashMap key.
527+
assert!(package.lib_rs.contains("Hash"));
528+
}
529+
401530
#[test]
402531
fn bundled_interpreter_function_object_new_does_not_retain_closure() {
403532
let source = r#"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// expect: RS0032
2+
3+
// A struct that does not derive Hash cannot be a Map key. The Hashable bound on
4+
// Map keys is checked in RSScript instead of leaking as a rustc trait-bound error.
5+
struct Coord derives(Clone) {
6+
x: Int
7+
y: Int
8+
}
9+
10+
fn main() -> Unit {
11+
let m = Map.new<Coord, Int>()
12+
let c = Coord(x: 1, y: 2)
13+
Map.insert(map: mut m, key: read c, value: read 10)
14+
return Unit
15+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// expect: RS0032
2+
3+
// A struct that does not derive Hash cannot be a Set element.
4+
struct Coord derives(Clone) {
5+
x: Int
6+
y: Int
7+
}
8+
9+
fn main() -> Unit {
10+
let s = Set.new<Coord>()
11+
let c = Coord(x: 1, y: 2)
12+
let added = Set.insert(set: mut s, value: read c)
13+
return Unit
14+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// A sealed sum (enum) that carries payload variants, deriving Eq+Hash, used as a
2+
// Map key. The Hashable bound accepts the enum because every variant payload
3+
// field (`Int`, `String`) is itself hashable; the RS0211 derive check separately
4+
// validates the payload fields. (v0.6 constructs nullary variant values and
5+
// `match`es payloads; user payload-variant construction is not yet an executable
6+
// form, so the key values here are the nullary variants.)
7+
8+
sum Token derives(Clone, Eq, Hash) {
9+
End
10+
Number(value: Int)
11+
Name(text: String)
12+
}
13+
14+
fn classify(token: read Token) -> Int {
15+
match token {
16+
End => { return 0 }
17+
Number(value) => { return value }
18+
Name(text) => { return 1 }
19+
}
20+
}
21+
22+
fn main() -> Unit {
23+
let counts = Map.new<Token, Int>()
24+
let end = End
25+
26+
Map.insert(map: mut counts, key: read end, value: read classify(token: read end))
27+
let here = Map.contains_key(map: read counts, key: read end)
28+
if here {
29+
Log.write(message: read "token seen")
30+
}
31+
return Unit
32+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// A user struct with several fields, deriving Eq+Hash, used as a Map key:
2+
// insert, lookup, overwrite (same key), and dedup.
3+
4+
struct Coord derives(Clone, Eq, Hash) {
5+
x: Int
6+
y: Int
7+
label: String
8+
}
9+
10+
fn main() -> Unit {
11+
let m = Map.new<Coord, Int>()
12+
let a = Coord(x: 1, y: 2, label: "a")
13+
let b = Coord(x: 3, y: 4, label: "b")
14+
let a_dup = Coord(x: 1, y: 2, label: "a")
15+
16+
Map.insert(map: mut m, key: read a, value: read 10)
17+
Map.insert(map: mut m, key: read b, value: read 20)
18+
// Overwrite: structurally equal key replaces the existing entry.
19+
Map.insert(map: mut m, key: read a_dup, value: read 99)
20+
21+
let here = Map.contains_key(map: read m, key: read a)
22+
let got = Map.get(map: read m, key: read a_dup)
23+
let n = Map.len(map: read m)
24+
25+
if here {
26+
Log.write(message: read "found")
27+
}
28+
return Unit
29+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// A Set of a user struct: membership and dedup.
2+
3+
struct Coord derives(Clone, Eq, Hash) {
4+
x: Int
5+
y: Int
6+
}
7+
8+
fn main() -> Unit {
9+
let seen = Set.new<Coord>()
10+
let a = Coord(x: 1, y: 2)
11+
let a_dup = Coord(x: 1, y: 2)
12+
13+
let first = Set.insert(set: mut seen, value: read a)
14+
// Structural duplicate: insert returns false, set stays size 1.
15+
let again = Set.insert(set: mut seen, value: read a_dup)
16+
17+
let present = Set.contains(set: read seen, value: read a_dup)
18+
let count = Set.len(set: read seen)
19+
20+
if present {
21+
Log.write(message: read "member")
22+
}
23+
return Unit
24+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Canonical target case: a small struct standing in for tinygrad's UOp
2+
// (an int op tag + a list of child ids + an optional arg) used as a
3+
// Map<Uop, Uop> intern key and in a dedup Set<Uop>.
4+
5+
struct Uop derives(Clone, Eq, Hash) {
6+
op: Int
7+
children: List<Int>
8+
arg: Option<Int>
9+
}
10+
11+
fn main() -> Unit {
12+
let intern = Map.new<Uop, Uop>()
13+
let a = Uop(op: 1, children: List.new<Int>(), arg: Some(7))
14+
let b = Uop(op: 2, children: List.new<Int>(), arg: None)
15+
let a_dup = Uop(op: 1, children: List.new<Int>(), arg: Some(7))
16+
17+
Map.insert(map: mut intern, key: read a, value: read a)
18+
Map.insert(map: mut intern, key: read b, value: read b)
19+
Map.insert(map: mut intern, key: read a_dup, value: read a_dup)
20+
let interned = Map.contains_key(map: read intern, key: read a_dup)
21+
22+
let seen = Set.new<Uop>()
23+
let added = Set.insert(set: mut seen, value: read a)
24+
let added_dup = Set.insert(set: mut seen, value: read a_dup)
25+
26+
if interned {
27+
Log.write(message: read "uop interned")
28+
}
29+
return Unit
30+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Graph interning / dedup over a user-defined managed type used as a hash key.
2+
//
3+
// `Uop` stands in for a tinygrad-style UOp node: an integer `op` tag, a list of
4+
// child ids, and an optional integer arg. Deriving `Eq, Hash` makes it a
5+
// `Hashable` value, so it can be a `Map<Uop, Uop>` key (interning each node to
6+
// its canonical instance) and a `Set<Uop>` element (dedup). No hand-written
7+
// hash or equality is needed; the compiler derives the structural ones.
8+
9+
struct Uop derives(Clone, Eq, Hash) {
10+
op: Int
11+
children: List<Int>
12+
arg: Option<Int>
13+
}
14+
15+
fn main() -> Unit {
16+
// Intern table: map each Uop to its canonical (first-seen) Uop.
17+
let intern = Map.new<Uop, Uop>()
18+
19+
let a = Uop(op: 1, children: List.new<Int>(), arg: Some(7))
20+
let b = Uop(op: 2, children: List.new<Int>(), arg: None)
21+
// `a_again` is a structurally identical, separately-constructed value.
22+
let a_again = Uop(op: 1, children: List.new<Int>(), arg: Some(7))
23+
24+
Map.insert(map: mut intern, key: read a, value: read a)
25+
Map.insert(map: mut intern, key: read b, value: read b)
26+
// Overwrite-with-same-key path: inserting a structural duplicate replaces.
27+
Map.insert(map: mut intern, key: read a_again, value: read a_again)
28+
29+
let size = Map.len(map: read intern)
30+
Log.write(message: read String.concat(
31+
left: read "intern size: ",
32+
right: read Int.to_string(value: read size),
33+
))
34+
35+
// Lookup the canonical node for a structural duplicate.
36+
let found = Map.contains_key(map: read intern, key: read a_again)
37+
if found {
38+
Log.write(message: read "a_again interns to an existing node")
39+
}
40+
41+
// Dedup with a Set<Uop>.
42+
let seen = Set.new<Uop>()
43+
let inserted_a = Set.insert(set: mut seen, value: read a)
44+
let inserted_dup = Set.insert(set: mut seen, value: read a_again)
45+
let unique = Set.len(set: read seen)
46+
Log.write(message: read String.concat(
47+
left: read "unique nodes: ",
48+
right: read Int.to_string(value: read unique),
49+
))
50+
51+
return Unit
52+
}

0 commit comments

Comments
 (0)