Skip to content

The ceiling

What may enter the language, and what may never. The structure that holds it is ARCHITECTURE; the rules a model must obey are SPEC §0; what is planned or refused is ROADMAP. This page is the argument in between: the test a candidate primitive has to pass, why capability is a second axis rather than part of the ceiling, and what composition would force.

It is a claim, so it carries evidence — math a ported model needed and this language could not state becomes a ledger row in the gallery.

Two tiers, and the ceiling

Primitives (operators, sum, group_sum, roll/shift, where predicates) set the expressive ceiling, and each costs the full two-backend tax: eager implementation, plan node + locality class, executor case, lowering case, differential tests, SPEC entry. macros: / expressions: are pure AST substitution — every composition of primitives at zero marginal cost and zero divergence risk. Formulations (piecewise:) are taxed like a primitive but compose like a macro: they emit new declarations before dispatch and never enter as plan expression nodes.

For any request, triage: macro, primitive, or escape? Most are compositions. New primitives must be macro-friendly — anything a user might parameterise goes in a value position like over=/by=, never a kwarg key; the roll(x, snapshot=1) dim-as-key design is the language's own counterexample.

A candidate primitive is admissible iff it is all three of degree 1 (affine)variable × parameter, never variable × variable, the one axis that is a scope choice rather than a consequence of streaming (ROADMAP); relational — filter / join / group-by-aggregate over tidy tables; and localpointwise or bounded-halo, which compose under partition-wise execution where global operators do not. Locality is judged in data space: reductions over a coordinate space ("the last snapshot") read only the small, already materialised dim tables and stay admissible even though they look global.

Read the verdict off the plan. Rules 2 and 3 are one question asked twice, and the compiler already answers it — write the candidate's query over the term stream first and read .explain():

Shape of the emitted query Locality Rules 2–3
filter on a column already in the frame pointwise admissible
equi-join against a parameter or mapping table pointwise admissible
join on the dim table at ord ± k, k fixed bounded-halo admissible
dim table only, no data join coordinate-space admissible (free)
window over unbounded rows, or a recursive CTE global reject, with the rewrite

This is the case analysis _sum_fragment, _group_fragment and _translate_fragment already implement — each rewriting one fragment on its own, which is what pointwise and bounded-halo mean in code — so a candidate fitting none of those shapes has no executor to be written into. Two limits: degree is not a property of the plan, and it presumes the terminal sum(coeff) over (row, col) stays the only aggregate a term passes through. A primitive is finished when lowering.py accepts it and the differential test against the linopy oracle passes.

The ceiling is a claim, so it needs evidence. In docs/models/index.md, math a ported model needed and this language could not state becomes a ledger row with its triage verdict — what docs/ROADMAP.md should be argued from. Those ports also cover a class no other test reaches: both lanes consume the same resolved AST by rule 1, so a shared misreading passes the differential suite green, and only an outside optimum catches it.

What is outside the closure splits three ways, and the split decides whether a request can ever be met:

Tier Bounded by Members Can it move?
Capability-bounded what a given sink can ingest SOS / indicator (#23); quadratic per sink — see below
Budget-bounded the escape label budget — a cap on the rows and columns an island may emit global operators, arbitrary Python, non-relational manipulation already movable — that is what an island is
Design-bounded our choice of where work belongs data prep, domain helpers, Python declaring structure movable any time; we don't want to

Impossible in the symbolic plan: conditionals, iteration, any data-dependent structure inside expressions. What is protected is that the plan's shape is fixed before any data is read — which declarations exist and which dims each spans. Cardinality is always data's to supply: foreach: [snapshot] does not know how many snapshots there are either.

That distinction decides more than it looks. A dimension whose members are computed in data prep is completely ordinary — a cycle basis for KVL, the subsets of a subtour-elimination family — because a graph algorithm run before the build is design-bounded, the row above. The line is temporal, not computational: it does not matter how clever the Python is or whether its output size depends on the data, only whether it can run before the model is built. What is outside is work that needs the solver's answer to decide the next row — lazy cut generation, a solve loop — because there is no "before" for it to happen in.

This is a different property from how much a build costs, though the two meet at the escape hatch. That is why an escape: island (#38) is admissible where a registered Python helper was not: its extent is fixed by the preceding where mask, it is terminal, and it is named in the file. Its label budget is what keeps it accountable — an island's cost is bounded by what it may emit, declared and enforced before any Python runs rather than discovered after it allocates.

An escape buys back the relational and local rules (it returns affine COO rows — a running-sum island still emits affine rows, just O(T²) of them) but never degree, because affine COO is what it returns. That refusal stands on what an island emits, not on what a sink accepts, so it is unaffected by the capability findings below.

Capability is not the ceiling

The ceiling above is about streamability and is solver-independent. What a sink can ingest is a separate axis, and conflating the two let one solver's limits read as architectural law — "no sink carries the stream" described HiGHS, not the architecture. Two findings, measured in docs/benchmarks.md: SOS is solver-bounded (HiGHS has no SOS concept at all, while lp_file carries it as a text section and Gurobi natively), and what blocks quadratic is a conjunction — HiGHS has integrality and a Hessian and refuses the pair — so capability is not a flat set. The whole-Hessian handoff is an implementation difference, not a rule-4 violation.

Making this a declared per-sink capability set, with check taking an optional sink, is ROADMAP Track 4.

Composition (component libraries)

A component library is a fixed set of parametrised templates agreeing on a port/flow convention, merged into one program, wired through a data connectivity table and a single group_sum balance. Topology is data, not structure — wiring a specific system is rows in a connectivity table, never generated YAML, so structure is bounded by the number of component types while cardinality lives entirely in data. Schema merge is therefore a pure compose-then-build step producing one MathSchema before a single lower/stream pass (linopy.extend is a linopy-lane shim; native merge is #30). Namespacing via qualified names is the missing primitive (#29) — the port/flow surface stays deliberately shared, as the coupling contract between templates — and signs and bidirectional flows need bounds-as-expressions (#31).

Whatever genuinely is not data (variable port counts, runtime-unknown component types) belongs in a thin layer emitting more rows or more templates, never per-instance YAML — but that layer has nothing supported to call. A seam does exist: api.load_schema accepts dict | MathSchema, so a programmatically built model already goes through validation, expansion, resolution and dim checking. It is just undocumented and unversioned, while rule 6 refuses a Python modeling API and this section forbids generated YAML. Composition therefore forces that contract earlier than the roadmap has it: not a general modeling API, but a narrow, versioned way to emit declarations. Should anything ever be blessed, it is the schema level and not the plan level — that much is settled; what is not is whether to bless the seam at all, and namespacing (#29) or a native schema merge (#30) is what would force the question.