std/replay
The record/replay engine behind MILO_RECORD and MILO_REPLAY. Run a program with MILO_RECORD=trace.mrr and every nondeterministic input it reads (clock, entropy, environment, files, sockets, subprocesses, the green scheduler's order, extern calls, OS thread ordering) is appended to the trace; run it again with MILO_REPLAY=trace.mrr and each of those inputs is answered from the trace instead, so the run repeats exactly. milo trace trace.mrr lists the records.
Programs do not call this module to get that: std's own IO, time, random and process functions are already hooked, and the compiler routes extern calls through generated recording wrappers. The functions here are the hook API those use, for a library that wraps a new source of input itself.
from "std/replay" import { replayOff, replayRet }Example
A hooked call: one compare when neither variable is set, recorded or answered from the trace otherwise.
from "std/replay" import { replayOff, replayRet }
extern fn sensorRead(): i64
fn readSensor(): i64 {
if replayOff() {
unsafe { return sensorRead() }
}
return replayRet("sensor.read", "", false, (): i64 => {
unsafe { return sensorRead() }
})
}See Record and replay for what is captured, the trace format, holes and divergence.
API reference
ReplayCall
pub struct ReplayCallOne extern call through the trace. The payload is a header line <ret> <errno> and one length per output buffer (-1 for one the call did not get), then the outputs' bytes back to back.
Fields: kind: string, arg: string, ret: i64, err: i32, lens: Vec<i64>, data: string, dataPos: i64, outIdx: i64.
ReplayCall.argBytes
fn ReplayCall.argBytes(self: &mut ReplayCall, p: *u8, n: i64): voidn bytes an argument points at, in hex.
ReplayCall.argCstr
fn ReplayCall.argCstr(self: &mut ReplayCall, p: *u8): voidA C string argument (a path, a variable name); - for null.
ReplayCall.argInt
fn ReplayCall.argInt(self: &mut ReplayCall, v: i64): voidA scalar argument, part of what names the call.
ReplayCall.nextLen
fn ReplayCall.nextLen(self: &mut ReplayCall): i64Replay: the recorded length of the next output buffer (-1 when the call did not write it); a trace with fewer lengths than outputs is corrupt.
ReplayCall.outCopy
fn ReplayCall.outCopy(self: &mut ReplayCall): *u8Replay: the next recorded output as a fresh heap copy (a returned string or structure), null when the recording returned null.
ReplayCall.outCstr
fn ReplayCall.outCstr(self: &mut ReplayCall, p: *u8, cap: i64): voidRecord: the NUL-terminated string at p (within cap bytes when cap >= 0), terminator included.
ReplayCall.outFrom
fn ReplayCall.outFrom(self: &mut ReplayCall, p: *u8, n: i64): voidRecord: n bytes at p as the next output (absent when p is null or n < 0).
ReplayCall.outInto
fn ReplayCall.outInto(self: &mut ReplayCall, p: *u8): voidReplay: copy the next recorded output into p (skipped when the recording had none, or p is null).
ReplayCall.put
fn ReplayCall.put(self: &mut ReplayCall, net: bool): voidRecord: write the record, then put errno back (the write is a syscall too).
ReplayCall.replaying
fn ReplayCall.replaying(self: &ReplayCall): boolTrue when the call is to be answered from the trace.
ReplayCall.retVal
fn ReplayCall.retVal(self: &ReplayCall): i64Replay: the recorded return value (a float's bits, a handle's address).
ReplayCall.saveErrno
fn ReplayCall.saveErrno(self: &mut ReplayCall, net: bool): voidRecord: errno right after the live call, before anything else can touch it.
ReplayCall.sep
fn ReplayCall.sep(self: &mut ReplayCall): voidSeparate the next argument from the previous one in the call's name.
ReplayCall.setRet
fn ReplayCall.setRet(self: &mut ReplayCall, r: i64): voidRecord: the call's return value (a float's bits, a handle's address).
ReplayCall.take
fn ReplayCall.take(self: &mut ReplayCall, net: bool): voidReplay: fetch this call's record and restore its errno.
ReplayLock
pub struct ReplayLockThe one lock around the trace. A spin lock with a sleep, not a pthread mutex: it has to work before any initializer has run (a hook can fire from another module's global initializer), and the critical sections are a record's worth of work. Reentrant per thread, so a divergence reported while it is held can still run the exit hook. @synchronized tells the checker's thread pass that the shared state above, written only inside run, is not racing.
ReplayLock.run
fn ReplayLock.run(self: &ReplayLock, f: () => void): voidRun f holding the record/replay engine's lock; re-entrant on the same thread.
Functions
miloReplayStart
pub fn miloReplayStart(argv: *u8): voidCalled first thing in main, before any global initializer: decides record/replay (so an initializer's allocations already come from the fixed heap) and restarts with ASLR off when needed, which must happen before the trace is opened.
Only the restart and the heap happen here: the rest of the decision waits for std/replay's own initializer, because its state (the trace window, the fd tables) is set up by the global initializers that run after this.
miloReplayStop
pub fn miloReplayStop(seq: i64): voidThe function a debugger breaks on to stop a replay at a chosen record: with MILO_REPLAY_STOP=<N>, replay calls it right after consuming record N, so the replaying program is stopped with record N's call just answered. External linkage keeps the symbol (b miloReplayStop); it then raises SIGTRAP, so a debugger attached without a breakpoint stops here too. With MILO_REPLAY_STOP_PRINT=1 it prints instead and the replay continues (for tests and a UI that only wants the position).
replayActive
pub fn replayActive(): boolTrue when hooked calls must return recorded answers instead of asking the OS.
replayBlob
pub fn replayBlob(kind: &string, arg: &string, live: () => string): stringRun a call that answers with arbitrary bytes the caller encodes itself (a stat result, a directory listing, a resolved address).
replayCallBegin
pub fn replayCallBegin(kind: string): ReplayCallStart one recorded extern call of kind kind (x.<name>).
replayClose
pub fn replayClose(fd: i32): i32close(2), unrecorded: when a program closes is deterministic given its inputs. A synthetic fd is only forgotten; the OS never had it.
replayF64Bits
pub fn replayF64Bits(v: f64): i64The bits of a float, for recording a float argument or return.
replayF64FromBits
pub fn replayF64FromBits(b: i64): f64The float with these bits: the inverse of replayF64Bits, for a replayed float.
replayFdCategory
pub fn replayFdCategory(fd: i32): u8What std recorded fd as (see replayFdOpened), 0 when nothing.
replayFdOpened
pub fn replayFdOpened(fd: i32, cat: u8): voidNote an fd std just opened (or that replay just handed out) and what it is: 'f' a file, 'n' a socket, 'p' a child's pipe, 't' a pty. Under replay the number came from the trace and has no OS descriptor behind it.
replayHeapDepthIn
pub fn replayHeapDepthIn(d: i64): voidRestore the engine depth replayHeapDepthOut took, after a green-task switch back.
replayHeapDepthOut
pub fn replayHeapDepthOut(): i64Green tasks switch inside a thread, so the engine depth belongs to a context, not to the thread: std/runtime takes it out before a swapcontext and puts it back after.
replayHeapFree
pub fn replayHeapFree(p: *u8): voidfree() for Milo allocations: blocks from the fixed-address heap go back to it, any other pointer (libc's, the engine's own) goes to libc.
replayHeapMalloc
pub fn replayHeapMalloc(n: i64): *u8malloc, free and realloc for every allocation Milo code makes: codegen calls these in place of libc's when std/replay is in the program (src/codegen.ts). With neither variable set they are one compare in front of libc.
replayHeapMap
pub fn replayHeapMap(len: i64): *u8Page-granular memory (a green task's stack) from the fixed heap, or null when the heap is off or this is the engine's own: the caller then maps it itself.
replayHeapOff
pub fn replayHeapOff(): boolTrue when no fixed heap is running, so replayHeapMap would hand out nothing and a green task's stack may come from anywhere (std/runtime's stack cache). Named "Heap" so a caller is not counted as engine code (src/lower.ts isReplayEngine), which would raise rrDepth around it and send its allocations to libc.
replayHeapRealloc
pub fn replayHeapRealloc(p: *u8, n: i64): *u8realloc() for Milo allocations: a block from the fixed-address heap grows within it, so its new address replays too; any other pointer goes to libc.
replayHeapUnmap
pub fn replayHeapUnmap(p: *u8, len: i64): boolGive back a block replayHeapMap handed out. False when p is not one (the caller unmaps it itself).
replayHex
pub fn replayHex(p: *u8, n: i64): stringLowercase hex of n raw bytes (socket addresses in a record's arg).
replayHole
pub fn replayHole(name: &string, site: *u8): voidReport, once per name, that this run called an extern the trace cannot answer, and note it in the trace (trace.hole: arg the name, payload where), so milo trace shows a recording with gaps. Under replay the note is consumed silently; the call itself runs for real in both.
replayHoleAt
pub fn replayHoleAt(name: *u8, site: *u8): voidreplayHole with C strings, for a report the compiler inserts (a thread closure that shares unsafe memory, see src/checker-program-passes.ts: checkThreadBoundary).
replayInts
pub fn replayInts(p: &string): Vec<i64>The space-separated decimals of a payload (several numbers in one answer).
replayJoinNul
pub fn replayJoinNul(items: &Vec<string>): stringStrings joined with a NUL after each: the encoding for argv and the environment, whose entries are C strings and so can never contain one.
replayLseek
pub fn replayLseek(fd: i32, off: i64, whence: i32): i64lseek(2) through the trace: a file's size and position are answers from the OS.
replayOff
pub fn replayOff(): boolTrue when neither recording nor replaying. The single check every hook makes before touching the OS; a not-yet-decided mode reads as "not off" so the slow path gets to decide it.
replayOpen
pub fn replayOpen(path: &string, flags: i32, mode: i32): i32open(2) through the trace. Under replay nothing is opened (a write-mode open does not create or truncate the file) and the recorded fd comes back as a synthetic one.
replayPut
pub fn replayPut(kind: &string, arg: &string, payload: &string): voidAppend one record: <seq> <kind> <argLen> <payloadLen>\n<arg>\n<payload>\n. arg names the call (an env var name, a byte count) so replay can tell a different call of the same kind from the recorded one; payload is the answer. No-op unless recording.
replayPutFrom
pub fn replayPutFrom(kind: &string, buf: *u8, n: i64): voidRecord: the n bytes the OS just wrote into buf.
replayPutI64
pub fn replayPutI64(kind: &string, arg: &string, v: i64): voidreplayPut with a decimal payload.
replayRead
pub fn replayRead(fd: i32, buf: *u8, len: i64, socket: bool): i64One read(2) or recv through the trace. The payload is = and the bytes, or ! and the errno (EAGAIN included: a green read that would block is recorded too, and the park that follows is what the scheduler records replay).
replayRecording
pub fn replayRecording(): boolTrue when hooked calls append their answers to a trace.
replayRet
pub fn replayRet(kind: &string, arg: &string, net: bool, live: () => i64): i64Run a call that answers with a number (an fd, a status, an offset; negative with errno set on failure) through the trace: recorded with its errno, or answered from the trace with errno restored. net picks the socket error channel (WSAGetLastError on Windows). Callers check replayOff() first, so the closure costs nothing when off.
replaySchedPut
pub fn replaySchedPut(payload: &string): voidRecording: one green-scheduler poll's outcome (std/runtime: pollAndWake). An empty payload is a poll that woke nothing and only bumps a counter; anything else is a sched.pick record naming the fds that came ready and the timer arms that fired.
replaySchedTake
pub fn replaySchedTake(): stringReplay: what the poll at this point woke when recorded ("" for nothing). A poll past the end of the trace is idle: the recorded run's trailing idle polls were never written.
replaySplitNul
pub fn replaySplitNul(payload: &string): Vec<string>The inverse of replayJoinNul.
replaySyncAtomic
pub fn replaySyncAtomic(op: &string, f: () => i64): i64One ordered atomic operation (std/sync's Atomic* types): f performs it and returns its result. Recording runs it inside the trace lock, so the record order is the order the operations really happened in; replay runs it on this thread's turn and checks it returned what it returned when recorded.
replaySyncEnter
pub fn replaySyncEnter(): voidReplay: wait until this thread's next record is at the head of the trace. std/sync calls it before taking a primitive's own lock, so the operation runs exactly when it ran in the recording, after every operation recorded before it. No-op when recording.
replaySyncNote
pub fn replaySyncNote(op: &string, outcome: i64): voidOne ordered sync operation, made while the primitive's lock is held: what it did (op, e.g. "recv") and how it came out (outcome: ready or not, the value an atomic returned). Recorded as sync.<op> with both in the arg; under replay the same operation must come out the same way, or the run has diverged.
replaySyncOn
pub fn replaySyncOn(): boolTrue when cross-thread sync operations are ordered: recording or replaying a run that has started an OS thread. Before the first thread exists every sync operation happens on the main thread, whose order the scheduler records already.
replayTake
pub fn replayTake(kind: &string, arg: &string): stringThe recorded answer to this call. Exits with "replay diverged at record N: expected <recorded call>, got <this call>" when the next record of this thread is a different call, or the trace has run out. With several threads, waits until this thread's record reaches the head of the trace.
replayTakeI64
pub fn replayTakeI64(kind: &string, arg: &string): i64replayTake of a decimal payload.
replayTakeInto
pub fn replayTakeInto(kind: &string, buf: *u8, n: i64): voidReplay: copy the recorded n bytes into buf. The byte count is the call's arg, so a request for a different count diverges instead of reading past the record.
replayThreadExit
pub fn replayThreadExit(): voidLast thing on a thread std started. Under replay a thread waiting for a finished thread's record is then a divergence, not a hang.
replayThreadSpawn
pub fn replayThreadSpawn(): i64Called where std starts an OS thread, before it starts: numbers the new thread and records that it was started (thread.spawn, payload the number), so the numbering replays. From here on every sync operation is ordered. Returns 0 when off.
replayThreadStart
pub fn replayThreadStart(tid: i64): voidFirst thing on a thread std started: take the number its parent recorded.
replayWrite
pub fn replayWrite(fd: i32, buf: *u8, len: i64, socket: bool): i64One write(2) or send through the trace: what is recorded is that it happened, to which fd, how many bytes were offered and how many went (or the errno). Under replay the write reaches the OS only for the inherited stdout and stderr, so the user sees the program's output; a write to a file, socket, pipe or pty is answered from the trace and never performed (the peer does not exist, and a replay must not change the filesystem).