Skip to content

phony-pgdl API ​

The generator layer: it defines generators and produces values. This page is the Rust API reference. For the language, see The PGDL Language.

Invoking generators ​

rust
fn generate_one(def: &GeneratorDef, registry: &Registry, seed: u64, index: u64)
    -> Result<Value, GenError>;

fn generate(def: &GeneratorDef, registry: &Registry, root_seed: u64, count: usize)
    -> Result<Vec<Value>, GenError>;
  • generate_one produces one value for invocation index, under seed. It first validates def.params against the resolved generator's manifest (unknown/mistyped/missing-required → BadParams, before any value is drawn), then rolls nullable, runs the generator, and applies transform.
  • generate is the unit of generation — call a generator count times under root_seed. Each invocation i gets its own derived seed and ordinal i, and generate enforces the unique modifier: values are de-duplicated by a type-tagged key (string "1" never collides with number 1), collisions re-roll with a varied seed, and nulls from nullable are exempt — they neither enter the dedup set nor burn retries.

A generator is pure: it knows nothing about tables, rows, or relationships, and its output may be a scalar, object, or array (nested, keys optional). To build structured or coherent values, compose generators (see Composition).

The envelope ​

Every generator invocation is a GeneratorDef:

rust
struct GeneratorDef {
    use_ref: String,        // serialized as "use" — e.g. "@phony/core:logic.int_between"
    params: Value,
    constraints: Value,
    modifiers: Modifiers,
}

struct Modifiers {
    unique: Option<bool>,      // distinct across a run of invocations
    nullable: Option<f64>,     // probability the value is null
    transform: Option<String>, // a PEL expression over `value`
}

compile_generator(pgdl: &Value) -> Result<GeneratorDef, String> maps a PGDL per-type definition ({ "type": "logic", "algorithm": …, "params": … }) — or a { "use": "@scope:name" } reference — onto this envelope.

A generator's manifest describes its accepted parameters so an envelope can be checked without the implementation:

rust
struct Manifest { name: String, version: String, summary: String,
                  params: BTreeMap<String, ParamSpec>, output: String, deterministic: bool }

fn validate(def: &GeneratorDef, registry: &Registry) -> Result<(), Vec<String>>;

The registry ​

An open lookup from namespaced reference to generator, plus the asset provider and reference instant used during generation.

rust
impl Registry {
    fn new() -> Self;                 // empty
    fn with_builtins() -> Self;       // all @phony/core:* generators
    fn register(&mut self, g: Arc<dyn Generator>) -> Option<Arc<dyn Generator>>;
    fn resolve(&self, reference: &str) -> Option<&Arc<dyn Generator>>;
    fn names(&self) -> Vec<&str>;
    fn with_assets(self, assets: Arc<dyn AssetProvider>) -> Self;
    fn set_assets(&mut self, assets: Arc<dyn AssetProvider>);
    fn with_reference_time(self, epoch: i64) -> Self;
    fn set_reference_time(&mut self, epoch: i64);
}

Custom generators ​

Implement Generator and register it under a namespaced name. execute must be a pure function of ctx.seed, params, and constraints.

rust
trait Generator: Send + Sync {
    fn manifest(&self) -> &Manifest;
    fn execute(&self, ctx: &GenContext, params: &Value, constraints: &Value)
        -> Result<Value, GenError>;
}

struct GenContext<'a> {
    seed: u64,            // derived for this exact invocation
    index: u64,           // invocation ordinal (sequence/cycle key off it)
    registry: &'a Registry,
    assets: &'a dyn AssetProvider,
    depth: u32,           // composition-recursion bound
    bindings: Option<&'a Bindings<'a>>, // the generator's own local scope
    reference_time: i64,
}

struct Bindings<'a> { values: &'a serde_json::Map<String, Value> }

bindings is the generator's own scope — its params and any let results. There is no table, row, or sibling field. Draw randomness only from SplitMix64::new(ctx.seed) — never the wall clock or a global RNG.

Composition ​

A generator authored as pure data ({ name, params, let, body }) — see Composition & packages.

rust
fn compile_composition(def: &Value) -> Result<Arc<dyn Generator>, String>;
fn register_composition(registry: &mut Registry, def: &Value) -> Result<(), String>;

In a structured output, every string leaf gets its own seed salted by the leaf's JSON path ("a", "items.0.b", …) — so two sibling fields with the same template still produce independent draws.

Packages ​

Load a .phony package — its data-defined generators and locale assets — into a registry + LocaleAssets:

rust
fn load_package(registry: &mut Registry, assets: &mut LocaleAssets, doc: &Value)
    -> Result<LoadReport, String>;
fn load_package_dir(registry: &mut Registry, assets: &mut LocaleAssets, dir: impl AsRef<Path>)
    -> Result<LoadReport, String>;

PEL ​

rust
mod pel {
    fn render_template(src: &str, ctx: &GenContext) -> Result<String, GenError>;
    fn eval_expression(src: &str, ctx: &GenContext, bound: Option<Value>) -> Result<Value, GenError>;
    fn referenced_names_in_template(src: &str) -> Vec<String>;
    fn referenced_names_in_expr(src: &str) -> Vec<String>;
}

render_template renders a {{ … }} template; eval_expression evaluates one expression (with an optional bound value, used by transform). References resolve against ctx.bindings. Every function call is arity-checked (wrong argument counts return GenError::BadParams, never a panic); string literals support the \n \t \\ \" \' escapes, and object literals ({ k: expr }) are expressions. The referenced_names_* helpers power a composition's topological let ordering. The full function set is in Expressions (PEL).

Assets & locales ​

rust
trait AssetProvider: Send + Sync {
    fn model(&self, name: &str) -> Option<Arc<NgramModel>>;
    fn list(&self,  name: &str) -> Option<Arc<Value>>;
}

In-memory:

rust
let assets = InMemoryAssets::new()
    .with_model("person.first_names", model)
    .with_list("geo.cities", json!(["Istanbul", "Ankara"]));

Locale-resolving — packages contribute (asset, locale) data; resolution walks a chain (most-specific first) with a * fallback:

rust
let assets = LocaleAssets::default()
    .contribute_list("coin.flip", "tr_TR", json!(["Yazı", "Tura"]))
    .contribute_list("coin.flip", "en_US", json!(["Heads", "Tails"]))
    .contribute_list("http.method", LOCALE_INDEPENDENT, json!(["GET", "POST"]))
    .with_chain(vec!["tr_TR".into(), "base".into()]);

Switching the chain switches the resolved data; the generator is unchanged. See Locales & the contribution model.

Determinism helpers ​

rust
fn derive_seed(root_seed: u64, key: &str, index: u64) -> u64;
fn fnv1a64(s: &str) -> u64;        // the portable string hash (pub — ports reimplement it)
const DEFAULT_REFERENCE_TIME: i64; // 2024-01-01T00:00:00Z

derive_seed is the exact recipe a non-Rust runtime reproduces:

rust
fn derive_seed(root, key, index) = mix64(root ^ fnv1a64(key) ^ index.wrapping_mul(0x9E3779B97F4A7C15))

key is a stable sub-identity (a let name, a column; empty "" at the top), FNV-1a/64 hashed; index is the invocation ordinal, spread across the word by a multiply with the golden-ratio constant; mix64 is the SplitMix64 finalizer. Because the terms are XOR-combined by name, adding or reordering fields never shifts the others. See How generation works.

The frozen known-answer vectors for all of this — SplitMix64, fnv1a64, derive_seed, PEL semantics, end-to-end generator runs, and the n-gram pipeline — live in the conformance corpus at phony-core/conformance/vectors.json; any runtime port must reproduce every vector byte-for-byte.

Errors ​

rust
enum GenError {
    Unknown(String),    // no such generator/asset/reference
    BadParams(String),  // params didn't satisfy the generator
}

Bad input is always a GenError, never a panic: every PEL function checks its argument count (wrong arity, an unknown function, divide/modulo by zero → BadParams), and generator params are validated against the manifest.

Phony Cloud — Documentation & Specification