Skip to content

Commit 9006ea0

Browse files
olwangclaude
andcommitted
Merge feat/scoped-views: view name = expr scoped borrowed-region form
Second spec-todo roadmap feature, developed in a git worktree and merged once fully working (full parity sweep + clippy green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 parents 0e7d4bf + 1d18738 commit 9006ea0

5 files changed

Lines changed: 128 additions & 21 deletions

File tree

crates/rsscript/src/syntax/parser.rs

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,8 +1267,19 @@ fn parse_generic_bound(token: &Token) -> Option<GenericBound> {
12671267
}
12681268

12691269
fn parse_block(tokens: &[Token], open: usize, close: usize) -> Block {
1270+
Block {
1271+
statements: collect_statements(tokens, open + 1, close),
1272+
span: tokens[open].span.clone(),
1273+
}
1274+
}
1275+
1276+
/// Collect statements in `[start, close)`. Handles the scoped-view desugar:
1277+
/// `view name = expr` followed by the rest of the block becomes
1278+
/// `with expr as name { <rest> }`, so a borrowed view's scope ends at the
1279+
/// enclosing block (spec §20.2-1) and it reuses every `with`-lease rule
1280+
/// (no escape / no managed capture) without a separate code path.
1281+
fn collect_statements(tokens: &[Token], mut index: usize, close: usize) -> Vec<Stmt> {
12701282
let mut statements = Vec::new();
1271-
let mut index = open + 1;
12721283
while index < close {
12731284
if tokens[index].symbol("}") {
12741285
break;
@@ -1281,15 +1292,58 @@ fn parse_block(tokens: &[Token], open: usize, close: usize) -> Block {
12811292
continue;
12821293
}
12831294

1295+
if is_view_binding(tokens, index, close) {
1296+
let span = tokens[index].span.clone();
1297+
let (binding, resource, after) = parse_view_header(tokens, index, close);
1298+
// The remaining statements of the block are the view's scope.
1299+
let body = Block {
1300+
statements: collect_statements(tokens, after, close),
1301+
span: span.clone(),
1302+
};
1303+
statements.push(Stmt::With(WithStmt {
1304+
resource: resource.unwrap_or_else(|| Expr::Unknown(span.clone())),
1305+
binding,
1306+
body,
1307+
span,
1308+
}));
1309+
break;
1310+
}
1311+
12841312
let (statement, next) = parse_stmt(tokens, index, close);
12851313
statements.push(statement);
12861314
index = next.max(index + 1);
12871315
}
1316+
statements
1317+
}
12881318

1289-
Block {
1290-
statements,
1291-
span: tokens[open].span.clone(),
1292-
}
1319+
/// `view <ident> = ...` — a scoped-view binding (distinct from `view` used as an
1320+
/// ordinary identifier, which is never followed by `<ident> =`).
1321+
fn is_view_binding(tokens: &[Token], start: usize, limit: usize) -> bool {
1322+
tokens[start].is_ident_text("view")
1323+
&& tokens
1324+
.get(start + 1)
1325+
.and_then(ident_name)
1326+
.is_some_and(|name| name != "=")
1327+
&& tokens.get(start + 2).is_some_and(|token| token.symbol("="))
1328+
&& start + 2 < limit
1329+
}
1330+
1331+
/// Parse the `view <name> = <expr>` header, returning the binding name, the
1332+
/// resource expression, and the index just past the statement.
1333+
fn parse_view_header(
1334+
tokens: &[Token],
1335+
start: usize,
1336+
limit: usize,
1337+
) -> (String, Option<Expr>, usize) {
1338+
let name = tokens
1339+
.get(start + 1)
1340+
.and_then(ident_name)
1341+
.unwrap_or("")
1342+
.to_string();
1343+
let end = statement_end(tokens, start, limit);
1344+
let equals = (start + 2..end).find(|index| tokens[*index].symbol("="));
1345+
let resource = equals.and_then(|equals| parse_expr(tokens, equals + 1, end));
1346+
(name, resource, end)
12931347
}
12941348

12951349
fn parse_stmt(tokens: &[Token], start: usize, limit: usize) -> (Stmt, usize) {
@@ -1488,7 +1542,10 @@ fn parse_let_stmt(tokens: &[Token], start: usize, limit: usize) -> (Stmt, usize)
14881542
// `let (a, b) = expr` destructures a tuple. The element names are recorded on
14891543
// the binding and expanded into a temporary plus per-element `let`s by the
14901544
// tuple desugar.
1491-
if tokens.get(name_index).is_some_and(|token| token.symbol("(")) {
1545+
if tokens
1546+
.get(name_index)
1547+
.is_some_and(|token| token.symbol("("))
1548+
{
14921549
return parse_let_tuple_stmt(tokens, start, kind, name_index, limit);
14931550
}
14941551
let parsed_name = tokens.get(name_index).and_then(ident_name);
@@ -2332,7 +2389,11 @@ fn split_match_pattern_guard(tokens: &[Token], start: usize, end: usize) -> Opti
23322389
/// Parse a pattern head name: a bare identifier (`ADD`) or a module-qualified
23332390
/// dotted name (`ops.ADD`, `a.b.Variant`). Returns the joined name and the token
23342391
/// index just past it.
2335-
fn parse_dotted_pattern_name(tokens: &[Token], start: usize, end: usize) -> Option<(String, usize)> {
2392+
fn parse_dotted_pattern_name(
2393+
tokens: &[Token],
2394+
start: usize,
2395+
end: usize,
2396+
) -> Option<(String, usize)> {
23362397
let mut name = ident_name(tokens.get(start)?)?.to_string();
23372398
let mut index = start + 1;
23382399
while index + 1 < end
@@ -2552,14 +2613,11 @@ fn parse_list_pattern(tokens: &[Token], start: usize, end: usize) -> Option<Matc
25522613
/// where `binding` is `None` for an ignored `..` (or `.._`) and `Some(name)`
25532614
/// for `..name`. Returns `None` (the outer option) when the range is an ordinary
25542615
/// element pattern. `..` may tokenise as one `..` token or two `.` tokens.
2555-
fn parse_list_rest_segment(
2556-
tokens: &[Token],
2557-
start: usize,
2558-
end: usize,
2559-
) -> Option<Option<String>> {
2616+
fn parse_list_rest_segment(tokens: &[Token], start: usize, end: usize) -> Option<Option<String>> {
25602617
let dots_end = if tokens.get(start)?.symbol("..") {
25612618
start + 1
2562-
} else if tokens.get(start)?.symbol(".") && tokens.get(start + 1).is_some_and(|t| t.symbol(".")) {
2619+
} else if tokens.get(start)?.symbol(".") && tokens.get(start + 1).is_some_and(|t| t.symbol("."))
2620+
{
25632621
start + 2
25642622
} else {
25652623
return None;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// expect: RS0702
2+
// A scoped-view binding (`view name = expr`, spec §20.2-1) desugars to a
3+
// `with`-lease whose scope ends at the enclosing block, so it cannot escape via
4+
// return — same rule as any `with` resource.
5+
features: local
6+
7+
fn bad(data: read Bytes) -> BytesView {
8+
view v = Bytes.view(value: read data, start: 0, len: 1)
9+
return v
10+
}
11+
12+
fn main() -> Unit {
13+
return Unit
14+
}

crates/rsscript/tests/vm_eval_parity/data.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,34 @@ fn main() -> Unit {
195195
);
196196
}
197197

198+
#[test]
199+
fn parity_scoped_view_binding() {
200+
// The `view name = expr` scoped-view form (spec §20.2-1) desugars to a
201+
// `with`-lease whose scope ends at the enclosing block. It must run
202+
// identically across backends, including a view derived from another view.
203+
let source = r#"
204+
fn main() -> Unit {
205+
let text = "hello world"
206+
view v = String.view(value: read text, start: 0, len: 5)
207+
Log.write(message: read StringView.to_string(value: read v))
208+
view w = StringView.slice(value: read v, start: 1, len: 3)
209+
Log.write(message: read String.from_int(value: StringView.len(value: read w)))
210+
Log.write(message: read StringView.to_string(value: read w))
211+
return Unit
212+
}
213+
"#;
214+
// The `with`-lease lowering binds the view as `let mut`, which rustc flags as
215+
// unused_mut for a read-only view (same benign warning as other `with`
216+
// resources); tolerate it — the values + stdout match across backends.
217+
common::assert_vm_eval_matches_backend_with_distinct_args_allowing_unused_mut_warning(
218+
"parity-scoped-view.rss",
219+
"rsscript_parity_scoped_view",
220+
source,
221+
&[],
222+
&[],
223+
);
224+
}
225+
198226
#[test]
199227
fn parity_char_and_string_chars_intrinsics() {
200228
let source = r#"

docs/RSScript_v0.7_Spec.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5351,7 +5351,10 @@ H. Stream<T> and await-for (async sequences)
53515351
or `Stream` combinators that hide allocation.
53525352
53535353
I. Scoped views and slices (zero-copy borrowed regions)
5354-
- now in scope: committed roadmap (§20.2).
5354+
- implemented (§20.2-1): the borrowed-view types (`StringView`/`BytesView`/
5355+
`BufferView`, lowered to Rust slices) plus the `view name = expr` form, which
5356+
desugars to a `with`-lease whose scope ends at the enclosing block, inheriting
5357+
the no-escape / no-retain / no-cross-await / no-managed-graph rules.
53555358
- for high-performance parsing, HTTP buffers, and binary protocols, RSScript
53565359
needs a "borrowed view" that avoids full-copy semantics:
53575360

docs/spec-todo.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ model without reversing a review-first tenet. Ordered by readiness/value.
2020
bindings loaded. Measured: ~54 ms (VM) vs ~14 s (AOT) for the same program.
2121
Tested: tier-label unit test + `cli_dev_two_tier.rs` end-to-end.
2222

23-
- [ ] **Scoped views / slices** (zero-copy borrowed regions) (§3.2, §20.1-I, §20.2-1)
24-
_large._ Lexically-scoped borrowed views — `with Buffer.view(...) as bytes { … }`
25-
and `view bytes = …` — that cannot be retained, escape scope, cross an `await`, or
26-
enter managed graphs. No surface lifetimes (the scope block replaces them). High
27-
perf value (parsing, buffers, the tinygrad port; pairs with the parked lazy-`Iter`
28-
in `TODO.md`). _Touches:_ parser, the escape/retention checks (`checks/local.rs`),
29-
lowering, reg-VM. Must hold VM↔compiled parity.
23+
- [x] **Scoped views / slices** (zero-copy borrowed regions) (§3.2, §20.1-I, §20.2-1)
24+
_done._ The borrowed-view *types* already existed and ran at parity
25+
(`String.view`/`StringView`, `Bytes.view`/`BytesView`, `Buffer.view`/`BufferView`,
26+
lowered to Rust slices). Added the spec's dedicated **`view name = expr`** form: a
27+
parser-level desugar turns `view v = e; <rest>` into `with e as v { <rest> }`, so
28+
the view's scope ends at the enclosing block and it inherits every `with`-lease
29+
rule — cannot escape (return), be retained, cross `await`, or enter a managed
30+
graph — with **no new borrow analysis and zero changes below the parser** (so
31+
VM↔compiled parity is automatic). Verified: `parity_scoped_view_binding` (incl. a
32+
view-of-a-view) and the `scoped-view-escape.rss` fail fixture (`RS0702`). The
33+
`with Buffer.view(...) as v { … }` form also already works.
3034

3135
- [ ] **Capability objects / explicit dynamic dispatch** (§3.2, §20.1-G, §20.2-2) —
3236
_large._ Review-visible `capability`-bounded dispatch

0 commit comments

Comments
 (0)