Skip to main content

Paint Syntax Quick Reference

Paint (.paint) is the layout and routing DSL that describes how Tungsten kernels are placed on the fabric and how data flows between them.

For the full specification, see the Paint Language Reference; each section below links to the part it condenses.

For a guided design path, continue with the tutorial. Lesson 4, Sockets, wavelets, colors and routes, traces a route from its sender to every receiver. Lesson 6, Blocking, buffering and deadlock, shows how a program deadlocks and how to check a protocol. The topic Large layouts: geovars, terminals and transports, link composes larger layouts. To choose a lesson to start a new kernel from, see Which lesson for which dataflow.


Basics​

Reference: 1. Lexical Elements

  • Comments: (* this is a comment *)
  • Variables: let x = expr
  • Attribute application: region : attribute(...) — applies an attribute to a region
  • Strings: "hello" (double-quoted), or 'name (a quoted name, used for names, tags and kernel names); 'name is a string too
  • Symbols: $name, a kernel symbol for link; not a string
  • No negative literals: write 0-1 for −1; inside a list, [0 -2 4] is the two-element array [-2 4]

Creating Regions​

Reference: 8. Regions

Single Tile​

let t = tile() (* 1×1 tile at origin (0,0) *)
let t2 = tile(3 5) (* 1×1 tile at position (3, 5) *)

Rectangle​

let r = rect(x y w h) (* w×h rectangle at position (x, y) *)
let r2 = rect(0 0 4 2) (* 4 wide, 2 tall, at origin *)

Empty Region​

let e = rect(0 0 0 0) (* empty region — placeholder *)

Attaching Code​

Reference: 9. Code Attachment

