From e1330133cc63d7764136e145706693908f195212 Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Sat, 25 Apr 2026 12:23:54 +0300 Subject: [PATCH 1/2] Add sanity check in for loops that end-start <= max_iterations --- silverscript-lang/src/compiler/for.rs | 23 ++++++++++++++ silverscript-lang/tests/compiler_tests.rs | 38 +++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/silverscript-lang/src/compiler/for.rs b/silverscript-lang/src/compiler/for.rs index 5f4c6bc..a4a97a0 100644 --- a/silverscript-lang/src/compiler/for.rs +++ b/silverscript-lang/src/compiler/for.rs @@ -102,6 +102,25 @@ impl<'a, 'i> ForLowerer<'a, 'i> { name_span: ident_span, }]; + // This is a sanity check to prevent situations where end-start > max_iterations. + // TODO: Consider moving check to debug-mode compilation. + lowered.push(Statement::Require { + expr: Expr::new( + ExprKind::Binary { + op: BinaryOp::Le, + left: Box::new(Expr::new( + ExprKind::Binary { op: BinaryOp::Sub, left: Box::new(end.clone()), right: Box::new(start.clone()) }, + span, + )), + right: Box::new(Expr::int(max_iterations)), + }, + span, + ), + message: None, + span, + message_span: None, + }); + for _ in 0..(max_iterations as usize) { let condition = Expr::new( ExprKind::Binary { @@ -147,6 +166,10 @@ impl<'a, 'i> ForLowerer<'a, 'i> { span: span::Span<'i>, ident_span: span::Span<'i>, ) -> Result>, CompilerError> { + if i128::from(end) - i128::from(start) > max_iterations as i128 { + return Err(CompilerError::Unsupported("for loop range must not exceed max iterations".to_string())); + } + let lowered_body = self.lower_block(body)?; let loop_var_type_ref = TypeRef { base: TypeBase::Int, array_dims: Vec::new() }; let mut lowered = Vec::new(); diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index 2878bef..9ef940d 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -4310,6 +4310,22 @@ fn rejects_non_constant_for_loop_max_iterations() { assert!(err.to_string().contains("for loop max iterations must be a compile-time integer")); } +#[test] +fn rejects_constant_for_loop_range_above_max_iterations() { + let source = r#" + contract Loops() { + entrypoint function main() { + for (i, 0, 4, 3) { + require(i >= 0); + } + } + } + "#; + + let err = compile_contract(source, &[], CompileOptions::default()).expect_err("compile should fail"); + assert!(err.to_string().contains("for loop range must not exceed max iterations"), "unexpected error: {err}"); +} + #[test] fn rejects_overflow_in_constant_for_loop_bounds() { let cases = [ @@ -4365,15 +4381,33 @@ fn runs_runtime_bounded_for_loop_example() { let result = run_script_with_sigscript(compiled.script.clone(), sigscript); assert!(result.is_ok(), "runtime-bounded for-loop should honor end-exclusive bounds: {}", result.unwrap_err()); - let sigscript = compiled.build_sig_script("main", vec![5.into(), 20.into(), 3.into(), 7.into()]).expect("sigscript builds"); + let sigscript = compiled.build_sig_script("main", vec![5.into(), 8.into(), 3.into(), 7.into()]).expect("sigscript builds"); let result = run_script_with_sigscript(compiled.script.clone(), sigscript); - assert!(result.is_ok(), "runtime-bounded for-loop should stop after max iterations: {}", result.unwrap_err()); + assert!(result.is_ok(), "runtime-bounded for-loop should allow ranges up to max iterations: {}", result.unwrap_err()); let sigscript = compiled.build_sig_script("main", vec![4.into(), 2.into(), 0.into(), (-1).into()]).expect("sigscript builds"); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "runtime-bounded for-loop should skip iterations when start >= end: {}", result.unwrap_err()); } +#[test] +fn rejects_runtime_for_loop_range_above_max_iterations() { + let source = r#" + contract RuntimeLoop() { + entrypoint function main(int start, int end) { + for (i, start, end, 3) { + require(i >= start); + } + } + } + "#; + + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + let sigscript = compiled.build_sig_script("main", vec![2.into(), 6.into()]).expect("sigscript builds"); + let result = run_script_with_sigscript(compiled.script, sigscript); + assert!(result.is_err(), "runtime-bounded for-loop should fail when end - start exceeds max iterations"); +} + #[test] fn allows_array_assignment_with_compatible_types() { let source = r#" From 7ab62f161b65e5f61bbc3501fe5d9b39d5c438f1 Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Tue, 28 Apr 2026 11:48:32 +0300 Subject: [PATCH 2/2] Update tutorial --- docs/TUTORIAL.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md index 528405c..b8670ba 100644 --- a/docs/TUTORIAL.md +++ b/docs/TUTORIAL.md @@ -470,7 +470,33 @@ contract ForLoop() { } ``` -The loop variable `i` takes values from `start` to `end - 1` (exclusive end), but the compiler emits exactly `MAX_ITERATIONS` guarded iterations. +The loop variable `i` takes values from `start` to `end - 1` (exclusive end). The range length must not exceed the compile-time unroll bound, so `end - start <= MAX_ITERATIONS` must hold. If the compiler can prove that a constant range exceeds the bound, compilation fails. For runtime bounds, the generated script checks the same condition before entering the loop and fails if the provided range is too large. + +If `start >= end`, the loop performs no iterations. Otherwise, the compiler emits exactly `MAX_ITERATIONS` guarded iterations, and each guarded iteration runs only while the current loop variable is still below `end`. + +This fails during compilation because the constant range has 4 values, but the unroll bound is only 3: + +```javascript +contract CompileTimeLoopFailure() { + entrypoint function check() { + for(i, 0, 4, 3) { + require(i >= 0); + } + } +} +``` + +This compiles because the range bounds are provided at runtime, but calling `check(2, 6)` fails during execution because `6 - 2` is greater than the unroll bound of 3: + +```javascript +contract RuntimeLoopFailure() { + entrypoint function check(int start, int end) { + for(i, start, end, 3) { + require(i >= start); + } + } +} +``` ---