Hmm, I must have been asleep earlier this moring, because all that extra
stuff isn't entirely necessary if I use a Proc instead of a String:

# A hack to allow a callback to be called the first time an expression
# evaluates to true
$as_soon_as_expressions = []
set_trace_func(proc {|*args|
  $as_soon_as_expressions.each_index do |idx|
    expr, cb = $as_soon_as_expressions[idx]
    if expr.call() then
      $as_soon_as_expressions.delete_at(idx)
      cb.call()
    end
  end
})
def as_soon_as(expr, &block)
  $as_soon_as_expressions.push [expr, block]
end

# Some test code
$x = 0
as_soon_as {$x == 42} do
  puts "woohoo!"
end

puts "1"
$x = 1
puts "2"
$x = 2
puts "3"
$x = 3
puts "42"
$x = 42
puts "done"

Much cleaner, same output.

Paul