Writing the abstract requires every other section to exist first, and several sections have real sub-tasks of their own (running experiments, gathering data, and analysing it are separate delegated tasks). Each task is assigned to a worker role, then gated by a QA review before it counts as done. Real projects aren't linear, though: a review can fail and blame an earlier stage instead of approving it, reopening it and everything downstream that depended on it - a dependency DAG resolved goal by goal, with feedback loops, replayed here from a single precomputed event log (a virtual clock, not real time - the whole simulation computes instantly). A red review segment means that attempt was sent back for rework; a task with more than one attempt shown is labelled (x2) etc.
# Project report simulator: goal-oriented dependency resolution, with each
# task delegated to a worker role (OO), gated by a QA review stage, and
# timestamped on a virtual clock (a plain counter, not real wall-clock time)
# so a browser can replay the schedule as an animated timeline. Real
# projects aren't linear: a review can fail and blame an earlier stage,
# which reopens it AND everything downstream that depended on it - design
# might get sent back to methodology, experiments might get sent back to
# implementation or methodology, data analysis might require experiments to
# be re-run, and so on. Each task can only trigger this once itself
# (attempts < 1), so the whole simulation is guaranteed to converge no
# matter how the dice land.
# Tiny LCG (same constants as lib/maze.patlang's, duplicated rather than
# shared since the two demos are otherwise unrelated) for the QA dice-roll.
make a function called rand_seed takes seed returns done
let s = seed % 32768
if s < 0 then
let s = s + 32768
end
set_var("rp_rand", s)
return true
end
make a function called rand_next returns r
let s = get("__vars", "rp_rand")
let s2 = ((s * 1103) + 12345) % 32768
set_var("rp_rand", s2)
return s2
end
make a function called rand_below takes n returns r
return rand_next() % n
end
make a function called registry_init returns done
new("Registry", "registry")
send("registry", "set", "names", [])
return true
end
make a function called task takes name, worker, duration, requires, rework_targets returns done
new("Task", name)
send(name, "set", "worker", worker)
send(name, "set", "duration", duration)
send(name, "set", "requires", requires)
send(name, "set", "rework_targets", rework_targets)
send(name, "set", "attempts", 0)
send(name, "set", "status", "pending")
send("registry", "set", "names", list_push(get("registry", "names"), name))
return true
end
make a function called vclock returns t
return get("__vars", "vt")
end
make a function called advance takes ms returns done
set_var("vt", get("__vars", "vt") + ms)
return true
end
make a function called review_time takes duration returns rt
let rt = duration / 4
if rt < 150 then
let rt = 150
end
return rt
end
make a function called log_event takes phase, name, worker returns done
let line = vclock() + "," + phase + "," + name + "," + worker
let cur = get("__vars", "report_log")
if cur == "" then
set_var("report_log", line)
else
set_var("report_log", cur + ";" + line)
end
return true
end
# Marks a task (and, transitively, every task that required it, directly or
# indirectly) as no longer approved, since their prior work assumed an input
# that has just changed. A fixed-point relaxation over the whole registry -
# cheap at this scale, and doesn't need a precomputed reverse-dependency map.
make a function called reopen_cascade takes name returns done
send(name, "set", "status", "pending")
let names = get("registry", "names")
let changed = true
while changed do
let changed = false
let i = 0
while i < names.length do
let t = names[i]
if get(t, "status") != "pending" then
let reqs = get(t, "requires")
let j = 0
let hit = false
while j < reqs.length do
if get(reqs[j], "status") == "pending" then
let hit = true
end
let j = j + 1
end
if hit then
send(t, "set", "status", "pending")
let changed = true
end
end
let i = i + 1
end
end
return true
end
# Achieving a goal: make sure every requirement is already approved,
# recursing into whichever aren't; delegate the work to its assigned
# worker; then gate it behind a QA review, which - once per task, at
# most - might fail and blame an earlier stage instead of approving it.
make a function called achieve takes name returns done
if get(name, "status") == "approved" then
return true
end
goal("complete", name)
let reqs = get(name, "requires")
let i = 0
while i < reqs.length do
achieve(reqs[i])
let i = i + 1
end
let worker = get(name, "worker")
let duration = get(name, "duration")
send(name, "set", "status", "in_progress")
log_event("start", name, worker)
advance(duration)
log_event("work_done", name, worker)
send(name, "set", "status", "in_review")
log_event("review_start", name, "qa")
advance(review_time(duration))
let attempts = get(name, "attempts")
let targets = get(name, "rework_targets")
let should_fail = false
if (attempts < 1) and (targets.length > 0) then
if rand_below(100) < 30 then
let should_fail = true
end
end
if should_fail then
let blame = targets[rand_below(targets.length)]
log_event("rework:" + blame, name, "qa")
send(name, "set", "attempts", attempts + 1)
reopen_cascade(blame)
emit("task_reworked", name + ":" + blame)
return achieve(name)
end
log_event("review_done", name, "qa")
send(name, "set", "status", "approved")
fact("approved", name, "1")
emit("task_done", name)
return true
end
make a function called report_reset returns done
set_var("vt", 0)
set_var("report_log", "")
registry_init()
rand_seed(7)
return true
end