Skip to content
Speculative. This is design exploration — it describes ideas and direction, not necessarily shipped behavior.

A Denotational Semantics for Constraints

This note gives a denotational semantics for GoFish's layout constraints, in the style of Spytial's Table 1 (Prasad et al., Patterns for Spatial Reasoning): each construct is given a meaning [[]] as a mathematical object, independent of how the engine computes it. It is the formal companion to layout-synthesis (the two-semiring engine) and constraints-as-core (the feasibility argument). Where those essays argue that operators reduce to constraints, this one says precisely what a constraint means.

The payoff of writing it down: the meaning of a layer is the composition of its constraints' denotations, and because each denotation lives in a semiring closed under composition, the composite is always well-defined and invertible. That is the completeness conjecture of layout-synthesis, stated semantically.

Semantic domains

A layout assigns every node n, on each axis d{x,y}, two quantities:

  • an extent ed(n)0 — its size;
  • a position pd(n) — where its origin sits.

Neither is a free number. Extents are claims: monotone functions of a scale factor σ (pixels per data unit), drawn from the carrier

Claim={f:R0R0f monotone}.

A fixed box denotes the constant claim λσ.c; a data value v denotes λσ.vσ; spacing and padding denote constant offsets. Positions, once σ is known, are plain reals related by difference constraints pd(b)pd(a)=w.

So the meaning of a node splits in two, matching the two passes:

[[n]]=([[n]]dsize:Claim,[[n]]dpos:a set of difference constraints).

The two semirings

Composition happens in two tropical semirings, one per quantity (see layout-synthesis Part 2):

  • Sizes compose in (max,+) over Claim: series (stacking) is +, overlay (alignment) is max, constants are +c, data is σ. Closed under composition, so any network folds to a single Claim — hence invertible.
  • Positions compose in (min,+): a system of difference constraints is a shortest-path problem; on a forest (one anchor, no alternative paths) the positions are just path sums from the anchor.

[[]]size denotes into the first; [[]]pos into the second.

The layer is the solver

A constraint is meaningless in isolation — it denotes a contribution to its enclosing layer L. The layer composes its children's claims under its constraints' size-folds into one claim per axis, [[L]]dsize, then — given an allotted budget Bd — solves the single unknown

σd=([[L]]dsize)1(Bd)

and evaluates: every node's extent is ed(n)=[[n]]dsize(σd). With σ fixed, the difference constraints [[]]pos become concrete and positions are read off by propagation. (This inversion is exactly Monotonic.inverse; the monotonic module is the implementation of Claim.)

The denotations

Below, a constraint relates a layer's ordered children c1ck. Write ed(c) for [[c]]dsize (the child's own claim) and anchora(c,β) for the predicate "c's chosen anchor on axis a sits at β" — start: pa(c)=β; end: pa(c)+ea(c)=β; middle: pa(c)+ea(c)/2=β; baseline: c's origin =β. s is spacing, p padding, d¯ the cross axis.

distribute (series on axis d)

[[distributed]]dsize=i=1ked(ci)+s(k1),[[]]d¯size=(no contribution)[[distributed]]pos={pd(ci+1)pd(ci)=ed(ci)+s}i=1k1

A series: claims add (plus the spacing constant), and positions are a chain of difference constraints — the running-sum walk.

align (overlay on axis a)

[[aligna]]asize=maxiea(ci),[[aligna]]pos={anchora(ci,β)}i=1k

for a single shared baseline β (taken from the first pre-placed child, else the axis fallback). Overlay: the extent is the max; positions collapse all children onto one anchor line.

position (a pin)

[[position(v)]]dsize=datum domain {v} (not an extent),[[position(v)]]pos={pd(c)=scaled(v)}

A literal pins to a pixel; a datum v pins through the axis scale. It is the boundary case that seeds a difference-constraint forest with an absolute anchor.

nest (unary, padding p)

[[nest(o,i)]]dsize=ed(o)=ed(i)+2p (inside-out)ored(i)=ed(o)2p (outside-in)[[nest(o,i)]]pos={centerd(i)=centerd(o)}

A unary +constant on the size side; concentric centering on the position side. Which variable is derived is discovered by which one is already determined (the firing rule of layout-synthesis Part 3), not fixed by the operator.

grid (the symmetric 2-D layout)

For cells cr,γ in R rows × C columns (constraints/grid.ts):

[[grid]]xsize=γ=1C(maxrex(cr,γ))+sx(C1),[[grid]]ysize=r=1R(maxγey(cr,γ))+sy(R1)[[grid]]pos={centerx(cr,γ)=γ(w+sx)+w2,centery(cr,γ)=r(h+sy)+h2}

This is the Σ-of-max form: a track is the max (overlay) of its cells, and the axis is the (series) of its tracks — (max,+) on both axes, symmetrically. It is exactly two cross-cutting overlay-then-series folds; that the two share the cells (a cell is in one column and one row) is the irreducible 2-D structure the grid constraint owns. The axes additionally denote ORDINAL(colKeys) / ORDINAL(rowKeys) for guide rendering.

v1 note. The implementation specializes the equal-track case: every cell fills its track, so maxrex=w and the equation collapses to W=Cw+sx(C1), solved by box-division (sliceExtent). The general Σ-of-max (content-sized tracks) and the symbolic flex claim — so this prints as Cσ+sx(C1) — arrive with flex-as-datum (the spread fill story).

overlay (a bare layer) and z-order

A layer with no size-folding constraint denotes pure overlay:

[[layer]]dsize=maxied(ci).

zAbove / zBelow denote only a paint order — a relation π(a)>π(b) on the render sequence, with no size or position contribution. They are the one constraint outside the geometric semirings.

Table 1: the constraints at a glance

constraintsize denotation [[]]size(max,+)placement denotation [[]]pos(min,+)role
distributeiei+s(k1) on dpd(ci+1)pd(ci)=ed(ci)+sseries
alignmaxiei on aanchora(ci)=β (shared)overlay
position(v)datum domain {v}pd(c)=scaled(v)anchor / pin
nest(o,i)e(o)=e(i)+2pcenter(i)=center(o)unary ± const
gridtracks(maxcellse)+s(n1), both axescell centered in its (col,row) tracksymmetric 2-D
layer (bare)maxieichildren at own positionsoverlay
zAbove/zBelow— (paint order π(a)>π(b))z-order

Why the composition is total

Read the table column-wise. Every size denotation is built from +, max, σ, and +c — the generators of (max,+) over Claim. Claim is closed under all four and monotone throughout, so the fold of any set of these over a layer is again a single monotone Claim; it has a (one-unknown) inverse; the budget solve always succeeds. Every placement denotation is a set of difference constraints; over a forest of anchors they have a unique solution by path-sum.

That is the completeness statement of constraints-as-core made precise:

The denotation of a layer of {distribute,align,position,nest,grid} over children with monotone claims is a (max,+) claim on the size side and a difference-constraint forest on the position side — hence always solvable, and closed under nesting.

Operators are then just notations for particular denotations: spread=align+distribute, stack=distribute (glue), table=grid. Two constructs sit outside the generators but inside the language (per layout-synthesis): z-order (a render-order relation, no geometry) and custom algorithmic layouts like treemap (arbitrary computation that emits claims, pins, and placements under the same denotations).