How generation works
The technical "how": what happens between a generator definition and the value(s) it produces.
The pipeline
generator definition (PGDL / JSON)
│ 1. compile per-type def → uniform envelope { use, params, constraints, modifiers }
▼
for each invocation i (0..N):
│ 2. seed derive a per-invocation seed; carry the ordinal i
│ 3. resolve registry → generator → value (a composition runs its `let` steps in order)
│ 4. modifiers nullable · transform · (unique, across the run)
▼
value(s)1. Compilation
A PGDL generator ({ "type": "logic", "algorithm": "int_between", … }) compiles to the uniform envelope ({ use, params, constraints, modifiers }). type(+algorithm) becomes a namespaced use like @phony/core:logic.int_between; the right keys gather into params; unique/nullable/transform lift into modifiers. A use reference and the per-type shape are the same thing after compilation — one execution path.
2. Composition order
A composition's let steps are sorted topologically by dependency: a step (or the body) that references tax runs after tax. A cycle is a hard error, caught at registration (not at generation) — along with any reference to a name that is neither a param nor a let, so typos surface when the package loads rather than as junk output later. Each step runs a generator (or evaluates a PEL expression) and binds its result under a name that later steps and the body can reference. A step's own string params are themselves PEL templates over the locals already bound, so a value computed early threads into a sub-generator later — e.g. a repeat whose item min/max read a parent let.
3. Seed derivation (why it's reproducible)
Every invocation — and every sub-step inside a composition — gets its own seed, derived deterministically by the exact recipe every runtime reimplements:
seed = mix64( root_seed XOR fnv1a64(key) XOR index × 0x9E3779B97F4A7C15 )root_seed— the seed you pass.key— a stable sub-identity (aletname, a compositionoutputfield's JSON path; empty""at the top level), hashed with FNV-1a/64.index— the zero-based invocation ordinal (whatsequence/cyclekey off), spread across the word by a multiply with the golden-ratio constant0x9E3779B97F4A7C15.mix64— the SplitMix64 finalizer (two multiply-xorshift rounds), a fast avalanche so nearby inputs land far apart.
The three inputs are combined by XOR, so each contributes independently.
Positional independence
Because key is a name, not a position, the seed for one binding never depends on its neighbours. Add a field to a composition's output, or reorder the let steps, and every other field keeps the exact value it had — only the added field is new. This is the property that makes a schema change a small data diff instead of a total reshuffle, and it is why a let can be inserted without invalidating a committed fixture.
There is no ambient randomness. Inside a template each inline generator draws a fresh sub-seed per occurrence, so two {{number}}s differ. Likewise, each string leaf of a composition output is salted by its JSON path ("a", "items.0.b", …), so sibling fields with identical templates still draw independently. The frozen known-answer vectors for the whole portable contract — SplitMix64, fnv1a64, derive_seed, PEL, and full generator runs — live in the conformance corpus at phony-core/conformance/vectors.json; a port is correct exactly when it reproduces every vector byte-for-byte.
4. Coherence is a let
A generator's references resolve to its own bindings (its params + let results), not to any outer scope. So when you want one value used in several places, bind it once and reference the name:
"let": { "first": { "type": "model", "source": "person.first_names", "generation": { "mode": "word" } } },
"body": "{{ first }} <{{ lowercase(first) }}@x.com>"Both references read the one first value. A composition that emits coherent fields (an output over prior let steps) does the same internally: one drawn record drives all its columns.
The frozen clock
"Now" never comes from the system clock — it comes from an injected reference instant. now(), today(), age(), and relative ranges (-1year, now+7days, today-30days) all resolve against it:
let registry = Registry::with_builtins()
.with_reference_time(1_704_067_200); // 2024-01-01T00:00:00ZSo a generator that says "in the last year" produces the same dates every run.
Uniqueness
A unique generator is de-duplicated across a run of invocations: if a value collides with one already produced, the engine re-rolls (varied seed, up to 200 tries) before failing with a clear error. Dedup keys are type-tagged, so a string "1" and a number 1 never collide; nulls from nullable are exempt — they neither enter the dedup set nor burn retries. Unique values need entropy that varies on a re-roll (a range, an id) — a fixed value can't be made unique.
Invoking a generator
The engine is a Rust library (phony-core), wrapped by the phony CLI (train / generate / info / validate / stats); language libraries (PHP first, then JS/Python) are on the way. Definition and invocation are deliberately separate:
use std::sync::Arc;
use phony_pgdl::{compile_generator, generate, generate_one, InMemoryAssets, Registry};
use ngram_core::NgramModel;
// assets the generators look up by name
let assets = InMemoryAssets::new()
.with_model("person.first_names", NgramModel::train_words(&["mehmet", "ayse"], 3));
let registry = Registry::with_builtins()
.with_assets(Arc::new(assets))
.with_reference_time(1_704_067_200);
let def = compile_generator(&generator_json)?; // definition
let one = generate_one(&def, ®istry, /* seed */ 2026, 0)?; // one value
let many = generate(&def, ®istry, /* seed */ 2026, 1000)?; // a thousand| Function | Produces |
|---|---|
generate_one(def, registry, seed, index) | one value for invocation index |
generate(def, registry, root_seed, count) | count values (honours unique/nullable) |
Both entry points validate the envelope's params against the referenced generator's manifest before running it — an unknown key, a wrong type, or a missing required param is a BadParams error at the call, not silent junk in the output. generate additionally derives a fresh seed per invocation i, carries i as the ordinal, and enforces unique/nullable across the whole run. The full Rust API is in The engine → phony-pgdl API. The runtime story across CLI / OSS libraries / Cloud is in Spec → Execution Model.