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 expect | Glue |
|---|---|
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 newline | a 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 tabs | tabs 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 line | no line continuations at all; multi-line logic uses block forms |
| closures capture the environment | functions are closed by default; capture is opted into with use/mutate |
| 0-based indexing | lists index from 1: list[1] is the first element |
| implicit declaration | first 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 int — TRUE = 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