PatLang Idioms & Patterns
Grammar and host-function references say what's available. This page is about what to actually reach for, and why — the patterns that recur across PatLang's own compiler, standard library, and demo programs, most of which aren't obvious from reading the grammar alone.
Facts instead of re-parsing
When a program (the maze solver, the point-of-sale discount rule) needs to reason about relationships between things it already knows, the idiom is to assert them as fact calls once, up front, and then query against that fact store — rather than re-deriving the relationship by walking a data structure every time it's needed. This trades a small amount of up-front bookkeeping for logic that reads as declarative statements of what's true, which is also what makes the maze solver's map readable as a list of fact("open", [a, b]) calls rather than a hand-rolled adjacency-list walk.
Contracts as one primitive, three keywords
require, ensure, and assert are three keywords precisely so that intent reads clearly at the call site — precondition, postcondition, or plain invariant — while all three collapse to a single contract_check host call underneath. The idiom this enables: write contracts as if they were free, because failure handling, message formatting, and the underlying check are unified in one place rather than three. There is no cost to using all three liberally in the same function.
Chunk composition and cross-chunk edges
Because host functions are grouped into named chunks that a compiled program only pulls in on demand, an idiom worth knowing is: keep an eye on which chunks a design will actually need, since two chunks pull in a second chunk implicitly — math pulls in numeric_tower, and oo pulls in logic (because send's relation-inference path reads state the logic chunk declares). This is invisible at the call-site level, but it means a program that just wants simple objects, with no logic programming anywhere in sight, still gets the logic chunk's prelude compiled in. It's a real cost worth knowing about for size-sensitive compiled output, not a bug to work around in ordinary code.
Handle-based vs list-based collections
PatLang has two parallel collection APIs: plain lists (list_get/list_len/list_push/list_set, in the core chunk) and a handle-based vector API (vec_new/vec_push/vec_set/vec_get/vec_len/vec_to_list, in collections_handles). The idiom: use plain lists for values you build once and pass around functionally (the common case, and the one that matches the functional paradigm's grain); reach for the handle-based vec_* API specifically when you need a mutable collection that's cheap to grow in a loop and shared by reference across calls rather than rebuilt and re-returned each time. vec_to_list is the escape hatch back to the plain-list world once mutation is done.
Closures via captured-name lists
A PatLang closure's runtime representation is a function name plus an explicit list of captured (name, value) pairs — not an opaque environment pointer. The practical idiom this produces: keep the set of names a closure captures small and explicit (the event-loop and event-driven-server examples both lean on single-purpose closures capturing one or two names), because every captured name is visible, inspectable state rather than something hidden inside a closure's environment. This is also why reflection (reflect.patlang) can turn closures into JSON as straightforwardly as it can turn anything else — there's no opaque environment to lose in translation.
The self-hosted-mirror-with-parity-test pattern
The compiler's own back end exists twice: once as Rust (rust-runtime/src/ir/codegen.rs), and once as its PatLang mirror (self_hosting/lib/runtime_rs.patlang, explicitly a generated file). A dedicated test (selfhost_runtime_text_parity) asserts the two stay byte-identical in the Rust text they emit. The idiom generalises beyond the compiler itself: whenever a piece of runtime behaviour needs to exist in two forms for two different execution paths, keep one canonical source, generate or mirror the other, and pin the relationship with a parity test — rather than trusting two independently-maintained implementations to agree by discipline alone. It's the same reason the interpreted, natively-compiled, and self-hosted-compiled paths for new features (concurrency, most recently) get checked against each other rather than just built once and assumed correct.
The division gotcha, as a cautionary idiom
PatLang's / promotes uneven integer division to an exact Rational rather than truncating — see the Paradigms Guide's numeric-tower section. The idiom this forces: never simulate integer modulo by hand as n - (n / i) * i, because the intermediate n / i is not the truncated quotient you'd get in a C-family language — it silently becomes an exact fraction, and the "modulo" computed from it is wrong. Use % directly; it has its own, correct remainder semantics and does not go through the same promotion rule as /. This bit a real primality-test implementation during development of the concurrency demos, which is exactly the kind of mistake this idiom exists to prevent.
Syntax DSLs for genuinely separate notations
The syntax NAME { ... } mechanism expands trigger tokens against raw source before lexing — powerful, but the idiom is restraint: reach for it only when a sub-problem is better expressed as its own compact notation (the router DSL's routing table shape) than as calls into a general API. Because expansion happens on raw text before the normal lexer/parser ever runs, a DSL that's too close to ordinary PatLang syntax buys confusion, not clarity.
See also
- Paradigms Guide — the paradigms these idioms sit on top of.
- Standard Library & Host Function Reference — the exact functions named above.
- Capabilities & Honest Limitations — what none of these idioms can currently work around.