Glue

The language reference — every rule, one page.

This page is the complete rulebook. If you are new, start with the gentle introduction; if you want the highlights, the feature overview. Lines beginning with >> in examples show expected output; they are not part of the program.

Source text

Tokens and the adjacency rule

The tokenizer splits each line into glyph runs — maximal sequences of non-whitespace characters (string literals are scanned as units first and may contain spaces). The core rule:

A glyph standing alone (whitespace on both sides) is an operator. A glyph glued to other characters is part of a larger token, or an affix whose meaning is selected by which side it is glued to. Every token records whether it was glued left and right, and the grammar constrains those flags — a-b, a - b, and a -b are three different programs. If a glyph run has no legal decomposition, that is a syntax error, not a best-effort split.

Identifiers

Glued affixes

FormMeaning
-x, +xunary minus / plus (glued right only)
\x, \[…]reference to a variable / literal
&x, &[…]copy of a variable / literal
line-final :block introducer — the line owns the following indented block
x?, list[3]?, a>b?test: defined (suffix — applies to the whole chain)
?xtest: exists; on a chain, a head guard (tests)
x?e, x?f(name), x?t(proto), x?{rw}attribute / flag / type / file-attribute tests
f(…)call with positional arguments (paren glued to the head)
=(v, alt), =(v, t, f), …compare-with-alternative pair / ternary (comparator glued to the paren)
cond ?(t, f)boolean ternary (query form)
x[…]index / slice
1<, 2<, $< (statement-initial)echo forms (I/O)
.name (statement-initial)function definition head
x++, x--, ++x, --xincrement / decrement on assignable numeric chains — postfix yields the old value, prefix the new

Literals

Numbers

Strings

Lists

my flat ([a, b, c])
my table ([
	1, 2, 3
	4, 5, 6
# same value as [[1, 2, 3] [4, 5, 6]]
])

Inside [ ] a newline separates rows; nested brackets for rows are optional sugar. Lists index from 1, and glued brackets index and slice: list[2], list[2 .. 4]. An out-of-range index is an error (see hashes for the lookup-style alternative).

The statement form

Every statement — keyword, declaration, function definition, or call — has the shape:

head {options} arguments (value):
	block

where everything after head is optional:

Statements must have effect

A pure expression statement whose value is discarded is a compile error:

# ERROR: no effect — value discarded
i > 8
# ERROR: no effect
a + b
# ok: call
f(x)
# ok: zero-arg call — bare callable reads call
f
# ok: assignment
x (5)
# ok: value consumed
say i > 8

The last expression of an in-place value form — a value-position if/choose/loop, or each iteration of a collecting loop — is consumed and therefore exempt. A function body is not: its value comes only from return, so a trailing pure expression there is diagnosed as a missing return.

Blocks

