Skip to content

DDSL Foundations and Towards dolo+

DDSL Phase 1.1 — Deck 2/2

Convening Meeting — January 2026


What problem is DDSL solving?

We want a language for recursive dynamic programs where:

  • the economic object (Bellman structure / stage semantics) is specified cleanly
  • the numerics (grids, interpolation, quadrature, solver choices) are separate
  • we can stay compatible with Dolo while moving toward a rigorous CORE representation (Scope pointer: see Numerics are Separated and Model Representation Boundaries.)

Design principle (top-down)

Specify what the actor knows (perches + shock timing).
The Bellman equations follow.
The numerics are a separate concern.

This principle is the reason the DDSL is “stage-first.” (Scope pointer: see Problem Chunks.)

Clarification (from the convening discussion): perches are information sets, not “endpoints of operators”.


Perch indices = filtration / adaptedness

In DDSL-SYM, a perch index is a measurability claim:

  • $z$ (unmarked) means $z$ is \(\mathcal{F}_{\text{dcsn}}\)-measurable (the policy can condition on it)

So transitions and equations must make this true:

  • the RHS can only use information available at that perch (so the mapping exists)
  • “shift rules” are exactly measurability/adaptedness checks (what you may write at each perch) (Scope pointer: see Information Structure Restriction.)

Two syntactic layers: SYM vs T core

DDSL-SYM — economist-facing, backwards-compatible surface - looks like (extended) Dolo

T core (formerly “DDSL-CORE”) — typed operator-theoretic internal form - explicit operators, domains, perches - not necessarily authored by economists; it is the mapping target that makes SYM precise (Scope pointer: see Model Representation Boundaries.)

Key goal: SYM and CORE should be round-trippable (no semantic loss).


Signs vs Symbols (à la Tillich)

Paul Tillich distinguished signs (arbitrary pointers) from symbols (participate in the reality they represent).

Signs: - Meaning assigned by convention - Arbitrary (red light could be blue) - Does not participate in what it signifies

Symbols: - Intrinsic connection to meaning - Irreplaceable (wedding ring ≠ mere metal) - Opens a window to deeper reality


The analogy:

  • DDSL-SYMsigns — convenient, economist-friendly notation; implicit math; could be replaced by equivalent syntax
  • T coresymbols — participates directly in the functional-analytic structure; typed operators reveal the Bellman reality

Both layers are syntax. But T core is the structure that is closer to the meaning map \(\Upsilon\); SYM merely points to it.

Our contribution is critical: make sure signs point to the right core.


"All things are like signs upon the earth and that is why they have come to be" - Judas Iscariot

Three layers of meaning (and two maps)

SYNTAX (SYM/T core)  -- Υ -->  MATH (Platonic)
      | ρ                 | ρ
      v                   v
COMPUTATION (dolang model objects / executable objects)

Interpretation: Υ and ρ are morphisms (in a topological / structural sense): they preserve compositional structure. In practice, we want the diagram to commute: compiling via “syntax → math → computation” agrees with compiling “methodized+calibrated syntax → computation.”

Core value-add: making these morphisms explicit, inspectable, and composable—and keeping SYM ↔ T core round-trippable—is the central contribution of DDSL. It turns “folk translations” (math ↔ code) into first-class, checkable objects.

In particular, the “computation” layer begins as a model representation: the parsed, typed in-memory stage object (e.g. dolang).


Syntax has a concrete in-memory representation

stage.yaml (SYM/T core)
   |
   | parse + typecheck
   v
Model representation (dolang / dolo)
= implementation of the symbolic stage

Key point: this object is still syntax; Υ interprets it, methodization/calibration enrich it, and ρ compiles it.


Υ vs ρ in one sentence each

Υ (Upsilon): turns well-typed syntax into the mathematical dynamic program.

ρ (rho): turns a methodized + calibrated stage into an executable representation.


Formal definition (sketch): Backus levels

Following Backus, we distinguish three levels:

  • Object level: primitive atoms and primitive symbols (numbers, @in, @def, E, argmax, method constructors, …)
  • Function level: well-formed function objects built from primitives (what appears in model files)
  • Functional level: higher-order operators that map functions to functions (movers, Bellman factorization, expectation operators, …)

This is the “Bellman–Backus–Euler calculus” viewpoint.


Formal definition (sketch): universes

Let:

  • \(\mathbb{O}\): all denotable objects in syntax
  • \(\mathbb{O}_P \subset \mathbb{O}\): primitive DDSL-native objects (atoms + primitive operator symbols)
  • \(\mathbb{F} \subset \mathbb{O}\): well-formed function objects (typed DDSL functions)
  • \(\mathbb{T} \subset \mathbb{O}\): functional operators (higher-order function objects)

On the semantics side:

  • \(\mathbb{DM}\): the “Platonic” DP universe (Bellman operators, push-forwards, integration/optimization operators, …)
  • \(\mathbb{F}^{\text{COMP}}\): computational representations (AST-like host-language objects)

Formal definition (sketch): the two maps and the definitional system

The definitional system \(\mathbb{D}\) fixes:

1) a typing discipline (what counts as a well-typed element of \(\mathbb{F}\)), and
2) the meaning map:

\[ \Upsilon : \mathbb{F} \to \mathbb{DM} \]

Separately (implementation level), the computational representation map is:

\[ \rho : \mathbb{F}_{\text{methodized, calibrated}} \to \mathbb{F}^{\text{COMP}} \]

Key point: \(\rho\) is not part of \(\mathbb{D}\); it is the constructive compilation layer.


Formal definition (sketch): “declared names are function objects”

In the Backus spirit, every declared name (outside bound variables) is interpreted as a function object.

Examples:

  • Xa @def R+: Xa is a nullary function object denoting a space.
  • β @in (0,1): β is a nullary function object constrained by a domain; calibration supplies its numeric value.
  • g_av: [xa, w] @mapsto xv @via | ...: g_av is a non-nullary function object with a typed domain/codomain.

Bound variables (xa, w, …) are purely syntactic binders; they disappear under ρ (become host-language parameters).


Symbols start as strings (object level)

At the raw YAML level, a symbol name is literally a string:

  • "β", "Xa", "w", "E_y"

The symbols: block builds a symbol environment (a typed “name table”) by parsing declarations:

symbols:
  spaces:
    Xa: @def R+

  parameters:
    β: @in (0,1)
    γ: @in R+

Key separation: - name = string token (element of \(\mathbb{O}\)) - declaration = a syntax object (element of \(\mathbb{F}\)) that gives the name a type/role in the model


Υ is a meaning map on well-typed syntax objects (not raw strings)

The meaning map is defined on well-typed function objects:

\[ \Upsilon : \mathbb{F} \to \mathbb{DM} \]

Informally:

  • \(\Upsilon(Xa @def R+)\) is “the space \(\mathbb{R}_+\)” (a Platonic object)
  • \(\Upsilon(β @in (0,1))\) is a mathematical parameter symbol constrained to \((0,1)\)
  • \(\Upsilon(g_{\text{av}})\) is a mathematical function \(X_a \times W \to X_v\)

So: syntax → math by denotation, not by numerical evaluation.


Calibration: assigning concrete values to nullary function objects

Calibration is a mapping from symbol names (strings) to numeric atoms:

calibration:
  β: 0.96
  γ: 4.0

Conceptually, calibration produces a calibrated syntactic stage where some nullary function objects now have values:

\[ \mathbb{C}(β) = 0.96 \]

This changes the instance of the model (numbers), not the symbolic structure of the stage.


Methodization: attaching schemes to operator instances (still syntax)

Methodization attaches numerical schemes to operator instances in the stage (expectations, argmax/rootfinding, APPROX stacks, draws/push-forwards, …).

Example (expectation operator keyed by RV name):

methods:
  - on: E_y
    schemes:
      - scheme: expectation
        method: !gauss-hermite
        settings:
          n_nodes: n_y_nodes

Methodization does not change \(\Upsilon\); it enriches syntax so that \(\rho\) can compile the stage.


The “stage” as the modular unit

A DDSL stage is a modular object containing: - perch structure (arrival/decision/continuation) - symbol declarations (types/domains) - kernels / function bodies (transitions, reward pieces, auxiliary defs)

A stage contains no numeric settings and no solver choices.


Key point: methodized + calibrated stages are still in the syntax layer

During the pipeline we produce variants of the same stage object:

  • \(S\): the symbolic stage (authored stage.yaml)
  • \(\mathcal{M}(S)\): a methodized stage (schemes/methods attached; still syntax)
  • \(\mathbb{C}(\mathcal{M}(S))\): a calibrated stage (numbers substituted/bound; still syntax)

Only after applying the representation map \(\rho\) do we leave syntax and get executable objects:

\[ \rho\big(\mathbb{C}(\mathcal{M}(S))\big) \in \mathbb{F}^{\text{COMP}} \]

