Skip to content

Commit 7358f50

Browse files
committed
feat(rss): Clone protocol + Clone.clone<T> / String.clone (managed-value clone)
Adds a Clone capability protocol (mirrors Ord/Eq): clone() deep-copies any builtin scalar, derives(Clone) user struct/sum, and List/Option/Result of Clone elements. Closes the clone-gap (could not copy a String/managed field out of a read value -> E0507): now a read field copies via Clone.clone<T>(self: read x). Wired across stdlib/clone/clone.rssi, interfaces, checks/calls protocol satisfaction, analyzer fresh-Self bound, reg_vm (RegIntrinsic::CloneClone + resolver + receiver call), runtime_abi + runtime clone_value, regenerated core index + vm parity. Full cargo test -p rsscript green (25 targets, 0 failed). Subagent-produced, applied + reverified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 489e94a commit 7358f50

10 files changed

Lines changed: 104 additions & 6 deletions

File tree

crates/rsscript/src/analyzer.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3336,9 +3336,15 @@ impl Analyzer<'_> {
33363336
bounds: &HashMap<String, Option<GenericBound>>,
33373337
) {
33383338
let target = fresh_return_target_type(return_ty);
3339-
if bounds.contains_key(&target.name)
3340-
&& bounds.get(&target.name).and_then(Option::as_ref) != Some(&GenericBound::Struct)
3341-
{
3339+
// A protocol method's implicit `Self` parameter is bound `Managed`, which
3340+
// still admits a `fresh Self` return: managed structs/sums are freshly
3341+
// ownable, and the per-instantiation derive (`derives(Clone)`) is checked
3342+
// at the use site. A `fresh Self` from a value scalar is impossible
3343+
// because scalars do not satisfy the protocol's `Managed` `Self` bound.
3344+
let bound = bounds.get(&target.name).and_then(Option::as_ref);
3345+
let fresh_bound_ok = matches!(bound, Some(GenericBound::Struct))
3346+
|| (target.name == "Self" && matches!(bound, Some(GenericBound::Managed)));
3347+
if bounds.contains_key(&target.name) && !fresh_bound_ok {
33423348
self.diagnostics.push(
33433349
Diagnostic::error(
33443350
code::INVALID_FRESH_RETURN_TYPE,

crates/rsscript/src/checks/calls.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1480,6 +1480,9 @@ fn type_satisfies_protocol_bound(
14801480
if (protocol == "Hashable" || protocol == "Eq") && builtin_type_is_hashable(actual_root) {
14811481
return true;
14821482
}
1483+
if protocol == "Clone" && builtin_type_is_clone(actual_root) {
1484+
return true;
1485+
}
14831486
// `List<T>`/`Option<T>`/`Result<A, B>` are `Hashable`/`Eq` exactly when their
14841487
// element types are, so a key like `List<Coord>` is satisfiable structurally.
14851488
if (protocol == "Hashable" || protocol == "Eq")
@@ -1491,6 +1494,15 @@ fn type_satisfies_protocol_bound(
14911494
.all(|arg| type_satisfies_protocol_bound(analyzer, function, arg, protocol));
14921495
}
14931496
}
1497+
// `List<T>`/`Option<T>`/`Result<A, B>` are `Clone` exactly when their element
1498+
// types are, mirroring the structural derive support for value containers.
1499+
if protocol == "Clone" && matches!(actual_root, "List" | "Option" | "Result") {
1500+
if let Some(args) = type_arg_names(strip_fresh_type(actual)) {
1501+
return args
1502+
.iter()
1503+
.all(|arg| type_satisfies_protocol_bound(analyzer, function, arg, protocol));
1504+
}
1505+
}
14941506
if function.type_params.iter().any(|param| {
14951507
param.name == actual_root
14961508
&& matches!(
@@ -1611,6 +1623,32 @@ fn builtin_type_is_hashable(type_name: &str) -> bool {
16111623
)
16121624
}
16131625

1626+
/// Builtin scalar types that are `Clone` directly (no derive needed). Every
1627+
/// value scalar is copyable, including `Float` (which is not `Eq`/`Hash`).
1628+
fn builtin_type_is_clone(type_name: &str) -> bool {
1629+
matches!(
1630+
type_name,
1631+
"Int"
1632+
| "Int8"
1633+
| "Int16"
1634+
| "Int32"
1635+
| "Int64"
1636+
| "UInt"
1637+
| "UInt8"
1638+
| "UInt16"
1639+
| "UInt32"
1640+
| "UInt64"
1641+
| "Bool"
1642+
| "Byte"
1643+
| "Char"
1644+
| "Unit"
1645+
| "Float"
1646+
| "Float32"
1647+
| "Float64"
1648+
| "String"
1649+
)
1650+
}
1651+
16141652
/// Whether a user-declared `type_name` satisfies a compiler-derived `protocol`
16151653
/// bound. `Ord` requires `derives(Ord)`; `Hashable` requires `derives(Hash)`;
16161654
/// `Eq` requires `derives(Eq)` or `derives(Ord)` (which implies `Eq`).
@@ -1621,10 +1659,11 @@ fn type_derives_protocol(items: &[Item], type_name: &str, protocol: &str) -> boo
16211659
"Ord" => has("Ord"),
16221660
"Hashable" => has("Hash"),
16231661
"Eq" => has("Eq") || has("Ord"),
1662+
"Clone" => has("Clone"),
16241663
_ => false,
16251664
}
16261665
};
1627-
if !matches!(protocol, "Ord" | "Hashable" | "Eq") {
1666+
if !matches!(protocol, "Ord" | "Hashable" | "Eq" | "Clone") {
16281667
return false;
16291668
}
16301669
items.iter().any(|item| match item {

crates/rsscript/src/interfaces.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ pub(crate) const CORE_INTERFACES: &[(&str, &str)] = &[
6565
"stdlib/clock/clock.rssi",
6666
include_str!("../../../stdlib/clock/clock.rssi"),
6767
),
68+
(
69+
"stdlib/clone/clone.rssi",
70+
include_str!("../../../stdlib/clone/clone.rssi"),
71+
),
6872
("stdlib/cmp/eq.rssi", include_str!("../../../stdlib/cmp/eq.rssi")),
6973
("stdlib/cmp/ord.rssi", include_str!("../../../stdlib/cmp/ord.rssi")),
7074
(

crates/rsscript/src/reg_vm/mod.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1997,6 +1997,7 @@ enum RegIntrinsic {
19971997
CharToLower,
19981998
CharToString,
19991999
CharToUpper,
2000+
CloneClone,
20002001
ClockNow,
20012002
ClockSystemUnixMs,
20022003
ConfigLoad,
@@ -3578,6 +3579,14 @@ impl RegLowerer<'_> {
35783579
.map(|arg| self.expr(&arg.value))
35793580
.collect::<Result<Vec<_>, _>>()?;
35803581
match (type_root_name(namespace), type_root_name(method)) {
3582+
("Clone", "clone") => {
3583+
self.emit(RegInstr::CallIntrinsic {
3584+
dst,
3585+
intrinsic: RegIntrinsic::CloneClone,
3586+
args: vec![receiver_reg],
3587+
});
3588+
return Ok(dst);
3589+
}
35813590
("Float", "to_string") => {
35823591
self.emit(RegInstr::CallIntrinsic {
35833592
dst,
@@ -4648,6 +4657,7 @@ impl RegLowerer<'_> {
46484657
("Option", "or") => RegIntrinsic::OptionOr,
46494658
("Option", "unwrap_or") => RegIntrinsic::OptionUnwrapOr,
46504659
("Option", "unwrap_or_else") => RegIntrinsic::OptionUnwrapOrElse,
4660+
("Clone", "clone") => RegIntrinsic::CloneClone,
46514661
("Ord", "compare") => RegIntrinsic::OrdCompare,
46524662
("OS", "close") => RegIntrinsic::OsClose,
46534663
("Patch", "apply_text") => RegIntrinsic::PatchApplyText,
@@ -4958,7 +4968,7 @@ impl RegLowerer<'_> {
49584968
("String", "char_at") => RegIntrinsic::StringCharAt,
49594969
("String", "contains") => RegIntrinsic::StringContains,
49604970
("String", "count") => RegIntrinsic::StringCount,
4961-
("String", "copy") => RegIntrinsic::StringCopy,
4971+
("String", "copy") | ("String", "clone") => RegIntrinsic::StringCopy,
49624972
("String", "ends_with") => RegIntrinsic::StringEndsWith,
49634973
("String", "env") => RegIntrinsic::EnvGet,
49644974
("String", "env_or") => RegIntrinsic::EnvGetOrDefault,
@@ -11156,6 +11166,10 @@ impl RegVm {
1115611166
))),
1115711167
}
1115811168
}
11169+
RegIntrinsic::CloneClone => {
11170+
let value = intrinsic_arg(&self.stack, base, args, 0)?;
11171+
Ok(deep_copy_value(value))
11172+
}
1115911173
RegIntrinsic::OrdCompare => {
1116011174
let left = intrinsic_arg(&self.stack, base, args, 0)?;
1116111175
let right = intrinsic_arg(&self.stack, base, args, 1)?;

crates/rsscript/src/runtime_abi.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,7 @@ const RUNTIME_INTRINSICS: &[RuntimeIntrinsic] = &[
923923
runtime_intrinsic("Deque", "push_back", "rsscript_runtime::deque_push_back"),
924924
runtime_intrinsic("Deque", "push_front", "rsscript_runtime::deque_push_front"),
925925
runtime_intrinsic("Deque", "to_list", "rsscript_runtime::deque_to_list"),
926+
runtime_intrinsic("Clone", "clone", "rsscript_runtime::clone_value"),
926927
runtime_intrinsic("Ord", "compare", "rsscript_runtime::ord_compare"),
927928
runtime_intrinsic("List", "all", "rsscript_runtime::list_all"),
928929
runtime_intrinsic("List", "any", "rsscript_runtime::list_any"),
@@ -1365,6 +1366,7 @@ const RUNTIME_INTRINSICS: &[RuntimeIntrinsic] = &[
13651366
runtime_intrinsic("String", "before", "rsscript_runtime::string_before"),
13661367
runtime_intrinsic("String", "concat", "rsscript_runtime::string_concat"),
13671368
runtime_intrinsic("String", "copy", "rsscript_runtime::string_copy"),
1369+
runtime_intrinsic("String", "clone", "rsscript_runtime::string_copy"),
13681370
runtime_intrinsic("String", "ends_with", "rsscript_runtime::string_ends_with"),
13691371
runtime_intrinsic("String", "env", "rsscript_runtime::env_get"),
13701372
runtime_intrinsic("String", "env_or", "rsscript_runtime::env_get_or_default"),

crates/rsscript/tests/vm_eval.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,7 @@ pub fn echo(message: &String) -> String {
803803
// parity: runtime:ChannelError.message
804804
// parity: runtime:CancellationSource.cancel runtime:CancellationSource.new
805805
// parity: runtime:CancellationSource.token runtime:CancellationToken.is_cancelled
806+
// parity: runtime:Clone.clone
806807
// parity: runtime:Clock.now runtime:Clock.system_unix_ms
807808
// parity: runtime:Config.load runtime:Config.name runtime:Config.new runtime:Config.rule_count
808809
// parity: runtime:ConfigStore.name runtime:ConfigStore.new runtime:ConfigStore.replace
@@ -947,7 +948,7 @@ pub fn echo(message: &String) -> String {
947948
// parity: runtime:SortedMap.values
948949
// parity: runtime:String.after runtime:String.before runtime:String.char_at runtime:String.concat
949950
// parity: runtime:String.chars
950-
// parity: runtime:String.contains runtime:String.count runtime:String.copy runtime:String.ends_with
951+
// parity: runtime:String.clone runtime:String.contains runtime:String.count runtime:String.copy runtime:String.ends_with
951952
// parity: runtime:String.env runtime:String.env_or
952953
// parity: runtime:String.format runtime:String.from_bool runtime:String.from_float runtime:String.from_int
953954
// parity: runtime:String.index_of runtime:String.is_empty runtime:String.join runtime:String.len

crates/runtime/src/collections.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ pub struct RssFalliblePipeline<T, E> {
1111
pub result: Result<Vec<T>, E>,
1212
}
1313

14+
pub fn clone_value<T: Clone>(value: &T) -> T {
15+
value.clone()
16+
}
17+
1418
pub fn ord_compare<T: Ord>(left: &T, right: &T) -> i64 {
1519
match left.cmp(right) {
1620
std::cmp::Ordering::Less => -1,

schemas/core-package-index.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@
4141
"Instant"
4242
]
4343
},
44+
{
45+
"path": "stdlib/clone/clone.rssi",
46+
"module": "clone.clone",
47+
"functions": [
48+
"clone"
49+
],
50+
"types": [
51+
"Clone"
52+
]
53+
},
4454
{
4555
"path": "stdlib/cmp/eq.rssi",
4656
"module": "cmp.eq",
@@ -877,6 +887,7 @@
877887
"String.before",
878888
"String.char_at",
879889
"String.chars",
890+
"String.clone",
880891
"String.concat",
881892
"String.contains",
882893
"String.copy",

stdlib/clone/clone.rssi

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// `Clone` is an explicit, deep value copy, modeled on Swift's value semantics
2+
// and Rust's `Clone`.
3+
//
4+
// A managed `struct` or `sum` satisfies `Clone` by listing `derives(Clone)`,
5+
// which the compiler expands into a structural copy of every field. The builtin
6+
// value scalars (`Int`, `String`, `Bool`, ...) satisfy `Clone` directly.
7+
//
8+
// `Clone.clone<T>(self: read x)` copies a value out of a shared (`read`) borrow
9+
// into a `fresh` owned value, closing the "clone gap" where a `read` field
10+
// cannot be moved into a constructor. `Clone` is a pure contract: it introduces
11+
// no mutation, retention, native, or unsafe boundary.
12+
protocol Clone {
13+
fn clone(self: read Self) -> fresh Self
14+
}

stdlib/string/string.rssi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ pub fn String.concat(left: read String, right: read String) -> fresh String
4646
pub fn String.copy(value: read String) -> fresh String
4747
effects(pure)
4848

49+
pub fn String.clone(value: read String) -> fresh String
50+
effects(pure)
51+
4952
pub fn String.from_int(value: Int) -> fresh String
5053
effects(pure)
5154

0 commit comments

Comments
 (0)