A head owns the block indented one level deeper than its own line. Block introduction is marked: an indented block may follow only a line ending in a glued line-final : or in a do form (bare do, do {opts}, a named do, also do, or a value-position (do …) — and a line ending in : requires the block. A block after an unmarked line, a colon with nothing under it, and do: (one marker per line) are all parse errors. A head that owns no block takes no colon — e.g. the body-less loop in my n (for i (1 .. 10)).

Two cases:

# head line carries an expression:
if x < 3:
	#   the block is the body
	say "small"

# head line carries no expression:
if:
	#   the block is the argument (here, the
	condition1 or
	#   condition), and the body needs do
	condition2
do
	stuff

Blocks as values

A block appears as a value by wrapping do in a value group. Named do blocks label a region of code:

my handler (do
	first statement
	return TRUE
)

do Optional Name Here
	this block is collapsible / referenceable by name

Every indented block is a scope (loops are the exception — the body and the loop's suffix blocks share one loop scope — and try regions add no scope at all). Blocks read their writing site's frame live: a block value invoked after the frame that wrote it has exited is undefined behavior. A block that references nothing outside itself (do {use ()}) is safe anywhere, as are top-level function definitions.

Calls

Adjacency selects the role of every bracket after a name:

ShapeMeaning
f {a, b (v)}call with named options (at a statement head)
f(x, y)call with positional arguments (parens glued)
f x, ycall with positional arguments (bare)
f {opts} x, yoptions and arguments together
f (v)assignment of v to f (paren detached)
f {opts} (v)assignment with options (e.g. my {ref} f (…))

Operand position

The full statement form — spaced options, bare argument list, suffix block — belongs to the statement head alone. Where an operand is expected (inside an expression, argument list, value group, or inline condition), a call is spelled glued: f, f(x, y), f{opts}, f{opts}(x, y). Bare argument lists do not nest: say f a is a parse error — write say f(a) or say f, a. A comma inside grouping parentheses is a syntax error; a grouping paren holds exactly one expression.

Auto-call

A bare read of a name or path bound to a callable — a dot-defined function or a variable holding a block — calls it with no arguments. With one shared namespace this gives uniform access: obj>size reads the same whether size is stored or computed, and the caller cannot tell.

Variables and assignment

Declaration is mandatory

Assigning to an undeclared name is a compile error. So is shadowing: a declarator, parameter, or loop binder introducing a name already visible is a compile error — the missing-my typo and the extra-my typo are equally loud. (Built-in bindings may be shadowed, and object member names may coincide with enclosing names.) A variable is visible from its declarator to the end of its block; a function definition is visible throughout its enclosing block, lines above included, so mutual recursion needs no forward declarations. A declarator takes no statement modifier — declare unconditionally, assign conditionally.

Assignment

name (value) — a detached parenthesized value; there is no = assignment, and = is pure equality. Full positional rules under calls. Assignment inside a condition is comparison-with-binding:

if variable (value) < 20:
	# variable now holds value; branch on its comparison

Conditional assignment

# coalescing lvalue: assign to a if a is
#   undefined, else to b
a ? b (10)

# set if currently = oldvalue
newvalue ?> variable = oldvalue
# same, mirrored
oldvalue = variable <? newvalue
# set if currently ≠ oldvalue
newvalue ?> variable <> oldvalue

The conditional-set arrow points toward the variable; the comparison rides on the far side.

Narrowing

# runs when var is not NULL
with var
# runs when var is NULL
without var

Tests and predicates

# defined?
variable?
# exists?
?variable
# attribute test: e=exists d=defined f=flag
variable?e
# zero/empty: 0 int, '' str, empty container
variable?z
# letters combine with AND: defined and zero
variable?dz
# named user flag
variable?f(name)
# is-a: proto on the prototype chain
variable?t(proto)
# brace = FILE attributes; int var → handle,
#   str var → path, file object → itself:
#   e exists, d directory, f file, r read-only,
#   w writable, a archive, l locked
variable?{e}

# the suffix attaches to a whole glued chain —
list[3]?
#   identifier, path, or index
obj>field?e

# head guard: undefined when list itself is
#   missing; if list exists, a normal index
?list[3]
# safe both ways
?list[3]?e

Aggregate predicates

# variable exists
exists x
# defined (not null)
used x
# defined and TRUE — scalar only
set x

# junction constructors double as predicates:
any(15, 19)
#   at least one / every / no item TRUE
all(\needed)
# .used maps the "defined" test per item
none(list.used)

exists/used/set are keywords; any/all/none are built-in functions with ordinary call grammar. In boolean position a junction collapses over its items' boolean values — a non-boolean item is an error (no truthiness). Emptiness is spelled explicitly: list?z or list.count > 0.

Expressions and operators

CategorySpellings
Arithmetic+ - * / // %/ is integer division and // float division on any numerics, each coercing its operands; + - * % need same-type operands
Comparison= <> < > <= >= — typed-strict: concrete mixed-type comparison is an error. eq ne — string-domain equality: numeric operands coerce to string (5 eq "5" is true; 5 = "5" is an error)
Range.. (inclusive), ..< (exclusive)
Logic (words)and nand or nor xor nxor not
Logic (glyphs)& (and), | (or) — short-circuit, boolean-strict, expression-only: never trailing joiners or line-initial branch heads
Bitwise&b |b ^b <<b >>b — glyph + glued b, integer-only
Coalescing? — left side if defined, else right (also an lvalue)
Conditional set?> <?

All standalone operators require whitespace on both sides. Precedence, tightest first:

  1. Affix tests and sigils (x?, ?x, \x, &x, unary -, ++/--)
  2. Index, member, call (glued [ ], ( ), { }, ., > paths)
  3. * / // %
  4. + -
  5. <<b >>b
  6. &b
  7. ^b
  8. |b — bitwise sits above comparisons, so x &b 3 = 1 masks, then compares
  9. .. ..<
  10. Comparisons and eq ne (junctions distribute here)
  11. &
  12. |
  13. ?, ?>, <?, and the query ?(…)
  14. and, nand
  15. or, nor, xor, nxor
  16. Statement modifiers (… if cond, … unless cond)

Junctions

# true if x is 15 or 19
x = any(15, 19)
# true if x is neither
x = none(15, 19)
# true if x equals every item of needed
x = all(\needed)

Under a comparison the junction distributes (any = or-fold, all = and-fold, none = negated or-fold); two junctions distribute pairwise.

Pair form and ternaries

A comparator glued to a parenthesized group — spaced from its left operand, like every comparison — yields the left operand's value when the comparison holds, and the second element otherwise; a third element makes it a full ternary. The query form ?(t, f) is the boolean ternary; its left operand evaluates in strict boolean context:

# equal → status; else 'failed'
status =('ok', 'failed')
# x > 50 → x; else -1
x >(50, -1)
# success → second; failure → third
x =(15, "ready", "waiting")
list.count > 0 ?("full", "empty")
# a test is a boolean here — composes
x? ?("set", "unset")

All comparators participate (=( <>( <( >( <=( >=(), each taking two or three elements; the query takes exactly two. Only the taken element evaluates. Composed with the defined-test, this is the custom-false-value idiom: varstr? =('nothing', 'else') — an undefined variable always takes the alternative.

Evaluation context

Every expression evaluates in a context supplied by its surroundings. Context never changes parsing; it decides what a context-polymorphic value produces:

Functions query the context they were called in with want — one of the constants BOOL CMP STR LIST VOID CMD — or dispatch on it directly in a signature (functions).

Branching

# branch when true
if <cond>
# branch when false — captures a pending exception
unless <cond>
# branch when false — lets exceptions propagate
nif <cond>
# else-if (line-initial)
or <cond>
# else-if-not
nor <cond>
# fallback
else

Multi-line conditions

An inline condition must be complete on its line. A multi-line condition uses the expressionless form: the condition is the block, and do introduces the body. Within a condition block, lines join with explicit trailing word operators, or bare nesting implies them — four levels, alternating: level 1 AND, level 2 OR, level 3 AND, level 4 OR. Siblings at a level join with that level's operator, and a line with children joins its child group with its own level's operator:

if:
	a
		b
			c
			d
		e
	f
do
	stuff

if a and (b or (c and d) or e) and f. An explicit trailing operator overrides the implicit joiner. A condition block may contain only expressions — statement forms there are parse errors pointing at the missing do.

Statement modifiers

say "Hello!" if cond

say "Hello!" if:
	cond1 or
	cond2 or
	cond3

At most one modifier per statement, in tail position, if or unless only — no stacking. A modifier with no inline expression ends the line with : and takes the following block as its condition; the statement itself is the body, so no do is needed.

Loops

# bare block; as a loop: first pass unconditional
do
# top-tested loop
while <cond>
# iteration
for <range|count|list>
# iterate the list itself (writes through)
foreach v (list)

Loop-control keywords are paired per loop type, so a mixed nesting can break either level with one word: for takes next/last, while takes continue/break.

for forms

One binder shape — for value (source) [index] — and loop modifiers are options on for (heads shown without their : and bodies):

# 0 ..< 3, no binder
for 3
# range
for i (1 ..< 10)
# value = item; index optional
for value (list) index
# every other item
for {by (2)} value (list)
# reverse
for {by (-1)} value (list)
# step 2 starting at item 4
for {by (2), from (4)} value (list)
# bind an existing variable; its final
#   value survives the loop
for value (list) use index
# use generalizes to either binder
for use value (list)

by takes any expression; negative iterates in reverse. A body-less loop head is legal and takes no colon — it iterates for effect.

Suffix blocks

# runs at natural end (loop not broken)
then
# runs every iteration, even on next; `also do` runs even
#   when the loop is broken — or unwound by an exception
also

A suffix keyword (then, also, else, catch) binds to the statement whose head sits at the same indentation, nearest above. There is no loop else.

Loop scope

A loop owns one scope, created at entry and reused across iterations — binders live in it and are updated in place; the body and the loop's then/also blocks share it (also sees the binders per iteration, then their final values). The scope dies at loop exit, so binders die with the loop — hoist a result with up, or bind an existing variable with the use marker. A my in the body is one declaration whose initializer re-runs per iteration.

foreach writes through

foreach value (list[2 .. 4]):
	# modifies list
	value ('New')
foreach value (my temp[] (list[2 .. 4])):
	# modifies the copy only
	value ('New')

Iterators

for/foreach accept an iterator wherever a list is accepted: any object answering more (do items remain? — never advances), take (yield the next item and advance; take past exhaustion is a runtime error), and optionally rewind. Exhaustion is a distinct signal, not a sentinel — items may freely be undefined or falsy. Loops consume without resetting: after last/break the iterator stays where it stopped and a later loop resumes there. Rewindability is ?it>rewind. Files iterate by lines (files).

Branch and loop values

An if used as a value yields the last expression evaluated in the taken branch (undefined if no branch ran); the branch ordinal is available as an option:

my label (if cond1:
	"a"
or cond2:
	"b"
else:
	"c"
)
my which (if {which} cond1:
	…
# 1, 2, …; 0 = else; undefined = no branch
)

Loops as values:

# last iteration's value
my v (for {last} … )
# list of every iteration's value
my l (for {list} … )
my n (for label (labels) i:
	say label
then:
	# then-result: the loop's own value —
	result i + 1
#   here, an explicit count
)

A bare loop's value is undefined — count explicitly (list.count, or the index binder plus then: result i + 1). The {list}/{last} modes consume iteration values — each iteration's explicit result or implicit last expression. A result in then sets the loop's own value; then runs at natural end only, so a broken loop yields the mode value ({list}: items so far), and a zero-iteration loop runs then — then-result doubles as the empty-source default. Reading a loop's value never mutates the source.

result

result expr sets the value of the innermost enclosing block and exits it; bare result sets undefined and exits; result {set} expr sets the value and continues (last one wins, superseding the implicit last-expression value).

my grade (if score >= 90:
	result "A"
or score >= 80:
	result "B"
else:
	log "below B: {score}"
	result "C"
)

my labels (for {list} n (nums):
	# finish this iteration early with "big"
	result "big" if n > 99
	# otherwise: implicit last expression
	classify n
)

In a branch arm, result sets the if-statement's value; in a loop body it sets that iteration's value and finishes the iteration early (also still runs); in a loop's then it sets the loop's value; in a do block it sets the do's value. It reaches only the innermost block. The doctrine: return is the invocation's value, result the in-place block'sresult in a function body's top block is a compile error pointing at return. Like throw, result is statement-only. try regions are transparent to it.

choose

choose:
	age < 18	"Too young to drink"
	age < 21	"Too young to drink in the US"
	default 	"Legal drinking age"

First matching row wins; the statement yields the row's value.

Functions

Definition

A statement whose head is a glued leading dot + name defines a function:

.name {option1, !option2 (), option3 (), option4 (value), "option5" ()} arg1, arg2 {int}, "arg3", arg4 (value):
	body

A type is any prototype name — built-in (int, str, list, …) or user-defined (p {point}) — an is-a constraint; i/s are accepted as aliases. A definition binding is immutable: assigning to a dot-defined name is a compile error (so greet (x), the typo of greet(x), is diagnosed). A reassignable function is an ordinary variable holding a block. Definitions may nest, and a top-level definition may carry {global} to export it (includes).

Multiple signatures: {multi} definitions

.name {multi} i {int}, j {str}:
	return "{j}{i}"

.name {multi} j {str}:
	return j

A name gains multiple signatures as separate, complete definitions, each carrying the {multi} option. Duplicates are loud: a second .name in the same block is a compile error unless every definition of the name carries {multi} — a mixed set is the same error. A sole {multi} definition is legal (a set of one, open to extension), and a set lives in one block.

First match, in textual order: the first definition whose required arity, declared types, and (if present) want context match the call wins — order members from specific to general. Per-signature options ride each member's options group beside {multi}: want (CTX) dispatches on the caller's context, returns (type) is a checked result contract, and raises (…) promises which error types can escape — the latter two never participate in dispatch. Set-level options must agree: {global} on every member or none.

.matches {multi, want (LIST)} filter:
	return rows(filter)

.matches {multi} filter:
	return rows(filter).count

Implicit-string slots must agree across the set (a position is quoted in every member or in none; an option name is quoted in every member that declares it, or in none).

Closed functions — use and mutate

Function bodies are closed by default: a body sees its parameters and options, its own declarations, the definitions lexically visible at its site, and built-ins — and no mutable outer binding. Outer names are admitted by an allowlist pair in the options group — use (…) by copy, mutate (…) by reference:

.myfunc {use (config), mutate (counter)} test, var2:
	# mutate: by reference — outside sees updates
	counter (counter + 1)
	# use: by copy
	say "{config} {test}"

Admission is per call: each invocation reads the current outer value — use copies it fresh (the shallow & copy), mutate aliases the live binding; nothing snapshots at creation. Objects are the exception: use admits them by alias — no copy preserves the identity that =, is-a, and catch filters ride, so a copied prototype would silently miss its own filter. The pair's distinction is binding write-back, not object interiors; admit &obj for a detached copy. An unlisted outer name is simply not in scope. Blocks, by contrast, stay open — control-flow bodies, block values, and trailing blocks see their writing site. On do, the same pair works as a restriction:

# sees only x (copy) and y (reference)
do {use (x), mutate (y)}
	…
# sees nothing outside its scope — pure
do {use ()}
	…

Implicit-string parameters

A parameter whose name is quoted in the definition takes bare, unquoted text at the call site:

.greet "who", punct ('!'):
	say "Hello, {who}{punct}"

greet old friend of mine
>> Hello, old friend of mine!

Parameter and option access

.sum first, rest[]:
	my t (first)
	for v (rest):
		t (t + v)
	return t

say sum(1, 2, 3)
>> 6

The nested call glues its parens — a bare argument list belongs to the statement head alone (calls).

Return values and want

A call's value comes only from return: bare return and falling off the end yield undefined, and a trailing pure expression is diagnosed as a missing return. The return operand evaluates in the caller's context. want evaluates to the immediate call's context — one of BOOL CMP STR LIST VOID CMD — and composes with junctions:

.report:
	# called for effect: skip the work
	return if want = VOID
	return build-summary

Block parameters

A call site may end in : and pass a trailing block; inside the body it is $do:

.twice:
	$do
	$do

twice:
	say "hi"
>> hi
>> hi

Reading $do runs the block (each read is one invocation, yielding its return value); \$do captures it; ?$do tests presence without running. A function that never mentions $do takes no block — a caller's trailing block is then a compile error. The block sees the caller's scope by reference.

Executed-do arguments

The inverse: a call line ending in do runs the block at the call site and passes its value — its result, or the implicit last expression — as the final positional argument. The marker says who runs the block: : hands it to the callee as $do; do executes it in place. Declarations join: my x do initializes x with the block's result.

render header, do
	my rows (build-rows)
	result rows.join(NL)

Parenthesized (do …) stays the block as data, unrun — parens pass the block, a line-final do runs it. In an implicit-string span a line-final do stays text (prose lines end in "do" constantly, unlike the colon); to pass an executed block to an implicit-string function, quote the arguments.

Errors

Exceptions are ordinary objects on the built-in error prototype, with a prototypal taxonomy: command (failed external command), bounds (index miss), type, dispatch (no matching signature), math (overflow, division by zero), io, and contract. User types derive normally: my parse-error {is (\error)}. Members: message, file/line (the raise site); command errors add code, command, stderr. An exception no handler accepts aborts the program with its message and raise site, and a nonzero exit.

Raising — throw

# fresh error, message set
throw "malformed header"
# options select the prototype
throw {io} "cannot open {path}"
# optional payload hash:
#   handlers read $err["path"]
throw {io} "no {path}", \["path" (path)]
# an is-a-error object, as-is
throw bad-input
# bare: rethrow $err (handlers only)
throw

Handling — catch

catch guards the immediately preceding statement — no wrapper needed. Its options are is-a filters; successive catch blocks are tried top to bottom, first match runs, bare catch matches everything, and an unmatched exception propagates. $err is the exception inside a handler:

run {dir (build)} make all
catch {command}:
	log "build failed ({$err>code}): {$err>message}"
catch:
	log "unexpected: {$err>message}"
	throw

A raise inside a guarded statement aborts it at the raise point with no partial assignment — my x (risky) leaves x undefined, x (risky) leaves the old value untouched.

The unless capture

Statement-position unless captures a pending exception from the statement above, parks it, and evaluates its condition — typically a test of the value that should now exist:

my cfg (run cat config.txt)
unless cfg?:
	log "no config, using defaults: {$err>message}"
	cfg ('')

Branch taken → the block runs with $err bound, and completing it handles the exception. Branch not taken → the exception propagates at once. nif is the same conditional but never captures, and a modifier … unless cond never captures either.

try regions

try: guards a group of statements, and has one special property: the region introduces no scope. Declarations inside land in the enclosing block and survive the region — declare inside, handle, use after. (For a scoped temporary around a guarded region, use do + catch instead.)

try:
	my text (run cat config.txt)
	my cfg (parse text)
catch {command}:
	cfg (\defaults)
# try added no scope — cfg lives on
say "port: {cfg>port}"

An options group on try is a raise contract: try {io, parse}: allows only the listed types to escape; try {}: promises none do. A violating exception is replaced at the boundary by a contract error carrying the original. The signature twin is the raises (…) option on a function row.

Unwinding

A raise aborts enclosing expressions, statements, loop iterations, and invocations outward to the nearest accepting handler. A loop unwound by an exception skips then and that iteration's plain also, but also do runs during the unwind — the loop's cleanup arm. Iterators stay where the raise left them. An exception raised mid-unwind replaces the in-flight one.

Objects and prototypes

An object is a namespace with a prototype — a scope whose bindings persist as the object's members. A declarator that owns a block builds one: ordinary declarations and dot definitions run inside it, and the scope they build is the object:

my point:
	my x (0)
	my y (0)
	.norm:
		return (x * x + y * y).sqrt

Prototypes and is-a

my child {is (\parent)}:
	…add or override members…

Reads fall back along the chain; assignment to an inherited member creates an own member shadowing the prototype's; assignment to a name on no chain link is an error. The is-a test is x?t(proto) — true iff proto is on x's chain; it works on primitives too (x?t(int)), and the same relation drives signature dispatch. object is the root prototype; prototypes are ordinary objects.

Namespace separators

The default member separators are . and >, interchangeable — point>x and point.x are the same read. The habit of > toward data members and . toward methods is style, not grammar. Additional separators are declared per object:

my obj {children (/, :)}
say obj/config:port

Legal separator glyphs are only those the language has not already claimed — /, : are free (. and > are the defaults, not declarable). Path segments are ordinary identifiers.

Hashes

A hash is an object addressed through index brackets; hash is a built-in prototype:

my config ([host ('localhost'), "key with spaces" (1), {key} (2)])
# the empty hash
my h ()
h["port"] (8080)
say h["port"] ? 8080
>> 8080

Types and primitives

The object model is the type model. The built-in prototypes — object int float str bool list hash block iterator error file — are ordinary global bindings, not reserved words. Signature types are is-a constraints, so dispatch covers user types with no extra machinery.

Standard members

TypeMembers
listcount, join(sep?) (bare read = empty separator), sort, reverse, push(v, …), pop, shift, unshift(v, …), sum, min, max, map (list.map[1 .. 3, 6] projects every row), used (per-item defined map). Mutators write through the receiver.
hashkeys, values, count (insertion order)
strcount (characters), upper, lower, trim, split(sep) → list, index(sub) → position or undefined (no −1 sentinel), replace(from, to), join(…) (the receiver is the separator: ''.join(a, b)), int, float. Strings index and slice like lists: s[2], s[2 .. 4].
numericpower, abs; float round, floor, ceil; sqrt (float result); conversions int, float, str, hex; tests zero, blank
boolint (0/1), str ("TRUE"/"FALSE")

Member and index suffixes apply to literals and groups directly: [a, b].join, ''.join(a, b), 3.power(2), (expr).member. x.zero is the type's zero/empty (0, '', empty container); x.blank is NULL or the type's zero/empty.

Filters: keep, toss, grep

keep/toss are postfix keywords that edit aggregates in place. Dispatch is on the expression's type: boolean → per-element predicate; integer → index; string → hash key. The predicate form takes a leading comparator whose left operand is each element implicitly:

my array ([7, 8, 9])
# keep index 2         → 8
array.keep 2
# drop elements = 9    → 7, 8
array.toss = 9
# predicate form       → 8, 9
array.keep > 7

grep is the assigning/creating variant (grep [[my] var] (list) filter); plain filtering should prefer keep/toss. The postfix-keyword set is fixed; ordinary operations are members, overridable by user prototypes.

Input and output

FormTarget
echo e / < ethe block's output stream
say ethe block's output stream, with newline
print e / 1< estdout, bypassing capture
log estderr, with newline
2< estderr
$< ethe enclosing interpolation string

Stream selection on the word forms is an options group:

# stderr, by number
say {2} "message"
# stderr, by name (out | err | 1 | 2 | …)
say {err} "message"
# to an open file
say {to (\f)} "message"
# prints: 2 message
say 2 "message"

The output model

Every block execution owns an output stream. echo and say append to the stream of the block containing the statement; uncaptured streams pass through to the parent's, and the root stream is stdout — an uncaptured say prints, from anywhere. print and log bypass the model entirely.

do {out} turns a do block into an immediate execution whose value is the captured output of its dynamic extent, calls included — the text in string context, lines in list context:

my report (do {out}
	render-header
	for row (rows):
		say "{row.join(TAB)}"
)

$< appends to the enclosing string — legal only inside a "{ … }" interpolation block. An interpolation block that uses $< contributes only its $< output; the block's expression value is discarded:

my html ("{
	for item (items):
		$< "<li>{item}</li>"
}")

Files

# read (default)
my f (open("log.txt"))
# write modes: write | append | rw
my g (open{append}("audit.log"))
# files are iterators, by lines
for line (f):
	say line
# raw write — newline explicit
g.write("done{NL}")
# explicit; no auto-close
g.close

Members: read (rest of the file as one string), write(s), close, path, fd. Failures raise io errors — open misses, operations on a closed file, permission errors. The ?{…} file tests dispatch on their receiver: int → handle, str → path, file object → itself. stdin/stdout/stderr are built-in file objects: for line (stdin):, stdin.read; say {to (\stderr)} …log ….

Program and environment

All built-in bindings, not keywords:

Running commands

Two spellings run external commands: run, where the language builds the argv list — structural, so quoting and injection hazards are absent by construction — and the backtick literal, one verbatim bash line for when you want real shell syntax.

run

# ls -l -a /tmp /home
run ls {l, a} /tmp /home
# glued: single-letter bundle — ls -la
run ls{la} /tmp /home
# quoted elements interpolate: one
#   argv element each, never re-split
run cp "{src}" "{dst}"
# glued into a word: splice
run tar{czf} backup-{stamp}.tgz

Execution options

run {dir (/tmp), ok (any(0, 1))} grep{c} pattern data.txt
# rc = raw exit code; never raises
my rc (run {code} risky-tool)
# don't wait; value: process handle
run {async} sleep 60
OptionEffect
dir (path)working directory
env (\hash)environment overlay
ok (codes)exit codes counted as success — ok (any(0, 1))
codevalue is the raw exit code (int, any context); never raises on exit status
asyncdon't wait; the value is a process handle
in (path) / out (path) / err (path)stream redirection
mergefold stderr into stdout (affects captures)

Unknown option names are compile errors, so run {src} cp … — the misplaced-escape typo — diagnoses instead of running cp --src.

Shell habits, handled loudly

The backtick literal

my n (`ls -1 {dir} | wc -l`)
`make clean && make {target} 2>&1 | tee build.log`
run {dir (/tmp)} `tar czf backup.tgz . && echo done`

The content runs as one bash command line — pipes, redirection, globbing, quoting, and expansions are bash's, untouched. { } interpolates in string context and splices textually: no quoting is added and bash re-parses the line — verbatim shell is the feature, so the never-resplit guarantee deliberately does not apply here. Two escapes: ${ passes through verbatim (bash parameter expansion needs no doubling), and other literal braces are doubled — brace expansion is {{1..5}}, an awk body {{print $1}}. A bare backtick statement executes for effect; to combine with execution options, the literal stands in place of the command region.

Command values and failure

The command's value — run and backticks alike — follows evaluation context:

ContextValue
booleansuccess: exit code in the ok set (default 0)
stringcaptured stdout, trailing newlines stripped
liststdout split on newlines; interior blank lines kept
voidstdio inherited; nothing captured
comparisonper the other operand; toward int, a compile error suggesting {code}

A non-ok exit raises a command error — carrying code, command, and stderr where captured — except in boolean context, which tests instead of raising. Spawn failure (missing binary, permission denied, bad dir) always raises, in every context: a command that never ran is not a false result. stderr inherits by default and is never part of a capture unless merged.

Programs and includes

A program is a root file plus the closure of its includes:

include lib/parser
include {only (trim, pad)} util
include {hide (debug-dump)} 'odd name.glue'

Word inventory

Reserved words:

my up global do if unless nif else or nor and nand xor nxor not eq ne
with without while for foreach next last break continue also then fn use
return result want self try catch throw choose keep toss grep exists used
set echo say print log run include default TRUE FALSE NULL NL CR LF CRLF
BOOL CMP STR LIST VOID CMD _

(fn is reserved but currently means nothing — definitions use the dot head. _ is the blank head of the spread form _(…) and no longer a legal identifier.) Reserved words are deliberately minimal. The built-in prototype names (object int float str bool list hash block iterator error file) and the built-in functions and values (any all none open exit dump args program env stdin stdout stderr) are ordinary global bindings, not reserved words — conversely, member names may not be reserved words: a reserved word in member position is a postfix keyword (array.keep > 7).

← Gentle introduction · Feature overview