Normative for the reference implementation.
A source file is a sequence of lines. Each line is a row in a fixed-width grid of characters. The
grid has rows rows (one per source line, excluding a single trailing
empty line) and cols columns (the maximum length of any line after
trailing whitespace is stripped).
Every cell holds one character. There are four kinds:
#. Does not fall. Blocks
falling pieces vertically and laterally. Invisible to the parser.> and
<. Do not fall. Do not block pieces. Form wind fields (§04).
A piece is a maximal horizontal run of piece characters on a single source row, optionally
annotated with drift. Drift is a horizontal velocity declared by a directive of the form
<§drift=N §>, where N is a signed integer.
Spaces break pieces. A string literal that contains a space (like
"hello world") is therefore not one piece but two
("hello and world") — they fall
independently, and wherever they settle has to read as a valid statement on its own.
A directive followed by {...} scopes the drift to the braced span;
drift reverts to the prior value after the closing brace. A directive without braces applies the
drift to the rest of the line, or until a subsequent directive.
Directive characters and brace delimiters are metadata. They are stripped before falling and contribute no characters to the piece content.
Falling proceeds row by row from the top. The pieces of row N begin falling at the start of phase N, in lockstep. Row N+1 does not begin until all of row N has settled.
Each tick, an active piece attempts to advance one row down and
drift + impulse columns sideways. A positive
drift or wind impulse moves a piece toward higher column indices
(rightward); a negative value moves it toward lower column indices (leftward).
0, and any wind impulse is cleared.Pieces stack. A falling piece comes to rest on whatever is beneath it — the floor, a wall, or a piece that settled earlier. It never passes through and never overwrites; a vertical column piles up. Pieces released in the same phase are mutual obstacles in flight; pieces from earlier phases are settled obstacles to stack on. Because earlier source rows fall first and rest lowest, later rows pile on top. The column a piece settles in is part of what it means: a piece that overlaps the rows below it joins their stack, while a token flung far past everything lands on its own.
A wind field is two same-length runs of the same direction character on a single row. A run of
N >s must close with a run of N >s; the span from the start of the opening run to the end of the
closing run is a field of strength N. The
< direction works the same way with the opposite sign.
An opening run with no matching closing run is a compile error
(unmatched wind segment).
A piece whose column range overlaps a wind field as it arrives at that row gains an impulse of
direction × strength — positive (rightward) for
>, negative (leftward) for <. The
impulse lasts exactly one effective push: it is applied on the tick after it is acquired and
then decays. Crossing another field replaces it. Lateral collision clears it.
After everything has settled, the whole grid is the program. Every settled row is tokenized independently — walls and wind markers are treated as whitespace, blank space contributes nothing, and everything else forms tokens. A row is classified by its first token:
let or print
is one statement.@[r, c] can read it. Data rows are never executed and
are not errors.
The interpreter reads the grid from the floor up. Because earlier source rows
fall lowest, the statements run in the order you wrote them, and they share a
single environment. A let low on the grid is
visible to a print above it — so a program of
let x = 6, then let y = 7, then
print x*y prints 42.
This makes a Liminal program a small puzzle. You arrange pieces at the top so that what lands on each row reads as a valid statement, or as the data a later statement needs. Several source rows can feed one settled row by landing in different columns; pieces that overlap stack into adjacent rows rather than merging onto one.
Statements are:
let NAME = EXPR — bind a name in the shared environment, visible
to every statement that runs after it.print EXPR — append a line to stdout.Expressions:
+ - * /, with usual precedence.@[r, c] — reads the raw settled grid char at row r,
column c, as a one-character string. It reads geometry, not variables. A space cell
returns " "; a wall cell or an out-of-bounds position returns the
empty string "". The tokenizer's mapping of
#/</>
to whitespace does not apply here — @[r, c] sees
the literal grid.@row(n) — reads a whole settled row as a string. It evaluates
n to a row index and returns that row joined, with every wall
(#) turned to a space and trailing whitespace trimmed. An
out-of-bounds row returns "".num(x) — coerce a value to a number. Errors on non-numeric input.
Note: num("") of the empty string returned by an out-of-bounds or
wall @ read coerces to 0 (JavaScript
Number semantics), so a missing cell reads as zero rather than
erroring.
No conditionals, no loops, no functions, no imports, no comments. A row of all
#s tokenizes to nothing — the closest thing to a blank line. The
@overlay directive is reserved for a future version. The reference
implementation is a tree-walking interpreter; execution semantics are defined operationally by
it.