region : code('kernel_name)
region : code('kernel_name [param1:value1 param2:value2])
  • 'kernel_name — matches the .w filename (without extension)
  • The parameter dictionary may be omitted when the kernel needs no bindings; omitting it is equivalent to passing [].
  • param:value — binds Tungsten params and sockets to paint variables or colors
  • Socket binding: socket_name:color_variable
  • Param binding: param_name:paint_variable or param_name:literal

Examples​

(* No params or sockets *)
t : code('standalone)

(* Single param *)
t : code('add [N:N])

(* Socket + param *)
t : code('send [tx:mycolor N:4])

(* Multiple sockets *)
t : code('compute [rx:color_in tx:color_out N:N W:W])

Colors (Routing Channels)​

Reference: 10. Colors

let c = color() (* allocate a symbolic color *)
let d = color()
let t = tile()
t : paint(c [C>R])
t : paint(d [C>R])
t

A color is one of the router's 24 virtual channels, and paint assigns a physical color number to each symbolic color. Multiple colors can use the same direction on one PE; each color has its own routing configuration and buffer, and the colors on a link share its bandwidth.


Message Passing (MP) Banks​

Reference: 30. Message Passing

Message passing uses dedicated banks of colors for coordinate-based routing:

(* Allocate MP bank 0 — uses physical colors 14-18 *)
let b0 = bank(0)

(* Get the color associated with the bank — for binding to sockets *)
let b0color = color(b0)

(* Enable MP on a tile *)
let t = tile()
t : mp(b0)

Complete MP Example​

let b0 = bank(0)
let b0color = color(b0)

let sender = tile()
sender : code('sender [sock:b0color])
sender : mp(b0)
sender : name('sender)

let receiver = tile()
receiver : code('receiver [sock:b0color])
receiver : mp(b0)
receiver : name('receiver)

hstack(receiver sender)

Key points:

  • bank(N) allocates MP bank N (bank 0 uses colors 14-18)
  • color(bank) returns the color for socket binding
  • mp(bank) enables MP routing on the tile — required on every tile in the MP path, including relay tiles
  • MP routing is automatic: no paint() rules needed for MP colors
  • MP and normal colors coexist on the same tile

See the topics Message passing: routing by address and Collectives over messages: tree reduction and prefix scan for complete MP examples.


Routing​

Reference: 11. Routing

region : paint(color [SRC>DST])

Direction Codes​

CodeDirectionDescription
CCenterThe PE itself (onramp/offramp)
RRightEast neighbor (+X)
LLeftWest neighbor (-X)
UUpNeighbor at +Y
DDownNeighbor at −Y

Common Routing Patterns​

(* Send from this PE to the right *)
t : paint(c [C>R])

(* Receive from the left into this PE *)
t : paint(c [L>C])

(* Pass-through: left to right *)
t : paint(c [L>R])

(* Receive from above *)
t : paint(c [U>C])

(* Send downward *)
t : paint(c [C>D])

Switch Positions (Semicolon)​

(* Initially inject, then advance to pass-through *)
t : paint(c [C>R ; L>R])

(* Three switch positions *)
t : paint(c [L>R ; L>U ; D>U ; ring pop=all])

The ; separates switch positions used successively, not simultaneous routes. The first position is initial unless * selects another. The router moves to the next position when a control wavelet with an advance command passes through it, or when its core finishes a flip send; the control wavelet itself leaves in the old direction. ring enables wrapping to position 0. ring and pop=... are modifiers after the last position, so the second example has three positions. Only 16 of the 24 colors can switch, and a switch has at most four positions; paint does not reject a fifth, and corrupts the earlier ones. Switch Positions gives the details. For simultaneous pass-through and delivery to the PE, use multiple destinations in one position, such as [L>RC].

Teardown with tip​

(* Begin in teardown; sending does not initially deliver data. *)
let c = color()
let t = tile()
t : paint(c [C>R; tip])
t

Colorpair (Swap) Routing​

Colorpair routing enables uniform bidirectional communication across arbitrary-sized grids using paired colors and the X swap modifier.

The Problem​

In a uniform row every tile has the same routing rules, so each tile receives from its left neighbour and sends to its right neighbour with the same colors. One color cannot do both, because a color's router takes input from one port at a time: paint rejects [LC>CR] (Multiple router inputs set). Receiving from the left and sending the core's data right needs two inputs, so it needs two colors. Color swapping lets the core read and write a single socket, a, while the wavelets travel between tiles on its partner ap.

Declaring Color Pairs​

let a = color() (* the socket's color *)
let ap = color() (* carries a between tiles — partner of a *)
region : colorpair(a ap) (* declare a and ap as swap partners *)

The X Modifier​

X in place of > makes a color take its inputs from its partner: what arrives on the partner continues on this color, inside the router. X swaps all four neighbour inputs and the core input (H swaps only left and right, V only up and down).

region : paint(ap [CXR|R]) (* ap takes what the core sends on a, and carries it right *)
region : paint(a [L|LXC]) (* a takes ap arriving from the left, for the core *)

|R and L| end the route at the edges of the region (Boundary Suppression).

How It Avoids Collisions​

Tile (x,y) Tile (x+1,y)
+----------------+ +----------------+
| core writes a | | core reads a |
| ap [CXR|R] +------- ap ------>+ a [L|LXC] |
+----------------+ +----------------+
router: a -> ap router: ap -> a
  • The core of tile (x,y) writes socket a; its router moves the wavelets onto ap and sends them right.
  • The router of tile (x+1,y) moves ap arriving from the left onto a for its core.

Both tiles use the same rules: the cores read and write only a, and the data travels between tiles on ap. Both colors need the swap, because an input that is not swapped still reads the color's own wavelets. Color Swapping builds this up step by step, from one swapped stream to a row of any length and to routes that turn around.

Complete 4-Direction Example​

For cardinal (N/S/E/W) neighbor exchange, use 4 colorpairs (8 colors):

let a = color() let ap = color() (* horizontal: send right, recv from left *)
let b = color() let bp = color() (* horizontal: send left, recv from right *)
let c = color() let cp = color() (* vertical: send up, recv from below *)
let d = color() let dp = color() (* vertical: send down, recv from above *)

let grid = rect(0 0 W H)
grid : paint(ap [CXR|R]) grid : paint(a [L|LXC]) grid : colorpair(a ap)
grid : paint(bp [CXL|L]) grid : paint(b [R|RXC]) grid : colorpair(b bp)
grid : paint(cp [CXU|U]) grid : paint(c [D|DXC]) grid : colorpair(c cp)
grid : paint(dp [CXD|D]) grid : paint(d [U|UXC]) grid : colorpair(d dp)

Lesson 11, A grid of blocks is a complete working example.

Boundary Suppression (|DIR)​

A |DIR term suppresses routing at the DIR edge of a rect() region, preventing wavelets from escaping. After the arrow it stops an exit; before it, an entry:

(* Send right, but suppress at the rightmost column *)
region : paint(ap [CXR|R])

(* Receive from left, but suppress at the leftmost column *)
region : paint(a [L|LXC])

The suppression side is relative to the rect():

TermWhereSuppresses atEffect
|Rafter the arrow, [CXR|R]Right edge (max x)Don't send/pass rightward
|Lbefore the arrow, [L|LXC]Left edge (min x)Don't receive from left
|Uafter the arrow, [CXU|U]Top edge (max y)Don't send/pass upward
|Dbefore the arrow, [D|DXC]Bottom edge (min y)Don't receive from below

Boundary suppression is typically used with colorpair routing to keep wavelets contained within a rect() region. It matters only where a PE on the edge does send outward: without suppression, that PE can stall.

Apply the | rules to one rect() that covers the grid, as the lesson 11 does. Paint applies a | term at the edges of each rectangle a region is built from, not at the outline of a union, hstack or vstack, and on such a region it also gets every rectangle after the first wrong, without a message. On hstack(tile() tile() tile()), [L|L>R] turns off the input of all three PEs, so nothing arrives.


Layout Combinators​

Reference: 12. Layout Combinators

Horizontal Stack​

hstack(a b c) (* a stays put; b and c follow, each against the right edge of the one before *)

hstack changes only x: each region keeps its own y.

Vertical Stack​

vstack(a b c) (* a at y=0, b at y=height(a), ... *)

Union (Arbitrary Placement)​

union(a b c) (* merge regions at their absolute positions *)

Use union when tiles are placed at explicit rect() coordinates that don't form a simple line.

Replication​

let row = rep(tile() 'i N 1 0) (* N copies, step +1 in X *)
let col = rep(tile() 'j M 0 1) (* M copies, step +1 in Y *)

rep(region 'index count dx dy) — replicates region count times, offsetting by (dx, dy) each step. 'index names the new dimension, which miget, miset and link use in their order argument.


Naming Regions​

Reference: 13. Naming

region : name('my_name)

Names are critical — they determine the symbol prefix for miget/miset:

miget symbol=my_name.a ... # accesses array 'a' inside named region 'my_name'

Important: apply name() after terminal(). For code() the order does not matter.

Name every region whose memory you read. Two code regions without names put the same symbol name in the map twice, and miget then reads only the first (it warns has 2 exact map matches; using first entry).


Fabric Adder​

Reference: 29. Fabric Adder

The fabadder flag turns a color into the fabric adder, an adder in the router:

let c = color()
let region = tile()
region : paint(c [L>R; fabadder])
region

Only color 10 can be the fabric adder, and paint assigns it for you. Each fp32 wavelet that arrives on the color from a neighbour waits for one fp16 (hp) wavelet from this PE's core. The router adds the pair and sends the fp32 sum out of one neighbour port, named after >; the sum cannot go to the core. A chain of such tiles computes a running sum, and the last tile receives it with a plain rule such as [L>C].

Required: fabadder needs the float16_dtype paint option, which names the format of the core's operand. For Tungsten hp, add float16_dtype=10 to PAINTFLAGS:

PAINTFLAGS=... arch=${ARCH} float16_dtype=10

See the topic Reductions in the router: the fabric adder for fabric adder usage.


Geo-Variables​

Reference: 15. Geo-Variables

Geo-variables allow passing position-dependent values to code. Define the value on each constituent region, then consume it with @name from a separate code region that covers them, and union it with them:

let left = tile(0 0)
left : geovar('rank 0)
let right = tile(1 0)
right : geovar('rank 1)
let both = rect(0 0 2 1)
both : code('kernel [id:@rank])
both : union(left right)

Define each referenced geo-variable over every tile receiving the code. GEOX, GEOY, and GEORANK are not predefined Paint variables.

Do not apply code() to the union of the geo-variable regions themselves: let both = union(left right) followed by both : code('kernel [id:@rank]) builds without a message, but both tiles compile with id=0. A geo-variable set on part of a region before code() is applied to the whole is ignored.


Paint Variables (from Makefile)​

Reference: 27. Command-Line Options

Paint files can reference the test.rc row variables that the Makefile lists in PAINTVARS:

PAINTVARS=N W H

These become available as paint variables N, W, H which can be used in expressions and passed to code().


User-Defined Macros (define)​

Reference: 6. Function Definitions

Paint supports user-defined macros (functions) for reusable layout patterns:

(* Define a macro with parameters *)
define spiral(ee a b c d e f g h i) {
ee : paint(g [L>C ]) ee : paint(h [U>CR]) ee : paint(i [U>C])
ee : paint(d [L>CU]) ee : paint(e [C>RULD]) ee : paint(f [R>CD])
ee : paint(a [D>C ]) ee : paint(b [D>CL]) ee : paint(c [R>C])
}

(* Define a helper that applies code to a tile *)
define lifecode(tt tx r0 r1 r2 r3 r4 r5 r6 r7) {
tt : code('life [tx:tx rx0:r0 rx1:r1 rx2:r2 rx3:r3
rx4:r4 rx5:r5 rx6:r6 rx7:r7 N:N])
}

(* Use macros to compose a grid *)
define grid(x y a b c d e f g h i) {
let aa = tile()
aa : spiral(i g h c a b f d e)
aa : lifecode(a i b c d e f g h)
aa : name('aa)
(* ... more tiles ... *)
}

Macros are invoked like functions:

let gr = grid(W H a b c d e f g h i)

Macros can return a region (the last expression) and can nest freely. They are the primary mechanism for building reusable layout libraries.

See the topic Eight neighbours: spiral routing on a budget for macros in practice.


Directional Stacking​

Reference: Stacking

stack() composes regions with explicit direction:

stack(HORIZONTAL a b c) (* a | b | c left-to-right *)
stack(VERTICAL a b c) (* a / b / c bottom-to-top *)

Equivalent to hstack/vstack but can be parameterized in macros.


Replicated Stacking (stackrep)​

Reference: 14. Replication

stackrep replicates a region across a named dimension:

let swath = stack(VERTICAL (* a 3 × 3 block of tiles *)
stack(HORIZONTAL aa bb cc)
stack(HORIZONTAL dd ee ff)
stack(HORIZONTAL gg hh ii))
swath : stackrep('x HORIZONTAL split(W 3)) (* replicate W/3 times in X *)
swath : stackrep('y VERTICAL split(H 3)) (* replicate H/3 times in Y *)
  • 'x / 'y — dimension name for symbol indexing (used in miget/miset)
  • HORIZONTAL / VERTICAL — replication direction
  • Count — number of repetitions

stackrep steps by the block's own width or height, so a 3 × 3 block repeated W/3 by H/3 times covers W × H. The named dimensions appear in symbol metadata: with W = H = 15 and the region named grid, grid.aa.cell [ y<5 x<5 ].

split(n k) — Ceiling Division​

split(n k) divides n by k and rounds up (split(16 3) is 6). It is used to compute replication counts:

let reps = split(15 3) (* = 5 *)

Explicit Placement (place)​

Reference: Placement

place() translates a region relative to its current position:

let swath = tile(3 5)
swath : place(1 1 R0) (* now at column 4, row 6 *)
swath : place(2 0 R0) (* now at column 6, row 6 *)
swath

R0 is the no-rotation orientation; R90, R180, and R270 select rotations. Successive translations add their offsets.


Custom Geo-Variables (@type)​

Reference: 15. Geo-Variables

Assign integer attributes to individual tiles using @name=value, which is shorthand for geovar('name value):

let aa = tile() aa : @type=0
let bb = tile() bb : @type=1
let cc = tile() cc : @type=2

These attributes are passed to the kernel as compile-time constants when referenced in code() parameters (e.g., code('kernel [type:@type])), causing the compiler to generate one variant per distinct value. Apply that code() to a separate region that covers the tiles, as in Geo-Variables: let all = rect(0 0 3 1), all : code('kernel [type:@type]), then all : union(stack(HORIZONTAL aa bb cc)).

The Game of Life does not use @type: its 9 tile types in the 3×3 spiral pattern differ only in the socket-to-color bindings that lifecode() gives each tile.

Note: @type values can be used as code() parameters but cannot be compared in setif() expressions — use per-tile code binding instead.

See the topic Eight neighbours for per-tile code binding.


Reference: 19. Link

link initializes symbol data from an external program at build time:

region : link($symbol "order" "program" [key:value ...])
  • $symbol — the symbol to initialize (e.g., $cell)
  • "order" — dimension traversal order (e.g., "y x")
  • "program" — an executable you provide that generates the data; the toolset ships none
  • [key:value] — arguments passed to the program

The $symbol is looked up relative to the region link is applied to: on region grid, $aa.cell names grid.aa.cell. milink runs the program when it links the image, as program key=value ... out=/dev/fd/N, with numbers and colors in hex; the program writes raw little-endian 16-bit words to the out= file. Link gives the full interface.

The Game of Life of Eight neighbours sets its initial grid with miset in the Makefile instead.


Arithmetic in Paint​

Reference: 23. Arithmetic Primitives

let x = 4
let y = x + 2 (* 6 *)
let z = x * y (* 24 *)
let w = idiv(10 3) (* 3 — integer division *)
let m = mod(10 3) (* 1 — modulo *)

Integer division truncates toward zero and % takes the sign of the left operand, as in C: idiv(0-7 2) is −3 and mod(0-7 2) is −1. Syntax rules that differ from C and Python lists the other surprises.


Conditionals in Paint​

Reference: 20. Conditionals

let N = 5
let x = setif(lt(4 N) "large" "small")
show(x)

Both value expressions are evaluated, so each must be valid even when it is not selected.


Includes​

Reference: 7. Includes

include "filename.paint"

Library paths are specified via lib=path on the paint command line or PAINTFLAGS in the Makefile.


Complete Single-Tile Example​

(* minimal.paint — one PE running kernel "add" *)
let t = tile()
t : code('add [N:N])

Complete Multi-Tile Example​

(* pipeline.paint — three PEs in a row *)
let color_ab = color()
let color_bc = color()

let a = tile()
a : code('send [tx:color_ab N:N])
a : paint(color_ab [C>R])
a : name('a)

let b = tile()
b : code('compute [rx:color_ab tx:color_bc N:N])
b : paint(color_ab [L>C])
b : paint(color_bc [C>R])
b : name('b)

let c = tile()
c : code('recv [rx:color_bc N:N])
c : paint(color_bc [L>C])
c : name('c)

hstack(a b c)

Auto-Routing (Terminals & Transports)​

Reference: 18. Auto-Routing: Terminals and Transports

For complex layouts, paint supports automatic routing via terminals and transports:

(* Define a terminal on a tile *)
region : terminal('port_name 'tag SIDE color [shape] [split] 1)

(* Connect two terminals *)
let arc = transport('region1.port1 'region2.port2 'arc_name)

(* Merge everything *)
let all = union(region1 region2 arc)

Terminal Sides​

SideBehavior
RIGHT / LEFT / UP / DOWNRouter begins/ends on the adjacent PE in that direction
CENTERRouter begins/ends on the PE itself (more flexible, but overwrites existing routing)

Library Includes​

Reference: 27. Command-Line Options

lib=DIR adds a directory paint searches for code() kernels and include files. flute runs each test.rc row in a test-<row>/ subdirectory of the example, so the tutorials pass lib=.. to find the .w files beside bench.paint:

PAINTFLAGS=lib=.. $(PAINTOPTS) verbose arch=${ARCH}