This is important: methodization/calibration enrich the stage, they do not replace it with “math” or “code.”


Kernels vs methods (the clean separation)

Kernel = a named function body (Υ-level object).
Examples: transition kernels, reverse-state kernels, auxiliary identities.

Methods attach to operator instances used to execute a stage under ρ: - expectations \(E_y\) - optimization / rootfinding (argmax, solves) - approximation (APPROX: grids + interpolation) - sampling / push-forward operators


Operator registry vs operation registry

Two registries play different roles:

  • Operator registry: identity + signatures + how instances are formed
    (e.g., E_{y}(...) → operator instance E_y)

  • Operation registry: what numerical schemes/methods exist
    (quadrature methods, interpolation families, solvers, …)

Practical rule for v0.1:

  • you need operator identity (what instance is this?) to bind methods consistently
  • a full operation registry is optional (a catalog / validation aid)

Methodization as a functor

\[ \mathcal{M} : (\mathbb{S}, \text{MethodConfig}) \to \mathbb{S}_{\text{method}} \]

Conceptually: - stage says what operations occur - methodization says how to compute operator instances and represent outputs


Settings vs calibration (two kinds of numbers)

We separate numeric data into:

  • calibration (economic parameters): \(\beta, \gamma, \sigma, \ldots\)
  • settings (numerical configuration): grid sizes, bounds, tolerances, quadrature nodes, …

This keeps “the math” distinct from “how we approximate it.” (Scope pointer: see Numerics are Separated.)


Methodization config: flat methods: list (simple authoring)

Methodization is a flat list:

  • one entry per operator instance (e.g. E_y)
  • one entry per mover / stage function (e.g. cntn_to_dcsn_mover)
  • each entry can carry a scheme stack (e.g. backward algorithm + interpolation/grid)

Expectation operators are naturally keyed by the shock RV: E_{y}(...)E_y.


One scheme vs scheme stack

Each method entry has a schemes: [...] list.

  • Length 1 = “atomic” choice
  • Length > 1 = a scheme stack (bundle)

Each scheme entry has scheme + optional method (+ optional settings wiring).

The “scheme stack” is useful when one conceptual operation is really a bundle (e.g., grid + interpolation).


Provisional simplification: grid lives inside interpolation

In this (provisional) view, a grid constructor like cartesian is not meaningful on its own:

the grid is meaningful only as part of “how we represent / approximate a function.”

So we attach grid construction as part of an interpolation/approximation scheme on the relevant methodization target (typically the backward mover).


Example: scheme stack for a policy approximation

From consumption_savings_iid_egm_doloplus_split/methods.yml:

- on: cntn_to_dcsn_mover
  schemes:
    - scheme: bellman_backward
      method: !egm

    - scheme: interpolation
      method: !Cartesian
      settings:
        orders: [n_w]
        bounds: [[w_grid_min, w_grid_max]]
        domain: [w_domain_min, w_domain_max]

Numeric values live in settings.yaml.


Example: expectation operator is unique per RV

If the stage has a single exogenous shock RV y:

  • you typically have one expectation operator instance: E_y
  • it can be reused wherever E_{y}(...) appears

If you truly need different numerical methods, you must create distinct instances (distinct RV names or explicit aliases).


File split (what lives where)

In the split-file examples we aim for:

  • stage.yaml: purely symbolic stage (perches, symbols, kernels)
  • methods.yml: methodization (schemes/methods + settings symbol wiring)
  • calibration.yaml: parameter-only values
  • settings.yaml: numeric settings values
  • initial_values.yaml: solver warm-starts (optional)

Part III — Worked example: consumption-savings (EGM-style) in split files

Folder: packages/dolo/examples/models/consumption_savings_iid_egm_doloplus_split/

The point of the example is not “new economics,” but “clean separation”:

  • stage = symbolic
  • methods = numerics (but no numbers)
  • calibration/settings = numbers

Symbolic stage: symbols: (no numbers)

# stage.yaml (symbolic stage: no numeric values)
symbols:
  spaces:
    Xb: "@def R+"
    Xw: "@def R+"
    Xa: "@def R+"
    Y:  "@def R"

  prestate:
    b: "@in Xb"

  exogenous:
    y:
      - "@in Y"
      - "@dist Normal(μ_y, σ_y)"

  states:
    w: "@in Xw"
  poststates:
    a: "@in Xa"
  controls:
    c: "@in R+"

  values:
    V: "@in R"
  shadow_value:
    dV: "@in R+"

  parameters:
    β: "@in (0,1)"
    γ: "@in R+"
    ρ: "@in R"
    r: "@in R++"
    μ_y: "@in R"
    σ_y: "@in R+"

  settings:
    n_w: "@in Z+"
    n_y_nodes: "@in Z+"
    n_sim: "@in Z+"
    sim_seed: "@in Z+"
    w_domain_min: "@in R+"
    w_domain_max: "@in R+"
    w_grid_min: "@in R+"
    w_grid_max: "@in R+"

