Robots signaling each other

Live demo of cross-thread events (when/emit) built directly on the fiber/threading work documented in the Paradigms Guide and the fiber demo โ€” see also Capabilities & Honest Limitations for the underlying EVENT_HANDLERS fix.

Three robots, each a real fiber, navigate toward two waypoints apiece on a small grid. When a robot reaches a waypoint it calls emit("signal", ...) -- a when signal handler registered on the main thread fires and announces it, even though the emitting code is running on a different real OS thread (or, compiled to threaded WASM, a different real Worker). This is the direct, hands-on demonstration of the cross-thread event fix: previously EVENT_HANDLERS was thread_local, so this exact program's emit() calls would have silently done nothing.

PatLang source
# Robots signaling each other: three robots, each a real fiber (a real OS
# thread under the hood, or a real WASI-threads Worker when compiled to
# wasm32-wasip1-threads), navigate toward waypoints on a small grid. When a
# robot reaches a waypoint it calls emit("signal", ...) -- a `when signal`
# handler registered on the MAIN thread fires and prints the announcement,
# demonstrating that events now cross real thread boundaries (see the
# EVENT_HANDLERS fix: it used to be thread_local, so a fiber's emit() was a
# silent no-op; now it's a shared, Mutex-protected registry visible from
# any thread this runtime spawns).
#
# Movement steps are yielded one at a time (fiber_yield) and printed by
# whichever thread calls fiber_resume -- the main thread in this program,
# driving all three robots round-robin -- so the full step-by-step path is
# always captured in program order regardless of execution target.

when signal do
  print("SIGNAL " + event_data)
end

make a function called robot takes info returns done
  let id = info[0]
  let mut x = info[1]
  let mut y = info[2]
  let waypoints = info[3]
  let mut wi = 0
  let wp_count = to_num(list_len(waypoints))
  while wi < wp_count do
    let wp = list_get(waypoints, wi)
    let tx = wp[0]
    let ty = wp[1]
    while (x != tx) or (y != ty) do
      if x < tx then
        x = x + 1
      else
        if x > tx then
          x = x - 1
        end
      end
      if y < ty then
        y = y + 1
      else
        if y > ty then
          y = y - 1
        end
      end
      fiber_yield(id + "," + x + "," + y)
    end
    emit("signal", id + " reached (" + x + "," + y + ")")
    wi = wi + 1
  end
  return id + ",DONE"
end

let r0 = fiber_new("robot")
let r1 = fiber_new("robot")
let r2 = fiber_new("robot")

let v0 = fiber_resume(r0, ["R0", 0, 0, [[5, 2], [9, 5]]])
print(v0)
let v1 = fiber_resume(r1, ["R1", 9, 0, [[4, 3], [0, 5]]])
print(v1)
let v2 = fiber_resume(r2, ["R2", 0, 5, [[7, 4], [9, 0]]])
print(v2)

let mut done0 = false
let mut done1 = false
let mut done2 = false
while not (done0 and done1 and done2) do
  if not done0 then
    let v0 = fiber_resume(r0, 0)
    print(v0)
    if v0 == "R0,DONE" then
      done0 = true
    end
  end
  if not done1 then
    let v1 = fiber_resume(r1, 0)
    print(v1)
    if v1 == "R1,DONE" then
      done1 = true
    end
  end
  if not done2 then
    let v2 = fiber_resume(r2, 0)
    print(v2)
    if v2 == "R2,DONE" then
      done2 = true
    end
  end
end
print("all robots done")

Interpreted run on the build machine (pat --ir-run):

R0,1,1
R1,8,1
R2,1,4
R0,2,2
R1,7,2
R2,2,4
R0,3,2
R1,6,3
R2,3,4
R0,4,2
R1,5,3
R2,4,4
R0,5,2
R1,4,3
R2,5,4
SIGNAL R0 reached (5,2)
R0,6,3
SIGNAL R1 reached (4,3)
R1,3,4
R2,6,4
R0,7,4
R1,2,5
R2,7,4
R0,8,5
R1,1,5
SIGNAL R2 reached (7,4)
R2,8,3
R0,9,5
R1,0,5
R2,9,2
SIGNAL R0 reached (9,5)
R0,DONE
SIGNAL R1 reached (0,5)
R1,DONE
R2,9,1
R2,9,0
SIGNAL R2 reached (9,0)
R2,DONE
all robots done

Real "run in browser" button below: replays the exact step-by-step transcript from a genuine run compiled to wasm32-wasip1-threads, executed live via the hand-rolled WASI-threads shim -- not a hand-authored mock. Real cross-thread synchronization has more overhead in a browser Worker than natively, so this takes roughly 30-60 seconds; the status line explains why rather than leaving you staring at a spinner.

Live run, real OS threads, real cross-thread signaling: