Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion docs/TUTORIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
```

---

Expand Down
23 changes: 23 additions & 0 deletions silverscript-lang/src/compiler/for.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -147,6 +166,10 @@ impl<'a, 'i> ForLowerer<'a, 'i> {
span: span::Span<'i>,
ident_span: span::Span<'i>,
) -> Result<Vec<Statement<'i>>, 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();
Expand Down
38 changes: 36 additions & 2 deletions silverscript-lang/tests/compiler_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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#"
Expand Down