Symbolic stage: equations: + timing + settings references

equations:
  arvl_to_dcsn_transition: |
    w = exp(y) + b[<]*r

  dcsn_to_cntn_transition: |
    a[>] = w - c

  cntn_to_dcsn_transition: |
    w = a[>] + c

  cntn_to_dcsn_mover:
    Bellman: |
      V = max_{c}((c^(1-γ))/(1-γ) + β*V[>])
    InvEuler: |
      c[>] = (β*dV[>])^(-1/γ)
    ShadowBellman: |
      dV = (c)^(-γ)

  dcsn_to_arvl_mover:
    Bellman: |
      V[<] = E_{y}(V)
    ShadowBellman: |
      dV[<] = r * E_{y}(dV)

Methods (how to compute / approximate)

From methods.yml (illustrative excerpt):

- on: E_y
  schemes:
    - scheme: expectation
      method: !gauss-hermite
      settings: {n_nodes: n_y_nodes}

- on: cntn_to_dcsn_mover
  schemes:
    - scheme: bellman_backward
      method: !egm
    - scheme: interpolation
      method: !Cartesian
      settings: {orders: [n_w], bounds: [[w_grid_min, w_grid_max]], domain: [w_domain_min, w_domain_max]}

- on: arvl_to_dcsn_mover
  schemes:
    - scheme: simulation
      method: !monte-carlo
      settings: {n_paths: n_sim, seed: sim_seed}

Calibration + settings (numbers)

From calibration.yaml (parameters only):

calibration:
  parameters:
    β: 0.96
    γ: 4.0
    ρ: 0.0
    r: 1.00
    μ_y: 0.0
    σ_y: 0.1

From settings.yaml (numerical configuration):

settings:
  n_w: 100
  n_y_nodes: 9
  n_sim: 10000
  sim_seed: 42
  w_domain_min: 0.01
  w_domain_max: 4.0
  w_grid_min: 0.0
  w_grid_max: 4.0

Part IV — SYM → CORE mapping: kernels (transition equations)

**dolo-plus YAML** *(with legacy Dolo label)*
equations:
  arvl_to_dcsn_transition: |  # Dolo: half_transition
    w = exp(y) + b[<]*r
  dcsn_to_cntn_transition: |  # Dolo: auxiliary_direct_egm
    a[>] = w - c
  cntn_to_dcsn_transition: |  # Dolo: reverse_state
    w = a[>] + c
**CORE math meaning** *(names from `equation_symbols`)* - $g_{ad}(b,y)=w=rb+\exp(y)$ - $g_{de}(w,c)=a=w-c$ - $g_{ed}(a,c)=w=a+c$ These are **kernels** (Υ-level function objects). Numerics attach to the **E / push-forward** operators when executing movers under ρ.

Part IV — SYM → CORE mapping: movers (operator blocks)

**dolo-plus YAML** *(legacy Dolo source)*
equations:
  cntn_to_dcsn_mover:      # Dolo: direct_response_egm
    Bellman: |
      V = max_{c}(u(c) + β*V[>])
    InvEuler: |
      c[>] = (β*dV[>])^(-1/γ)
  dcsn_to_arvl_mover:      # Dolo: expectation
    Bellman: |
      V[<] = E_{y}(V)
    ShadowBellman: |
      dV[<] = r * E_{y}(dV)
**CORE math meaning** *(higher-order operators)* - $T_{ed}: (V_e,dV_e)\mapsto (V_d,c,dV_d)$ (EGM-style update) - $T_{da}: (V_d,dV_d)\mapsto (V_a,dV_a)$ with expectation over $y$ $$ V_a(b)=\mathbb{E}_y\!\left[V_d\!\left(g_{ad}(b,y)\right)\right] \quad,\quad dV_a(b)=r\,\mathbb{E}_y\!\left[dV_d\!\left(g_{ad}(b,y)\right)\right] $$

Reference: DDSL ↔ Dolo ↔ T core mapping

Transitions (kernels):

