Appendix A: language quick reference
Everything the course taught, compressed for the working desk. Shapes use angle brackets for what you supply and square brackets for what is optional; the normative grammar lives in the specification at cfdl.dev.
Model structure
version 0.1
model "<name>"
use pack "<id>" version "<ver>" -- optional; root file only
import "<file>.cfdl" -- root and imported files; no cycles,
-- never outside the model directory
time calendar <daily|monthly|quarterly|annual> from <date> for <N> [project <n>]
-- project: a tail read by lookups only
phase <name> from <date> to <date>One time grid per model; header statements live in the root file only. Everything else — entities, assumptions (a curve is one shape of assume), lifecycles, accounts, streams, contracts, events, options, waterfalls, metrics, slices, statements — may live in any file, in any order. Order never carries meaning (metrics excepted: they compose in declaration order).
Entities and fields
entity <family> <name> : <Type> -- family: asset | party |
entity <family> <name> : <Type> { -- container | reference
<field> = <literal | inputs.<name>> -- a fact: a literal, or a
-- single-valued assumption
-- or a set's field
<field> init <expr> -- a rule: the recurrence
next <expr> -- `prev` = own prior value
part of <family>.<name> -- hierarchy; rollups follow
state <s> -- opening state
lifecycle <name> -- bind a machine
account <name> <owed|due> init <expr> -- an entity-owned claim
}An entity may also carry an id (docs/01 §7.1). Reads, from any expression: asset.x.field (this period, at close), prev.asset.x.field (prior close), entity.field (the attached entity's own field). Rules see only the completed prior column — same-period cycles are unwritable.
Types form a refines chain ending at a master. The language base ships abstract contract masters (Contract.Debt, Contract.Lease, Contract.Option, …) that bind no lowering and cannot be instantiated — they exist so a slice's type Contract.Debt reaches every pack's debt transitively, and fields inherit down the chain.
Streams
stream <a>.<b>[.<c>] on entity <family>.<name> <inflow|outflow|accrual|writeoff>
currency <CCY> [moves <account>] {
[category <dotted.path>] -- bare, like `currency USD`;
schedule <see below> -- rooted operating |
[active when <boolean expr> -- investing | financing
| active in state <name>]
amount = <expr>
}Names are dotted, at least two segments, unique. Direction is declared; amounts are written positive. accrual and writeoff are non-cash: an accrual moves a balance with no cash, a write-off retires one (docs/01 §9.1). moves <account> names the account the stream raises or lowers as it pays, and a later expression reads that balance as prev.<account>. category says what the cash is — a dotted path into the cash flow statement, first segment operating, investing, or financing. active in state gates on the owner's lifecycle, checked against the declared machine — where a string comparison against entity.status could be misspelled and false forever.
Schedules
schedule on <date> [start|mid|end] [convention <conv>] [calendar "<cal>"]
schedule every <day|week|month|quarter|year> [start|mid|end]
[on day <n> | on eom]
[net <n> [days|months]]
from <date|phase_start("p")> to <date|phase_end("p")>
[except [<dates>]] [also [<dates>]]
schedule on phase_enter("<phase>")Placement: one axis, three positions, at most one — start (advance), mid (mid-period convention), end (arrears). Both every and on <date> take all three; only the default differs, end for a stride and start for a one-shot. Conventions: none, following, preceding, modified_following, modified_preceding. Calendars: "weekend", "us", "uk", "target". net counts days, or steps calendar months. Occurrences pushed off the grid are compile errors.
Assumptions and curves
assume <name> [: <type>] = <expr> ["<unit>"] [within [lo, hi]]
assume <name> [: <type>] ~ Normal(mean=, stdev=, clip=[lo, hi]) [within [lo, hi]]
~ LogNormal(mu=, sigma=) ~ Uniform(min=, max=)
~ Triangular(min=, mode=, max=)
assume <name> [: <type>] = curve [step|linear] [from <date>] [to <date>] { <date>: <value> ... }
assume <name> [: <type>] = quantile [step|linear] [by exceedance] { <share>: <value> ... }
assume <name> : <SetType> = { <field> = <value> <branch> = { ... } } -- a set
assume <name> : <type> -- bodiless: the run supplies it
... source { publisher "<who>" as_of <date> } -- provenanceRead as inputs.<name>, a set's field as inputs.<name>.<field>; a series at another date as curve_value(inputs.<name>, <date>). The type slot (rate, fraction, a set type) checks the value; within is the deal's own bound, checked wherever the value arrives and never clamped; a value may carry its unit, 60 "USD/sf"; source records where a number came from. Deterministic runs use central values: Normal → mean, LogNormal → exp(mu + sigma²/2), Uniform → midpoint, Triangular → (min + mode + max) / 3 — not the mode. A read outside a curve's stated range is refused (E5040); a curve with no to holds its last value forward, with a warning (W5024) (docs/01 §12.5).
A quantile is a curve turned sideways: indexed by cumulative share rather than by date, for a quantity whose dispersion drives the answer. Shares in 0..1, values non-decreasing; by exceedance writes the points worst-first, the way a duration curve reads, and is authoring surface only. Read with quantile_at, quantile_mean, quantile_of (expression table below). Never sampled: it is declared data.
Lifecycles and accounts
lifecycle <name> { -- a finite state machine
initial <state>
state <a>, <b>, <c>
on enter <state> { set <field> = <expr> } -- arrival action: the state
<from> -> <to> [when <boolean expr>] -- guard: checked each period
[{ set <field> = <expr> }] -- in <from>; re-entry re-arms;
} -- an edge action: the path
account <name> [owed|due] { -- cash that accumulates
[owner <family>.<name>] -- a party may own several
[from <expr>] -- per-period inflow; no floor
}An entity binds a machine with lifecycle <name> in its block (or from its
pack type, which a model may augment with its own states and edges). A
contract may bind a machine too, and gate its lines with in <state> run <line>, <line>; a machine may nest within a state (docs/01 §7.3, §8.6). Edges are declared only as used; a guard-less edge
is a permission an event's write may take. Declaration order resolves
simultaneous guards; one transition per entity per period; a self-edge
re-anchors. Logic reads series strictly backward — windows end at
time.t - 1 — and reads a balance as prev.<account>. A window lying entirely before the
first period makes a guard false; prev.<account> in the first period is the
account's init, zero by default, never absent (docs/01 §7.3, §10.6). A waterfall draws an account with from <account>
and a step credits one with pay <step> to account <name> = <expr>. A party
that owns several accounts is paid by naming one; to party.<name> is then
refused (E1324). The
balance publishes as the non-cash series account.<name>. The third
schedule anchor: every <interval> from state_enter(<ref>, <state>) for <n> periods — a window per entry, re-anchored on re-entry.
A stream has no window: an amount folding time.t + k is refused. A sale
pays a valuation — amount = metric.cre.exit.gross_value — and
is struck after the figure folds over the projection tail; logic reading
what it pays is a refused cycle, with the path named.
Contracts, events, options, waterfalls
contract <pack>.<type> [<instance>] on entity <ref> { -- or the fused
term <date>..<date> -- <pack>.<type>.<instance>
term from state_enter(<ref>, <state>) for <n> periods -- or opened by a state
terms { <key> = <literal | inputs.name | expression> ... } -- a term may
-- name a set: inputs.<set>
parties { <role> = party.<name> ... } -- the type's roles
payment <line> <start|mid|end|net ...> -- a line's placement
lifecycle <name> -- the contract's machine
on start { set status = "<state>" } -- at its boundaries
on end { set status = "<state>" }
effects { ... } -- (docs/01 §8)
}
event <a>.<b> [schedule <as streams>] when <boolean expr> { -- rising edge:
-- fires on each occurrence, re-arms
-- when the condition falls
set entity <family>.<name>.<field> = <value>
activate stream <name> | deactivate stream <name>
exercise option <name>
}
option <a>.<b> [on entity <ref> | on contract <name>] type <Type>
[exercisable [<n> times] [in <phase>]] {
parties { <role> = party.<name> ... }
terms { <key> = <value> ... } -- read as contract.<key>
schedule <as streams> -- optional; a Bermudan right
exercise when <boolean expr> -- the election; rising edge
payoff <expr>
set entity ... | activate stream ... | deactivate stream ... -- on exercise
}
waterfall <a>.<b> on entity <ref> {
schedule <as streams>
from <expr | available | <account>> -- the pot; an account's balance
pay <step> to <family>.<name> = <expr> -- receives min(max(0,owed),remaining)
pay <step> to account <name> = <expr> -- into an account
pay <step> to ... for contract <c> line <l> = <expr> -- a contract's line
pay for contract "<selector>" line <l> -- the steps the contracts contribute
}Waterfall step expressions may read remaining, owed.<step>, paid.<step> (earlier steps only). The five step shapes: stated amount, capped (min), pay-to-target (balance − target), shortfall (owed.x − paid.x), sweep (remaining). Every step publishes shortfall.<waterfall>.<step>, owed less paid, which a later waterfall or a metric reads by name.
Metrics, slices, statements
The figures-and-views tier: constructs that read a finished projection and produce no cash.
metric <name> = <expr> -- one number at the horizon; folds
-- published series; composes in
-- declaration order
slice <name> { -- a named partial selection
entity <family>.<name> -- plus its part-of descendants
type <TypeId> -- matches through refines
category "<path or path.*>" -- quoted; series_sum's dialect
stream "<selector>"
line <role> -- a contract line, with type
except stream "<selector>" -- kinds intersect; excepts subtract
except entity <family>.<name>
except category "<path>"
window from <date> to <date> -- bounds the PERIODS, not the
} -- streams; at most one
statement <name> { -- generated: the tree renders
label "<title>"
structure <entity|category>
depth <N> -- level of aggregation
[grain <calendar>] -- re-buckets values; total stays
[slice <name>] -- lifetime; ratios recompute
[metrics <a>, <b>] -- declared metrics, beside the rows
}
statement <name> { -- authored: the rows are stated
label "<title>"
line "<label>" { category "<sel>" | stream "<sel>" | slice <name>
| type <TypeId> [line <role>]
| entity <family>.<name> [display positive] [depth <N>] }
subtotal "<label>" { category "<sel>" } -- folds rows stated elsewhere
subtotal "<label>" { series "<key>" } -- presents a published fold; claims
-- nothing, out of the bottom line;
-- a claim beside it is E1370
spacer
ratio "<label>" { of <slice> to <slice> display positive }
}A metric is evaluated once, at the horizon, over the finished projection. It may fold any series the model publishes — a stream by either spelling (ops.rev or stream.ops.rev), a waterfall step, entity.<sym>.net_cash_flow, account.<name>, an entity field, a money subtotal, model.net_cash_flow — and folding an unpublished name is refused at compile (E1365), not read as zero. irr(party.x) and moic(party.x) are legal only in a metric: folds over the party's own account, refused elsewhere (E1355).
A statement is authored or generated, never both, and never neither. slice filters orthogonally to the structure, and a sliced statement reconciles against the slice's total. A slice and a statement are views — they change no value and move neither hash; a metric is a figure the model claims, and does move the model hash. A model that declares no statement gets a default one: the entity hierarchy, marked default in results.
Expressions
Scopes: time.t (period index from 0), time.date, time.phase, time.ppy (periods per year), time.days_in_period · inputs.* (the model's assumptions, and what the run supplies into a bodiless one) · contract.<term> (in an option or a contract's own blocks) · reference.<name>.<field> · slice.<name> · field reads as above. Arithmetic is decimal; % is remainder, not percent.
| Family | Functions |
|---|---|
| Bounds and shape | min max abs clamp(x, lo, hi) sum avg |
| Growth | pow exp ln |
| Rounding | round(x, digits) round_up round_down · round_to(x, step) — nearest multiple |
| Conditional | if(cond, then, else) |
| Dates | date parse_date edate eomonth days_between months_between year_frac is_business_day add_business_days roll |
| TVM | pv fv pmt(rate, nper, pv) ipmt ppmt nper rate |
| Series | series_sum series_avg series_min series_max series_prod series_count, each ("name", from_t, to_t) — dependency-ordered; chains to any depth, cycles refused |
| Quantiles | quantile_at(inputs.name, share) quantile_mean(inputs.name, from, to) — exact partial expectation · quantile_of(inputs.name, value) — the inverse: threshold → share |
| Domain | cpr_to_smm cpr_to_periodic(x, ppy) macrs_rate(year, life) curve_value wal |
| Structure and state | part_sum(<ref>, <field>[, <filter>]) part_count state_enter(<ref>, <state>) — the period of the latest entry |
| Draws | sample(inputs.name) — a draw for the reader's entity and period |
| Returns and valuations (metrics only) | irr(party.x) moic(party.x) · npv("selection", rate[, from_t]) — present value of what the selection pays · yield("selection", outlay[, from_t]) — the annual rate at which that value equals the price paid · spread("selection", outlay, inputs.index[, from]) — the spread over an index path at which it equals the price paid, a floater's discount margin; from a period or a date, the outlay's; each null when nothing solves |
Every series reduction folds the per-period aggregate: streams a selector matches are added within each period first, then the fold runs over that one series. An empty selection returns each fold's identity — sum 0, product 1, count 0 — and series_max/series_min, which have none, return null. series_avg divides by the requested window less the undefined cells in it — the periods it actually folded, which for a cash series is the whole requested window; series_count counts the periods whose aggregate is non-zero, and if(series_count("x.*", 0, t) == 0, 0, series_max("x.*", 0, t)) is how a legitimately-empty selector says so.
Full signatures: the expressions reference at cfdl.dev.
Run configuration
{
"deterministic": { "annual_discount_rate": 0.08, "valuation_grain": "annual",
"as_of": "2026-01-01", "arithmetic": "excel_compat",
"parameters": { "inputs.z": 42 }, "inputs": "inputs.json" },
"scenarios": { "<name>": { "annual_discount_curve": "<assumption>",
"parameters": {}, "inputs": "<scenario inputs>.json" } },
"monte_carlo": { "trial_count": 1000, "seed": 42,
"distributions": { "inputs.g": { "kind": "uniform", "min": 0.01, "max": 0.05 } } }
}Annual rates convert to the grid geometrically: (1 + r)^(1/n) − 1; annual_discount_curve names a series-shaped assumption instead. Parameters override inputs.<name> per scenario without touching the model; each scenario may name its own inputs file. arithmetic is decimal unless set to "excel_compat", for reconciling against a workbook (docs/09 §11). A run monte_carlo trials N seed N statement in the model is overridden by the configuration when both exist.
The disciplines, one line each
Model at the grain cash moves; roll up, never slice down. Opinions on the assumption page; structure literal. Dates in phases. Expressions that read aloud. One hand-checked anchor per file. Match a source's method, then its answer. Recompute ratios from rolled-up flows. Fix the first error only. Predict before you run.