Skip to content

Commit c49ab9e

Browse files
olwangclaude
andcommitted
rsscript: flatten deep &&/|| chains to balanced trees in codegen (#2)
A left-linear chain of hundreds of `&&`/`||` lowered to a deeply-nested Rust expression that overflows rustc's recursive parser/type-checker (the RUST_MIN_STACK=2g workaround). `&&`/`||` are associative for both value AND short-circuit/evaluation order, so the lowerer now emits a BALANCED tree (O(log n) nesting). The chain is collected iteratively (its left spine is the deep part) so this pass doesn't recurse n-deep. Generated crates also now allow(unused_parens) for the (benign) regrouping parens. Verified: a 400-deep `&&` chain compiles+runs on the DEFAULT stack (no RUST_MIN_STACK) with VM==compiled results; balanced-shallow-output lowering test (checker_lowering); full vm_eval_parity (87) + vm_eval (17) green; golden header updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0e07f0b commit c49ab9e

3 files changed

Lines changed: 104 additions & 17 deletions

File tree

crates/rsscript/src/rust_lower/lowerer.rs

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ use std::collections::{BTreeMap, BTreeSet};
22

33
use crate::diagnostic::Span;
44
use crate::syntax::ast::{
5-
BinaryOp, Block, CallArg, Callee, ConstDecl, DataEffect, EffectDecl, Expr, FieldDecl,
6-
ForStmt, FunctionDecl, GenericBound, Item, LetStmt, MatchPattern, MatchStmt, Param, Program,
7-
Stmt, SumTypeDecl, TypeAliasDecl, TypeDecl, TypeKind, TypeRef,
5+
BinaryOp, Block, CallArg, Callee, ConstDecl, DataEffect, EffectDecl, Expr, FieldDecl, ForStmt,
6+
FunctionDecl, GenericBound, Item, LetStmt, MatchPattern, MatchStmt, Param, Program, Stmt,
7+
SumTypeDecl, TypeAliasDecl, TypeDecl, TypeKind, TypeRef,
88
};
99

1010
use super::helpers::*;
@@ -140,7 +140,7 @@ impl<'a> RustLowerer<'a> {
140140
// Module-isolated symbols carry a lowercase module prefix (`device__Device`,
141141
// `helpers__count`), so the camel/snake/upper-case lints don't apply.
142142
out.push_str(
143-
"#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)]\n",
143+
"#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals, unused_parens)]\n",
144144
);
145145
let feature_names = lowered_feature_names(&self.program.features);
146146
if feature_names.is_empty() {
@@ -1886,8 +1886,7 @@ impl<'a> RustLowerer<'a> {
18861886

18871887
fn lower_match_stmt(&mut self, stmt: &MatchStmt, out: &mut String, pad: &str, indent: usize) {
18881888
let scrutinee_type = self.infer_expr_type(&stmt.value);
1889-
let mut scrutinee =
1890-
self.lower_match_scrutinee_expr(&stmt.value, scrutinee_type.as_ref());
1889+
let mut scrutinee = self.lower_match_scrutinee_expr(&stmt.value, scrutinee_type.as_ref());
18911890
let by_ref = self.match_scrutinee_by_ref(&stmt.value);
18921891
// Native Rust slice patterns match a `[T]`, not a `Vec<T>`; view the
18931892
// list scrutinee as a slice so `[a, b]` / `[first, rest @ ..]` apply.
@@ -1896,11 +1895,8 @@ impl<'a> RustLowerer<'a> {
18961895
}
18971896
out.push_str(&format!("{pad}match {scrutinee} {{\n"));
18981897
for arm in &stmt.arms {
1899-
let pattern = self.lower_match_pattern_typed(
1900-
&arm.pattern,
1901-
scrutinee_type.as_ref(),
1902-
by_ref,
1903-
);
1898+
let pattern =
1899+
self.lower_match_pattern_typed(&arm.pattern, scrutinee_type.as_ref(), by_ref);
19041900
let guard = arm
19051901
.guard
19061902
.as_ref()
@@ -1912,8 +1908,7 @@ impl<'a> RustLowerer<'a> {
19121908
pattern,
19131909
guard
19141910
));
1915-
let scoped_binding =
1916-
match_binding_type_ref(&arm.pattern, scrutinee_type.as_ref());
1911+
let scoped_binding = match_binding_type_ref(&arm.pattern, scrutinee_type.as_ref());
19171912
let previous_value_type = scoped_binding
19181913
.as_ref()
19191914
.and_then(|(binding, _)| self.value_types.get(binding).cloned());
@@ -2308,6 +2303,48 @@ impl<'a> RustLowerer<'a> {
23082303
op, left, right, ..
23092304
} => {
23102305
let binary_op = *op;
2306+
// Flatten a deep `&&`/`||` chain into a *balanced* tree so the
2307+
// generated Rust nests O(log n) deep instead of O(n): a left-linear
2308+
// chain of hundreds of `&&` overflows rustc's recursive parser/
2309+
// type-checker (the `RUST_MIN_STACK=2g` workaround). `&&`/`||` are
2310+
// associative for both value AND short-circuit/evaluation order, so
2311+
// balanced regrouping is behavior-identical. The chain is collected
2312+
// iteratively (its left spine is the deep part) so this lowerer pass
2313+
// does not itself recurse n-deep.
2314+
if matches!(binary_op, BinaryOp::LogicalAnd | BinaryOp::LogicalOr) {
2315+
let mut rev: Vec<&Expr> = vec![right.as_ref()];
2316+
let mut node: &Expr = left.as_ref();
2317+
loop {
2318+
match node {
2319+
Expr::Binary {
2320+
op: inner,
2321+
left: inner_left,
2322+
right: inner_right,
2323+
..
2324+
} if *inner == binary_op => {
2325+
rev.push(inner_right.as_ref());
2326+
node = inner_left.as_ref();
2327+
}
2328+
other => {
2329+
rev.push(other);
2330+
break;
2331+
}
2332+
}
2333+
}
2334+
if rev.len() >= 8 {
2335+
rev.reverse();
2336+
let op_str = if binary_op == BinaryOp::LogicalAnd {
2337+
"&&"
2338+
} else {
2339+
"||"
2340+
};
2341+
let leaves: Vec<String> = rev
2342+
.iter()
2343+
.map(|operand| self.lower_binary_operand(operand, binary_op, false))
2344+
.collect();
2345+
return balanced_logical_join(&leaves, op_str);
2346+
}
2347+
}
23112348
if matches!(op, BinaryOp::ShiftLeft | BinaryOp::ShiftRight) {
23122349
let method = if *op == BinaryOp::ShiftLeft {
23132350
"wrapping_shl"
@@ -3816,9 +3853,7 @@ impl<'a> RustLowerer<'a> {
38163853
Expr::Ident(name, span) if name == "true" || name == "false" => {
38173854
Some(simple_type_ref("Bool", span))
38183855
}
3819-
Expr::Ident(name, span) if name == "null" => {
3820-
Some(simple_type_ref("JsonLiteral", span))
3821-
}
3856+
Expr::Ident(name, span) if name == "null" => Some(simple_type_ref("JsonLiteral", span)),
38223857
Expr::Ident(name, span) => self.value_types.get(name).cloned().or_else(|| {
38233858
self.find_sum_type_for_variant(name)
38243859
.map(|sum_name| simple_type_ref(&sum_name, span))
@@ -5571,6 +5606,26 @@ fn expr_contains_try(expr: &Expr) -> bool {
55715606
}
55725607
}
55735608

5609+
/// Join already-lowered operands with a short-circuit operator (`&&`/`||`) as a
5610+
/// *balanced* tree, so the resulting Rust expression nests O(log n) deep rather
5611+
/// than O(n). `&&`/`||` are associative for both value and evaluation order, so
5612+
/// any grouping evaluates the operands left-to-right with identical short-circuit
5613+
/// behavior. Recursion depth here is O(log n), so this helper is itself safe.
5614+
fn balanced_logical_join(leaves: &[String], op: &str) -> String {
5615+
match leaves.len() {
5616+
0 => String::new(),
5617+
1 => leaves[0].clone(),
5618+
n => {
5619+
let mid = n / 2;
5620+
format!(
5621+
"({} {op} {})",
5622+
balanced_logical_join(&leaves[..mid], op),
5623+
balanced_logical_join(&leaves[mid..], op)
5624+
)
5625+
}
5626+
}
5627+
}
5628+
55745629
fn rust_binary_precedence(op: BinaryOp) -> u8 {
55755630
match op {
55765631
BinaryOp::LogicalOr => 1,

crates/rsscript/tests/checker_lowering/control_flow.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,3 +693,35 @@ impl Writer for BufferWriter {
693693
&& diagnostic.label == "protocol implementation signature mismatch"
694694
}));
695695
}
696+
697+
#[test]
698+
fn deep_logical_chain_lowers_to_balanced_shallow_tree() {
699+
// A long `&&` chain must lower to a *balanced* Rust expression (O(log n) paren
700+
// nesting) rather than a left-linear nest, so rustc doesn't overflow compiling
701+
// the generated crate (replacing the `RUST_MIN_STACK=2g` workaround). `&&`/`||`
702+
// are associative for value AND short-circuit order, so regrouping is sound.
703+
// 40 operands: enough to trigger balanced lowering (threshold 8) and to
704+
// distinguish balanced (~log2(40) ≈ 6 deep) from left-linear (~40 deep). Kept
705+
// modest so the recursive parse/HIR passes don't overflow the small cargo-test
706+
// thread stack (the `rss` binary handles far deeper on its main-thread stack).
707+
let conds = (0..40)
708+
.map(|i| format!("x > {i}"))
709+
.collect::<Vec<_>>()
710+
.join(" && ");
711+
let source = format!("fn deep(x: Int) -> Bool {{\n return {conds}\n}}\n");
712+
let rust =
713+
lower_source_to_rust("deep-bool-chain.rss", &source).expect("deep && chain should lower");
714+
715+
// Left-linear lowering of 40 operands would produce ~40 consecutive `(`;
716+
// a balanced tree's deepest path is far shallower.
717+
assert!(
718+
!rust.contains(&"(".repeat(20)),
719+
"deep `&&` chain must lower to a balanced (shallow) tree, not a left-linear nest"
720+
);
721+
// Generated code over-parenthesizes for structural correctness; the crate
722+
// header allows the resulting (benign) unused_parens lint.
723+
assert!(
724+
rust.contains("unused_parens"),
725+
"generated crate should allow(unused_parens) for the balanced regrouping"
726+
);
727+
}

crates/rsscript/tests/golden/lowering/simple.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Generated by RSScript. Edit the .rss source instead.
22
// Runtime hooks are intentionally explicit while Rust lowering is stabilizing.
3-
#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)]
3+
#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals, unused_parens)]
44
// RSScript features: <none>
55

66
// rss:span kind=type file=simple.rss line=1 column=1 length=6

0 commit comments

Comments
 (0)