Aiksi, the system language
Everything above the kernel is written in Aiksi. The intended relationship is C to Unix, or HolyC to TempleOS: GLaDOS is written in Rust, and Aiksi is how anything that is not the kernel reaches it. A program is code.ai&xi. The extension is deliberately unusual and costs nothing. Nothing on the host claims it, the shell does not parse &, and the path resolver is a plain splitter.
Source becomes tokens, tokens become a tree, and the tree is walked. That is three files and no intermediate representation, and it was chosen for a reason that paid off later: a code generator written against the same tree can be checked against the same results, which is much easier than debugging a code generator with nothing to compare against.
Records are values
use "/lib/text"
rec Host { name: str, port: int }
fn reachable(h: Host): int {
if (tcp_connect(h.name, h.port, 600)) { tcp_close() return 1 }
return 0
}
A record is a declaration and a constructor in one, so the name becomes callable with the fields in order and the constructor's arity is the declaration's by construction.
Records are values, like lists. b = a copies, and a.x = 9 afterwards leaves b alone. That single decision is why nothing in this language has to explain aliasing, and it is also why a.b.c = 1 is refused: there is no shared object to reach through, so only a plain variable can be assigned back to. Refusing is the honest answer. Accepting it and dropping the write is the version that costs somebody an afternoon.
Types are optional and never inferred
Absent means any, so every program written before types existed still means what it meant. They are checked where a value crosses a boundary somebody annotated: a call, a return, a record field at construction and at assignment.
Inference would mean a solver. The thing actually worth having is much smaller, and it is that a model passing a string where a number belongs gets f wants int for 'a', got str. Without it, int() quietly answers 0 and the wrong number surfaces four calls later. That is the whole return on the feature, and a solver is not needed to collect it.
Imports cannot be an escalation
use is textual inclusion that happens once. There is nothing to qualify against, and inventing a prefix would mean inventing a spelling and then explaining it.
The imported program runs with the importer's capabilities, and that is the security property. Capabilities live on the interpreter, and there is one interpreter, so an import can never grant more than the importing program already had. The jail on top, where a sandboxed program may use only its own files or /lib, is therefore about legibility: it keeps a stored program's dependencies somewhere a person can find them.
Cycles terminate because a path is marked imported before it is evaluated. That ordering matters more here than in most languages: running out of stack in ring 0 with no guard page is a triple fault, which is not an error message but an instant reboot.
The allowlist, and why it replaced two denylists
Every builtin is a row of (name, Touch, min args, max args), and builtin refuses anything absent from that table before dispatch. The two failure modes are then both harmless. An arm added to the match without a row is unreachable, which is dead code. A row without an arm answers "no implementation", which is broken but harmless.
It replaced two denylists that were correct for eleven raw builtins and stopped being correct the moment the language was wired to the network. A denylist grants by default, so the builtin anybody forgets is exactly the one that matters.
Touch has seven classes and the sandbox question stays binary: Pure, Read and Write are allowed to a stored program, and everything else needs app trust. That follows Manifest.raw, which carries one bit for the reason it states. An operator approving a request has to hold the whole of it in their head, and "may write outside itself but not open sockets" is a sentence nobody can check against a program. The line for Net is whether a packet leaves the machine, which is why net_ifaces is Read and tcp_connect is not.
words prints the table grouped by class. That is the reference.
Builtins are named after the Rust path
Flattened, and without exception. crate::net::tcp::connect is tcp_connect; crate::dev::rtc::now is rtc_now.
The audience is a 0.6B model and whoever is reading the kernel source beside it, and both can apply a rule they were told once to a subsystem they have never seen. A hand-picked name per builtin reads better in isolation and has to be memorised one at a time, which is the cost that actually matters at this scale. Where the rule reads badly the rule still wins, because one exception means every name has to be checked against a list again.
What the kernel hands back
KERNEL_RECS declares the record types the kernel itself returns, and they are known to every interpreter so an annotation checks against something real. pci_list answers a list of Device. It answered text only because there was nowhere to put a field, and every caller then wrote the same fragile split to take it apart.
A program may not redeclare one of these. A builtin would go on returning the kernel's shape while every annotation in the program checked a different type of the same name, which is the kind of disagreement that produces a wrong field and no error.
Everything that is genuinely a struct answers a record: Device, Time, Iface, plus net_config, mem_stats, task_list, stat and tcp_status. Atomic answers stayed atomic, deliberately. mem_used() is not improved by becoming mem_stats().used, and converting scalars into records for uniformity would make the language worse to make a rule tidy. The test is whether a caller would otherwise re-parse: substr(rtc_now(), 11, 2) to get an hour, or a character-by-character line counter to count what ls reported. The seeded /ai/tools/count tool contained exactly that second one, and calls len(ls(path)) now.
Two shapes answer nil: rtc_now when the clock cannot be read, and stat on a path that does not exist. A record whose fields all read as "absent" is indistinguishable from a real empty one, and a program checking existence would have to know which field to trust.
kernel::rec builds a record by name and checks it against KERNEL_RECS, so an arm that adds a field without adding it to the shape, or gets the order wrong, fails there, before it can hand back an Iface whose .ip is its netmask. Both are strings, so that mistake is invisible at a glance.
Three bounds, and why each exists
range and repeat are capped at 65,536. They are the easiest way for a generated program to ask for a billion-element list, and this kernel has no OOM killer and one address space, so the step budget never gets to see the single call that takes the heap.
Socket timeouts are clamped to 30 seconds, because an unbounded one in a repaint path hangs the desktop and the step budget cannot see a blocking call either.
And app::document runs under a smaller draw budget. Its own comment claimed for a long time that this was because it runs per repaint, and that was wrong. desk::refresh_routed is the only caller and rebuilds a window's panel after a command runs, since a command is the only thing that changes what a route would produce. The bound is right and the reason was wrong, which is worth recording as its own kind of bug.
What it reaches
Text, integer arithmetic, lists, the namespace, the clock and counters, tasks and memory, PCI, network status, sockets, the model, the framebuffer, and raw memory and I/O ports.
There are no floats. Adding them for one builtin changes every arithmetic path in the language, and nothing that Aiksi is for has needed them: the arithmetic here is indices, counts, ports and byte offsets.
The split between the two files that implement all this is deliberate. eval.rs owns the gate, the arity check and the table; kernel.rs owns the arms that reach subsystems. That keeps "what may a program do" to one screen, and it means adding a subsystem cannot accidentally edit the gate.
