A statically-typed, Python-flavored programming language compiled ahead-of-time through LLVM.
gusty is a small, modern programming language that pairs Python's familiar
indentation-based syntax and ergonomic feel with the performance of native
compiled executables. Source goes through a clean, inspectable pipeline —
lex → parse → semantic (type inference) → codegen — down to LLVM IR, which
is verified, optimized, and lowered to a native binary, or JIT-executed in
the REPL.
There is one backend. Everything gusty runs — --file, --eval, the REPL — goes through LLVM
codegen and the resulting artifact; the AST interpreter that used to answer those paths is retired
(ADR 0302), and its recorded answers, per source, are what the test suite checks the compiler against
(docs/operations.md §"The suite's own interface"). --aot/--jit are accepted and ignored; --interp
is a usage error that says so.
The toolchain is built for both humans and agents: a friendly REPL/CLI for people, plus structured, machine-readable output (JSON diagnostics, JSON AST/IR dumps, a JSON Schema, stable flags, deterministic exit codes) so scripts and AI workflows can discover and consume the language without guessing.
print(40 + 2) # -> 42
x = 5
print(x + 1) # -> 6
if x < 2:
print(10)
else:
print(20)
s = 0
for i in range(5): # range(n) and range(a, b)
s = s + i
print(s) # -> 10
def double(x):
return x * 2
print(double(5)) # -> 10
x = 2
match x:
case 1:
print(1)
case 2:
print(2) # -> 2
i = 0
while i < 100:
i = i + 1
if i == 3:
break
print(i) # -> 3gusty ships an indentation-based syntax covering:
- Functions —
defwith default/keyword args, inferred return types, and anonymouslambdafunctions (lambda x: int: x + 1) lowered to closures exactly likedef. - Control flow —
if/elif/else,while,for ... in range(n)/range(a, b)/range(a, b, step),for x in [...], optional loopelse:clauses,break/continue, andpass. A parameter is a local variable (assigning to one is ordinary and local), and aforloop binds its variable per element — it keeps the last value bound, and writing to it does not move the iteration (ADR 0196). - Pattern matching —
matchwith integer-literal equality,_wildcards, and list-destructuring patterns (case [a, b]:). Matches also support guards (case x if cond:), or-patterns (case 1 | 2:), dict patterns (case {"k": v}:), and class patterns (case Point(x, y):with subclass walk + attribute binding). - Data structures — inline
list/dict/setliterals, indexing, and list / dict / set comprehensions. - Slicing —
s[a:b],s[::step], negative indices, lowered by thert_sliceruntime helper; the index arithmetic (pySliceIndices,normPosIndex) is shared code inpkg/lang/fold.go. - Generators —
def g(): yield a; yield bcollects yielded values. - Async —
async def/await/async for/async with, with the await/return discipline checked in the shared front end: dropping a coroutine, awaiting one twice, or yielding inside anasync defis a compile error, and each function's effect signature is readable withgustyc --effects(ADR 0195). - Context managers —
with expr as name:/with expr:, dispatching__enter__/__exit__(including exception suppression); supported in the compiled backend. - Exceptions —
try/except/finallywith typed built-in exception classes (Exception,ValueError,TypeError,KeyError,IndexError,RuntimeError,StopIteration,ZeroDivisionError) andraise. - Classes & inheritance —
class Name:with methods (self), instance attributes,__init__,class Child(Base):multi-level inheritance, andsuper()delegation. - Operator overloading — binary operators dispatch to dunder methods
(
__add__,__mul__,__lt__, ...) with reflected fallbacks (__radd__,__rmul__, swapped comparisons), resolved from one table (dunderForBinOp/reflectedDunderinpkg/lang/fold.go) so the name a program defines and the name the runtime dispatches can never disagree. - Decorators —
@dec def f:→f = dec(f)at definition time; wrapping (fnptr-valued) decorators compile in AOT via compile-time specialization. - Modules —
import modloadsmod.gyand bindsmodas a namespace withmod.name/mod.fn(args)access. - Standard library — data-only on-disk modules folded as AOT constants:
import math—pi,e,tau(the reference's names, and the literals the module declares) plusPI,E,TAU(this language's own, aliased to the same literal), andPHI,SQRT2,LN2,LN10(each with a lower-case spelling too) — constants the reference's module does not carry.import string—DIGITS,LOWERCASE,UPPERCASE,HEXDIGITS,WHITESPACE,PUNCT.import collections—EMPTY_DICT,EMPTY_LIST,ZERO,ONE.import json—NULL(None),TRUE(True),FALSE(False).
Behind the annotations, a runtime value is one of fifteen kinds — int, float, bool,
None, str, list, dict, set, tuple, class, instance, method, closure,
exn, module — and one Go table says which. The compiled runtime's tagged values, the exported C ABI and
the garbage collector's root tracing all read those numbers, and the heap's own object-header kind is a
projection of them
(list, dict, set, instance, with 0 meaning "not allocated — an immediate or an
interned string"). gustyc --lang prints both tables and --schema's valueTag
definition documents the numbering (ADR 0182).
A compiled container can mix numbers, strings and None: print([1, "a", None]) prints
[1, 'a', None] on the compiled backend, because each element slot carries its own tag (ADR 0184). Dicts
and sets take the same rule, so print({"a": 1, "b": "x", "c": None}) and print({1, "a", None})
print correctly compiled, for k in d / for x in s bind the tag beside the value, and a container
that grows a second kind is promoted rather than refused (ADR 0232). The tag is not only for mixed
containers: a lookup compares payload and tag, because an interned string and an integer of the
same number are the same bits — {1: "one"} asked for "a" raises KeyError compiled, where it
used to answer one (ADR 0189 wrote the tags; ADR 0232 made the runtime read them).
Where the tag does not decide yet, one rule does: str(x) folds to the same text on both
backends and on CPython — str(None) is "None", str(1.5) is "1.5", str("x") is x
(ADR 0183).
A slot is read back by the tag its builder wrote, remembered at compile time (ADR 0241): while a name is
bound exactly once to a container literal and nothing mutates it or takes it past an unseen callee,
len(xs[0]), xs[0][1], d["a"][1], t[0][0][0], xs[0] == [1, 2], 2 in xs[0], for v in xs[0] and
y = xs[0][1] all answer on the compiled backend — and a slot the compiler can see holding a number is that
number for arithmetic, so xs = [1, "a"]; print(xs[0] + 1) prints 2 and ys = [1.5, "a"]; print(ys[0] * 2)
prints 3.0 (ADR 0243). Where the promise runs out — including rebinding the very name the read goes
through — the answer is a refusal naming the promise, not a payload read back as a handle.
A {…} display ends at its brace: [{1, 2} for x in xs] is a list of two sets, not a set, because the
for after the brace belongs to the enclosing list comprehension — only a display that is the whole
expression (len({x*x} for x in xs)) finishes itself into one. And a comprehension's element is a value:
a container, a float, None or text enters the slot with the tag that says what it is, payload and tag in
one write — the rule xs.append(v) already followed — so [[1, 2] for x in [1]] prints [[1, 2]] rather
than the inner list's address, [1.5 …] prints [1.5] rather than a box handle, and [None …] prints
[None] rather than the 0 that if None: folds to (ADR 0244).
What the loop variable knows is part of the same rule: an element that is the loop variable carries its
kind — and, over a container whose slots mix kinds, its tag — into the list it builds, and it asks the
container being iterated, not the loop variable, whose facts are gone once the loop closes. So
print(out) and print(out[0]) tell one story (['a'] and a) instead of one printing text and the
other the interned index (Gap R.46, closed), and [x for x in {1, "a", None}] prints [1, 'a', None]
where the compiled backend printed [1, 0, 0] (Gap R.76). Iterating a dict walks its keys at the
stride its two-word entries need, so [k for k in d] over {1: "x", 2: "y"} is [1, 2] and not the
[1, 0] — a key and a value — the compiler used to hand back (Gap R.77); and a dict comprehension writes
each entry as two (payload, tag) pairs with the tag its key really has, so {k: 1 for k in d} over a
text-keyed dict prints {'a': 1} and out["a"] finds it instead of dying with KeyError (Gap R.78).
A dict is a key → value mapping however it is built (ADR 0260, closing Gaps R.118 and R.120) — which is
another way of saying the interpreter used to append entries. print({"a": 1, "a": 2}) printed
{'a': 1, 'a': 2} and len printed 2; {1: 2 for x in [1, 2]} printed {1: 2, 1: 2} — the literal, the
comprehension and the dict(d) copy each grew the entry arrays, and only item assignment asked the dict
whether it already held the key. All four now walk one door, so the entry keeps its position from first
insertion ({"a": 1, "b": 2, "a": 3} is {'a': 3, 'b': 2}, because for k in d walks insertion order)
and takes its value from the last write, while the key that survives is the first one written —
{1: 'a', True: 'b'} prints {1: 'b'} and {True: 1, 1: 2} prints {True: 2}, agreeing with CPython
because 1, True and 1.0 are one key (ADR 0259's key equality). This is the rare row where the
interpreter was the diverging engine and the compiled answer was CPython's; both legs had disagreed
about an ordinary dictionary until a program wrote one.
A container the program built rather than spelled out — appended to, assigned into, rebound — no longer
has a literal behind it, so ADR 0241's compile-time promise has run out, and the read used to refuse. It
asks the object instead, because every writer wrote payload and tag together (ADR 0187): len(xs[0]) of a
list built by xs.append([7, 8]) is 2, a text slot is measured in characters, and a slot holding 5
raises CPython's TypeError: object of type 'int' has no len() rather than being measured as if it were a
container (ADR 0246). An equality asks the slot the same question the printer asks it: both sides of
==/!= are (payload, tag) pairs and the one equality the container comparisons already use answers,
so xs[1] == "a", xs[0] == xs[1], xs[i] == "a" over a position the program computes, and
d["k"] == [1, 2] of a dict filled by assignment all answer CPython's answer instead of being refused,
interned text stops equaling the number it is indexed by, and two float slots holding 1.5 finally
compare equal rather than by box handle (ADR 0247, closing Gap R.79). An ordering reads the value
too: <, <=, >, >= between two texts compares the bytes behind their interned indices, so
print(1 if "b" > "a" else 0) is 1 compiled, interpreted and run under CPython — the index records
which spelling the program mentioned first, and reading it as an ordering was the answer
(ADR 0248, closing Gap R.84; equality stays an index comparison, because interning is
content-addressed). A slot used as a number asks the object the same question, index included: a read
through a position the program computes arrives at the arithmetic as a (payload, tag) pair, and the tag
decides whether to unbox a float, convert an int or bool, or raise the TypeError CPython raises for that
operator and that kind — so xs = [1.5, "a"] / i = 0 / print(xs[i] + 1) is 2.5 compiled where it used
to be refused, print(xs[i] / 4) over [10, 4] is 2.5 where it used to reach llc as fdiv double , %t1
and exit 2, print(-xs[i]) carries negation's own sentence, and a text slot says
can only concatenate str (not "int") to str at run time like the oracle does (ADR 0249, closing
Gap R.88). An ordering between slots asks the same object which pair it was: two numbers are compared as
numbers, two texts by their characters, and a number against a text raises
'>' not supported between instances of 'int' and 'str' with the left operand's type named first, so
xs = [1, "a"] / print(1 if xs[1] > "a" else 0) prints 0 and print(1 if xs[i] > "z" else 0)
raises, compiled as interpreted (ADR 0250, closing Gap R.82). The arms nobody can reach are not emitted,
because a merge that names an unreachable predecessor is a module llc rejects — exit 2, the compiler's
own bug — and a pair whose types the tags cannot name is refused in words instead of answered. One level
below, the same object is asked twice: xs[0][0] reads the outer slot's tag to learn what kind of object
its payload names and then reads that object — a position with the bounds check and the IndexError an
explicit subscript raises, a key with its KeyError, a character of a text, a member of a set — so
xs = [] / xs.append([7, 8]) / print(xs[0][0]) prints 7, d["a"] = [1, 2] / print(d["a"][1])
prints 2, xs.append("abc") / print(xs[2][1]) prints b, and a slot that holds 5 raises
TypeError: 'int' object is not subscriptable instead of being refused at compile time. Nothing behind it
needs a literal, and the answer is a (payload, tag) pair again, so a binding, an equality, a len and a
further subscript all take it (probe_nested_list.gy is parity surface now, and a comprehension-built
d[1]["k"] came with it) — roadmap L11.1, ADR 0251. An ordering of those slots asks the object the same
question (ADR 0252, closing Gap R.93): xs = [] + xs.append(i) in a loop + print(1 if xs[0] > "a" else 0)
used to print 1 compiled for a program CPython crashes, and now raises '>' not supported between instances
of 'int' and 'str' on both legs — as do the dict the program filled (d["k"] = 5 / d["k"] >= 5) and the
slot one level below (xs.append([3, "a"]) / xs[0][0] > 1). Two numbers become doubles, two texts go to
strcmp, and every other pair raises CPython's sentence with the kind the slot really holds in it: the raise
is one test per tag, because the sentence names both operand types, and it may end in an else only because
the tags ADR 0187's writers can store are a closed set. One arithmetic operator can ask the same question,
because it is the one whose result kind is settled before the slot is asked: true division (ADR 0253,
closing Gap R.96). xs = [] / xs.append(3) / print(xs[0] / 4) printed 0.0 with exit 0 — ADR 0249's
empty-operand fdiv substituted into silence — and now prints 0.75 on both legs, because / is a float
whatever arrives: a float slot unboxes, an int or bool slot converts, and a text, None, a list, a dict or
a set raises CPython's own unsupported operand type(s) for / sentence naming the kind it really holds. The
zero trap is emitted inside each arm rather than after the merge, because which wording the pair earns is
itself a run-time question — 3 / 0 is division by zero, 1.5 / 0 is float division by zero — and a
program's except ZeroDivisionError: reads that sentence. The rest of arithmetic follows the same road
(ADR 0265, roadmap L11.1's last clause): xs = [] / xs.append([7, 8]) / print(xs[0][0] + 1) is 8 and
xs.append([7.5, 8]) / print(xs[0][0] * 2) is 15.0 compiled, where the answer's kind used to be the
reason the module could not be written at all — the pair goes to the target, which does the sum in a double
and answers a pair back, so the tag that decides 8 from 8.0 is the one the object carries. + and *
take that road only under a program-wide proof that the slots hold numbers, because CPython answers
"a" + "b" and [1] * 2 and this backend builds neither from a slot (Gap R.82); where the proof is
absent the old refusal stands, and the positions the door does not reach are filed (Gaps R.139, R.143, R.144).
The answer is a value, so it survives being named before it is read: n = xs[0][0] * 2 / print(n) is 14
compiled too, and the rebinding n = [1, 2] behind it prints [1, 2], because a binding that is not a pair
retires the tag the arithmetic left behind (Gap R.142, ADR 0267) — and a binding retires every other
status the same way now (Gap R.145, ADR 0270): x = "text" then x = [1, 2] used to print text
compiled at exit 0, because the interned-text record of the first binding was still where the print dispatch
reads it, and the compiled leg still refused x * 2 and raised bad operand type for unary -: 'str' for a
program whose answer is -5. A name answers with what its latest binding gave it, on both legs. Every position that asks that name for a
number now asks the pair: n + 1, -n, n > 13, if n:, a while head, f"{n}", str(n), n += 1
(ADR 0268) — a name the arithmetic door bound is provably a whole number or a float, so the position lifts it
into the one word that holds both families rather than guessing. What still refuses is a position that keeps
one word for a whole value — a builtin's argument the fold door does not reach, a pair handed across a
call (Gap R.146) — while the list
element (ADR 0306), the f-string field (ADR 0307), the signless call (ADR 0309), the dict entry and set
member (ADR 0310), the mutation roads — xs.append(n), s.add(n), xs[i] = n, d[k] = n (ADR 0311) —
and a fold's argument (with n the 14 above, print(min(n, 3)) is 3, print(max([n, 2.5])) is 14,
print(sum([n, 1])) is 15, and m = min(n, 3) / print(m) prints the winner by the winner's own kind,
because the fold is handed two (payload, tag) pairs and writes back the winning pair — ADR 0316), and since ADR 0318 the fold that gets an
element-wise ordering rather than a handle: over two containers the program built, min(a, b) is [1, 2] and
max(a, b) is [3] (a list compares element by element, the shorter list the lesser when the shared elements are
equal), a set compares by the subset operator and the pair that is neither one’s subset orders neither way
without raising, a dict has no ordering against anything including a copy of itself, and a container folded against a
number raises CPython’s TypeError naming the two kinds that failed instead of answering the container’s element
count (roadmap Gap R.197)
all take the pair now, and so does the float road (n / 4 is 1.75, ADR 0305). An and's operand is not one of those
positions any more: the operator that chooses an operand can ask the pair directly, so print(n and 3) prints
CPython's 3 and print(n or 3) its 14, on all three legs (Gap R.147, ADR 0269).
What still refuses by naming itself: a numeric use whose result kind is only knowable while
the program runs (xs = [1, 2.5], ints here and floats there — answering it would print 2.0 for 2), a
slot used as a number on a container this pass cannot see (xs.append(1.5), or a container handed to a
function), the numeric, membership and loop uses of a slot only the run time can describe
(xs[0][0] + 1, -xs[0][0], 7 in xs[0], for v in xs[0] after xs.append([7, 8])) — the first two because
a use whose kind only the object knows has no untagged lowering, the last two because they need the object's
kind where the read asks only its tag. The numeric shapes that still refuse are the operators whose answer
is a fact about the slot rather than about the operator (xs[0] + 1 or xs[0] ** 2 on a container that may be
holding a float; an all-int slot answers both today), and the division of two such slots at once
(xs[0] / ys[0], filed as Gap R.101 beside the ordering's Gap R.97). Two more come out of the sweep that
opened the division: a float element of a comprehension over a container the program built is appended by the
static path ([xs[0] / 2] prints [2], Gap R.99), and when such an element raises, the guard's blocks move
the comprehension's loop back edge and llc rejects the module (Gap R.100). So does the door's double handed
to a context that stores an i32 word — a call argument, str()'s argument, a dict slot written by key, +=
onto a variable that started life an int (Gap R.98; printing, a comparison, a condition, a float binding and
a container element are all in the double domain and take it) — and an ordering of two such slots against
each other (xs[0] > ys[0], filed as
Gap R.97: one side whose kind comes from the object is a chain, two is a table the compiler would be
inventing), and a comparison against an expression whose kind cannot be proven — which until ADR 0247 answered
1 where CPython answers 0 (Gap R.83, whose ordering side is measured by the same table).
Unary minus is an operator, so it asks the same question (ADR 0266, closing Gap R.89 and Gap R.137).
print(-"hi") printed -281474976710658 interpreted — the negation of the text's interned index, which is
why the digits looked like an address — and 0 compiled, both at exit 0; print(-[1, 2]) reached llc as
%t1 = sub i32 0, @.lst1, spending the contract's exit 2 on a program CPython merely stops on. Both legs
now stop too, with the reference's own TypeError: bad operand type for unary -: 'str' at exit 3 and
reachable by except TypeError:: a text, None, a list/dict/set/tuple literal, a container variable, an
instance (which names its own class), a character read out of a text, and a slot the literal says holds no
number. the record asks the value (operandKind, the table the binary operators and len already
read); the compiled backend asks the expression, and for the text question it asks the printer's own
predicate, so print(x) and print(-x) cannot disagree about what x holds (ADR 0229's rule, one operator
further out). The constant folders were closed on the shape — a folded -None is a silent 0 with no
instruction that could have disagreed (Gap R.37's rule, at its last operator) — and a slot is raised only
where the literal says nothing it holds is a number and the program never stores anything else into it,
because a raise where CPython answers -1 would be the same bug wearing a class. the record still
calls a tuple 'list', which is pinned with its own row (Gap R.141) rather than hidden in this one.
A slot read reaches a function, in both directions (ADR 0273, closing Gap R.139).
def twice(v): return v * 2 with xs = [] / xs.append([7, 8]) / print(twice(xs[0][0])) is CPython's
14 and the interpreted leg's 14; the compiled leg spent exit 1 on index cannot reach into xs's slots, because the value has two words and the parameter had one — and the code that consumes it lives in
the callee, which has never seen xs. The argument now arrives as payload and tag (a second i32 on
the define, bound through the door every tagged value uses) and the answer's kind comes back in a word the
callee stores beside its own return, read by the caller immediately after the call — so the same file
prints a float slot's 3.0, the keyword form's 16, an argument that is itself arithmetic's 16, an answer
bound to a name's 14, and None for a callee that falls off its end. Three return roads store the tag, and
the two that give no value — fall-off-the-end and the raise — store None, because a stale kind read by the
next caller is a number-shaped lie; except TypeError: in the caller reaches a trap the callee's operator
threw. Which parameters carry the pair is decided by one pure scan of the AST, before any IR exists, so the
define and every call agree about the arity (exit 2 was the first version's answer to a def written
below its call), and a parameter is opened only where every call site can supply a pair from its own spelling
and the body reads it back somewhere a pair door answers — the rule the function_calls benchmark taught the
day this landed, when (a * 31 + b * 17) % 100003 was handed a tag it had nothing to read. Refusals moved
none of their words: twice(twice(xs[0][0])), twice(xs[0][0]) + 1, [twice(xs[0][0])] and
show(twice(xs[0][0])) still exit 1 naming the one-word position (Gap R.146).
A number handed to a function keeps the kind its argument had (ADR 0276, closing Gap P.1's remaining
half, Gap R.154, Gap R.157, Gap R.158).
def twice(v): return v * 2 / print(twice(2.5)) is CPython's 5.0 and the interpreted leg's 5.0; the
compiled leg printed 4, and print(twice(1.5)) printed 2, print(area(2.5, 2)) printed 4, a
greet(name, times=1.5) called without times printed 1 — every one at exit 0, because the parameter had
one word and the argument had two. One double convention cannot express the function: CPython's answer kind
follows the argument, so twice(2) must stay 4 while twice(2.5) is 5.0. The pair crosses now, and the
scan asks the question ADR 0273 never asked — not would the ordinary road refuse this argument but would it
refuse it, or answer a double it cannot carry — so a float literal, a variable the program rebound to a double,
a quotient, a slot of a literal list holding one, and a default no call site mentions all take the two words,
while the parameters every call site hands integers are named (knownIntParams) so w * h has a kind for both
operands. The answer direction came with it: def g(y): return y * 2 under
def f(x): x = x + 1.5; return g(x) used to print 2 for CPython's 5.0 and now hands the callee's pair on,
lifted by @rt_lift_num where the caller's own word is a double. Two rules keep the ladder honest, both of
them scan answers rather than emission-order answers: a body written under ADR 0274's double return or
ADR 0174's string index keeps its parameter words (define double @gy_f(double %p0) stays exactly that spelling,
and def fmt(v): return str(v) keeps its refusal instead of printing an interned index), and a condition is
served where a value is not — if v > 10: asks an operand's truth and takes the pair, return v > 1.5 asks
a road that has one word and keeps the True/False it always printed. What still owes an answer is filed
rather than guessed: a pair-call answer bound to a name inside a forwarding frame prints 4 (Gap R.164),
and def g(): return str(42) prints the interned index (Gap R.163,
pre-existing, found by this cycle's whole-corpus sweep).
A number handed on by a body keeps the kind its caller's argument had (ADR 0277, closing Gap R.161).
def twice(v): return v * 2 / def outer(x): return twice(x) / print(outer(2.5)) is CPython's 5.0 and
the interpreted leg's 5.0; the compiled leg printed 4, at exit 0, and print(shift_it(2.5)) over
def shift_it(z): return add(z, 1) printed 3 for 3.5. Nothing inside outer says x can be a float: a
parameter is written by the caller and never by the body, so no assignment records a binding for it and the
only evidence is the argument outer's own call site was written with. The scan now brackets a def's body,
which is how a call site learns whose parameter it hands over, and marks propagate along those edges in both
directions — the caller holds a pair, so the callee position must carry it; another call site proved the
callee's position, so every caller must supply the tag — in bounded rounds, growing only. A mark that rests on
an edge is closed when the edge is: close the callee (it takes a power of the value, renders it a text, indexes
with it)
and the caller loses its tag word too, because a pair half-read one frame down is the wrong number this file
exists to remove. The answer direction moved to the same scan, and the tag word is declared where the callee's
define is emitted: def f(v): return other(v) now compiles identically written above or below
def other(w): return w * 2, where before it refused for a reason that had nothing to do with the program.
The tag definition and the flag that records "this body carries a tag" cannot share a guard, because the read
can precede the write. programs/probe_forward_a_pair_through_a_function.gy is the closing event — eight
lines, three legs, one answer — and integration/forwarded_pair_test.go pins the chain that must not take
the pair by its exit class rather than by a number.
What floors a number, and what remains, ask the tag the number arrived with (ADR 0278, closing
Gap R.162). def floorit(v): return v // 2 / print(floorit(5.0)) is CPython's 2.0 and the interpreted
leg's 2.0; the compiled leg printed 2, at exit 0 — and print(modit(-7.5)) printed 1 for 0.5,
print(modop(7.5, 2)) printed 1 for 1.5, print(outer(5.0)) over def outer(x): return floorit(x) printed
2 for 2.0. + - * were served over the pair; // and % were in no served list at all, so the parameter
never took the tag and the double was truncated before the floor ran. They now go through the same tagged door,
as operator codes 4 and 5 of @rt_num_arith, with ADR 0216's rules kept in one helper the statement road
shares — @llvm.floor.f64 for the floor (down, not toward zero) and frem corrected to the divisor's sign for
the remainder, so floor(x/y)*y + (x%y) == x holds of the doubles. The tag decides which of the reference's
four divide-by-zero sentences a line gets: one def f(v): return v % 0 says "integer modulo by zero" for
f(5) and "float modulo" for f(5.0), and // has its own pair of them — the first draft of the table shared
the floor's wording with the remainder, which is the drift a whole-corpus sweep exists to catch.
The combination landed the same day, one ADR later: see ADR 0279 below, and ADR 0280 below that, which pays
Gap R.164. What the pair door still refuses is a position that keeps one word — a pair answer stored in a
container's element or handed to a call (Gap R.146) and a pair answer read back by an augmented assignment
(Gap R.143) — and the reference's text-%, print("%.2f" % 3.5) printing 0.0 for CPython's 3.50
(Gap R.165): refusals and one measured wrong number, filed instead of answered by payload.
The arms of a pair expression are asked the same question as the whole (ADR 0279, closing Gap R.166).
def identity(v): return (v // 2) * 2 + (v % 2) / print(identity(7.5)) is CPython's 7.5 and the interpreted
leg's; the compiled leg exited 1 on it, as it did on v * 2 + 1 (since ADR 0276), v + 1 + 1, 2 * (v + 1)
and (x + 1) * 2 over a name that had become a double. Three of the four questions the pair door asks of an
operand walk it recursively — may this be a text, would the ordinary road have refused this, is this a shape the
door serves. The fourth, "is this a slot the program is shown to hold only numbers?", looked at the node it was
handed: a bare v passed, (v - 1) was not a name and not a chain, so the arm was declined, the expression fell
back to the road with one word for a parameter, and that road refused. Walking the leaves is the whole fix.
What was rejected is the version that prints something: valuing an arm by its payload with tag = 0 makes
identity(7.5) print 7 — a number, at exit 0, and the wrong one — which is why ADR 0278 pinned these shapes
as refusals first, and the pinned refusals are what failed when the honest question was finally asked.
programs/probe_combine_a_floored_pair.gy is the closing event: twelve lines, three legs, one answer.
A name holding a pair answer is read as a pair wherever it is read (ADR 0280, closing Gap R.164).
def outer(x): y = twice(x); return y / print(outer(2.5)) is CPython's 5.0 and the interpreted leg's; the
compiled leg printed 4 at exit 0, and the same was true of return y + 0, of h = floorit(x); return h
printing 2 for 2.0, and of that body three frames deep. Nothing about the call changed — return twice(x)
already answered (ADR 0277) and print(y) already answered (ADR 0273) — the pair simply was not bound when the
answer went through a name. Three questions had to be asked of the name instead of the expression: does this
body hold a pair at all (pairBoundCallNames, which retires a name the moment the program writes anything else
to it — y = twice(x); y = 3 still answers 3), is a call's answer a number (exprNumberish had no answer for
a call and said "unknown", which hands the whole function to the float-return road), and can an arm be a call
(it can, through the same door print uses). Two silent wrong numbers surfaced on the way and both are pinned:
half(7.5) + twice(7.5) printed 30.0 for 18.0 in one arm order only, because a double answer leaves the
arithmetic door as a heap box that no root held and the next allocation recycled its slot; the same cause one
door deeper printed 3.0 for floorit(v) * 2 + modit(v). What the cycle could not answer it refuses instead of
truncating — z = twice(y) and y += 1 are exit 1 naming the parameter and the missing kind, where both had
printed 8 and 5 — and the two rows are pinned as refusals. programs/probe_bind_a_pair_call_answer.gy is the
closing event: eleven lines, three legs, one answer.
An int that meets /= becomes a float (ADR 0274, closing Gap P.1's /= half and Gap R.155).
x = 7 / x /= 2 / print(x) is CPython's 3.5 and the interpreted leg's 3.5; the compiled leg printed
3, and x = 1 / x /= 3 printed 0 where the reference prints 0.3333333333333333 — both at exit 0,
because the operator asked the integer road. The operator now names the domain, the way / always did, and
where that leaves a double standing in a slot the variable's first binding made for an i32 the rebinding
takes the statement: the double goes into a float box, the name is bound to the (payload, tag) pair with
the float's tag, and every later read asks the tag — the same pair print, a number position, an ordering, a
condition, str and a while head already ask (ADR 0166, ADR 0265, ADR 0267), so no read road needed a new
case and a variable that never changes state still emits the plain slot and the plain load it always had.
The corruption the row was hiding is the reason a wider store was not the answer: y = 12345 / x = 8 /
x = 2.5 / print(x, y) printed 1074003968 for y, a store double reaching four bytes past the
allocation into its neighbour, and the module verified because an opaque pointer hides the width of what a
ptr points at. Measured, not predicted: with the variable's GC root missing, h = 1 / h /= 3 answered
print(h + 1) correctly and print(h * 2) with the first print's answer — the collector had recycled the
box under a slot it was told held no handle. What still exits 1 naming the position that keeps one word is
the state travelling further: returned from a function (Gap R.156), stored as a container element
(Gap R.159), ordered against a float literal (Gap R.160). The two rows this line used to carry — the
t += i / 2 accumulator (Gap R.157) and a float-state name handed to a call (Gap R.158) — answer 6.5
and 5.0 now, and Gap P.1's other half, an untyped parameter that receives a double, closed with ADR 0276
above.
A data module's constant keeps the type the module declares (ADR 0272, closing L11.6's typed stdlib
constants). import math / print(math.PI) printed 3 compiled at exit 0 — the census table's own row —
while the record beside it printed 3.141592653589793 and a hand-written pi = 3.141592653589793 was
right on both legs. The value behind mod.NAME is a literal the importing program never wrote, and only
value() knew it: every predicate that decides how to write the value saw an attribute with no kind and
took the integer word, so math.PI * 2 was 6, -math.PI was -3, and x = math.PI / print(x > 3.14)
was False. One read (pkg/lang/module_const.go) now answers the kind questions the print formatter, the
arithmetic, the binding, the double road and ADR 0271's signless door ask — and the same read that turned
print(abs(NAME)) on a module text from 0 into CPython's sentence is what proves it is a kind rule and not
a stdlib patch. Half-fixing it is a trap the tests now hold: formatter-only printed 3.0, because the value
still came from sitofp of the truncated word. What the probe found and this did not fix is filed: a float
written into a container slot the program recorded as ints (Gap R.152), and a verdict the module declares
printing 1 on both legs (Gap R.153).
abs is the other operator that needs the answer (ADR 0271, closing Gap R.140). CPython stops on the
absolute value of anything without a sign — TypeError: bad operand type for abs(): 'str' — and this
compiler answered every one of them: print(abs("hi")) printed hi (the text reached the print door holding
the interned index it is stored as), abs(None) and an instance printed the number 0, and abs([1]),
abs({"a": 1}), abs({1}) each handed llc a sub i32 0, <heap global> and came back exit 2. It is
paid through the door above rather than a new one — one predicate names the operand for -x and for abs(x),
so the two sentences can never disagree about what the operand holds — with the wording coming from the one
sentence table, abs spelled as its own operation because CPython names the call there and the operator
under the minus. Both lowering roads ask it: the double road builds its operand by lifting to a float, which
yields nothing for a text, and had been printing 0.0 for print(abs("hi") * 2.5) at exit 0. What a name
asks is its latest binding, which is why this row needed Gap R.145 (ADR 0270) fixed in the same commit —
x = "text" / x = [1, 2] printed text, and x = "text" / x = 5 / abs(x) raised on a program whose
answer is 5. The three-engine program is programs/abs_names_its_kind.gy; what the sweep around it found
broken is filed, not folded in: a builtin or an imported module used as a value is exit 2 compiled and
NameError interpreted (Gap R.150), and -(lambda x: x) reaches sub i32 0, lambda_0 — also exit 2
(Gap R.151).
A function's return word is now read from what its body does rather than from the shape of its return
line (ADR 0254, closing Gap R.3c). def addf(x): x = x + 1.5 / return x / print(addf(1.0)) printed the
argument — 1, with the exit code of success — because the answer's type came from return x, which says
nothing about a kind, while the body had already stored a double into the parameter's slot. The gate asks the
same question the emitted instructions ask (isFloat, over the body with the rebound names read as the doubles
they became), so the ret and the value it writes cannot disagree, and the family answers: the bare name,
return x * 2, x % 3, abs(x), abs(-x), return -x — which had been an llc rejection, ret i32 under a
define double, ever since the body learned to negate a double — the local spelling y = x + 0.5; return y,
and any of them inside if, while, try, recursion or a default. What is not that double keeps its own word
(int(x), round(x), x > 2, a user callee), and where the one convention would have to hand a container
handle or an interned text the double word — a parameter the body also reads, or any method — the program is
refused in words, naming the variable whose double has nowhere to go. Three shapes came out of that sweep and
are filed rather than absorbed: the ternary arm (return x if x > 2 else 0.0, refused since ADR 0254 and a
truncated 1 before it, Gap R.102), a container returned from a function (print(f(1.0)) printing 0 for
{'k': 2.5} — filed as Gap R.103 and re-measured as Gap R.67's, since the same dict written with no function around it prints
{'k': 2.5} on both legs), and min/max with two arguments refusing on the record (Gap R.104,
closed two cycles later by ADR 0256 — which found the compiled leg had its own half of the same question).
The element of a comprehension over a container the program built is free to branch, and the loop had to
be told (ADR 0255, closing Gap R.100). xs = [] / xs.append(6) / print([v / 2 for v in xs]) was exit
2 — llc rejecting PHI node entries do not match predecessors — because the zero guard / carries
(ADR 0253) ends the body in a block the induction phi had never heard of, while the phi went on naming
the block the body starts in. The increment now lives in a latch block that every path the element can end
on branches to, the entry list says where the back edge really comes from, and the program prints [3.0] on
both legs: [v / 0 for v in xs] dies with division by zero (exit 3, behind a filter or in front of
one), {v / 2 for v in xs} prints {3.0}, and a for whose body divides came with it. Two shapes from the
same sweep are filed rather than absorbed, both silently-wrong answers with exit 0: the dict comprehension's
value loses the double's tag ({6: 3} for {6: 3.0}, Gap R.105), and a loop variable whose slot holds text
is divided by the static arm ([0.0] where every honest engine raises TypeError, Gap R.106).
A fold returns the candidate it chose, not the comparison that found it (ADR 0256, closing Gap R.104 and
the source-visible half of Gap R.73). print(min(1.0, 2), max(1, 2.5)) printed 1.0 2.5 compiled and refused
outright interpreted; print(min(2.5, 1)) printed 1.0, because the compiled path promoted every candidate to
double and selected a double — while Python hands back the winning element, so the answer's kind is the
winner's own. min/max now take values side by side or one container on both legs, and the family
answers: ints (1 5), an int among doubles (min(2.5, 1) → 1, max(1, 2.5) → 2.5), three candidates, a
container written inline, and text ordered by its content through rt_str_order rather than by its
position in the intern table (ADR 0248's rule, met again where an interned text is also an i32) — so
t = min("pear", "apple") still has a working .upper(). Candidates with no ordering for the operator the
builtin asks (min asks <, max asks >) are raised, not refused and not answered: the old compiled
path compared heap and intern indices and exited 0 with a number, where both legs now print CPython's
TypeError: '<' not supported between instances of 'str' and 'int', catchable by except TypeError, at exit 3
— with the two kinds in the order the fold met them, because that is the order CPython's operands had. Four
shapes stay honest about the half they are missing and are filed rather than absorbed: a container the program
built (Gap R.107), a text container the program built (Gap R.108), two runtime candidates whose kinds straddle
int and double — comparable, but the winner's kind cannot leave the call until the tagged value word exists
(Gap R.109) — and a callee whose winner was settled by one call site (def choose(a, b): return max(a, b) /
print(choose(2.0, 1)) printing 2 for 2.0, Gap R.110).
The candidate the comparison chose decides what the answer is — and it is asked once (ADR 0261,
closing Gap R.117). print(max([True, 0])) printed 1 compiled where CPython and the record print
True; print(max(True, 0)) printed 1 on both engines; print(max([True, 1.5])) printed 1, which
was not a rendering bug at all but the double underneath being truncated. One rule now covers all three:
a verdict is a candidate like any other number (1 or 0 — ADR 0259's numeric family applied where it had
not been), one strict comparison picks the winner, and the renderer asks that candidate what it is.
So max([True, 0]) prints True through print, str(), an f-string and a container slot
([True]), while the number stays underneath (max([True, 0]) + 1 → 2, max([True, 1.5]) → 1.5).
The tie rows are what no element-level rule can produce, and both spellings are pinned:
max([True, 1]) is True and max([1, True]) is 1, because the comparison is strict and the first
candidate stays — the same reason min(1, 1.0) is 1. The fold's TypeError names the candidate's kind
too: max(["a", True]) raises '>' not supported between instances of 'bool' and 'str' on both legs.
A ternary takes the same rule where the test is a value the source wrote (print(False if 1 else 2) →
False). Two shapes are filed instead of guessed: a candidate the compiler cannot read — i = 0 /
max([True, i]), whose select keeps the payload with nothing beside it to name its kind (Gap R.124) —
and a ternary whose test it cannot read, where the record agrees with the compiled backend and
neither agrees with CPython (Gap R.125).
A verdict is a value, and it prints its own name (ADR 0257, L11.1's bool step). print(True) is True,
print(1 == 1) is True, print(0 == None) is False, str(True) is 'True' — an ordinary string with a
working .upper() — and an f-string writes flag: True rather than flag: 1. None of that changed what a
bool is: it is still the untagged 0/1 in the word it always occupied, still adds (True + 1 → 2),
multiplies, negates (-True → -1) and sums (sum([True, True, False]) → 2), and a condition still tests
it the way it always did. What changed is who answers "what is this?": the printer asks the expression,
through one predicate the compiled backend share, so and/or of two verdicts are verdicts while 1 and 2 is still
2 (Python yields the operand), a ternary is a verdict only when both arms are — unless its test is a value
the source wrote, in which case the arm that runs decides (ADR 0261), all/any and a call whose
every return is a verdict (ADR 0254's rule, read from the body) are verdicts, and a name is a verdict only
until something that is not an expression rebinds it — for flag in [1, 2] prints 1 and 2, because a loop
binds elements. A comparison that reached a class's own __lt__ is not a verdict at all: the program's method
returned an int, and print(a < 4) prints 1 on both legs. Two shapes stay where a tag has to travel
rather than be read off an expression. A bool in a container used to be one of them — print([True, 1])
printed [1, 1] on the compiled backend, because the element tag vocabulary had no bool to read back — and is
paid: the slot now carries a bool tag, the container printer, the set dedup and the ordering sentence all
read bool, and [True, 1], {'k': True} and {True} come out as CPython writes them while every
numeric question about the same slot still answers as its number (ADR 0259, closing Gap R.112). What stays
filed is the shape no tag can reach from the caller's side: a bool handed to a function prints the number it
is stored as (Gap R.111), and the comprehension and fold shapes that sweep measured are filed with
their per-engine answers — Gaps R.116, R.117 and R.119, since R.118 turned out to be a dict story and was
paid by ADR 0260 the same day. --json names the
type bool, and --eval '1 == 1' echoes True.
and and or hand back the operand the test chose (ADR 0269, closing Gap R.147). They are the two
operators that are not operators: print(2 and 3) is 3, print(0 or 5) is 5, print("" or "d") is the
text d, print([1] and [2]) is [2], and the pair that proves the rule is print(True or 1) → True
beside print(1 or True) → 1. Both engines printed 1 for all four before ADR 0269 — exit 0, digits wrong, no
refusal, on an operator every Python program uses — because the lowering composed two predicates and
zero-extended the verdict, and the truthiness table asked whether the test passed rather than what the
expression is. A condition still wants only a verdict, and truth(a and b) is truth(a) and truth(b),
so if x or "d": branches on operands that could never share a word; a value position needs one, and the
compiled backend merges it from the two arms — in the i32 word, in the double word, or as the
(payload, tag) pair the module's one tag-reading printer takes, which is what lets print(x or "d")
print a word instead of the interned index underneath it. A shape none of those roads can state names both
operands and the missing tag at the capability exit instead of answering 1, and the checker types the
expression by its operand, not by a bool it never returns. The operators also short-circuit, on both
engines (ADR 0275, closing Gap R.149): the test decides whether the other operand is in the program at
all, so x and boom() with x false is silent, x and (1 // 0) answers 0 without trapping, and the operand
the test does reach runs exactly once — which the compiled leg had been getting wrong twice over, once by
running the excluded operand and once by evaluating the tested one twice, because a select between two
operands evaluates both and the truth and the value were each lowered on their own.
A ternary with text arms prints the text, not the intern table's position (ADR 0290, closing Gap R.173
and paying Gap R.127's text half). print("y" if 1 else "n") exited 0 with 0, and
print("big" if x > 2 else "small") with 0 or 1 depending on which way the test went — those digits are
the slots y, big and small hold in @str_tab, and they were printed because four different
"what kind is this expression?" predicates in the compiled backend (stringVal, exprIsString,
printsAsInternedStr, methodReturnsStr) had no ternary arm. ADR 0262 had already established the rule — a
ternary hands back one of its arms, so the answer's kind is the arm's kind — but applied it only to numbers.
Fixing three of the four changed nothing: print folds through stringVal first. That is the "one road" rule
with the quantifier made explicit. The run-time-test pair is now a real select over interned indices (legal
because interning is idempotent), while a constant test still evaluates only the arm that runs. A container
arm still refuses — that half belongs to L11.1's tagged value word, and the test says so rather than claiming
it.
A list and a dict have the methods their reference's containers have (ADR 0301, closing Gap R.188
and advancing Gap R.63 / L12.11). xs.extend([2, 3]), xs.insert(0, 9), xs.index(2),
xs.remove(x), xs.clear(), d.update(o), d.pop(k), d.setdefault(k, v) and d.clear() all answered
no such list method / no such dict method on both engines. That is a missing answer rather than
a wrong one — the class no pin can catch, because there was no output to compare — and the exit-1 sentence
blamed the program for a feature the language lacked. d.setdefault(k, []).append(v), which is how
anybody groups rows, now works; index finds by value equality (so [1].index(True) is 0) and raises
ValueError: 5 is not in list rather than answering -1, which is a legal index a program would obey
into reading from the end of the list; insert clamps rather than validating. d.popitem() exists and
refuses, naming why: it answers a pair, and there is no tuple value until L11.3 — answering a list
would print [1, 2] where the reference prints (1, 2). Along the way a KeyError learned to name the
key (KeyError: 'a') on the leg that can: the compiled leg's raise is a module constant, and that
half is filed as Gap R.189 PARTIAL rather than left looking closed.
A method that changes a container in place answers the void (ADR 0300, closing Gap R.187).
print(xs.append(2)) printed [1, 2] on the record at exit 0 and made the compiled leg emit
printf(i8* @.fmt1, i32 ) — a call with a missing operand, which llc rejects — so the same one-line
program spent exit 2, the forbidden class. add, discard, remove and clear were the same, and
sum([1, 2, 3].append(4)) answered 10 where the reference raises TypeError: 'NoneType' object is not iterable. The mutation was never wrong; only the answer the statement throws away was, which is exactly
why nobody hit it — a program writes xs.append(2), not print(xs.append(2)). The fix is one table both
roads read, keyed on the shape of the call: the record's return recv, nil ("so the REPL can show
the resulting list") went, and the print door asks isNoneExpr, which already knew a folded dict.get
miss was a void (ADR 0291). pop and popitem still answer with what they removed — while xs: x = xs.pop() is why — and your own method named append keeps its own answer.
An f-string's format spec is part of the program (ADR 0299, advancing L12.8 / Gap R.60).
stripFormatSpec used to cut the : suffix off and drop it on the floor — no AST field ever held it —
so f"{3.5:.2f}" printed 3.5, f"{7:05d}" printed 7, f"{255:x}" printed 255 and f"{3.5:>6}"
padded nothing, on both engines, at exit 0. Eleven shapes, engines in perfect agreement, which is
precisely why nothing noticed: parity compares the engines to each other, and only the oracle leg compares
either to CPython. The spec and the !r/!s conversion now live in the AST and both legs format
through one shared engine, whose digits never come from printf — the two diverge on exactly the cases
that matter (%.0f rounding, %g's exponent rules, %d on what the language calls a float). A spec
this language cannot honour refuses with a sentence naming it; there is no "print the plain value"
fallback, because that is the bug with a parser bolted on. f"{[1,2]}" used to hand a list's global
address to printf's %d and die in llc — exit 2, the forbidden class — and refuses now.
A text iterates one character at a time (ADR 0298, closing Gap R.185). [c for c in "abc"] answered
[] on the record at exit 0 — not a refusal, not a trap, just an empty list a program would happily
iterate and never enter — and max("abc") answered abc, the whole string, because a text fell through to
the arm that treats anything unrecognised as a one-element collection. for c in "abc" had always been right
beside them: three roads asked "iterate this text" and read two different stores. All three now range over
the text per code point, so max("aé") is é. The compiled leg still refuses at exit 1 with a sentence
naming what it cannot lower — owed to L11.1 — and was deliberately not brought into agreement by taking
the record's new answer away.
A text answers truth like any other value, and a string method answers with its own kind (ADR 0297,
closing Gap R.183 and Gap R.184). print(not "x") said True on the compiled leg while if "x": —
asking the same question about the same value two lines away — said truthy: not used a helper that compares
the lowered word against zero, which is right for a number and, for a text, reads the intern slot. Nine text-
returning methods printed the intern INDEX (0 for "ab".zfill(5)) because the print road knew three
methods where the fold implements fourteen, in a list that existed twice. And "-42".zfill(5) answered
00-42 on both engines — the reference says -0042 — so the parity matrix reported nothing and only the
oracle leg saw it; the rule now lives in one function the compiled backend call.
A dict view prints as a view (ADR 0296, closing Gap R.182). print({"a": 1}.keys()) printed 0 on
the compiled leg — the heap handle through %d — and print({1: 2}.values()) handed the address of a
compile-time global to a heap walker, which is exit 2, the compiler's own bug on a two-line program. Both
engines also printed a bare ['a'] where the reference prints dict_keys(['a']), so the compiled backend agreed
and only the oracle leg could see the third defect. A view is now a list-shaped value that carries its own
name: sum, max, min, for and in keep working on it, and only the rendering knows it is not a list.
items() still refuses on the compiled leg, because a key/value pair has no value representation until
L11.3 — refusing is honest where a plausible-looking [(…)] would be fabricated.
A set counts distinct members, and a dict.get answers with the kind its slot has (ADR 0295,
closing Gap R.180 and Gap R.181). Both were wrong numbers at exit 0 that no amount of correct-looking
test data would catch: len({1, 2}) answers 2 on any implementation, so only a duplicate reveals that the
static set global reserved one slot per source element and len({1, 1, 1}) said 3. Same for get — an
int-valued dict always looked fine, while {1: "a"}.get(1) printed the text's interned INDEX (0),
{"k": None}.get("k") printed the void word (0) and {1: True}.get(1) printed 1. One of them
({1: True}.get(9, True)) was wrong on the interpreter too, so the compiled backend agreed on the wrong
answer and only the oracle leg saw it. The float case ({1: 1.5}.get(1) → 1) stays open as Gap R.105
and is pinned by a test that reports it — not by a refusal, because the record answers it and taking
that away would be trading an answer for silence.
A slice of a container answers the list, not the machine's handle for it (ADR 0294, closing Gap R.179). print([1, 2, 3][1:]) was three bugs at once, none of them on the record: the container
literal lowered to the address of a compile-time global and went into rt_slice where a heap handle
belongs, so llc rejected our own module (exit 2); once that was fixed, print took the numeric road and
printf'd the handle — 1 at exit 0; and rt_slice copied element payloads without their tags, so
print(["a", "b"][1:]) printed the interned text's INDEX — [1] where the reference prints ['b']. The
last one is ADR 0187's pairing rule ("the operation that writes a slot's payload writes its tag") violated by
a runtime builder that had simply never been asked to follow it. Deferring a wrong number because a redesign
will eventually make it impossible is how wrong numbers survive.
x ** y answers the kind the reference answers with (ADR 0293, closing Gap R.176). print(2 ** -1)
said 0 on both legs and print(4 ** 0.5) said 1 — both at exit 0, both legs agreeing, which is
the configuration a parity matrix is blind to. The rule is not "either side has a dot": int ** int is an
int only when the exponent is non-negative, so 2 ** -1 is the float 0.5; a float anywhere makes the
answer a float, so 2.0 ** 10 is 1024.0 and not 1024. The compiled leg's root cause was one missing
token — ** was absent from the operator list that tells print what kind an expression answers with —
so llvm.pow.f64 computed the right double and the caller truncated it back through fptosi into %d.
0 ** -1 now raises the reference's own ZeroDivisionError from a run-time guard (so a base bound to a
name traps exactly as a literal does), and (-8) ** (1/3) and 2 ** 100 are refused in words — a complex
value and an unbounded integer are things this language does not have, and inf, nan or a wrapped 0
are not substitutes for either.
A container in a numeric operand raises or refuses — it never spends the contract's exit 2 (ADR 0292,
closing Gap R.175). print([0] * 3), print([1, 2] + [3]), print([1] / 2), print([1] - [2]),
print({1: 2} * 2), print([] < {}) and ten siblings emitted a module llc-20 rejected —
mul i32 @.lst1, 3, add i32 @.lst1, @.lst2, sitofp i32 @.lst1 to double — because value() renders a
container literal as the address of a compile-time global rather than a value. Exit 2 is reserved for a
bug of ours, so a harness could not distinguish "the compiler is broken" from "my program has a type
error", and --interp answered every one of them correctly. The split follows the reference, not what
this backend lacks: where CPython raises, both legs raise CPython's own sentence (exit 3, catchable, so
print(e) prints what python3 prints); where CPython answers and no sequence helper exists, the compiled
leg declines in words (exit 1) and the record answers. Two things were deliberately left alone after
each was measured breaking an answer: a container as a call argument (half([1.5]) = 0.75) and a
comparison over two names (a < b = True, the tagged order road's question).
A dict lookup with nothing to find hands back None (ADR 0291, closing Gap R.174).
print({"a": 1}.get("z")) answered 0 on the record, so print(v == 0) answered True where the
reference answers False — a program could not even test for the missing key — while the compiled leg
refused the whole program with get: key not found and no default, although its own fold already knew the key
was absent. Both are the same representation Gap R.171 caught leaving a function body: a void written as the
word 0, reached this time from a builtin. The third defect was only visible in the refusal —
d = {"a": 1} / d.keys() was answered with "string method keys on non-constant string", calling a dict a
string in a message quoted back to the author. That road now asks the same records the iteration and subscript
roads read before it speaks, so a diagnostic cannot contradict the codegen beside it.
A text predicate prints a verdict (ADR 0289, closing Gap R.172). print("abc".startswith("ab")) said
1 and print("abc".isdigit()) said 0 on both engines at exit 0. The eight methods — startswith,
endswith, isdigit, isalpha, isalnum, isspace, islower, isupper — had been answering correctly
all along, with the same 0/1 a comparison answers with; only the printer never learned, because a method
call's callee is an attribute (Call{Fn: Attr{…}}) and the "is this a bool?" question gave up on its very
first line for anything that was not a plain name. So the answer was right and its rendering was wrong — the
one thing every check misses: exit 0, a valid module, a plausible digit. Thirteen tests in
integration/lang_test.go had pinned 1/0 as the expected output, with a comment explaining the fold as
though it were the contract; those pins moved to the reference's answer in the same commit, because a pinned
wrong answer is the wrong answer with a green check. The predicate table asks the method's own name, and asks
the receiver first: class Box: def isdigit(self): return 1 prints 1, since what a class's own method
returns is a fact about that class body (ADR 0257's dunder rule).
Comparison chains answer what Python answers (ADR 0288, closing L12.1 / Gap R.53). a < b < c is one
construct — a < b and b < c, with the middle operand evaluated once — and the grammar had been
folding it left-associatively into (a < b) < c, comparing an int against a boolean. Because this front end
answers that question instead of refusing it, both legs printed the opposite verdict at exit 0:
print(1 > 2 < 3) said True where Python says False, print(1 < 2 > 1) said False where Python says
True, and if 1 < 5 < 3: took the branch. print(1 < 2 < 3) said True too — the accidental pass, and the
reason a chain table must contain chains the nested reading gets wrong. The fix is L12.1's own design: a
comparison node carrying n operands and n−1 operators, not a desugaring to a < b and b < c, which would
run a call in the middle twice and let and skip the tail comparisons. The compiled leg gets "evaluated once"
by storing each operand into a local slot and reading the slot — and my first version of that handed the links
the original expressions, printing a middle call's output three times at exit 0: the bug re-imported by its
own fix. A chain over a container operand refuses on the compiled leg rather than emitting
store i32 @.lst1, i32* %_chain1 (ADR 0234's compiler bug); the record answers it, and L11.1's tagged
value word owns the rest.
A builtin called with no argument is asked which kind of call it is, before its operand is reached
(ADR 0287, closing Gap R.131). print(int()) died with a Go stack trace and exit 2 — the code the
exit-code contract reserves for a compiler bug — and so did float(), bool(), str(), ord(), chr(),
abs() and repr(); chr() panicked the compiler too. They are two rules the single dispatch had
never separated: int()/float()/bool()/str() are constructors the reference answers (0, 0.0,
False, the empty text), while ord()/chr()/abs()/repr() convert a required value and the reference
raises TypeError: <name>() takes exactly one argument (0 given) — so the first four now answer and the
other four raise that sentence, at exit 3, in the reference's own words rather than ours (ADR 0215 makes trap
wording observable). Two wrong answers were produced on the way there and both are pinned as tests:
selecting between the interned "True"/"False" strings made rt_print_bool print inverted verdicts
(it asks only whether its word is zero), and answering float() with a textual double made the print road
widen a value that already was one — sitofp i32 0.0 to double, which llc-20 rejects. bool("")/bool("x")
measure a text by its length rather than its interned index, and bool([1]) refuses on the compiled leg
rather than reporting "non-empty" for every allocated handle — which is also what bool([]) would have
gotten wrong. repr() is not str(): it has no default, and the asymmetry is pinned because the pair shares
one renderer and one table.
A text the body rendered into a name is a text-returning function (ADR 0286, closing Gap R.170).
def f(v): s = str(v); return s printed 0 for f(3) on the compiled leg at exit 0 — and 2 for
s = "x" + str(v) where the reference prints x3 — because ADR 0281 taught the compiler that
return str(v) renders, and a body that bound the rendering one statement earlier still looked
number-returning to its callers: print handed printf an index into @str_tab with %d. The verdict
now follows the assignment, the same two-hop question ADR 0285 asked of numbers. Underneath it sat the
reason a wrong number was possible: str() of anything "not a text and not a float" fell to
rt_str_of_int, which writes the decimal digits of the word it was handed — right for an int, a
fabrication for a handle — so that road is now asked what it was given. Narrowing it nearly cost more than
it bought: refusing every parameter broke four programs that had always worked (x = str(v); print(x)
printing 3), because a text argument is already refused at the call site by ADR 0174, and "gate the road"
had to become "ask the road" — with x = "abc" / x = 5 / print(str(x)) pinned at CPython's 5 so the
latest binding, not "was it ever a number", is what the gate reads. A None handed to a function is still
the number 0 (Gap R.171, verified pre-existing, filed not absorbed).
A number the body computed out of its own parameter keeps the kind the argument arrived with (ADR 0285,
opening Gap R.169). def f(x): y = x + 1; return y printed 1 for f(0.1) on the compiled leg at exit 0 —
and 0 for y = x * 2, -1 for y = x - 1, 3 for y = (v - 1) * 2, 5 for a float default — four
believable truncated digits, all from the AOT engine while the record and the reference answered 1.1,
0.2, -0.9, 3.0, 5.0. The scan's own comment named the hole: a parameter is written by the caller,
so no assignment in the body records it, exprNumberish answered the returned leaf "not a number", the
ordinary return road claimed the body, and define i32 @gy_f(i32 %p0) truncated the double the tagged door
had computed. The question is now asked knowing the function's own parameter names — scoped to the one body
being asked, because cycles 0280 and 0281 both measured what a module-wide widening costs. Three shapes
refuse rather than guess (y = x / return y, a binding under a condition beside an int arm, two bindings
deep), and two of their refusals were themselves false about the program — blaming "arithmetic over a
slot the program built at run time" for a value that came in through a call site, and claiming an integer arm
in a function that had none. Both now say what the arithmetic actually read (Gap R.38: a refusal the reader
cannot check against their own source is a defect of its own), and a test keeps the original container
sentence alive for the container that earned it.
A wrong-arity call stops the program on both legs (ADR 0284, closing Gap R.168). A callable read
out of a variable was called without being asked how many arguments there were: g = lambda x: x * 2 then
print(g(1, 2)) printed 2, print(g()) printed 0, and g = lambda x, y: x - y / print(g(3)) printed
3 — all at exit 0, all arithmetic on values that were never passed, where CPython raises
<lambda>() takes 1 positional argument but 2 were given. callClosure handed callFunc a slice of however
many arguments there were, and the bind loop walks parameters, so extras were never indexed and a missing
parameter with no default kept its zero value. The count moved into callFunc, the road every caller shares,
before the bind loop: a fifth road added later inherits the question instead of having to remember it. A
def keeps the name the program gave it (too many arguments for f), a lambda is <lambda>, and the
compiler's generated lambda_0 is never shown to a reader who never wrote it. Nothing about a correct call
tightened — defaults fill (6/9/12), keywords work out of order (12), recursion, methods and a lambda
through a parameter are CPython-exact on both legs, which is what the eleven-line probe exists to prove.
A declared name is not a variable, and a function is not a number (ADR 0283, closing the exit-2
half of Gap R.150 and Gap R.151). print(f), f + 1, xs = [f], str(f), print(math + 1) and
print(lambda x: x) all left --aot through exit 2 — llc-20 rejecting a module that emitted
load i32, i32* %_f for a slot no def ever allocated (a def writes a global @gy_f and allocates
nothing), or writing the closure's function global into an operand: printf(…, i32 lambda_0),
sub i32 0, lambda_0. The guard sits at the read, in value()'s *Name case, because the first draft
put it in the arithmetic roads and f + 1 exited 2 anyway — the float road lowers its operands before the
operator asks anything, so by the time the guard ran, the load was already in the module. In the numeric
door the answer is a raise, not a refusal: abs(f), -f, abs(math), -(lambda x: x) print CPython's
bad operand type for abs(): 'function' / 'module' byte-for-byte on both legs and are catchable.
A binding is a different thing and still works: g = f / g(21) is 42 interpreted, a lambda through a
parameter is 12 on both legs — nameIsAValueWithNoSign asks params first so the fix is not a ban on
functions as arguments. Binding the def'd name on the record is what made reading it stop being a
NameError for a name the program had just declared, and it briefly routed f(1, 2) into the closure road,
which pads and drops arguments without asking: it printed 2 at exit 0. That trade is the one the ladder
forbids, so the declared road is consulted before the closure value, and the pin fails on silence
(Gap R.168 records the road's own remaining defect).
A text on the left of % refuses instead of answering (ADR 0282, closing Gap R.165's wrong
number). print("%.2f" % 3.5) is CPython's 3.50, a TypeError on the record, and 0.0 on the
compiled leg at exit 0 — the format string was interned, and its @str_tab index was widened with
sitofp and used as a remainder's dividend. The sibling spellings already refused ("%d items" % 3,
"%s!" % "hi", "%x" % 255) because with no float operand the program stays on the integer road, whose
text guard was the refusal; a %.2f conversion is what moved an expression onto the double road, and
that road's lift asked no question about kind. floatValue now refuses a text the way ADR 0249 refuses
an unlift-able operand — record, return empty, let the caller name the missing feature — and the refusal
quotes the reference's own 3.50, so "not implemented" is readable without the IR. % is still the
remainder, and that is the other half of the tests: 7 % 3 is 1, -7 % 3 is 2, 7.5 % 2 is 1.5,
-7.5 % 2 is 0.5, 7.5 % 2.5 is 0.0, v % 2 over a pair-marked parameter is 1.5 and 1, and
1.0 == "a" still answers 0 — the last of those is a narrowing the first draft broke, because the
equality road lifts both of its operands too.
A function that renders its answer is a string-returning function, and its callers are told (ADR
0281, closing Gap R.163). def g(): return str(42) / print(g()) was 0 at exit 0 on the
compiled leg — and 0 for str(x) of a name, str(2.5), repr(42), str([1, 2]) and str(None)
too. The body was never wrong: it asked the rendering door and returned the interned index. The
caller printed that index with %d, because the program-wide predicate that decides which functions
yield text counted literals, f-strings, string parameters and concats, and had never been told about
a str() call — ADR 0224's bug class (print(Dog().sound()) printed 0) one door earlier. One case
in that predicate, with Gap R.6's guard still standing: def str(x): return x + 7 beside that body
answers 49, because a program that took the name owns it. programs/probe_return_str.gy is ten
lines, three legs, one answer.
str() and repr() are one pair over one renderer (ADR 0258, closing Gap L.2). print, str()
and a container element ask the same table: in the compiled backend the value printers no longer call
printf — every write goes through a sink that is either stdout or, while the pair renders, a capture
buffer whose bytes come back interned — so a form that exists for print exists for str() and
repr(), and the module fails its own test the day a second value renderer appears. A text is the
only value the two halves disagree on, and it disagrees the way CPython does: str("hi") is hi,
repr("hi") is 'hi', and inside a container both quote, which is why print(xs) and str(xs)
write one line. What that closed is a class of answers rather than one bug: str([1, 2]) compiled
answered 0 and str(None) answered 0, both with exit 0 — a missing rendering returning the number
underneath the value — while str({1}), str(set()) and str(1.5) refused or reached llc with a
module it rejected, and a text built at run time printed (null) inside a container because only the
compiler had ever been able to produce a repr. Three container builders wrote per-slot tags without
saying so on the object, so print(["a", 1]) was right while str(["a", 1]) answered [0, 1] from
the same object; objects now describe their own slots on every assignment path. A value whose kind is a run-time fact asks
the same door: str() and repr() of a slot read out of a container the program built, of a name bound
from such a slot, and of a loop variable stepping over a container that mixes kinds all route through the
module's one tag-reading printer (rt_str_of_value → rt_print_mixed_value), so str(v) can never
disagree with print(v) (ADR 0303). The same pair answers arithmetic where the operator cannot
answer a non-number: n - 1, -n and n // 2 for n = xs[0] sum the number the tag says and raise
CPython's own sentence, per kind, when the slot holds a text or a container (ADR 0304) — and it reaches the
double domain the same way, by its tag: n / 4 is 1.75, n >= 7 is True, n + 2.5 is 9.5, and a text
slot in an ordering raises '>' not supported between instances of 'str' and 'int' naming both types in source
order (ADR 0305). It is a container element too: print([n]) is [7], print([n, "x", 2, None]) is
[7, 'x', 2, None], and a text slot prints ['a'] rather than the interned index it stores, because the
literal is heap-built and the element writes its payload and its tag while the object is marked
self-describing (ADR 0306). It is an f-string field as well: print(f"{n}") is 7, print(f"{n - 1}") is
6, a text field prints xay rather than the interned index, and !r asks the same printer with the quote
flag — the field, str(n) and print(n) are one renderer pointed at three sinks, which also ended a
conversion that printed nothing at exit 0 and a quoted literal that llc rejected outright (ADR 0307,
Gap R.192). It is the signless call as well: print(abs(n)) is 7, abs(neg[0]) of a slot holding -8
is 8, abs(half[0]) of -2.5 is 2.5, abs(flag[0]) of True is 1, abs(table["k"]) of -9 is 9,
and the answer is itself a pair so y = abs(n); print(y) is 7 — abs is the arithmetic door's own operand
code, taking the magnitude of the lifted value, so the answer's kind follows the operand's and a text slot
raises the call's own sentence, bad operand type for abs(): 'str', never the unary minus's (ADR 0309,
ADR 0271). It is a dict entry and a set member as well: print({"k": n}) is {'k': 7}, print({n: 1}) is
{7: 1}, print({n}) is {7}, and a text slot gives {'k': 'a'} and {'a'} rather than the interned index
it stores, because a dict and a set each have one builder that takes the tag with the payload
(rt_dict_put_tagged, rt_set_add_tagged), a register is an i32, and the pair's tag is a register the
objects wrote — the same shape ADR 0306 paid for a list element, asked of two more builders, with nothing added
to the runtime. The binding keeps its tags too, so d = {"k": n} answers d["k"], len(d), k in d, d[k],
str(d), d == {"k": "a"} and a for k in d, and s = {n} answers 7 in s, len(s) and a for v in s. A
key or a member asks one question an element never asks — can this value be hashed — and the tag is what
knows: {c: 1} over a container slot raises TypeError: unhashable type: 'list', catchable by the program's
own except TypeError:, where reading the payload alone would have put an address in the bucket and printed a
plausible container at the exit code of success (ADR 0310, ADR 0232, Gap R.81). What still refuses is a
position that keeps one word for a whole value — n + 1, abs(n) + 1, round(abs(n) / 2), min(n, 3),
a literal sum/min/max folds into a static array, a pair handed across a call, and an f-string used as a
value — and the refusal names
what the value is and where it came from, never a loop that the program does not contain (Gap R.38, Gap R.146; the residual shapes are Gap
R.115, a container returned from a function is Gap R.67's, a tuple is L11.3's, print(f"{xs}") is Gap
R.114, and an unrelated container's nested slot read refusing is Gap R.191's). --json --eval 'repr("hi")' reports
{"result": "'hi'", "type": "str"}, and programs/probe_render_pair.gy is match on both legs.
A comprehension that folds is the literal it folds to: sa = {x for x in [1, 2, 3]} and
sa = {1, 2, 3} reach one lowering — a heap object, every slot written with its payload and its tag,
the variable's kind recorded — so print, in, subscript and for treat a bound set or dict
comprehension exactly like the literal (ADR 0234). {x for x in xs if x > 1} parses on the compiled backend:
the if is the comprehension's, not a ternary's.
A class pattern is a question about a class, in the compiled backend: case Point(x, y): matches an instance
of Point or of any subclass of it and binds x and y to the instance's attributes of those
names — and an attribute the instance does not have fails the case, which the compiled backend
could not ask until @inst_set started recording which slots have been written (ADR 0235). The class
may be named directly or reached through a binding (Alias = Point, in a function body too); a case
whose pattern is a call — case f(): — compares the call's result to the subject.
Optional annotations on variables, parameters, and returns are checked
statically by --verify, with any as the dynamic escape hatch; untyped code
falls back to dynamic dispatch.
-
Union types —
int | str,int | float, andNone | intsugar forOptional; inferred and checked across assignments, call boundaries, and returns. In AOT, a union-annotated scalar variable gets a tagged%unionboxslot (runtime member tag 0=int, 1=float, 2=str) soprintdispatches on the live member — anintmember prints as%d, afloatas%f, astras%s, even after cross-member reassignment under branches/loops. -
Literal types —
Literal[1, 2]annotations feedmatchexhaustiveness + narrowing on constants. -
Type narrowing — after
if isinstance(x, int):, the checker narrowsxfromanytointin the then branch and away from it in the else branch;not isinstance(x, T)flips those; union complement narrowing usesdropType. -
Walrus operator — assignment expressions
name := exprusable insideifconditions and comprehensions (if (n := len(x)) > 0:), scoped per Python 3.8+. -
Variance + generics (L6.6) — one subtyping relation implements a declared variance table:
list[T]/set[T]/dict[K, V]are invariant (they are writable),Sequence[T]/iter[T]/tuple[...]are covariant (read-only, so an element type may widen),Callable[[P...], R]is contravariant in its parameters and covariant in its return, and user classes are nominal —a: Animalaccepts aDogbecause the declared base chain says so. A freshly built container literal may widen its element type to the destination (x: list[int | str] = [1]). Every rejection names its rule and carries a stablecode(type.variance.invariant,type.variance.contravariant, …) plus an actionablesuggestion; the whole model is machine-readable viagustyc --variance. -
printbehaves like Python's —print("n =", 42)writesn = 42, not two lines: arguments are joined withsep=" "and terminated byend="\n"(both honoured for every argument kind, including runtime containers, whose printers take the newline as a flag rather than baking it in). Interpreter and AOT agree byte-for-byte, including how an argument that prints interleaves with its line (ADR 0165). -
Containers are references everywhere — pass a
list,dictorsetto a function as a literal, variable, keyword argument, default, comprehension or generator result and the callee sees the same live object on the compiled backend: the AOT backend materialises container literals into the runtime heap and infers each parameter's container kind from annotations, defaults and call sites (forwarding included), sofor x in xs,len(xs),xs[i]andxs.append(v)work on parameters exactly as on variables. Binding one is the same story (ADR 0163):ys = [x * 2 for x in [1, 2]]— even constant-folded, even at module scope — yields a rooted heap handle, soprint,len, indexing, iteration and calls all see the container, not a folded global's address. -
The verifier is a pipeline stage (L8.2) — the AOT backend emits textual IR, so
Buildruns LLVM's own module verifier (opt -passes=verify,llc -filetype=nullfallback) over the module it is about to link and reports the verdict inBuildResult.verification;gustyc --verify-llvm <src>exposes it as a machine-readable record (ok/tool/skipped/pipeline/errors/note) so an agent can tell "the compiler emitted bad IR" apart from "my program is wrong" — without scrapingllcoutput. A missing toolchain is reported asskipped, never as a pass. Turning it on is how Gap I.3 was found. -
The line table lives in the module, and the report is read back from the artifact (L8.5, ADR 0231) —
--debugused to add-gto a link step that had nothing to stringify: DWARF is written byllcfrom!dbgmetadata, and the module had none. Codegen now records which statement each stretch of emitted code was written for, and a post-pass lays the LLVM debug metadata over the finished module: aDICompileUnitthat names the language (DW_LANG_Python, not a generic guess), oneDISubprogramper program function — never the compiler's own GC, exception or printer blocks, which are not code the program wrote and must not be blamed for it — and aDILocationper instruction. Then the toolchain reads it back:--debug-inforeports the table out of the emitted IR's own metadata (definitions.debugInfo, one entry per function, an IR-line-to-source-line row table, and adefectfield for when the module disagrees with the emitter), and--build --debugrunsllvm-dwarfdumpover the object it just linked (definitions.dwarfReport) so a claim about DWARF is a claim about the artifact. That is how aDISubprogramwith a malformedtype:was caught:llcprintedinvalid subroutine type, exited 0, and wrote an empty.debug_linewhile every internal count looked perfect.--emit-source-mapv2 carries the same table, andintegration/debug_info_test.goends by askingllvm-addr2linewhere a function lives. Fixing it required closing a Gap-K.6-class hole first: assignment to an attribute and tuple assignment were built with no source position at all, so their instructions inherited the previous statement's line (§ Every statement has a position,docs/language.md). -
Declaration order that matches the language — mutually recursive functions, and helpers declared below the code that calls them, check clean and compile; a call at module level and a decorator still require the name above them, because that code runs where it is written (
programs/forward_defs.gy, ADR 0197) -
Your function names are your own —
def sync,def main,def exitare emitted asgy_sync,gy_main,gy_exit, so the linker can never answer the program's own call from libc, whileextern fnkeeps the C name it binds;nmon the built binary and the source map'ssymbolfield both show the link name (programs/host_symbol_names.gy, ADR 0198) -
Built-ins are shadowable, on both paths —
def str,def float,def lenmean what the program says they mean, exactly as in CPython, instead of being answered by the compiler's own reading of the name (float(1)printed1.0for a function returningx + 7); the constant folding still runs whenever nothing shadows the name (programs/shadowed_builtins.gy, ADR 0199) -
A method and a helper may share a name —
def timebesideclass Timer: def time(self, x)is ordinary vocabulary, and the two definitions are keyed apart instead of the method overwriting the module function (a call then measured againstself-inclusive arity, refusing a program every other layer ran) (programs/method_function_name_clash.gy, ADR 0200) -
A dropped argument is refused at the call — too many arguments was already an error, too few was not, so an unbound parameter came back later as an
undefined nameblamed on the callee's correct source while the interpreter had been refusing it all along (programs/arity_defaults.gy, ADR 0201) -
A diagnostic is said once — per-call-site return inference used to re-report everything inside a callee, so one warning appeared two or three times and the length of the JSON
diagnosticsarray was not a count of findings (ADR 0202) -
Only grammar words are reserved —
printandrangewere keywords, sodef print, a parameter namedrange, a keyword argument namedprintand a method namedrangeall failed to parse; built-ins are ordinary names, and the keyword table is now exactly the words that change grammar (programs/builtin_names_as_defs.gy, ADR 0203) -
A program's stdout is only what it printed — the interpreter used to echo a file's final bare expression (
f(5)last printed10) while the compiled backend and CPython printed nothing, so the same source had two stdouts depending on the engine (ADR 0204) -
A built-in name you claim must be defined above your uses —
for i in range(2)above adef rangewas the built-in to the record and the program's function to the compiled backend (two different outputs, no diagnostic); it is refused at the call instead (ADR 0205) -
A default may sit anywhere in a signature —
def f(a, b=1, c)is aSyntaxErrorin CPython and ordinary source here, because positional binding fills left to right and a keyword call names what it fills, so every parameter is reachable (programs/param_default_order.gy, ADR 0206) -
A module never calls a runtime helper it does not define — which runtime blocks a module carries is derived from the code it emits, not from flags each codegen path had to remember, so the failure that surfaced as an
llc"undefined value" error is gone; and where the compiled backend cannot act (iterating a run-time string) it refuses with a message instead of compiling a loop that silently does nothing (ADR 0209) -
A negative subscript means what it means —
xs[-1],xs[-1] = v,"abc"[-1]and the folded[1, 2, 3][-1]agree with CPython on both backends (the literal used to crash the compiler), while a dict's-1stays a key, because a subscript is either a position or a key and only positions count from the end (programs/negative_index.gy, ADR 0210) -
A failure class has one code, whichever path produced it — a program that trapped exits 3 whether the record or native code ran it, an
llcrejection of our own module is the compiler-bug class 2 on the run path too, and--json'sexitfield is derived from the process status rather than written down (ADR 0211) -
A built-in trap is a typed exception everywhere —
7 % 0raisesZeroDivisionErrorwith CPython's wording andexcept ZeroDivisionError:catches it on the compiled backend; the compiled backend used to emit the instruction and keep walking, printinginfor a fresh garbage integer and exiting 0 (programs/zero_division.gy, ADR 0212) -
Every
exceptarm is a real arm — arms are dispatched in source order on the compiled backend, a bareexcept:works in any position, a nestedtryreaches its outer arm, and an exception no arm matches propagates instead of being deleted (the compiled backend used to lower only the first arm and clear the flag, exiting 0 on a program whose error nobody handled, ADR 0213) -
A trap the program cannot name is not a trap it can handle (ADR 0214) — nine interpreter shapes (a missing attribute,
int("abc"), a bad unpack,x()on an int,len(5),5[0]) raised errors with a message and no exception class, so everyexceptclause written for them was dead code. They raise what CPython raises, in CPython's words, and the tests pin class and wording:'P' object has no attribute 'nope',invalid literal for int() with base 10: 'abc',not enough values to unpack (expected 2, got 1). One of them had reportedcannot index nullabout an integer — not untyped, just false, and the kind of wrong that sends someone hunting a null. -
An operator is a question about two runtime kinds (ADR 0215) —
print("a" * "b")used to print1099516870662and exit 0, because an operand that wasn't a known container went into the arithmetic path holding a heap handle. So did1 + None,[1] + 1,"a" < 1. Mistyped pairs now raiseTypeErrorin the reference implementation's words (22 shapes verified by runningpython3and diffing the report line), and the legal pairs the same path was silently eating —[1] + [2],[1] * 3,"ab" * 2,"a" < "b"— compute values instead of numbers-no-one-wrote. Chasing it also found the record's untagged values colliding with ordinary arithmetic: a bench loop computingi * ireached the heap's id range ati = 1024and read back the class's own method object, so the heap now starts at1 << 48and one predicate decides what an object is. -
//and%floor, and they are one rule (ADR 0216) —print(-7 // 2)printed-3compiled and-4interpreted, andprint(-7 % 2)printed-1on both, because Go's/and%truncate toward zero while Python floors (the remainder carries the divisor's sign, so-7 % 2is1). The compiled backend now emit the correction (sdiv/sremplus aselect;fremplusfaddandcopysignfor floats), and 312 integer and 392 float sign combinations are checked against CPython on both paths. Two integration tests had pinned-3.5 % 2.0 == -1.5as correct — with a comment namingfrem— because they had been written from the emitted IR rather than from the language; the new tests asserta == (a // b) * b + (a % b)instead, which a consistently truncating pair can never satisfy. -
A compound statement is not a scope (ADR 0217) — this refused to compile:
def f() -> int: try: a = 7 except: a = 0 return a
--checksaidundefined name "a"and--aotexited 1, while--interpprinted7and CPython printed7. The analyser put atrybody, each arm, thefinallyclause, awhilebody and eachmatcharm in a child scope it then threw away — and never walkedfinallyat all, so nothing inside afinallywas ever checked, a call to a nonexistent function included. Visibility and definiteness are now separate questions: bindings join the enclosing function or module, and a name only some paths assign is read with apossibly unboundwarning instead of the old error, which is what the program actually does. Two compiled-backend defects surfaced on the way and are recorded with their measurements rather than bundled: a handled exception that the next call re-raises (Gap R.21's compiled half), and an untouched slot being loaded and printed as a value (Gap R.36). -
A handled exception is over (ADR 0218) — the compiled half of that first finding, fixed the cycle after it was measured. The compiled backend holds the exception in one module-wide bit, and nothing was ever told the search had ended: the arm ran, the program continued, and the next call to a user-defined function found the bit still set and reported the exception a second time — after the handler had already handled it.
print(5)after thetrywas safe,print(f())was not, andprint("handled")inside the arm printedhandledbefore dying, which is what proved the arm had run. Every edge that leaves an accepting arm now clears the flag, including thereturn,breakandcontinuethat stepped past the one edge the clear was on; an arm's ownraiseand the unmatched re-raise deliberately do not, and a program whose only arm always raises must emit no clear at all. -
Citations in the record must resolve (ADR 0219) — the roadmap said two
%-formatting shapes "stay pinned asprograms/probe_percent_format.gy"; that program did not exist, had no ledger row, and its sentence had long since gone stale (the interpreter no longer returns0there, it raises). It was one of seven dangling citations found in one pass — renamed files still cited under their old names, a historical name left in prose after the file was promoted, a claim of measured debt with no measurement.TestRecordCitationsResolveToRealProgramsnow scans roadmap, README,docs/and every ADR:programs/NAME.gymust exist unless marked(planned)(the notation for a program a roadmap item still owes), a bareprobe_*.gymust exist because the prefix is a claim about the corpus, and a near-miss fails with "did you mean programs/X.gy". The session-learnings file is exempt — it is allowed to name a file precisely to report that it is missing.%formatting itself remains a gap (R.31), but now it has the artifact its entry always claimed. -
An unwritten slot raises — it does not answer (ADR 0228) —
def f(c): if c: x = 1; return xcalled withFalseprinted0and exited 0;while 0: w = 1in a function printed8555776; atrycut short before its second assignment printed518208;if 0: x = 1thenprint(x)at module level printed64. Those numbers were the frame's previous contents — leftover words and stale heap handles — read as values and reported as successes. The compiled backend now raise what CPython raises:UnboundLocalErrorwhen the frame owns the name,NameErrorwhen nothing does, catchable by class on either engine, exit 3 either way. The mechanism is one byte on each slot the checker cannot prove was written (codegen gets no dataflow rule of its own), cleared on entry, set by every write, tested at the read — and absent, with no instruction emitted, wherever assignment is provably definite. Three checker rules turned out to be the cause: aforbody may run zero times, amatchmay match nothing, and a loop variable is certainly bound inside its own body. The probes also caught the linked binary exiting 1 — the compile-error code — for a program that merely raised, which ADR 0211 does not permit. -
A string is an index into a table the runtime can add to (ADR 0229) —
s = get(); print(s[1])refused as "index of a non-literal variable",def f(s): return s[1]printed1, andget()[1].upper()printed2, all with exit 0 while CPython and the record printedbandB. Thirteen shapes were compile-time refusals for programs Python runs. A compiled string is an@str_tabindex and the table is content-addressed and already grows at run time, so the fix was not a new representation but six runtime helpers that take indices and return them — and one question the compiler had been asking too narrowly: is this a string?, not can the compiler read its text?. Subscripts at run-time positions,len,ord, andupper/lowernow answer on the compiled backend, and because equality is by content, a string built while running compares equal to the literal that spells it. The table's overflow path used to reuse its last entry — printing a different string than the program had built — and now raises a catchableRuntimeError. -
A string built at run time is a buffer, an intern, and the same index (ADR 0230) — the write half of the same gap:
"a" + word(),s[i:i+2]where the bounds are values,str(get()),.strip(), andfor c in <runtime string>were compile-time refusals for programs Python runs. Iteration had been worse than a refusal before it was refused: the string's table index was read as a repeat count, so the loop printed nothing and exited 0 (Gap R.16). Five more runtime helpers close it, and since interning dedups by content, a built"ab"and the literal"ab"are one value with no special case. The suite lesson:for c in txt()had quietly become the canonical "the backend refuses" fixture in four tests — a pinned refusal is a claim about the future, and when the gap closes its fixtures have to move or those tests go green while saying nothing. -
A comprehension that folds is the literal it folds to (ADR 0234) —
sa = {x for x in [3, 1, 2]},print({x for x in [3, 1, 2]})and2 in {x for x in [1, 2]}each reachedllcas a folded container global in a value slot (store i32 @.set1, i32* %_sa,rt_print_list_mixed(i32 @.set1, 0),rt_contains(i32 @.set1, i32 2)) and came back as exit 2, the compiler blamed for ordinary Python. The list spelling had been fixed twice (print builds the object, the binding copies the fold into the heap); the set and dict spellings had been left behind, and their print branch asked one printer for all three kinds — a valid module would still have rendered{1, 2}as[1, 2]. The{x for x in xs if x > 1}form never reached codegen at all: the iterable was parsed as a full expression, the ternary inside it ate the comprehension'sif, and the file died onexpected keyword "else"while the list twin parsed. One fold now produces the literal, and one binding rule binds it — plus the module-wide guard that a folded container global never appears in an operand position. -
A numeric rule is an IEEE operation, not a habit (ADR 0236) —
round(2.5)answered3on both backends where CPython answers2:math.Roundin the evaluator,@llvm.round.f64in the compiled runtime,math.Roundagain in the compiled constant fold, and four tests — two of them stating "half-away-from-zero" in a comment, one pinningi32 3in the IR. Four authorities agreeing is what makes a wrong answer survive review, and parity cannot see this class at all: it compares the two implementations to each other. The compiled backend now name the operation —math.RoundToEven,llvm.roundeven.f64— andprograms/round_ties.gyentered the corpus with no ledger row, which here means "print what CPython prints", so the old answer is a CI failure. What the same probe found and did not fix, until ADR 0263:round(2.345, 2)is2.35in CPython,2in the interpreter (the digit count is ignored) and an exit-1 refusal compiled — a compile-error exit code for a program CPython runs, which was Gap R.69 and the exit-code contract's own subject (ADR 0211). -
The digit count rounds the decimal, so the compiled backend borrow the conversion (ADR 0263, closing Gap R.69) —
round(x)had been taught to name the IEEE operation;round(x, ndigits)arrived with three answers, one per engine, and the third was an exit code. The row's own suggested fix — scale by 10ⁿ, askroundeven, unscale — was measured and thrown away, on 1,077 of 375,224 fractional pairs and 74,838 of 156,048 negative-digit ones. It fails in the worst available way: it prints CPython's2.35for the row's own example, because that double happens to sit above its tie, and disagrees elsewhere. The mechanism is that scaling manufactures ties the value never had —0.005is 0.00500000000000000010408… and rounds up to0.01, but times 100 it is exactly0.5, and a nearest-even rule answers that0;0.075scales to7.5and comes back0.08where the reference says0.07. What the reference rounds is the exact decimal value of the double, which is not a question the binary domain can answer at any level of cleverness about ties. It is a correctly-rounded double→decimal conversion, an operation neither backend owns: the evaluator asks Go'sstrconv.FormatFloat/ParseFloat, the compiled runtime asks the C library'ssnprintf("%.*f")/strtodthrough onert_round_digits, and both are reached from one Go rule,roundToDigits. Neither is trusted on sight — 531,272(value, ndigits)pairs were swept against CPython and compared bit for bit, the residual 143 (all between |x| = 4.117e18 and 1e300 with a negative digit count, all one ULP, where the scale itself is the inexact step) got a roadmap row instead of a passing test, and the same scale-and-roundevenshape at the negative branch sat 74,838 cases from the reference where the%.0f+strtodshape shipped sits 143. The answer is the kind the value arrived as (round(3.5, 0)is4.0,round(5, 2)is5), a digit count that is not an integer is raised as CPython'sTypeErrorrather than refused into exit 1, and the two clamps are arithmetic facts with reasons: 324 digits is past every double's expansion, and 10³⁰⁹ is past the largest finite one. -
Three names, both legs, three behaviours — and the docs agreed with the wrong one (ADR 0264, closing Gap R.51 and Gap P.2) —
floor,ceilandsqrtwere in the checker's predeclared table since it existed, sogustyc checkcalled any program that used them well-typed. Behind that table:print(floor(3.7))was CPython's3, an interpretedNameErrorat exit 3, and compiled3.0;print(ceil(-0.5))was0,NameError,-0.0;print(sqrt(-1))was aValueError,NameError, andnanwith exit 0;print(floor("a"))was aTypeError,NameError, and0.0. The test said so too —TestExecFloorCeilpinned2.0\n-3.0\n3.0\n-2.0under a comment quoting the language's own "the largest double <=", and roadmap's Gap P.2 quoted the same phrase back as a decision. Three voices agreeing with each other and with nothing outside, which is why every row here is asserted againstfrom math import floor, ceil, sqrton the first line of the same bytes. What shipped is one rule per name inpkg/lang/math_names.go, read by the compiled backend:floor/ceilanswer a whole number (the reference'smath.floor/math.ceilreturn anint) andsqrta float, the checker types them that way too, and a whole number beside a float is widened, not truncated —floor(2.7) + 1.5is3.5,[floor(2.7), ceil(2.2)]is[2, 3],str(floor(2.7))is2. A text, aNoneor a container argument, a negative undersqrt, a NaN or an infinity underfloorare all raises with the reference's sentence, exit 3 and catchable by class on both legs —must be real number, not str(unquoted, unlike this language's otherTypeErrorfamily, because CPython writes two different sentences and a program'sexceptreads them),math domain error,cannot convert float NaN to integer,cannot convert float infinity to integer— the last of which neededOverflowErrorin the exception table. Beyond the compiledintword the legs disagree on purpose:floor(3000000000.0)is answered by theint64evaluator and raised by the compiled leg rather than lettingfptosireturn poison, and the split is a ledger row with a pin per leg (Gap R.133, owned by L12.12). Walking that boundary found the bug the middle would have hidden: the lower guard was-2147483649.0, which lets-2147483649.0through into the poison truncation, where it printed2147483647. And two findings the row did not own got IDs of their own: a foldedinfcould not be emitted at all —x = float("inf")died inllcwithinf.0e+00, exit 2, and now writes its IEEE bit pattern with both bad spellings on theforbiddenIRblacklist (Gap R.134, closed here) — and1e18does not lex, filed with the ten other predeclared names no engine can call (pow,divmod,hash, …),print(pow(2, 3))being a program CPython evaluates and the compiled leg answers with exit 1 (Gaps R.135, R.136). -
A class pattern asked two backends the same question, and got two answers (ADR 0235) —
case Point(a, b):on an instance withxandymatched compiled and printedpt 0 0, while the record anddocs/language.mdboth say a missing attribute fails the case: the compiled arm checked the class chain and never asked the instance, whose data words cannot tell an attribute that was never written from a stored0. The alias form was worse in both directions —case Alias(x, y):inside a function was exit 2 (%t6 = icmp eq i32 %t5,, anicmpwith nothing after the comma, fromalias, _ := g.value(...)throwing away the error) and exit 3 interpreted (TypeError: 'type' object is not callable— the pattern fell through to calling the class) — andcase f():loaded%_f, a variable that does not exist, because the same branch had decided any non-class name must be one. One front-end table now says what a pattern-position name denotes and what attributes exist; the body's scope reaches the module for a bare class name (ADR 0227); andrt_inst_putwrites presence with the value, cleared per instantiation because heap slots are recycled — 300 instantiations under GC stress and aghostattribute nobody wrote stays absent.--langnever mentioned patterns; it does now. -
A container slot is a word — ask what fits before writing it (ADR 0226) —
[1] == [1.0],print([1.5, 2])and1.0 == [1]reachedllcas invented operands ([1 x i32] [@env_store = ...,%t1 = sitofp i32 to double,%t2 = sitofp i32 @.lst1 to double) and came back as exit 2, while three neighbouring shapes were green on truncation:{1.5} == {1.6}compiled to True. A container now holds what a word can carry — ints, interned strings,None— and a float element is refused with a message naming the missing representation. Two mechanisms were behind it: a global written into the module before its elements were validated (leaving an unterminated definition that downstream paths shipped), andvalueTextdiscarding a lowering error and returning"". A number compared with a container is also answered by kind now, the way CPython answers it, not by coercing the container. -
A subscript of a string is a one-character string (ADR 0225) — the compiled backend answered
s[1]with98, and the missing type spread to everything the value touched:s[0] + s[2]did arithmetic and printed196,s[1] == "b"said false (compiled as well as interpreted — the compiled backend agreeing is what hid it from a green suite), andlen(s[1]),s[1].upper(),ord(s[1])trapped. A string is counted in code points everywhere position is asked about (s[i],s[a:b],len,ord), solen("café")is 4 and"café"[3]isé. Measuring it also found a condition emitter that replaced an un-lowerable condition with a false branch and printed0for1 if s[1] == "b" else 0: a part that cannot be lowered is now a compile error, never a default value. -
A string value is an index, not a pointer (ADR 0224) —
x == "hi","a" in xs,self.w = "hi"; print(C().w)and a method's-> strresult reachedllcas ani32holding the address of a string global (icmp eq i32 @.str1, %t1,ret i32 @.str1) and came back as exit 2, the compiler blamed for an ordinary program. A string value is an index into the runtime interned table everywhere a value is asked for —rt_str_intern2on the way in,rt_str_ptron the way out toprintf— and the address of a literal stays only where bytes are the question. Printing an index with%dwas the silent twin of the same bug:print(f"hi {n}")answeredhi 0. Removing the refusal that covered this also exposed a filtered comprehension loop whosephinamed a predecessor that never branches to it. -
A method is a call like any other (ADR 0223) — three different wrong interfaces came out of one emitter that had never been brought back to parity with functions:
class C: def m(self) -> int: try: return 3 finally: print("fin") # compiled: exit 2, `br label %` — an empty target def raiser(self) -> int: raise ValueError("boom") print(C().raiser()) # compiled: prints 0 and exits 0 · CPython: traceback
A
tryin a method emitted a branch to an empty label because onlyfuncDefset a raise-exit; araiseout of a method was invisible because no call site checked the exception flag after a method call — the program printed a value and carried on; andemitClassMethodthrew away the errorg.stmtreturned, so any construct the compiler refuses inside a method became half a function and anllcrejection: exit 2, blaming the compiler for a source error (ADR 0166's rule). Methods now own their unwind path (which closes the GC frame they opened), clear the enclosing statement's handler/deferred state, name their traceback frameClass.method, report their refusals as compile errors, and every call site into program code — static dispatch,super(), the class-idswitch, and a constructor's__init__— checks the flag. Inside theswitchthe check had to finish the arm and the join'sphiname the check's continuation: a call site that can raise cannot also be a value producer for the join. What is still wrong is recorded with a minimal repro and a pre-existingness check against two older binaries: a method returning astrreturns the raw string global (roadmap Gap R.42). -
A deferred body belongs to every exit (ADR 0222) — this ran the cleanup on the boring path only:
def f() -> int: try: return 1 finally: print("fin") # was: prints nothing, returns 1 · CPython: fin, then 1
Eleven shapes measured against CPython, nine wrong, and the compiled backend wrong identically — the deferred body ran on fall-through and after a handled exception and was skipped for
return,break,continue, and for an exception no arm matched. Parity could not see it because the two implementations agreed. The same statement hid a second bug: transfers travel as Go errors in the interpreter, exactly like raised exceptions, and the arms asked "did something come out?" instead of "did an exception come out?", so a bareexcept:caught areturnand dropped the value. Now afinallyruns once on every exit in both paths, innermost first, with Python's ordering — the return value is taken by thereturn, soreturn nhands back the oldneven if thefinallyreassigns it — and areturn/raiseinside thefinallyreplaces what was in flight. In codegen an escaping exception runs only the innermost pending body (the outer ones run on their own way out, and running the whole stack printedouter fintwice), and whether a body already left the block is judged by where control went, not by opcode, because anifalso ends its block with abr. -
Two numbers are one question (ADR 0221) — this printed two different answers depending on which flag you used:
print(1 == 1.0) # CPython True · interpreter 0 · compiled 1 print(1.0 == 1) # CPython True · interpreter 1 · compiled 1
An integer was compared as a word against a float object's handle, so an int never equalled the float with the same value — and only when the integer was on the left, which is why it survived: a test written from the direction that worked never saw the one that didn't. Measured as 300 comparisons (5 ints × 5 floats × 6 operators × both orders) the compiled leg was right on all of them and the record was wrong on exactly 8. Equality between two numbers is now one question about their values in either order, gated by the same
isHandlepredicate operators use, so1 == [1]and1.0 == "a"remain False rather than becoming errors — and container equality inherited it for free ([1] == [1.0],{"a": 1} == {"a": 1.0}). What the tests turned up on the way is recorded rather than bundled: a literal[1] == [1.0], and1.0 == "a", emit modulesllcrejects, so those programs exit 2 with a temp-file path where they should refuse (roadmap Gap R.40). -
The module is a scope too (ADR 0220) — this program did not exist:
def twice() -> int: return MAX * 2 MAX = 40 print(twice()) # CPython 80 · interpreter: NameError · compiled: refusal
The scope chain reached an enclosing function but stopped before the module, so a script could not read a constant from a function — the most ordinary shape there is. A function's name is now resolved in its frame, then the captured closure environment, then the module it was defined in (a nested def gets its enclosing function's module; a function in an imported module gets that module), and because the lookup happens at call time the assignment may sit below the
def. Each module scope became a permanent GC root for the same reason. The checker pre-collects top-level binding names and consults them only inside function bodies — module code still runs line by line, and two existing tests caught my first attempt doing it globally. The compiled leg still cannot reach a module binding: a name bound to a literal the module never rebinds is read as the value it is, and one the module rebinds lives in a@gy_mod_*global the callee can read (ADR 0227). Both legs ofprograms/module_scope_in_functions.gyandprograms/module_calltime_lookup.gyprint CPython's line now; a body reading a module container is still refused, with the reason that names module state rather than blaming a string. -
The corpus has a third opinion (L11.9) — parity between the compiled backend can be satisfied by two implementations that share a bug, and for a hundred ADRs it was. The conformance matrix runs each program on the record, the compiled binary and CPython, and each case declares its state in a ledger (
matchby default,debtwith a reason, an owner and a pin of the wrong answer, ornot_applicablefor gusty-only surface). Drift fails the build in both directions.gustyc --oracle '<src>'exposes the same classifier interactively —--jsonfor the leg-by-leg report, exit 6 when gusty disagrees with Python and 7 when the oracle could not judge the source (ADR 0186). -
Benchmark suite + regression gate —
gustyc --bench-suitemeasures a corpus on the compiled backend and prints (or--json-emits) a stable artifact;--bench-baselinegates a run against a saved baseline, so "the compiler got slower" is a number with its own exit code (5) instead of a hunch.--bench-dir integration/programsbenchmarks the parity programs too.
- Error-recovering lexer — on an unexpected character, emits a
TokErrortoken carrying the span + message and resumes instead of aborting the file, so the parser/semantic pass can report multiple diagnostics per run. - Rich token spans — each token carries
startANDend(byte + rune offsets) plus an optional multi-line flag, giving f-strings, slices, andmatchpatterns exact ranges for hover/diagnostics/formatting. - Unicode identifiers — identifiers scan by Unicode
ID_Start/ID_Continue(not just ASCII), NFC-normalized viagolang.org/x/textso decomposed and precomposed spellings are one symbol, and aTokWarningdiagnostic flags Greek/Cyrillic homoglyph lookalikes (e.g.ΟU+039F vs LatinO). - Numeric-literal modernization — hex (
0xFF), binary (0b101), octal (0o17), and_digit separators (1_000,0x_FF), with exact integer semantics and rejection of misplaced separators. - Raw & triple-quoted strings —
r"..."/R'...'raw strings and"""..."""/'''...'''multi-line strings; docstring extraction reuses both forms. - Line continuation — a trailing
\joins the next physical line into one logical line (Python-compatible), skipping the continued line's leading indentation and blank/comment-only continuation lines. - async/await + effectful syntax (L5.6) —
async def,async for,async with, andawait exprparse as first-class syntax; under the minimal synchronous-coroutine model (no suspension primitives yet) they lower identically to their sync counterparts in the compiled backend, matching CPython (seeasync_basic.gy). The cooperative event-loop runtime is Phase 7. - Pratt parser — a precedence-climbing expression parser keyed off a
precedence table (unary,
**right-assoc, multiplicative, additive, comparison,and/or, ternary) with panic-mode recovery (recoverStmt— nest-awareINDENT/DEDENTskipping) producing a forest of*ParseErrors and a partial AST. - Trailing commas —
f(a, b,),[1, 2,],{1: 2,}andmatchcase arg lists tolerated, and normalized away by the canonical formatter.
Every feature ships once, in the one backend, and is judged against two witnesses (ADR 0302 retired the AST interpreter that used to be the second engine):
- LLVM AOT codegen —
pkg/lang/codegen.go+pkg/lang/closure.go, entryCompile: every path that runs a program —--eval,--file,--repl,--emit-llvm,--verify,--build, the REPL included, which compiles in-process through the LLVM JIT (pkg/lang/jit_llvm.go, entryJIT). Emits deterministic opaque-pointer LLVM IR with realdoublefloat IR (float arithmetic viasitofppromotion,%.17gfloat print), tagged-union lowering,__doc__folding to string constants, string slicing viart_slice, literal-container membership viart_contains, andwith/yield-from runtime protocols. The runtime it links carries the two-generation (nursery + old) tracing GC that reclaims unreachable pure-data heap objects; roots are top-level bindings, walking container elements, dict values, closure envs, and attr tables. - the record leg —
pkg/lang/testdata/interpreter-golden.json: the answer the retired interpreter gave for each of its 5.5k+ sources (value repr, type name, stdout, trap class and message, front-end refusal), read bypkg/lang/golden.go. A missing record fails the case, and a compiled answer that disagrees registers a drift row that fails the build in both directions. - the reference leg — CPython, via
gustyc --oracleand the conformance matrix; disagreements are rows inintegration/testdata/cpython-debt.json, each naming the roadmap item that owns it.
- Constant folding — integer-literal binops fold at codegen time
(
x = 1 + 2emitsstore i32 3, noadd). - Escape-analysis heap elision — never-read top-level list literals skip
their runtime heap allocation (
[x * 2 for x in ...],{k: v for ...},{x for ...}). - Dead-global / dead-object elimination — unused
@.strN/@.lstNglobals and dead heap objects are pruned. - Real LLVM
optpipeline —pkg/lang/opt_llvm.godrives the externalopt-20tool over the raw module IR (instcombine, gvn, licm, sroa, simplifycfg, ...), so AOT emits verified, optimized IR — while preserving GC-correctness by rooting heap slots through module-global arrays (@gc.roots/@gc_roots_used).
gustyc is the command-line interface and REPL:
gustyc --eval "x = 2 + 3\nx" # evaluate source, print result
gustyc --file prog.gy # compile & run a source file
gustyc --build out a.gy b.gy # compile a set of files into a binary
gustyc --verify "def f(x): return x * 2" # static analysis only
gustyc --emit-llvm "x = 1 + 2" # print emitted LLVM IR
gustyc --emit-ast "x = 1" # print the AST as JSON
gustyc --emit-source-map "x = 1" # JSON source map (fn -> IR symbol+line)
gustyc --emit-source-map-file src.gy # the same, reading the program from a file
gustyc --build out prog.gy --debug # DWARF: !dbg records, .debug_line, reported back
gustyc --debug-info "x = 1" # the compiled line table as JSON (--json)
gustyc --check <src> | check file1.gy ... # mypy-style type-check without executing
gustyc --oracle '<src>' | --oracle-file prog.gy # interpreter + compiled backend + CPython, one verdict
gustyc --json ... # machine-readable JSON output
gustyc --schema # print the JSON Schema for AST/IR dumps
gustyc --lang # self-describing feature list
gustyc --variance # JSON variance table (list/dict invariant, Sequence covariant, Callable params contravariant)
gustyc --effects <src> | effects file1.gy ... # per-function effect signatures: effects performed, return shape, termination (--json for the document)
gustyc --jit "..." # in-process dlopen JIT path
gustyc --gc-stats --file prog.gy # report what the collector did (stderr; --json adds a gc member)
gustyc --bench '<src>' --bench-runs N --bench-opt L # wall-clock benchmark
gustyc --fmt <src> | --fmt-check <src> | --fmt-file <path> # canonical formatter
gustyc --lsp # stdio language server (hover, completion, diagnostics)
gustyc --stdlib <dir> # set stdlib root (default: ./stdlib)
gustyc --version # compiler version
gustyc # start the interactive REPL
Exit codes are deterministic (full contract in docs/operations.md § Exit codes):
| Code | Meaning |
|---|---|
| 0 | success / clean (check, fmt-check, an --oracle run that matches CPython) |
| 1 | compile error — parse, analysis, a codegen refusal, or llc/cc failed; the program never ran |
| 2 | LLVM rejected the module we emitted (a compiler bug, not a source error) |
| 3 | runtime error — the program compiled, ran, then trapped |
| 4 | CLI usage error (bad/unknown flags, no source, unreadable file) |
| 5 | benchmark regression (the --bench-baseline gate fired) |
| 6 | the oracle leg: the program ran and printed something other than what CPython prints |
| 7 | the oracle leg: CPython could not run the source, so there is no verdict |
- Unit tests — lexer, parser, semantic, codegen, and runtime tests in
pkg/lang/; benchmarks (Benchmark*) and Go-native fuzz targets (Fuzz*) for the record. - Integration suite —
integration/drives the full pipeline (lex → parse → typecheck → codegen → run) and asserts stdout matches expected output. - The suite runs sharded —
make testshards(and CI) runs the cases in one process per core throughtools/testshards. The reason is arithmetic rather than taste: nearly every case ends inside a subprocess —llc-20lowering the module (58 ms),cclinking it (13 ms),lli-20running it (57 ms),python3answering the reference leg — against 1 ms of Go, so a serial suite is a single-core suite andpkg/langdid not fit a four-core runner ingo test's ten minutes (it died with a timeout panic naming the 70 ms case that happened to be running, not the slow suite). The split comes fromgo test -list, every case runs exactly once, and each case asserts what it always asserted;-listprints the partition and-jsonreports what ran, how long and what failed.make testremains the plain serial command, and is the one for the artifact-writing modes (GUSTY_GOLDEN_UPDATE,GUSTY_GOLDEN_MISSING), which write per-run ledgers. ADR 0313; ADR 0312 gives every toolchain call a budget (GUSTY_TOOL_TIMEOUT,GUSTY_ORACLE_TIMEOUT) so a tool that stops answering fails as exit 8 instead of hanging the run. - Conformance matrix —
integration/conformance_cases.go+conformance-matrix.json: 170 rows over two legs — the LLVM AOT binary and CPython — 131 asserting parity and 39 recorded without it (the probe and merged rows, which record an answer rather than assert one), the oracle verdict being 116match, 33debtand 21not_applicable. Witness-leg parity was necessary but not sufficient: two witnesses that share a bug agree, and this project's history is the proof (print(True)printed1everywhere,len("café")printed5). A row is conformant when the compiled backend prints what CPython prints. Each case declares its state —match(the default),debt(with a reason, a roadmap owner, and a per-leg pin of the wrong answer), ornot_applicable(gusty-only surface the oracle cannot run) — and drift fails the build in both directions, so a new divergence and an unrecorded fix are equally caught (roadmap L11.9, ADR 0186). Corpus growth follows a standing rule (ADR 0190): every feature ships its least interesting program — the tutorial one,print([1, 2]),xs.sort()— because a corpus grown from bug reports only tests what we already had reason to doubt. The oracle's first catch was not a refusal but a passing build:xs[0] = "z"on a mixed list answered[1, 'a', None], the interned index printed through the slot's stale tag, and it is now ADR 0187 and two parity programs (mixed_element_reads.gy,mixed_element_writes.gy). Its third catch went the other way:xs == ysfor two equal lists answered False on the compiled backend (the comparison compared heap handles), while[0] == ["zero"]answered True — an interned index matching a number. Containers now compare by value, element by element, as(payload, tag)pairs (ADR 0189). The same sweep's second find is closed too:xs.sort(),xs.reverse()andsorted(xs)are now language surface on both backends, with one comparator that orders interned strings by their text rather than by the index they were interned at (ADR 0191). And[f(x) for x in range(5)]— a comprehension whose element is a call — now compiles: the AOT path had made the constant folder the meaning of a comprehension, and the ordinary list-building idiom refused with "comprehension element must be constant" while the record ran it happily (ADR 0192). Its second catch was a crash in the most ordinary program in the corpus —print([1, 2])handed the static elements global toprintfas ani32andllcrefused the module, whileprint(set())printed the handle0andprint([["a"], ["b"]])printed[1, 2](ADR 0188). Parity cases includeprograms/truthiness.gy(Python's truthiness rules),programs/subscript_assign.gy(container iteration andd[k] = v/xs[i] = vitem assignment),programs/container_methods.gy(xs.pop(),set()/list()/dict(),s.add/s.discard) andprograms/none_values.gy(Noneas a singleton, void functions returningNone,f() == None) — seedocs/language.md§ Truthiness / § None / § Iterating and mutating containers / § Container methods. The probes are the roadmap's measured TODO list: nested and heterogeneous containers, tuples, negative indexing (including the one that panics the compiler), code-point strings, stdlib constant types, floored///%,sorted/enumerate, calling a function through a parameter,print(set()), and print atomicity.tools/oracleprobeprints the three legs for any program, which is how a ledger row is written from data. - The Python-visible surface survey (Phase 12, 2026-10-01) — 76 programs written the way
someone writes Python, each run through
--interp,--aotand CPython 3.12.3, classify the language into the states a construct is allowed to be in: 9 CPython-equal on the compiled backend, 37 absent (both legs refuse, honestly), 16 refused by the compiled backend alone (5 of those emit a modulellcrejects, which is exit 2 rather than a refusal), 8 that run everywhere and answer wrong, 2 that hang or iterate nothing. The last two groups are what Phase 12 exists to delete: its rule is that every construct is implemented, refused by a stable code, or absent from--lang— never "parses, runs, prints something". RowsL12.1–L12.13inroadmap.md, method and per-class examples indocs/roadmap-details.md§ Phase 12; making the census a standing artifact instead of a dated measurement isL12.13. - Property testing — seeded deterministic whole-program generation.
- LLVM 20 —
llc-20for lowering andopt-20for the optimization pipeline (the AOT backend emits textual opaque-pointer IR verified by these external tools). - Go — build with the LLVM 20 tag:
go build -tags=llvm20 ./...
go test -tags=llvm20 ./pkg/... # unit tests, serial — the form for the ledger-writing modes
make testshards # the same cases, one test process per core (what CI runs)
go run ./tools/testshards -tags llvm20 -json ./pkg/... # the same, as data- C linker —
cc/gccto link the emitted native object into a binary.
pkg/lang/ compiler: lexer, parser, semantic (types), codegen, jit runtime
cmd/gustyc/ the CLI + REPL
integration/ end-to-end pipeline + conformance suite
docs/ language spec (language.md) + operations guide (operations.md)
stdlib/ on-disk modules: math.gy, string.gy, collections.gy, json.gy
The language spec lives in docs/language.md; the toolchain & CLI reference
in docs/operations.md; the status of every item, gap and queued next step in
roadmap.md, with the reasoning behind each row in docs/roadmap-details.md.