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:

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

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

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

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:

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

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:
	my x (2)     # ERROR: x is already visible — no shadowing

Expressions

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

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

Junctions collapse a comparison over several values:

x = any(15, 19)       # true if x is 15 or 19
x = none(15, 19)      # true if x is neither
x = all(\needed)      # true if x equals every item of 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 =('ok', 'failed')          # status equals 'ok' → status; else 'failed'
x >(50, -1)                       # x > 50 → x; else -1
x =(15, "ready", "waiting")       # three elements: pick the second or third
list.count > 0 ?("full", "empty") # boolean query form

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

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

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

for 3                          # 0 ..< 3, no binder
for i (1 ..< 10)               # range
for value (list) index         # value = item; index optional
for {by (2)} value (list)      # every other item
for {by (-1)} value (list)     # reverse

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:

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

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):
	result "big" if n > 99     # finish this iteration early with "big"
	classify n                 # otherwise: implicit last expression
)

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.

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 definition may hold multiple signatures; dispatch is by arity, types, and calling context, first match top to bottom:

.name:
	i {int}, j {str}:
		return "{j}{i}"
	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:
	counter (counter + 1)     # mutate: by reference — outside sees updates
	say "{config} {test}"     # use: by copy

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.

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:

my config {is (\hash)}
config["host"] ('localhost')
say config["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 int — TRUE = 1 is a type error. Postfix filter keywords edit aggregates in place:

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

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)
say "port: {cfg>port}"       # try added no scope — cfg lives on

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:

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

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

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

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?
	return (choose:                 # a function's value is its return;
		age < 18	"minor"         #   a paren group may hold one
		age < 21	"US minor"      #   block-carrying expression
		default 	"adult"
	)

my ages ([12, 19, 30])

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

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

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

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

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

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

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

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

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

Want every rule? — the language reference →

← Back to the gentle introduction