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