Skip to content

Latest commit

 

History

264 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

!~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/).

Implementation

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.

!~ATH Spec and Reference

The full !~ATH spec is located at ./ath/athSpec.md, but a quick reference is located below.

Quick Reference

ENTITIES

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.

~ATH LOOPS

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

BIFURCATION

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();

VARIABLES

BIRTH x WITH 5;           // mutable variable
ENTOMB PI WITH 3.14159;   // constant (immutable)
x = x + 1;                // reassignment

DATA TYPES

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

Sylladices are mutable structured collections where reads consume values. Write with CAPTCHALOGUE, read with EJECT. No random access or iteration. To traverse, eject repeatedly.

Write

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)

Read

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

Per-type summary

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

Example (STACK)

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();

Example (JUJU — cross-branch communication)

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();

Truthiness and COUNT

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

OPERATORS

+ - * / %                     // 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

CONTROL FLOW

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();

RITES (FUNCTIONS)

RITE add(a, b) {
    BEQUEATH a + b;       // return value
}
BIRTH sum WITH add(2, 3);

ERROR HANDLING

ATTEMPT {
    BIRTH x WITH PARSE_INT("bad");
} SALVAGE error {
    UTTER("Error: " + error);
}

CONDEMN "Something went wrong";  // throw error

FOREIGN SESSIONS (FFI)

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.

BUILT-IN RITES

I/O
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
Type operations
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
Array operations
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
Map operations
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
String operations
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
Utility
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
Portals (raw networking)

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)

Usage

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 && ./program

Linking 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 && ./program

Works 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-wasm

If 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; LENGTH and SUBSTRING operate 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; BUFFER and CALLBACK as return types are not supported
  • On Windows: FFI INTEGER maps to C long (4 bytes, not pointer-sized); use RELIC for pointer-sized Windows API arguments (HWND, HANDLE, etc.), and SCOOP a zeroed RELIC field out of a baked !^CAKE recipe when you need a NULL pointer. See ath/apps/winbox/winBoxDemo.~ATH, which calls user32.dll's MessageBoxA directly with no wrapper.
  • On WASM: no FFI/sessions, no process/connection entities, and watcher is limited to --dir-granted paths
  • portal entities (raw/datagram sockets; SENDIFICATE/APPEARIFY/RECKON) are POSIX-only; "raw" mode needs CAP_NET_RAW/root. On Windows and WASM import portal raises a catchable runtime error

Windows

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.

WebAssembly / WASI

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.wasm

On 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.

Test suite

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 check

The test harness is itself an !~ATH program (tests/harness.~ATH).

Debugging !~ATH programs

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.

About

The !~ATH esoteric programming language.

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages