Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lens DSL: denotational semantics

A lens DSL document describes a bidirectional transformation between schemas. A document may contain field or sort edits, value-level expressions, a composition, a symmetric pair of pipelines, or a schema-dependent generated body. Compilation produces a CompiledLens containing a protolens chain and any value-level field transforms. Deserialization and compilation do not establish the lens laws for every instance.

Lenses and round-trip laws supplies complements and the three laws used here. Expression language supplies the value-level computations embedded in a lens specification.

Surface syntax

Nickel is the canonical authoring form. JSON and YAML represent the same structures through serde.

{
  id = "user.v3-to-v4",
  description = "Rename `name` and replace `age` with `years`",
  source = "dev.example.user.v3",
  target = "dev.example.user.v4",
  steps = [
    { rename_field = { old = "name", new = "display_name" } },
    { remove_field = "age" },
    { add_field = { name = "years", kind = "integer", default = 0, expr = "old.age" } },
  ],
}

Each step is a single-key object whose key selects the variant. The full step grammar is in crates/panproto-lens-dsl/src/document.rs.

Abstract syntax

The listing below is a schematic inventory of document fields and step variants. It is text because supporting types, derives, imports, and representation details are omitted.

pub struct LensDocument {
    pub id: String,
    pub description: String,
    pub source: String,
    pub target: String,

    // Body: exactly one of the six variants is present.
    pub steps:     Option<Vec<Step>>,
    pub rules:     Option<Vec<Rule>>,
    pub compose:   Option<ComposeSpec>,
    pub auto:      Option<AutoSpec>,
    pub from_diff: Option<FromDiffSpec>,
    pub symmetric: Option<SymmetricSpec>,

    // Modifier: oriented rewrites appended to the compiled chain.
    pub directed_equations: Option<Vec<DirectedEquationSpec>>,

    // Rule-variant metadata.
    pub passthrough: Option<Passthrough>,
    pub invertible:  Option<bool>,

    // Protocol-specific extension metadata.
    pub extensions: HashMap<String, serde_json::Value>,
}

pub enum Step {
    // High-level field combinators
    RemoveField { remove_field: String },
    RenameField { rename_field: RenameSpec },
    AddField    { add_field: AddFieldSpec },

    // Value-level transforms
    ApplyExpr    { apply_expr: ApplyExprSpec },
    ComputeField { compute_field: ComputeFieldSpec },

    // Structural combinators
    HoistField { hoist_field: HoistSpec },
    NestField  { nest_field: NestSpec },
    Scoped     { scoped: ScopedSpec },
    Pullback   { pullback: PullbackSpec },

    // Sort-level coercions and merges
    CoerceSort { coerce_sort: CoerceSortSpec },
    MergeSorts { merge_sorts: MergeSortsSpec },

    // Elementary theory operations
    AddSort      { add_sort: AddSortSpec },
    DropSort     { drop_sort: String },
    RenameSort   { rename_sort: RenameSpec },
    AddOp        { add_op: AddOpSpec },
    DropOp       { drop_op: String },
    RenameOp     { rename_op: RenameSpec },
    AddEquation  { add_equation: EquationSpec },
    DropEquation { drop_equation: String },
}

The top-level type is LensDocument. Its source and target fields are schema identifiers, and exactly one of steps, rules, compose, symmetric, auto, or from_diff must be present. A directed_equations modifier may accompany that body and appends oriented rewrites to the compiled chain.

Compilation has two entry points. compile handles the schema-parametric bodies and rejects auto and from_diff with LensDslError::AutoRequiresSchemas. compile_with_schemas also handles those generated bodies. For every nonsymmetric body, it instantiates the chain at the supplied source schema and compares the declared target with the NSID of the output schema’s primary entry. A mismatch yields LensDslError::TargetMismatch. The check is skipped when the declared target is empty or the output schema has no primary-entry NSID, so successful compilation does not always verify the target identifier.

Semantic domain

For source instances , view instances , and complement values , panproto’s operational lens interface has the shape

The complement is returned by get; callers do not pass the original source separately to put. The concrete panproto_lens::Lens stores source and target schemas and a compiled migration containing the field transforms. This is an asymmetric-lens interface with explicit saved state, related to the models of Foster et al. (2007) and Litt et al. (2020).

For a steps body, compilation returns a schema-parameterized chain and a map of field transforms:

Instantiating the chain at a source schema produces a concrete lens. auto and from_diff require the source schema, target schema, and protocol during compilation as well.

The three laws

A lens is lawful when the following equations hold for the relevant source values and views. Write .

For an edited view , write and . Then

The current PutPut checker uses the original view for its first put. If , , and , it checks a supplied second view by comparing

panproto_lens::laws::check_get_put, check_put_get, and check_put_put check supplied instances deterministically. check_put_put obtains the intermediate complement as shown above. The checkers compare complete instance structure through the crate’s instance-equivalence predicate, but each invocation covers only its supplied case.

Semantic equations

Schema-level steps compile to protolenses, which are schema-parameterized lenses described in Protolens composition. Value-level apply_expr and compute_field steps instead compile to FieldTransform values keyed by the body vertex. An add_field step contributes a schema-level protolens and, when it has an expression, a ComputeField transform. The steps are processed from left to right.

Here , and is the target schema after step is applied to . The semicolon denotes sequential lens composition:

The step compiler is the authoritative mapping from every Step variant to these two outputs. Adjacent schema-level steps must satisfy protolens_composable; Protolens composition gives the exact predicate.

Complement composition

Sequential composition combines complements through the partial operation supplied by ComplementCompose. The empty complement is a two-sided identity. Property tests check commutativity and associativity on generated compatible complements. Composition rejects two nonzero, unequal source fingerprints with ComplementFingerprintMismatch; it rejects incompatible values stored under the same keyed complement field with ComplementConflict. Vector and set-like fields are merged with deduplication. ComplementCompose::is_compatible tests whether composition would succeed without allocating the result.

The fingerprint is a 64-bit hash computed by panproto_lens::asymmetric::schema_fingerprint. Compatibility means equality of the resulting nonzero fingerprints, not a proof that distinct schema values are isomorphic.

Checks and limits

Lawful lens composition preserves the three equations when its premises hold. Property tests in crates/panproto-lens/src/laws.rs and constructed DSL cases in crates/panproto-lens-dsl/tests/step_laws.rs test selected generated inputs and step constructors. Finite generated samples do not prove lawfulness for every document or instance.

A coerce_sort step and directed equations are also checked for honesty against registered samples. A detected violation returns LensDslError::CoercionNotHonest. Passing means that the sampled values satisfied the declared round-trip class.

The runtime checkers test one supplied case at a time, while property tests cover generated cases. Documents containing lossy or opaque transforms require scoped law claims; compilation alone does not prove them lawful. The semantics fixes the result of put, not its running time, and preserves step order without defining an equivalence on distinct documents that compile to the same lens.

See also