Meaning Map to Code (Υ → Ρ)¶
Companion note for the port-cons FFP notebook
V[>] as a Backus object¶
In Backus's FFP (1978), there is a clean separation between objects (syntactic atoms that don't carry their own meaning), functions (well-formed expressions built from objects), and the definitional system (Def f ≡ r) that assigns meaning to names.
V[>] in a stage YAML is exactly this kind of object. It's a string — a perch-qualified symbol sitting in the YAML file. By itself it means nothing. It becomes a well-formed function object only when the definitional system D processes it: the grammar checks that V[>] is well-typed (a value at the continuation perch), and the stage context supplies the transition g that connects decision to continuation.
The Υ (meaning) map then assigns mathematical semantics to this function object:
V[>] → Υ → V_cont(g(m, c)) → Ρ → np.interp(...)
string in YAML mathematical expression executable code
(object) (meaning) (representation)
This is the Backus architecture applied to Bellman problems: objects are syntactic, meaning comes from the definitional system, and computation is a separate map (Ρ) that requires methodization and calibration on top of the meaning.
The whisperer¶
The notebook's make_stage_operator factory — the whisperer — performs both maps in sequence:
- Read the equations from the stage YAML (objects in O)
- Resolve perch tags via the stage context, applying Υ (objects → meaning)
- Read the method tag (
!egmor!vfi) and compile into callables, applying Ρ (meaning → code) - Return a stage operator:
ValueFn → ValueFn
The whisperer returns an operator, not a solution. The solver calls the operator to find the fixed point.
Matsya as the interpretation layer¶
The notebook calls Matsya at runtime, inside the solver pipeline to perform the Υ map. The full stage YAML — including perch tags, symbol declarations, spaces, transitions, and mover sub-equations — is sent to Matsya with a binding context:
V_cont(x)is whatV[>]means in code (a callable)dV_cont(x)is whatdV[>]means in code (marginal of the callable)ais the poststate grid variablem_dis the decision grid variable
Matsya reads the perch tags, finds the transitions that connect perches, substitutes, and returns a structured JSON recipe — Python expressions keyed by DDSL equation block names:
{
"vfi_maximand": "(c**(1-rho))/(1-rho) + beta*V_cont(m_d - c)",
"inv_euler": "(beta*dV_cont(a))**(-1/rho)",
"cntn_to_dcsn_transition": "a + c",
"marginal_bellman": "c**(-rho)",
"dcsn_to_cntn_transition": "m_d - c",
"utility": "(c**(1-rho))/(1-rho)"
}
The whisperer then compiles these returned strings into numpy callables via eval(). Matsya executes the definitional system — it doesn't define it.
What this means architecturally¶
The pipeline becomes:
Matsya substitutes for hard-wired, solver-level interpretation of the dolo-plus syntax. We don't need the solver to understand perch tags, bracket notation, or transition structure. The whisperer sends the raw YAML to Matsya, gets back de-sugared expressions in the target variable namespace, and compiles them. The solver receives callables — it never touches equation strings.
This decouples the equation compilation from the solver logic. We can code the solver however we want; the Υ map is handled externally.
The recipe as structured Υ output¶
The recipe returned by Matsya has a specific structure worth examining. Each key is a DDSL equation block name, and each value is a Python expression. The keys correspond to the block names declared in the stage YAML's equations: section:
| Recipe key | Stage YAML source | What it means |
|---|---|---|
dcsn_to_cntn_transition |
a = m_d - c |
Forward transition \(g\): decision → continuation |
cntn_to_dcsn_transition |
m_d[>] = a + c[>] |
Reverse transition (EGM): continuation → decision |
inv_euler |
c[>] = (β*dV[>])^(-1/ρ) |
Inverse Euler equation |
marginal_bellman |
dV = (c)^(-ρ) |
Envelope condition |
utility |
u = (c^(1-ρ))/(1-ρ) |
Flow utility |
vfi_maximand |
V = max_{c}(u + β*V[>]) |
De-sugared VFI objective |
The de-sugaring that Matsya performs is visible in the vfi_maximand: the YAML says u + β*V[>], and Matsya returns (c**(1-rho))/(1-rho) + beta*V_cont(m_d - c) — substituting the utility definition for u, the transition m_d - c for the argument of V[>], and the code-level callable V_cont for the perch-tagged symbol.
Unicode–ASCII parameter aliasing¶
A practical issue at the Ρ level: the stage YAML declares parameters using Unicode (β, ρ), which is what the calibration functor produces. But recipe expressions use ASCII (beta, rho) because that's what Matsya returns (and what Python can evaluate without Unicode identifier support issues in some contexts).
The whisperer bridges this with an alias map when building the eval namespace:
Both forms are present in the namespace, so recipe expressions resolve correctly regardless of which convention the RAG assistant or the stage YAML uses.
Composition and accretion¶
Stages compose as functions: a period is backward composition of its stages (V = S_cons(S_port(V_cont))). A connector is a rename between adjacent components. The nest is an interleaved sequence of periods and connectors, built backward one period at a time (accretion).
Method independence¶
The stage YAML declares all sub-equations for any method. The methods YAML selects which ones the whisperer compiles. Swap !egm for !vfi and only the Ρ-level changes — the Υ image is identical. The notebook verifies this: both methods converge to the same value functions (differences at the 4th decimal place, consistent with VFI grid-search discretization).
The same Matsya recipe serves both methods. The EGM operator uses inv_euler, cntn_to_dcsn_transition, marginal_bellman (the EGM-specific sub-equations). The VFI operator uses vfi_maximand (the fully de-sugared objective). Both read from the same recipe dict — the method tag determines which keys are consumed, not which expressions exist.
Open question: recipe expression scope #ambiguity¶
When Matsya returns dcsn_to_cntn_transition: m_d - c, what does c refer to?
In the current implementation, recipe expressions are composable fragments evaluated sequentially in a namespace where previous results are bound. The EGM operator computes c from the inverse Euler first, binds it into the namespace, and then evaluates m_d - c against that binding. The c in the recipe is a bare variable, not the inverse Euler expression.
But this raises a Υ-level question. The stage YAML declares:
Here c[>] in the reverse transition is explicitly the consumption at the continuation perch — the same object defined by the inverse Euler. Should Matsya substitute it, producing m_d = a + (beta*dV_cont(a))**(-1/rho)? Or leave c bare, trusting the sequential evaluation to bind it?
The answer may be method-dependent: EGM naturally evaluates fragments sequentially (compute c, then use it), while VFI needs the fully-composed maximand (everything substituted into one expression). If so, the Υ map produces the same mathematical objects regardless of method, but the recipe format (composable fragments vs closed forms) is a Ρ-level decision.
This is an open design question — see the roadmap todo for the current status.
Related¶
- Full research note — deeper treatment: three failures and what they teach, sequential eval as imperative composition, scope ambiguity analysis, implications for the compiler pipeline
- Technical overview — architecture-level view of Υ vs Ρ
- Core FFP concepts — Backus objects, functions, functional forms, definitional system
- Mathematical definition — D, Υ, Ρ, and the three-layer architecture
- Port-with-shocks example — the stage YAMLs used in the notebook
- Dev-spec overview — implementation milestones
- Methodization — how method tags select sub-equations
- Periods and connectors — composition syntax
- Periods and nests (theory) — mathematical foundations of accretion