!~ATH (pronounced "until death") is an esoteric programming language where all control flow is predicated on waiting for things to die. Inspired by the fictional ~ATH language from Homestuck. Loops wait for entitites to die, then computation happens in death callbacks. The language is deliberately inconvenient.
This repository houses two Homestuck-inspired languages: !~ATH (everything under ath/, and the bulk of this README) and !^CAKE, a baking-themed schema definition language (see cake/).
The primary implementation of !~ATH is a transpiler to C89 that compiles via CPS transform. It is self-hosting, the transpiler itself is written in !~ATH. See the Usage section for how to compile your code.
There is also a deprecated Python interpreter for !~ATH 1.3, and a partial deprecated JavaScript interpreter for !~ATH 1.3.
The JavaScript interpreter powers a Web playground for !~ATH 1.3.
The full !~ATH spec is located at ./ath/athSpec.md, but a quick reference is located below.
Entities are mortal things that can be waited upon. Each entity is either ALIVE or DEAD. Create entities with import:
import timer T(1s); // dies after 1 second
import timer T2(500ms); // dies after 500 milliseconds
import process P("cmd"); // dies when process exits
import connection C("host", 80); // dies when connection closes
import watcher W("file.txt"); // dies when file is deleted
import watcher Lib("lib.~ATH"); // loads .~ATH file as module
import portal Raw("raw"); // raw IPv4 socket (craft whole packets; needs root)
import portal Sock("datagram", 9000); // UDP socket, bound to :9000 to receive
THIS is an implicit entity representing the program itself. Kill entities manually with .DIE(). The program ends when THIS.DIE(); is called.
The fundamental control structure. Waits for an entity to die, then runs the EXECUTE clause:
import timer T(1s);
~ATH(T) {
} EXECUTE(UTTER("Timer died!"));
THIS.DIE();
Combine entities with && (both must die), || (either dies), or ! (dies immediately when created):
~ATH(T1 && T2) { } EXECUTE(...); // wait for both
~ATH(T1 || T2) { } EXECUTE(...); // wait for either
~ATH(!T) { } EXECUTE(...); // runs immediately
Split execution into concurrent branches:
bifurcate THIS[LEFT, RIGHT];
~ATH(LEFT) {
// code for left branch
} EXECUTE(VOID);
~ATH(RIGHT) {
// code for right branch
} EXECUTE(VOID);
[LEFT, RIGHT].DIE();
BIRTH x WITH 5; // mutable variable
ENTOMB PI WITH 3.14159; // constant (immutable)
x = x + 1; // reassignment
42, -7 // INTEGER
3.14, -0.5 // FLOAT
"hello\nworld" // STRING (escapes: \\ \" \n \t)
ALIVE, DEAD // BOOLEAN (truthy/falsy)
VOID // absence of value
[1, 2, 3] // ARRAY
{name: "Karkat", age: 6} // MAP
STACK(n) // fixed-size LIFO sylladex
QUEUE(n) // fixed-size FIFO sylladex
TREE() // unbounded BST sylladex (TREE(ALIVE) for AVL)
HASHMAP(n) // fixed-size key-indexed sylladex
OUIJA(n) // random-slot sylladex
BOTTLE(n) // single-use-slot sylladex
TECHHOP(g, s, gp, sp) // 2D predicate-routed sylladex
JUJU(n) // cross-branch sylladex
Sylladices are mutable structured collections where reads consume values. Write with CAPTCHALOGUE, read with EJECT. No random access or iteration. To traverse, eject repeatedly.
CAPTCHALOGUE value INTO S; // STACK, QUEUE, TREE, OUIJA, BOTTLE, TECHHOP
CAPTCHALOGUE value WITH key INTO H; // HASHMAP (WITH key required)
CAPTCHALOGUE value INTO J SLOT n; // JUJU (SLOT required)
EJECT FROM S // STACK, QUEUE, OUIJA
EJECT FROM B // BOTTLE: lowest non-dead slot
EJECT SLOT n FROM B // BOTTLE: specific slot (becomes dead)
EJECT SLOT n FROM H // HASHMAP: by physical slot index
EJECT "key" FROM H // HASHMAP: by name (returns VOID on miss/collision)
EJECT ROOT FROM T // TREE: removes all, returns in-order ARRAY
EJECT LEAF FROM T // TREE: removes leftmost deepest leaf
EJECT GROOVE g SHADE s FROM TH // TECHHOP: specific cell (required)
EJECT SLOT n FROM J // JUJU: must be different branch from writer
| Type | Size | Write inserts at | Read returns from | Overflow |
|---|---|---|---|---|
| STACK | fixed | front (slot 0), others shift right | front (slot 0) | last slot discarded |
| QUEUE | fixed | front (slot 0), others shift right | back (slot n-1) | last slot discarded |
| TREE | unbounded | BST position (string-coerced comparison) | ROOT (all, sorted) or LEAF (deepest left) | never overflows |
| HASHMAP | fixed | abs(hash(key)) % size |
by key or by slot index | collision discards old pair |
| OUIJA | fixed | random slot | random slot | random slot discarded |
| BOTTLE | fixed | lowest empty slot | lowest non-dead slot (then slot dies) | discarded if no empty slots |
| TECHHOP | fixed 2D | lowest valid cell per predicate rites | explicit (groove, shade) required | discarded if no valid empty cell |
| JUJU | fixed | explicit SLOT n; records writer branch | explicit SLOT n; different branch required | error if slot occupied |
BIRTH S WITH STACK(3);
CAPTCHALOGUE 1 INTO S;
CAPTCHALOGUE 3 INTO S;
CAPTCHALOGUE 5 INTO S;
// S is STACK[5, 3, 1]
BIRTH top WITH EJECT FROM S;
UTTER(top); // 5
UTTER(STRING(S)); // STACK[3, 1, VOID]
THIS.DIE();
BIRTH J WITH JUJU(2);
bifurcate THIS[CALIBORN, CALLIOPE];
~ATH(CALIBORN) {
CAPTCHALOGUE "HELLO WORLD" INTO J SLOT 0;
import timer TA(10ms);
~ATH(TA) { } EXECUTE(VOID);
} EXECUTE(VOID);
~ATH(CALLIOPE) {
import timer TB(5ms);
~ATH(TB) { } EXECUTE(
BIRTH msg WITH EJECT SLOT 0 FROM J;
UTTER("Calliope got:", msg); // Calliope got: HELLO WORLD
);
} EXECUTE(VOID);
[CALIBORN, CALLIOPE].DIE();
A sylladex is truthy if it contains any non-VOID value. A dead JUJU is always falsy. COUNT(s) returns the number of non-VOID occupied slots/nodes. TYPEOF(s) returns the uppercase type name ("STACK", "TREE", etc.).
+ - * / % // arithmetic (/ is integer div for ints)
& | ^ ~ << >> // bitwise (AND, OR, XOR, NOT, shifts)
== != < > <= >= // comparison
AND OR NOT // logical (short-circuit)
arr[0] map["key"] map.key // indexing
SHOULD condition {
// if truthy
} LEST {
// else
}
No loops in the expression language. Use ~ATH for iteration:
RITE countdown(n) {
SHOULD n > 0 {
UTTER(n);
import timer T(1s);
~ATH(T) { } EXECUTE(countdown(n - 1));
}
}
countdown(5);
THIS.DIE();
RITE add(a, b) {
BEQUEATH a + b; // return value
}
BIRTH sum WITH add(2, 3);
ATTEMPT {
BIRTH x WITH PARSE_INT("bad");
} SALVAGE error {
UTTER("Error: " + error);
}
CONDEMN "Something went wrong"; // throw error
Sessions are the !~ATH foreign-function interface. A shared library is another universe in paradox space. import session M(libpath) { ... } opens a shared library via dlopen and exposes transcribed C functions as M.foo(...). The session is itself an entity whose death can be awaited; ~ATH(M) waits for it to die, and M.DIE() triggers orderly cleanup.
import session Lc("libc.so.6") {
TRANSCRIBE getpid() -> INTEGER;
TRANSCRIBE strlen(STRING) -> INTEGER;
TRANSCRIBE fopen(STRING, STRING) -> RELIC DROPS fclose;
TRANSCRIBE fclose(RELIC) -> INTEGER;
}
UTTER("pid =", Lc.getpid());
BIRTH f WITH Lc.fopen("/tmp/out", "w");
// fclose runs automatically when Lc.DIE() tears down the session.
Lc.DIE();
~ATH(Lc) { } EXECUTE(UTTER("session collapsed"));
THIS.DIE();
Type tags: INTEGER, FLOAT, BOOLEAN, STRING, VOID, RELIC (opaque void*), BUFFER (mutable byte array), and CALLBACK(types) -> type for C-calls-into-!~ATH closures. DROPS attaches a destructor that runs in LIFO order at orderly death. A foreign fault (SIGSEGV / SIGBUS / SIGFPE / SIGILL) becomes a catchable runtime error and triggers session death (best-effort, not isolation; add UNSAFE after session to disable for debugging). See ath/athSpec.md for the full semantics.
UTTER("Hello", x); // print (space-separated, newline appended)
BIRTH line WITH HEED(); // read line from input
BIRTH s WITH SCRY(VOID); // read STDIN until EOF
BIRTH f WITH SCRY("filename"); // read file
INSCRIBE("file.txt", s); // write file
TYPEOF(x) // "INTEGER", "FLOAT", "STRING", etc.
LENGTH(arr), LENGTH(str) // length of array or string
PARSE_INT("42") // string to integer
PARSE_FLOAT("3.14") // string to float
STRING(42) // value to string
INT(3.7) // float to integer (truncates)
FLOAT(42) // integer to float
CHAR(65), CODE("A") // int to char / char to int code
BIN(10), HEX(255) // int to binary/hex string
APPEND(arr, val) // add to end (returns new array)
PREPEND(arr, val) // add to start
SLICE(arr, start, end) // subsequence
FIRST(arr), LAST(arr) // first/last element
CONCAT(arr1, arr2) // concatenate arrays
KEYS(map), VALUES(map) // get keys/values as arrays
HAS(map, key) // check if key exists
SET(map, key, val) // set key (returns new map)
DELETE(map, key) // remove key
SPLIT("a,b,c", ",") // split to array
JOIN(arr, ",") // join array to string
SUBSTRING(s, start, end) // extract substring
UPPERCASE(s), LOWERCASE(s), TRIM(s)
REPLACE(s, old, new) // replace all occurrences
RANDOM() // random float 0 to 1
RANDOM_INT(min, max) // random integer in range
TIME() // Unix timestamp in ms
COUNT(sylladex) // number of non-VOID values held by a sylladex
BUFFER(n) // FFI: allocate a mutable n-byte buffer
BYTE_AT(b, i) // FFI: read byte at index (0–255)
SET_BYTE(b, i, v) // FFI: write byte at index
BUFFER_TO_STRING(b[, n]) // FFI: copy buffer bytes to string
STRING_TO_BUFFER(s) // FFI: copy string bytes to fresh buffer
BANISH x; // FFI: free a RELIC (run destructor) or BUFFER
portal entities are raw/datagram sockets for crafting and sending your own packets (POSIX-only). Header layouts are best described as DENSE IMPERIAL !^CAKE recipes (packed, network byte order); see ath/apps/rawpacket/ for a full IPv4+UDP crafting example.
SENDIFICATE(P, buf, host, port) // send buf's bytes to host:port; returns bytes sent
APPEARIFY(P, buf) // pull-based receive into buf; returns bytes read
RECKON(buf) // RFC 1071 internet checksum over whole buffer
RECKON(buf, offset, length) // ...or over a slice (for pseudo-header checksums)
All commands run from ath/transpiler-to-c/:
# Transpile stdin → C89 stdout
./athtoc-bin < program.~ATH > program.c
# Build the runtime library and link against it (recommended)
make lib
gcc -std=c89 program.c -L. -lath_runtime -Iruntime -lffi -ldl -o program && ./programLinking the runtime requires libffi and libdl (Arch: libffi; Debian/Ubuntu: libffi-dev; Windows: included in the repo). Put -lffi -ldl after the sources/library so the linker resolves them.
If you'd rather compile the runtime sources directly instead of using make lib, exclude runtime/test_runtime.c — it has its own main (it's the runtime's standalone test driver) and will collide with your program's main:
# Compile and run (note: skip test_runtime.c)
gcc -std=c89 program.c $(ls runtime/*.c | grep -v test_runtime) \
-Iruntime -lffi -ldl -o program && ./programWorks with any C89-compatible compiler (gcc, clang, etc.). The repo ships four pre-built bootstrap binaries:
| Binary | Target |
|---|---|
athtoc-bin |
x86_64-pc-linux-gnu |
athtoc-bin-i686 |
i686-pc-linux-gnu |
athtoc-bin-win64.exe |
x86_64-pc-windows-gnu |
athtoc.wasm |
wasm32-wasi (WASI module, run under wasmtime) |
# Rebuild athtoc-bin from source (x86_64)
make
# Build the i686 (32-bit) binary
# Requires a multilib gcc and 32-bit libffi:
# Arch: pacman -S lib32-libffi
# Debian/Ubuntu: apt install gcc-multilib libffi-dev:i386
make bin-i686
# Build the Windows x86_64 binary (cross-compile)
# Requires mingw-w64-gcc (Arch: pacman -S mingw-w64-gcc).
# Vendored libffi (MSYS2 package) is in vendor/win64/libffi/
make bin-win64
# Build the self-hosting WASM transpiler, athtoc.wasm
# Requires a wasi-sdk clang + wasmtime (Arch: pacman -S wasi-libc wasi-compiler-rt
# wasi-libc++ wasmtime; or set WASI_SDK=/opt/wasi-sdk for a monolithic install).
make bin-wasmIf you're on a non-supported platform, you'll need an athtoc-bin cross-compiled from another machine to bootstrap.
Current limitations of the implementation (may be worked around in the future):
- Integers are C
long(64-bit on LP64 systems, 32-bit on Windows LLP64 and on WASM LP32), not unbounded - Strings are byte arrays;
LENGTHandSUBSTRINGoperate on bytes, not Unicode codepoints - Sync rites recurse on the C call stack; deep recursion will stack-overflow
- The FFI supports at most 16 parameters per transcription;
BUFFERandCALLBACKas return types are not supported - On Windows: FFI INTEGER maps to C
long(4 bytes, not pointer-sized); useRELICfor pointer-sized Windows API arguments (HWND, HANDLE, etc.), andSCOOPa zeroedRELICfield out of a baked !^CAKE recipe when you need a NULL pointer. Seeath/apps/winbox/winBoxDemo.~ATH, which callsuser32.dll'sMessageBoxAdirectly with no wrapper. - On WASM: no FFI/sessions, no
process/connectionentities, andwatcheris limited to--dir-granted paths portalentities (raw/datagram sockets;SENDIFICATE/APPEARIFY/RECKON) are POSIX-only;"raw"mode needsCAP_NET_RAW/root. On Windows and WASMimport portalraises a catchable runtime error
You can (cross-)compile programs with the MinGW-w64 toolchai:
ath/transpiler-to-c/athtoc-bin-win64.exe < program.~ATH > program.c
x86_64-w64-mingw32-gcc -std=c89 -O2 program.c \
$(ls ath/transpiler-to-c/runtime/*.c | grep -v test_runtime) \
-Iath/transpiler-to-c/runtime \
-Iath/transpiler-to-c/vendor/win64/libffi/include \
-Wl,-Bstatic ath/transpiler-to-c/vendor/win64/libffi/lib/libffi.a \
-Wl,-Bdynamic -lws2_32 -static-libgcc \
-o program.exe
program.exe
Session imports on Windows use LoadLibraryA/GetProcAddress instead of dlopen/dlsym. Signal-fault protection is disabled on Windows (all sessions behave as UNSAFE); foreign faults crash the process rather than being caught as a recoverable error.
athtoc.wasm is a WASI module that reads .~ATH on stdin and emits C89 on stdout just like the native binaries. Transpiled programs compile to standalone .wasm against libath_runtime_wasm.a. You need a wasi-sdk clang and wasmtime (Arch: pacman -S wasi-libc wasi-compiler-rt wasi-libc++ wasi-libc++abi wasmtime; or a monolithic wasi-sdk via WASI_SDK=/opt/wasi-sdk).
wasmtime run -W exceptions=y ath/transpiler-to-c/athtoc.wasm < program.~ATH > program.c # (-W exceptions=y is required: setjmp/longjmp lower to the wasm exception-handling proposal)
# Compile + run
cd ath/transpiler-to-c && make lib-wasm
clang --target=wasm32-wasi --sysroot=/usr/share/wasi-sysroot -std=c89 -O2 \
-mllvm -wasm-enable-sjlj -mllvm -wasm-use-legacy-eh=false \
program.c -Iruntime libath_runtime_wasm.a \
-lsetjmp -Wl,-z,stack-size=268435456 -Wl,--stack-first -o program.wasm
wasmtime run -W exceptions=y -W max-wasm-stack=1073741824 --dir .::. program.wasmOn WASM, foreign sessions (FFI) and process/connection entities are unavailable (they raise a catchable runtime error); watcher works only within --dir-granted paths; and TIME() returns a positive 32-bit value rather than a full Unix-epoch timestamp.
cd ath/transpiler-to-c
make test # runs the harness over all cases on every target
make test-linux # Linux/native only
make test-win64 # Windows only, via wine + mingw (requires wine + mingw-w64-gcc)
make test-wasm # WebAssembly/WASI only, via wasmtime + wasi-sdk clang
make smoke # quick hello-world sanity checkThe test harness is itself an !~ATH program (tests/harness.~ATH).
No specialized tools beyond existing C debugging tools exist for !~ATH.
The deprecated Python interpreter at ath/deprecated/python-interpreter/ includes a stepping debugger (--step), TUI debugger (--tui), and non-interactive JSON trace mode (--trace). These are useful for debugging !~ATH 1.3 logic. Run from that directory with python3 untildeath.py --help.
This project is licensed under the GNU General Public License v2.0. See the LICENSE file for details.