Loops

By default, Ghost records all function calls as they are executed. If a particular function is executed several times inside of a loop, Ghost will record each execution separately:

using Ghost


function loop1(x, n)
    while n > 0
        x = 2x
        n = n - 1
    end
    return x
end

_, tape1 = trace(loop1, 2.0, 3)
(16.0, Tape{Dict{Any, Any}}
  inp %1::typeof(Main.loop1)
  inp %2::Float64
  inp %3::Int64
  %4 = >(%3, 0)::Bool
  %5 = *(2, %2)::Float64
  %6 = -(%3, 1)::Int64
  %7 = >(%6, 0)::Bool
  %8 = *(2, %5)::Float64
  %9 = -(%6, 1)::Int64
  %10 = >(%9, 0)::Bool
  %11 = *(2, %8)::Float64
  %12 = -(%9, 1)::Int64
  %13 = >(%12, 0)::Bool
)

Ghost also has experimental support for tracing loops as a special Loop operation which can be turned on using should_trace_loops!(true)

using Ghost
import Ghost: should_trace_loops!

should_trace_loops!(true)

function loop1(x, n)
    while n > 0
        x = 2x
        n = n - 1
    end
    return x
end

_, tape2 = trace(loop1, 2.0, 3)
(16.0, Tape{Dict{Any, Any}}
  inp %1::typeof(Main.loop1)
  inp %2::Float64
  inp %3::Int64
  %4 = Loop(%3, %2)
  %5 = getfield(%4, 1)::Float64
)

Unlike fully static tape which always executes as many iterations as there were during the tracing, tape with loops follows the control flow of the original function:

play!(tape1, loop1, 2.0, 3)  # ==> 16.0
play!(tape1, loop1, 2.0, 4)  # ==> 16.0
play!(tape1, loop1, 2.0, 5)  # ==> 16.0

play!(tape2, loop1, 2.0, 3)  # ==> 16.0
play!(tape2, loop1, 2.0, 4)  # ==> 32.0
play!(tape2, loop1, 2.0, 5)  # ==> 64.0

Note that Loop itself contains a subtape and is quite independent from the outer tape.

tape2[V(4)].subtape
Tape{Dict{Any, Any}}
  inp %1::Int64
  inp %2::Float64
  %3 = >(%1, 0)::Bool
  %4 = *(2, %2)::Float64
  %5 = -(%1, 1)::Int64
Warning

To work correctly, Loop expects at least one full iteration during both - tracing and execution.