Designing well-behaved PatLang programs: common conventions for long-lived processes
Most PatLang example programs in this site run once, print something, and exit — fine for a demo, wrong for anything meant to run as a daemon, be discovered by other processes, or be controlled from a second invocation while the first is still running. This page collects the conventions that make a PatLang program a good citizen in that second world: predictable signal handling, discoverability, and durable handoff — all built from real, working library code (self_hosting/lib/signals.patlang, signal_discovery.patlang, queue.patlang), not invented for this page. The build daemon demo is the single most complete worked example of everything below, in one real program.
Declare your events up front, not conditionally
when EVENT do ... end handlers are collected once, statically, when a program is lowered — they are a genuinely top-level-only declaration, not ordinary control flow. Nesting a when inside an if, a function body, or a loop is a hard parse error, on purpose: an event handler that only sometimes exists, depending on runtime state, is exactly the kind of implicit, hard-to-predict behaviour this convention rules out by construction. Write every handler your program will ever respond to at the top level, unconditionally, even if some of them will rarely fire:
when status do
signal_reply("...")
end
when rebuild do
set_var("should_rebuild", "1")
end
when quit do
set_var("should_stop", "1")
end
A reader (or another process's author, deciding whether to talk to yours) can see your program's entire external control surface by scanning for when at the top of the file — they shouldn't have to trace through conditional logic to find out what it can respond to.
Handle the standard trio: status, an action verb, and quit
Three signal names show up, by convention rather than by any enforced rule, across every long-running PatLang example in this codebase: status (report what you're doing right now, synchronously, to whoever asked), an action verb specific to what your program does (rebuild, refresh, recheck — whatever the one thing a caller might want to trigger is called), and quit (stop cleanly). Keeping to this shape means anyone who has used one PatLang daemon already knows how to poke at another one.
The mechanism underneath is self_hosting/lib/signals.patlang's claim/poll/reply pair. One process claims a port (becoming the daemon); every other invocation of the same program, on the same port, becomes a client instead:
let port_id = signal_claim(MY_PORT)
if port_id >= 0 then
# We're the daemon: this invocation holds the port.
while get("__vars", "should_stop") == "0" do
signal_poll(port_id, 200) # dispatches to the when handlers above
end
else
# We're a client: someone else already holds the port.
let reply = signal_query(MY_PORT, "status", "")
print(reply)
end
signal_query blocks for a reply (right for status, where the caller wants an answer); signal_send fires and forgets (right for rebuild/quit, where the caller just wants the action to happen). Inside a handler that owes the caller an answer, call signal_reply(text) exactly once — that's what a blocking signal_query on the other end is actually waiting on.
Make yourself discoverable, if you want to be found
Claiming a port and answering signals is enough for a human to control your program from a second terminal, knowing the port number in advance. It is not enough for another PatLang program to find yours without being told where to look. That's what self_hosting/lib/signal_discovery.patlang is for: signal_announce(task_name, port, actions) writes a small, TTL-expiring beacon recording your program's name, port, and the list of actions it accepts; signal_discover_all(max_age_ms) (or signal_discover_one for a specific name) lets any other program enumerate what's currently alive and callable, filtered by freshness so a crashed process's stale beacon doesn't look alive forever. signal_wrap/signal_call turn a discovered task into something closer to an ordinary callable object, and signal_find_capable(keyword, max_age_ms) lets a caller ask "who can currently do X" instead of hard-coding a task's name at all — the mechanism the self-orchestrated microservices demo is built entirely around. Announce right after you claim your port, with the real action list your when handlers actually implement — a beacon advertising a capability you don't have is worse than no beacon at all.
Use the message queue, not signals, for anything that must survive a crash
Signals are live, in-memory, and gone the moment either side isn't currently running. That's the right tradeoff for "are you there, and what's your status" — wrong for work that must not be silently dropped if the daemon restarts mid-task. self_hosting/lib/queue.patlang's queue_publish/queue_consume/queue_ack give you a small, file-backed, at-least-once queue instead: a publisher writes a durable row, a consumer reads it back (even after a restart, even from a different process entirely), and only an explicit queue_ack after the work is genuinely done removes it. If a consumer crashes between reading and acking, the message is still there on the next run — the queue's own durability, not a client program's own reliability, is what a caller is trusting. Reach for signals to ask "what's happening right now"; reach for the queue when the honest answer to "what happens if this process dies right now" needs to be "nothing is lost," not "start over."
A short checklist
- Are every one of your program's
whenhandlers declared at the top level, unconditionally — not nested inside anifor a function? - Does your program respond to
status(synchronous, viasignal_reply), and toquit— even if you add nothing else? - If your program does one specific useful action, does a single, clearly-named verb signal trigger it, rather than overloading
statusto also mean "and also do the thing"? - If another PatLang program might reasonably want to find yours without being told the port number in advance, have you called
signal_announcewith an accurate action list, right after claiming the port? - For anything that must not be silently lost if your process crashes mid-task, are you using the message queue's publish/consume/ack cycle instead of a signal?
- Does a first-time reader of your source file understand your program's entire external control surface just from the
whenhandlers at the top, without having to trace runtime state?
See also
The build daemon demo for the single most complete real example of every convention on this page working together (logic + GOAP + self-timing + events + signals, all in one program). Presence, Discovery, and Non-Blocking Spawn for the full design and worked examples behind signal_announce/signal_discover_all/signal_wrap. The self-orchestrated microservices demo for discovery driving real dispatch between processes that started with no prior knowledge of each other. The queue recovery demo for the durable message-queue mechanism under real crash-and-restart conditions. The Paradigms Guide's Event-Driven section for why when is a top-level-only declaration, not ordinary control flow.