Layout DSL (YAML, version 1)

A small, versioned YAML file that says how a design should be arranged without touching what it is. Three constraint kinds, two per-node tweaks, a defaults block. Everything else is automatic.

Shape of a file

version: 1

defaults:               # optional
  nodeGap: 32           # vertical gap between nodes in a layer (world units)
  layerGap: 80          # minimum horizontal gap between layers
  direction: right      # the only value accepted today
  routing: orthogonal   # the only value accepted today

scopes:                 # one entry per module *instance* you want to influence
  - scope: top/compute          # hierarchical instance path, starting at the top module
    constraints: [ ... ]
    ports:       [ ... ]
    frames:      [ ... ]

Everything is addressed by instance path, not module name: top/compute and a second instance top/compute2 are separate scopes even if both are the same module. Node names inside a scope are the immediate children of that instance's module: instance names such as pe0, or generated operator cells such as $add_5 (visible in the inspector). module: is accepted as an alias for scope:.

Constraints

constraints:
  - id: pe-chain                 # ids are unique within a scope
    kind: leftOf                 # right(a) + gap <= left(b)
    a: pe0
    b: pe1
    gap: 60                      # world units, default 0
    strength: required           # required | preferred (default required)

  - id: ctl-up
    kind: above                  # bottom(a) + gap <= top(b)
    a: controller
    b: compute
    gap: 40
    strength: preferred
    weight: 5                    # preferred rules are applied heaviest first

  - id: row
    kind: alignY                 # equal vertical centers
    nodes: [input_fifo, compute, output_fifo]
    strength: preferred
    weight: 10
KindMeaningNotes
leftOfright edge of a plus gap is at or before the left edge of bAlso feeds the layering step: a required leftOf against the signal flow turns the conflicting wire into a feedback route instead of failing. There is no rightOf; swap a and b.
abovebottom edge of a plus gap is at or above the top edge of bSolved by pushing b down or, if that breaks another rule, pulling a up. No below; swap a and b.
alignYall listed nodes share one vertical centerTwo or more distinct nodes. Nodes in the same layer cannot share a center without overlapping, so the rule is reported as relaxed.

Required versus preferred

Port order

ports:
  - node: compute
    side: west                   # west | east | north | south
    order: [data_in, valid_in, start, mode]     # top-to-bottom on west/east, left-to-right on north/south

Unlisted ports follow in declaration order. By default inputs sit on the west side and outputs on the east; listing a port under another side moves it there.

Frames

frames:
  - node: compute
    width: 640                   # includes the header and the internal viewport
    height: 420

Only expandable module instances take a frame. The frame is allocated at layout time and never grows: when expanded, the child's schematic is scaled to fit inside it. A large interior in a small frame will be too small to read inline; that is what Focus module is for. The default frame is 260 × 170.

Validation

A file is validated against the loaded design when applied, and every problem is reported at once with its scope and the rule ids involved:

Any error rejects the whole file; nothing is applied partially. The bundled Conflicting sample triggers one of each.

Layout tab showing validation errors for the conflicting sample
The Layout tab after applying the conflicting sample: every error names the scope and constraint id, and the automatic layout stays on the canvas.

How rules are applied

  1. Automatic layered layout computes a candidate (required leftOf rules already influence which layer each node lands in).
  2. Required rules are iterated to a fixed point by nudging: layers shift right for leftOf gaps, nodes move vertically for above and alignY. Non-overlap is maintained.
  3. Preferred rules are tried in weight order and kept or reverted.
  4. Wires are routed against the final geometry, never before.
  5. Everything is verified again and reported.

Rules only reach the immediate children of a scope. You cannot relate top/compute/pe0 to top/output_fifo because each module is laid out in its own coordinate system.

A worked example

The rule file shipped with the demo. It fixes the datapath order and spacing, keeps the three datapath blocks on one row, puts the controller above the compute block, gives compute a frame big enough to read inline, and orders its ports to match the data flow.

version: 1
defaults: { nodeGap: 32, layerGap: 80 }
scopes:
  - scope: top
    constraints:
      - { id: input-before-compute,    kind: leftOf, a: input_fifo, b: compute,     gap: 80, strength: required }
      - { id: compute-before-output,   kind: leftOf, a: compute,    b: output_fifo, gap: 80, strength: required }
      - { id: datapath-alignment,      kind: alignY, nodes: [input_fifo, compute, output_fifo], strength: preferred, weight: 10 }
      - { id: controller-above-compute, kind: above, a: controller, b: compute,     gap: 40, strength: preferred, weight: 5 }
    ports:
      - { node: compute, side: west, order: [data_in, valid_in, start, mode, coeff_sel, ready_in, clk, rst_n] }
      - { node: compute, side: east, order: [data_out, valid_out, ready_out, done] }
    frames:
      - { node: compute, width: 640, height: 420 }
  - scope: top/compute
    constraints:
      - { id: pe-chain, kind: leftOf, a: pe0, b: pe1, gap: 60, strength: required }
      - { id: pe-row,   kind: alignY, nodes: [pe0, pe1], strength: preferred, weight: 3 }

The ProcBase example has a second, larger rule file.