Glue

Feature overview for experienced programmers.

Glue aims at a natural top-to-bottom flow, speed over safety, one universal statement shape, and whitespace that is meaningful horizontally as well as vertically. Lines starting with >> in the examples are expected output.

What's different here

The fastest way in is a list of the assumptions Glue breaks:

You probably expectGlue
x = 5 assigns= is only equality; assignment is x (5) — a spaced paren group
f(x)f (x)adjacency changes meaning: f(x) is a call, f (x) an assignment
blocks open with { or a bare newlinea line introducing a block ends in a glued :, and the block is tab-indented
/ divides/ is integer division; // is float division
truthiness (if list)boolean context is strict; spell it: if list.count > 0
!=<> (plus eq/ne for string-domain comparison)
spaces or tabstabs only; one tab = one level; a leading space is a syntax error
string concat (+ or .)none — interpolation and .join
backslash escapes (\n)none — named constants: "line{NL}"; raw '…' strings
trailing operator continues the lineno line continuations at all; multi-line logic uses block forms
closures capture the environmentfunctions are closed by default; capture is opted into with use/mutate
0-based indexinglists index from 1: list[1] is the first element
implicit declarationfirst binding requires my/up/global; no shadowing; one namespace

Lexing: the adjacency rule

The tokenizer records, for every token, whether it was glued to its neighbours. A glyph standing alone is an operator; glued, it is part of a token or an affix whose meaning depends on which side it's glued to. So a-b (one identifier — hyphens are legal inside names), a - b (subtraction) and a -b (head a applied to the negated value) are three different programs.

The safety valve: if a glyph run has no legal decomposition, that is a syntax error — not a best-effort split. The design compensates for its one-space typo surface by making the wrong reading illegal wherever it can (“loud over lenient”).

Indentation is tabs, one per level. There are no line continuations: an inline expression must complete on its line, and only bracketed regions span physical lines — where newlines are structural separators (rows in [ ], entries in { }), never plain whitespace.

One universal statement form

head {options} arguments (value):
	block

Everything after head is optional, and every construct — keyword, function definition, function call — uses this shape. A brace group after a head is always named options (the one bracket that is not adjacency-sensitive); arguments are comma-separated; a spaced paren group is a value being assigned; an indented block hangs off the end. Compare:

# keyword head + argument + block
if x < 3:
	say "small"

# definition head + typed parameter + block
.area w {int}:
	return w * w

# call head + option + argument (to stderr)
say {err} "oops"
# head + value group: assignment
x (5)

A line that introduces an indented block ends in a glued line-final : (the one other block door is a do form — see the multi-line conditions below), and a colon-terminated line requires its block. Block ownership is stated twice — marker and indentation — so a stray tab or a missing body is a parse error, never a silent rebinding.

Statements must have an effect. A pure expression 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

Note f alone: reading a callable calls it (uniform access — callers can't tell a stored value from a computed one). To get the callable itself, take a reference \f or a copy &f.

Declarations and scope

First binding takes a declarator: my (this block), up (exactly one enclosing block), global (file scope, top level only). Assignment to an undeclared name is a compile error; so is declaring any name that is already visible — no shadowing. Functions and variables share one namespace.

my x (1)
if x = 1:
	# ERROR: x is already visible — no shadowing
	my x (2)

Expressions

Division is split by result type, and mixed-type arithmetic is an error rather than a coercion (+ - * % require same-type operands; convert explicitly):

# integer division
say 7 / 2
>> 3
# float division
say 7 // 2
>> 3.5

Junctions collapse a comparison over several values:

# 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)

The pair form glues a comparator to a two-element group: if the comparison holds, the expression yields the tested value, otherwise the alternative. A third element makes it a full ternary, and ?(t, f) is the boolean ternary:

# status equals 'ok' → status; else 'failed'
status =('ok', 'failed')
# x > 50 → x; else -1
x >(50, -1)
# three elements: pick the second or third
x =(15, "ready", "waiting")
# boolean query form
list.count > 0 ?("full", "empty")

A single ? between operands is coalesce — the right side supplies a value when the left is undefined. Ranges are .. (inclusive) and ..< (exclusive). Word logic (and or not nand nor xor nxor) reads naturally in conditions; the glyphs &/| are short-circuiting, boolean-strict, and expression-only; bitwise operators carry a glued b: &b |b ^b <<b >>b.

Evaluation context — want

Perl heritage: every expression evaluates in a context supplied by its surroundings — Boolean, Comparison, String, List, Command, Void. Context never changes parsing; it only decides what a context-polymorphic value produces. Boolean context is strict (a concrete non-boolean is an error — no truthiness), interpolation is string context, loop sources are list context, and functions can query the context they were called in with want, or dispatch on it.

The variable-test family shows the idea off. varint? tests “defined”; in a comparison, a passing test evaluates as the variable's value, and a failing one as undefined — which coalesce then catches:

my varint (77)
say varint? > 50
>> 77
say varint? = 50
>>
say varint? > 100 ? "small"
>> small

The same varint? in an if head is a plain boolean. One form, context decides.

Control flow

# branch when true
if <cond>
# branch when false — and 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

Inline conditions never span lines. A multi-line condition uses the expressionless form — the condition is the block, and do introduces the body. Sibling lines join with explicit trailing word-operators, or bare nesting implies them (alternating and/or by depth):

if:
	i < 3 or
	i > 8
do
	say "{i}!"

A statement can carry at most one tail modifier (if or unless): say "Hello!" if cond. And when branching is a value table, choose yields the first matching row's value (see the introduction and the worked example below).

Loops: do (bare block), while, for, and foreach (which writes through to the source). Loop-control words are paired with their loop — for takes next/last, while takes continue/break — so control statements name the loop kind they belong to. A loop can carry suffix blocks: then runs on natural completion, also after every iteration. There is no loop else.

for is one head with option modifiers, not a zoo of loop forms (heads shown here 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)

Loops are expressions when asked: for {list} collects each iteration's value, for {last} keeps the final one, and a result in the loop's then block sets the loop's own value — the loop computes, then summarizes:

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

result — the value of a block, in place

An if, choose, or loop used as a value yields the last expression evaluated in the taken branch or iteration. result states the value outright: it sets the value of the innermost enclosing block and exits it. In a loop body it finishes that iteration early with a value; in a branch arm it completes the branch:

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
)

The doctrine is a clean split: return is the invocation's value, result the in-place block's. Using result where return is meant (a function body's top level) is a compile error pointing at return — and vice versa, a function's value comes only from return.

A statement block's value has somewhere to go: a call or declaration ending in a line-final do runs the block in place and consumes its result — as the call's final argument, or as the declared variable's initial value:

my x do
	my rows (build-rows)
	result rows.join(NL)

render header, do
	result build-body

Functions

A definition head is a glued leading dot — .name — and the binding is immutable. Parameters are bare, options go in braces, and the head line ends in :. A name gains multiple signatures as separate definitions, each marked {multi} — a duplicate definition without the marker is a compile error. Dispatch is by arity, types, and calling context, first match in textual order:

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

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

Functions are closed by default: the body sees no mutable outer bindings. Outer state is admitted explicitly — use takes a copy, mutate a reference:

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

Options bind ordinary variables in the body, just like positional parameters — declare {verbose}, read verbose. An option the caller passes bare binds TRUE; unpassed means its default, or undefined (verbose? tests that). The prefix form ?name asks whether the caller explicitly passed it. return is mandatory — falling off the end yields undefined, and a trailing pure expression is diagnosed as a missing return. A quoted parameter name ("who") makes an implicit-string parameter: the call site passes bare, unquoted text. A trailing block passed to a call is available in the body as $do — and because a glued line-final : is always the block marker (never span text), a bare description and a trailing block mix without quoting: test the parser handles empty input:.

Objects, hashes, filters

An object is a namespace with a prototype: a declarator plus a member block. Data members are addressed with a glued > path (point>x); . is the default member accessor for methods and built-in members (list.count, i.hex):

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

Prototypal inheritance is an option — {is (\parent)} — and hashes are just objects on the hash prototype, indexed with brackets. Literal keys are implicit strings — bare words are text, quotes cover spaces, and {expr} computes a key; () is the empty hash and _(…) spreads one:

my config ([host ('localhost'), {key} ('computed')])
# spread + override
my server ([_(config), port (8080)])
# empty hash
my counts ()
counts["word"] ((counts["word"] ? 0) + 1)
say server["port"] ? 8080
>> 8080

Built-in prototype names (object int float str bool list hash block iterator error file) are ordinary global bindings, not reserved words. bool is genuinely separate from intTRUE = 1 is a type error. Postfix filter keywords edit aggregates in place:

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

Errors

Exceptions are ordinary objects on the error prototype, with a prototypal taxonomy (command bounds type dispatch math io contract…) that catch filters by is-a. There is no mandatory wrapper for handling: catch guards the immediately preceding statement:

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

$err is the exception inside a handler; bare throw rethrows. Raising is throw "malformed header", or throw {io} "cannot open {path}" to pick a prototype.

The second mechanism is idiomatic for “did that work?” checks: 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 ('')

(nif is the same conditional but never captures — the exception propagates.) For guarding several statements at once there is try: — a region with a special property: it adds no scope, so names declared inside survive past it. Declare inside, handle, use after:

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 contract: try {io, parse}: promises that only the listed error types escape the region.

Command execution

run puts the rest of the line in command context: bare words are literal argv text, brace groups are the command's flags, and interpolation builds argv structurally — a quoted element is one argv entry, never re-split by a shell:

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

There is no shell in the middle, so shell metacharacters as bare words are compile errors. When you actually want bash, say so with a backtick literal — its content is one verbatim shell line (with { } interpolation):

# ERROR: '|' is not a shell here
run ls | sort
# a real bash line, when you mean it
`ls | sort`

A failed command raises a command error carrying code, command, and stderr — which is what the catch {command} and unless examples above are handling. In value position a command yields its output.

A worked example

Definitions, options, loops as values, tests, junctions, and context, all in one program:

.classify {verbose} age {int}:
	say "checking {age}" if verbose?
	# a function's value is its return;
	return (choose:
		#   a paren group may hold one
		age < 18	"minor"
		#   block-carrying expression
		age < 21	"US minor"
		default 	"adult"
	)

my ages ([12, 19, 30])

# loop as value: for {list} collects
my labels (for {list} age (ages):
	#   each iteration's value
	classify {verbose} age
)

# then-result sets the loop's own
my n (for label (labels) i:
	log "row {i}: {label}" if label = any("minor", "US minor")
then:
	#   value — here, the count
	result i + 1
)

say "{n} rows: {labels.join(', ')}"
>> 3 rows: minor, US minor, adult

# a block expression in a string starts
#   on its own line, one level in
say "batch was {
	if n > 2:
		"big"
	else:
		"small"
}"
>> batch was big

# element test + pair form: defined-and-
#   equal yields the element, else the
#   alternative
say labels[3]? =("adult", "needs review")
>> adult

# comparison context: the failed test
#   yields undefined, so ? fires
say labels[1]? = "adult" ? "unconfirmed"
>> unconfirmed

# head guard: no settings object exists
#   → undefined without error, and ?
#   supplies 21
my cutoff (?settings.cutoff ? 21)

# boolean context: the same test form
#   is a plain boolean in an if
say "under {cutoff}: {labels[1]}" if labels[1]?
>> under 21: minor

# .used maps the predicate per item;
#   all() collapses the booleans
say "all classified" if all(labels.used)
>> all classified

Want every rule? — the language reference →

← Back to the gentle introduction