Skip to content

Commit bede8db

Browse files
olwangclaude
andcommitted
fix(reg-vm,analyzer): stream cursor write-back + None exhaustiveness arm
Two pre-existing bugs found during the module decomposition (both fail on 9447c41, neither refactor-induced): - reg-VM Stream: expect_stream_ref materialized a detached copy of the struct's items list, so Stream.next's head-removal mutated a throwaway and never advanced the cursor seen by a later collect_list/next on the same stream. Share the underlying Rc<RefCell<TypedVec>> so the advance writes back through every read-clone of the stream, matching the interpreter. Fixes reg_vm_runs_cancellation_and_stream_intrinsics_like_interpreter. - analyzer: a leftover private builtin_value_type_name copy lacked the "None" => Option arm the canonical checks::shared copy carries, so a bare `None` match scrutinee fell back to the constructor-name path and a wildcard-only match was spuriously reported non-exhaustive (RS0021). Delete the duplicate; reuse checks::shared::builtin_value_type_name. Regression test: checker_accepts_wildcard_match_on_bare_none_literal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7cf95e5 commit bede8db

5 files changed

Lines changed: 35 additions & 17 deletions

File tree

crates/rsscript/src/analyzer.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1814,7 +1814,7 @@ fn hir_expr_type_name(expr: &HirExpr) -> Option<&str> {
18141814
name, type_name, ..
18151815
} => type_name
18161816
.as_deref()
1817-
.or_else(|| builtin_value_type_name(name)),
1817+
.or_else(|| crate::checks::shared::builtin_value_type_name(name)),
18181818
HirExpr::Call { type_name, .. }
18191819
| HirExpr::Effect { type_name, .. }
18201820
| HirExpr::Manage { type_name, .. }
@@ -1834,15 +1834,6 @@ fn hir_expr_type_name(expr: &HirExpr) -> Option<&str> {
18341834
}
18351835
}
18361836

1837-
fn builtin_value_type_name(name: &str) -> Option<&'static str> {
1838-
match name {
1839-
"true" | "false" => Some("Bool"),
1840-
"null" => Some("JsonLiteral"),
1841-
"Unit" => Some("Unit"),
1842-
_ => None,
1843-
}
1844-
}
1845-
18461837
fn constructor_pattern_is_irrefutable(pattern: &MatchPattern) -> bool {
18471838
match pattern {
18481839
MatchPattern::Binding { .. } | MatchPattern::Wildcard(_) => true,

crates/rsscript/src/reg_vm/intrinsics/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2355,7 +2355,8 @@ impl RegVm {
23552355
"stream collect_list would block on an open channel stream",
23562356
)));
23572357
}
2358-
let values = stream.items.borrow_mut().drain(..).collect::<Vec<_>>();
2358+
let values = stream.items.borrow().to_vec();
2359+
stream.items.borrow_mut().clear();
23592360
Ok(value_ok(VmValue::List(Rc::new(RefCell::new(TypedVec::from_values(values))))))
23602361
}
23612362
RegIntrinsic::StreamFromList => {

crates/rsscript/src/reg_vm/resources.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ pub(super) fn stream_collect_error_value(message: impl Into<String>) -> VmValue
408408

409409
#[derive(Debug, Clone)]
410410
pub(super) struct VmStreamState {
411-
pub(super) items: Rc<RefCell<Vec<VmValue>>>,
411+
pub(super) items: Rc<RefCell<TypedVec>>,
412412
pub(super) collect_error: Option<String>,
413413
pub(super) channel_id: Option<i64>,
414414
}

crates/rsscript/src/reg_vm/value_access.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,14 +218,15 @@ pub(super) fn expect_resource_pool_ref(value: &VmValue) -> Result<VmResourcePool
218218
pub(super) fn expect_stream_ref(value: &VmValue) -> Result<VmStreamState, EvalError> {
219219
match value {
220220
VmValue::Struct(data) if data.name().as_ref() == "Stream" => {
221-
let items_list = data
221+
// Share the struct's underlying items list (an `Rc<RefCell<TypedVec>>`)
222+
// rather than copying it: `Stream.next` removes the head element, and a
223+
// `read stream` clone of the struct shares this same `Rc`, so the cursor
224+
// advance must write back through it to be visible to a later
225+
// `collect_list`/`next` on the same stream.
226+
let items = data
222227
.get("items")
223228
.ok_or_else(|| EvalError::Runtime("Stream value is missing items.".to_string()))
224229
.and_then(expect_list_ref)?;
225-
// The stream buffer is a plain `Vec<VmValue>`; materialize the typed
226-
// list's logical values into one (TV1: a `Floats`/`Ints` list yields the
227-
// same `VmValue` sequence a `Boxed` list would).
228-
let items = Rc::new(RefCell::new(items_list.borrow().to_vec()));
229230
let collect_error = data
230231
.get("collect_error")
231232
.map(option_payload_value)

crates/rsscript/tests/checker_frontend/types.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,31 @@ fn pick() -> Int {
12521252
));
12531253
}
12541254

1255+
#[test]
1256+
fn checker_accepts_wildcard_match_on_bare_none_literal() {
1257+
// A bare `None` scrutinee resolves to `Option`, so a wildcard arm is
1258+
// exhaustive. Regression: the analyzer's exhaustiveness helper formerly
1259+
// failed to classify `None` as `Option` (its `builtin_value_type_name`
1260+
// copy lacked the `None` arm the canonical `checks::shared` copy carries),
1261+
// so it fell back to the constructor-name path and spuriously reported the
1262+
// wildcard match as non-exhaustive.
1263+
let source = r#"
1264+
fn pick() -> Int {
1265+
match None {
1266+
_ => return 0
1267+
}
1268+
}
1269+
"#;
1270+
let diagnostics = analyze_source("match-bare-none-wildcard.rss", source);
1271+
1272+
assert!(
1273+
!diagnostics
1274+
.iter()
1275+
.any(|diagnostic| diagnostic.code == "RS0021"),
1276+
"wildcard arm covers a bare `None` scrutinee; got {diagnostics:?}"
1277+
);
1278+
}
1279+
12551280
#[test]
12561281
fn checker_reports_non_exhaustive_sum_type_match() {
12571282
let source = r#"

0 commit comments

Comments
 (0)