Skip to content

(WIP) [Monitor] Support repeat ... loops #194

Draft
ngernest wants to merge 50 commits intomainfrom
monitor_repeat_loops
Draft

(WIP) [Monitor] Support repeat ... loops #194
ngernest wants to merge 50 commits intomainfrom
monitor_repeat_loops

Conversation

@ngernest
Copy link
Contributor

@ngernest ngernest commented Feb 20, 2026

Addresses #183, still WIP

Other miscellaneous changes:

  • Define a file macros.rs with some helper macros for logging that print out the line number, function name and file name (the default info! macro from the log crate doesn't do this)
  • Changed the AugmentedTrace type from a type alias to a tuple struct so that we can define methods over it (see types.rs)
  • Renamed Stmt::BoundedLoop to RepeatLoop in the interpreter code

Current status (EoD 2/26):

1. Loop with assignments
Consider loop_with_assigns.prot:

// Each iteration of the loop performs the full add protocol:
//   assign inputs -> step -> release inputs -> assert output
// This tests that assignments, DontCare releases, and assertions
// all work correctly inside a bounded loop body.
prot loop_add<DUT: Adder>(in a: u32, in b: u32, in num_iters: u64, out s: u32) {
  repeat num_iters iterations {
    DUT.a := a;
    DUT.b := b;
    step();
    DUT.a := X;
    DUT.b := X;
    assert_eq(s, DUT.s);
  }
  fork();
  step();
}

This is the original transaction file supplied to the interpreter:

trace {
    loop_add(1, 2, 3, 3);   // assert 1+2=3 on each of 3 iterations
    loop_add(10, 20, 1, 30); // assert 10+20=30 on 1 iteration
}

The monitor currently produces the following output:

// trace 0
trace {
    loop_add(1, 2, 3, 3);
    loop_add(10, 20, 1, 30);
    loop_add(198490043, 4254467669, 1, 30);
}

// trace 1
trace {
    loop_add(1, 2, 1, 3);
    loop_add(1, 2, 2, 3);
    loop_add(10, 20, 1, 30);
    loop_add(198490043, 4254467669, 1, 30);
}

// trace 2
trace {
    loop_add(1, 2, 2, 3);
    loop_add(1, 2, 1, 3);
    loop_add(10, 20, 1, 30);
    loop_add(198490043, 4254467669, 1, 30);
}

// trace 3
trace {
    loop_add(1, 2, 1, 3);
    loop_add(1, 2, 1, 3);
    loop_add(1, 2, 1, 3);
    loop_add(10, 20, 1, 30);
    loop_add(198490043, 4254467669, 1, 30);
}

The issue is with the final spurious loop_add protocol in each of the traces suggested by the monitor. This is caused after a legitimate loop_call transaction completes and triggers an ExplicitFork (i.e. another loop_add protocol) at time = 4:

This new thread reads the values of DUT.a and DUT.b at time 4, both of which are randomized values (caused by DontCare), it does a step() (time is now 5), but assert_eq(s, DUT.s) succeeds since DUT.s is still 30. This thread finishes and emits loop_add(random_a, random_b, 1, 30).

  1. Add Busy Wait
    Protocol:
prot add_busy_wait<DUT: Adder>(in a: u32, in b: u32, in num_iters: u64, out s: u32) {
  DUT.a := a;
  DUT.b := b;
  // We want `assert_eq(s, DUT.s)` to hold for every iteration of this loop,
  // so we cannot put `DUT.a := X; DUT.b := X` in the loop body, since
  // that would cause the `assert_eq` to fail on later iterations of the loop
  repeat num_iters iterations {
    step();
    assert_eq(s, DUT.s);
  }
  DUT.a := X;
  DUT.b := X;
  // Ensure `s = a + b` still holds when we exit the loop
  assert_eq(s, DUT.s);
  fork();
  step();
}

Current monitor output (output parameter s is wrong for some of these transactions):

// trace 0
trace {
    add_busy_wait(1, 2, 0, 0);
}

// trace 1
trace {
    add_busy_wait(4, 5, 3, 3);
}

// trace 2
trace {
    add_busy_wait(1, 2, 1, 3);
    add_busy_wait(4, 5, 2, 9);
}

// trace 3
trace {
    add_busy_wait(4, 5, 2, 3);
    add_busy_wait(4, 5, 1, 9);
}

// trace 4
trace {
    add_busy_wait(1, 2, 1, 3);
    add_busy_wait(4, 5, 1, 9);
    add_busy_wait(4, 5, 1, 9);
}

3: Nested busy wait
Monitor hangs on this protocol (probably because of exponential blowup in the no. of threads):

// Nested bounded loops: the outer loop runs `outer_iters` times,
// and the inner loop runs `inner_iters` times per outer iteration,
// for a total of `outer_iters * inner_iters` steps.
// Since inputs persist on the adder, the final result is
// the same regardless of how many steps we take.
prot nested_busy_wait<DUT: Adder>(in a: u32, in b: u32, in outer_iters: u64, in inner_iters: u64, out s: u32) {
  DUT.a := a;
  DUT.b := b;
  repeat outer_iters iterations {
    repeat inner_iters iterations {
      step();
      assert_eq(s, DUT.s);
    }
    step();
    assert_eq(s, DUT.s);
  }
  DUT.a := X;
  DUT.b := X;
  assert_eq(s, DUT.s);
  fork();
  step();
}

@ngernest
Copy link
Contributor Author

ngernest commented Mar 3, 2026

Notes (from 3/3 meeting):

  • Optimization: if a candidate trace doesn't cover the whole waveform, get rid of it (it's not a valid trace)

  • (Trace 0 in the 2nd example suffers from this example)

  • 2nd example is the most problematic one which warrants fixing

  • Don't check the nested loop example into CI for now (the body of the two loops is the same, so there's nothing to distinguish them)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant