The engine (Rust)
Today, generators are defined and run by phony-core, a Rust workspace; the phony CLI (train / generate / info / validate / stats, plus the package commands install / add / remove / update / list) wraps it for the command line. (Language libraries — PHP first, then JS/Python — are planned and will read the same portable artifacts; this page documents the engine you can use now.)
phony-core has two crates:
| Crate | Role | API |
|---|---|---|
phony-pgdl | the generator layer — the envelope, registry, the core generators, PEL, composition, packages, the locale resolver | phony-pgdl API |
ngram-core | the N-gram engine — the Model generator (train + generate + the .ngram format) | ngram-core API |
phony-pgdl calls into ngram-core for model-type generation.
Install
Until published to crates.io, use a path dependency:
# Cargo.toml
phony-pgdl = { path = "../phony-core/crates/pgdl" }
ngram-core = { path = "../phony-core/crates/ngram-core" }
serde_json = "1"Quick start — call a generator
Define a generator, then invoke it. Definition and invocation are separate steps.
use std::sync::Arc;
use phony_pgdl::{compile_generator, generate, register_composition, InMemoryAssets, Registry};
use ngram_core::NgramModel;
use serde_json::json;
// 1. Assets — the models/lists the `model`/`list` generators look up by name.
let assets = InMemoryAssets::new()
.with_model("person.first_names", NgramModel::train_words(&["mehmet", "ayse", "mustafa", "zeynep"], 3));
// 2. Registry — built-ins + assets + (optionally) a frozen clock. Register any
// data-defined (composition) generators you want available.
let mut registry = Registry::with_builtins()
.with_assets(Arc::new(assets))
.with_reference_time(1_704_067_200); // 2024-01-01T00:00:00Z
register_composition(&mut registry, &json!({
"name": "@demo:user_email",
"let": { "first": { "type": "model", "source": "person.first_names", "generation": { "mode": "word" } } },
"body": "{{ lowercase(first) }}@example.com"
})).unwrap();
// 3. Invoke it — one value, or many. Byte-identical for the same seed.
let def = compile_generator(&json!({ "use": "@demo:user_email", "unique": true })).unwrap();
let emails = generate(&def, ®istry, /* seed */ 2026, 100).unwrap(); // 100 unique emails (Vec<Value>)Entry points
| Function | Produces |
|---|---|
generate_one(def, registry, seed, index) | one value for invocation index |
generate(def, registry, root_seed, count) | count values (honours unique/nullable) |
compile_generator(json) | a per-type PGDL def → the uniform GeneratorDef envelope |
register_composition(registry, json) | register a data-defined (Tier 1) generator |
load_package / load_package_dir | register a .phony package's generators + assets |
Assets
Generators never embed data — they resolve it from an AssetProvider:
pub trait AssetProvider {
fn model(&self, name: &str) -> Option<Arc<NgramModel>>;
fn list(&self, name: &str) -> Option<Arc<serde_json::Value>>;
}InMemoryAssets— register models/lists directly.LocaleAssets— resolve(asset, locale)through a chain (see phony-pgdl API → Assets & locales).
The frozen clock
now()/today()/age() and relative date ranges resolve against an injected reference instant, never the wall clock:
let registry = Registry::with_builtins().with_reference_time(1_704_067_200);So (definition, seed, index, reference-time) fully determines the output — see How generation works.
The portable contract
Everything cross-runtime — the SplitMix64 PRNG, FNV-1a hashing, seed derivation, PEL semantics, generator output, and the n-gram train → serialize → generate pipeline — is pinned by a frozen conformance corpus (phony-core/conformance/vectors.json). A language port (PHP, JS, Python, …) is correct when it reproduces every vector byte-for-byte; any engine drift fails CI.
Build & test (contributing)
cargo test # whole workspace
cargo clippy -p phony-pgdl
cargo bench -p ngram-core # engine throughput