Skip to content

Commit c8b36c5

Browse files
olwangclaude
andcommitted
feat(params): scalar mut param write-back (SH-018) + spec §5A
Allow a `mut` parameter of a Copy scalar type (Int/Bool/Float/Char, …) to be reassigned inside the callee, with the new value written back to the caller (call-by-reference, `&mut`). Non-Copy `mut` params stay non-reassignable. The reg-VM already writes every `mut` param's final register back (effect-based mut_args), so no reg-VM/native change is needed. Two frontend touch-points: - analyzer/assign.rs: permit rebinding a `mut` Copy-scalar parameter (gated on checks::local::is_copy_type_name); keep RS0311 for plain params and non-Copy `mut` params. - rust_lower/lowerer.rs: emit `(*pos)` on read and as assignment target for a `mut` Copy-scalar param, since `mut T` already lowers to `&mut T`. Adds pass fixture mut-scalar-writeback.rss (Int + Bool). Updates spec §5A and the grammar comment, and ledger SH-018. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d5ecad6 commit c8b36c5

5 files changed

Lines changed: 89 additions & 13 deletions

File tree

crates/rsscript/src/analyzer/assign.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,13 +358,25 @@ impl<'a> AssignChecker<'a> {
358358
self.check_assignment_type(name, &stmt.value, &span);
359359
return;
360360
}
361+
// A `mut` parameter of a Copy scalar type (Int/Bool/Float/Char, …)
362+
// MAY be reassigned: it lowers to `&mut T`, and the new value is
363+
// written back to the caller, matching `&mut` semantics. Non-Copy
364+
// `mut` params (struct/collection) stay non-rebindable below.
365+
Some(AssignBinding::MutParam)
366+
if self
367+
.resolve_type(name)
368+
.is_some_and(|ty| crate::checks::local::is_copy_type_name(&ty)) =>
369+
{
370+
self.check_assignment_type(name, &stmt.value, &span);
371+
return;
372+
}
361373
Some(AssignBinding::ImmutableLocal) => (
362374
format!("`{name}` is an immutable binding"),
363375
format!("Declare `{name}` with `let mut` to allow reassignment."),
364376
),
365377
Some(AssignBinding::Param | AssignBinding::MutParam) => (
366378
format!("`{name}` is a parameter, not a reassignable local"),
367-
"Parameters are not reassignable (even `mut` ones): a `mut` parameter's fields/elements may be updated, but the parameter binding itself can't be rebound. Bind a `let mut` local instead."
379+
"Parameters are not reassignable (except a `mut` Copy-scalar one, which is written back to the caller): a non-Copy `mut` parameter's fields/elements may be updated, but the parameter binding itself can't be rebound. Bind a `let mut` local instead."
368380
.to_string(),
369381
),
370382
None => (

crates/rsscript/src/rust_lower/lowerer.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,10 +600,28 @@ impl<'a> RustLowerer<'a> {
600600
|| self.native_bindings.contains_key(&key)
601601
}
602602

603+
/// Whether `name` is a `mut` parameter of a Copy scalar type (Int/Bool/Float/…).
604+
/// Such a parameter lowers to `&mut T` (call-by-reference write-back), so a bare
605+
/// ident read/target must dereference it. Non-Copy `mut` params (struct/collection)
606+
/// keep their `&mut Struct` binding and are never dereferenced here.
607+
fn is_mut_copy_scalar_param(&self, name: &str) -> bool {
608+
matches!(self.param_effects.get(name), Some(DataEffect::Mut))
609+
&& self
610+
.value_types
611+
.get(name)
612+
.is_some_and(is_copy_type_ref)
613+
}
614+
603615
pub(super) fn lower_expr(&mut self, expr: &Expr) -> String {
604616
match expr {
605617
Expr::Ident(name, _) => {
606-
if self.drop_field_names.contains(name) {
618+
if self.is_mut_copy_scalar_param(name) {
619+
// A `mut` Copy-scalar parameter lowers to `&mut T`; a bare read
620+
// dereferences it so the value (and assignment through it) works
621+
// against the `&mut i64`/`&mut bool`/… binding. As an assignment
622+
// target this yields `(*pos) = …`, writing back to the caller.
623+
format!("(*{})", rust_value_ident(name))
624+
} else if self.drop_field_names.contains(name) {
607625
format!("self.{}", rust_ident(name))
608626
} else if self.read_view_bindings.contains(name)
609627
&& self
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// A `mut` parameter of a Copy scalar type may be reassigned inside the callee,
2+
// and the new value is written back to the caller (call-by-reference, `&mut`).
3+
// This lets self-hosted code pass a cursor as `mut pos` and advance it in place
4+
// instead of threading an index through return values (SH-018).
5+
6+
fn advance(pos: mut Int) -> Unit {
7+
pos = pos + 1
8+
return Unit
9+
}
10+
11+
fn toggle(flag: mut Bool) -> Unit {
12+
flag = flag == false
13+
return Unit
14+
}
15+
16+
fn main() -> Unit {
17+
let mut cursor = 0
18+
advance(pos: mut cursor)
19+
advance(pos: mut cursor)
20+
// cursor is observably 2 after two write-backs.
21+
Log.write(message: read String.from_int(value: cursor))
22+
23+
let mut flag = false
24+
toggle(flag: mut flag)
25+
if flag {
26+
Log.write(message: read "on")
27+
}
28+
return Unit
29+
}

docs/ledgers/rss-selfhost-ledger.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -459,11 +459,24 @@ gap is VM value-representation / intrinsic-dispatch cost (the next big lever).
459459
- **Classification:** language (no method syntax / cursor mutation) + docs.
460460
- **Decision:** worked around by the return-the-new-index convention and a
461461
`code_at` sentinel helper; it reads cleanly enough and reaches full tier-0
462-
parity (544/544). The general lever (method syntax or a mutable-cursor pattern)
463-
is a language-ergonomics follow-up, not a blocker. Recorded so the plumbing
464-
cost of self-hosting stateful passes is visible.
465-
- **Tests:** `crate::selfhost_parity::lexer_parity_corpus` (tier 0, 544/544).
466-
- **Status:** decided (worked around).
462+
parity (544/544). The mutable-cursor lever is now available: a `mut`
463+
**Copy-scalar** parameter (Int/Bool/Float/Char, …) may be reassigned inside the
464+
callee and the new value is written back to the caller (`&mut` semantics), so a
465+
scanner can take `i: mut Int` and do `i = i + 1` instead of returning the new
466+
index. Method/`impl` syntax remains a separate follow-up.
467+
- **Fix:** the reg-VM already wrote a `mut` param's final register back to the
468+
caller for every `mut` param (scalar included), so no reg-VM/native change was
469+
needed. Only two frontend touch-points were added: (1) the assignment gate
470+
(`analyzer/assign.rs`) now permits rebinding a `mut` Copy-scalar parameter
471+
(checked via `checks::local::is_copy_type_name`), keeping RS0311 for plain
472+
params and non-Copy `mut` params; (2) AOT lowering (`rust_lower/lowerer.rs`)
473+
emits `(*pos)` on read and as the assignment target for such a param, since
474+
`mut T` already lowers to `&mut T`. Non-Copy `mut` params keep their `&mut Struct`
475+
lowering and stay non-reassignable (only fields/elements are mutable).
476+
- **Tests:** `crate::selfhost_parity::lexer_parity_corpus` (tier 0, 544/544);
477+
`tests/fixtures/pass/mut-scalar-writeback.rss` (Int + Bool write-back).
478+
- **Status:** fixed (scalar Copy `mut` params are reassignable with caller
479+
write-back; non-Copy `mut` params stay non-reassignable).
467480

468481
### SH-019 — a `fresh`-returning fn can't build its result via `mut` + `List.push`
469482

docs/spec/RSScript_v0.7_Spec.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -978,15 +978,19 @@ place-conflict, effect, and resource checking as `mut` API calls (it is not an
978978
"expression-shaped" side effect). It comes in two forms:
979979

980980
- **Rebind a `let mut` local:** `x = e`. The target's root must be a reassignable
981-
local (a `let mut` binding). Parameters are never reassignable — even a `mut`
982-
parameter: its fields/elements may be updated, but the parameter binding itself
983-
cannot be rebound. The assigned value's type must match the local's type.
981+
local (a `let mut` binding) or a `mut` **Copy-scalar** parameter. A non-Copy
982+
`mut` parameter (struct/collection) is not reassignable: its fields/elements may
983+
be updated, but the parameter binding itself cannot be rebound. A `mut`
984+
parameter of a Copy scalar type (Int, Bool, Float, Char, …) **may** be reassigned;
985+
the new value is written back to the caller, matching `&mut` (call-by-reference).
986+
The assigned value's type must match the local's/parameter's type.
984987
- **Update a place inside a local:** `obj.field = e` or `list[i] = e`. The place
985988
must start from a `let mut` local in scope, and the same type rule applies.
986989

987990
The target is validated to be a *place* during checking; assigning to a
988-
non-place, to a parameter root, or with a mismatched value type is a frontend
989-
diagnostic. Assignment to a place obeys the place-conflict rule (see pattern
991+
non-place, to a non-Copy parameter root (a `mut` Copy-scalar parameter root is
992+
allowed and written back to the caller), or with a mismatched value type is a
993+
frontend diagnostic. Assignment to a place obeys the place-conflict rule (see pattern
990994
matching), so a single statement cannot alias-mutate overlapping places. All
991995
*other* mutation — of container, managed, and resource state — is still expressed
992996
through explicit `mut` API calls (`Map.insert(map: mut m, ...)`), so mutation
@@ -5884,7 +5888,7 @@ stmt = let | view | assign | return | if | match | for | loop | while
58845888
| with | task-group | select | break | continue | expr ;
58855889
let = ( "let" | "local" ) [ "mut" ] pattern [ ":" type ] "=" expr ;
58865890
view = "view" ident "=" expr ; (* borrowed-region binding, §20.2-1 *)
5887-
assign = place "=" expr ; (* §5A; place not parameter root *)
5891+
assign = place "=" expr ; (* §5A; place root not a non-Copy parameter; a `mut` Copy-scalar param root is allowed and written back *)
58885892
return = "return" [ expr ] ;
58895893
if = "if" expr block [ "else" ( if | block ) ] ;
58905894
match = "match" data-effect expr "{" { arm } "}" ;

0 commit comments

Comments
 (0)