DDSL-SYM (stage.yaml) Dolo name T core (Υ)
arvl_to_dcsn_transition half_transition \(g_{av}: X_a \times Y \to X_v\)
dcsn_to_cntn_transition auxiliary_direct_egm \(g_{ve}: X_v \times A \to X_e\)
cntn_to_dcsn_transition reverse_state \(g_{ev}: X_e \times A \to X_v\)

Reference: Movers ↔ Dolo ↔ T core

Movers (operators on functions):

DDSL-SYM Dolo name T core (Υ)
cntn_to_dcsn_mover.Bellman (implicit) \(\mathcal{B}_{dcsn}: V_e \mapsto V_v\)
cntn_to_dcsn_mover.InvEuler direct_response_egm \(c^*(x_v) = (\beta \cdot dV_e)^{-1/\gamma}\)
cntn_to_dcsn_mover.ShadowBellman expectation integrand \(dV_v(x_v) = u'(c^*(x_v))\)
dcsn_to_arvl_mover.Bellman expectation \(\mathcal{B}_{arvl}: V_v \mapsto V_a = \mathbb{E}_y[V_v \circ g_{av}]\)
dcsn_to_arvl_mover.ShadowBellman expectation (mr) \(dV_a = r \cdot \mathbb{E}_y[dV_v]\)

EGM: from c[>] points to an interpolated policy \(c(w)\)

This is the step that is implicit in Dolo (hard-coded in the solver), but becomes explicit in SYM → T core.

Given a continuation (poststate) grid \(\{a_i\}\) and the continuation shadow value \(dV_{\succ}(a)\):

  1. Inverse Euler on the continuation grid (SYM: InvEuler): \(c_i \equiv c_{\succ}(a_i) = \left(\beta\, dV_{\succ}(a_i)\right)^{-1/\gamma}\)

  2. Endogenous wealth points (SYM: cntn_to_dcsn_transition / reverse-state):
    \(w_i = a_i + c_i\)

  3. Approximate policy function on the decision state: build an interpolant \(\hat c(\cdot)\) from the point cloud \(\{(w_i, c_i)\}\), and evaluate it on the decision grid (e.g. the !Cartesian grid declared via methodization/settings).

Key point: writing c[>] makes the "policy on the continuation grid" representation explicit, and methodization supplies the interpolation scheme that turns \((w_i,c_i)\) into a decision-rule object.


Reference: Symbol groups ↔ ADC perches

Dolo group DDSL-SYM group ADC perch CORE type
prestate Arrival \(x_a \in X_a\)
states states Decision \(x_v \in X_v\)
poststates poststates Continuation \(x_e \in X_e\)
controls controls Decision \(a \in A\)
exogenous exogenous Shock \(y \sim D_y\)
values values all perches \(V: X \to \mathbb{R}\)

Crowd Q: does DDSL describe forward and backward operators?

Yes — both are part of the same stage-level Bellman structure:

  • Backward operator: the Bellman/mover updates (e.g. cntn_to_dcsn_mover) are explicit in SYM. Methodization chooses algorithms like !egm and interpolation schemes.
  • Forward operator: distribution evolution / simulation is the push-forward induced by kernels + shock distributions. Methodization chooses how to compute it (e.g. !monte-carlo vs quadrature).

In the split-file example this appears as:

  • methods.yml: on: cntn_to_dcsn_mover (backward) and on: arvl_to_dcsn_mover (forward simulation).

Crowd Q: why not start with a time sequence of equations?

Two distinct composition problems:

  • Within a stage: there is a sequence (arrival → decision → continuation), but it is best understood as information progression (“what is known when”), not as “time unfolding” in the syntax.
  • Within a period: a period is a namespace + an ordering of stages.
  • if names are unique within the period, many connections are “trivial by naming”

  • Across periods: you often need explicit “twisters” (connectors) to map end-of-period names to next-period names.

For branching models, stage composition becomes a graph/category of stages + connectors.


Compatibility-first (practical constraint)

North star: - build around dolang and legacy Dolo - avoid breaking existing Dolo models - keep dolo-plus stages down-compilable to a runnable “horse” representation (when possible) (Scope pointer: see Phase 1.1 Exemplar Models and Factorizations in Scope.)

This keeps the project “clinical” while expanding expressivity.


Summary

  1. Perches describe information (deck 1).
  2. Stages are modular semantic units: perches + symbols + kernels.
  3. Methodization attaches schemes/methods to operator instances and approximations.
  4. Calibration/settings provide numbers.
  5. The end goal is a clean SYM ↔ T core pipeline that stays compatible with Dolo.