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);'nameis a string too - Symbols:
$name, a kernel symbol forlink; not a string - No negative literals: write
0-1for −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.wfilename (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_variableorparam_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 bindingmp(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
| Code | Direction | Description |
|---|---|---|
C | Center | The PE itself (onramp/offramp) |
R | Right | East neighbor (+X) |
L | Left | West neighbor (-X) |
U | Up | Neighbor at +Y |
D | Down | Neighbor 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 ontoapand sends them right. - The router of tile (x+1,y) moves
aparriving from the left ontoafor 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():
| Term | Where | Suppresses at | Effect |
|---|---|---|---|
|R | after the arrow, [CXR|R] | Right edge (max x) | Don't send/pass rightward |
|L | before the arrow, [L|LXC] | Left edge (min x) | Don't receive from left |
|U | after the arrow, [CXU|U] | Top edge (max y) | Don't send/pass upward |
|D | before 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:
fabadderneeds thefloat16_dtypepaint option, which names the format of the core's operand. For Tungstenhp, addfloat16_dtype=10toPAINTFLAGS: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 inmiget/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:
@typevalues can be used ascode()parameters but cannot be compared insetif()expressions — use per-tile code binding instead.
See the topic Eight neighbours for per-tile code binding.
Link (External Initialization)
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
| Side | Behavior |
|---|---|
RIGHT / LEFT / UP / DOWN | Router begins/ends on the adjacent PE in that direction |
CENTER | Router 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}