Glass box pipeline
Tokens, AST, symbol table, and IR are each a named, machine-readable
output. Pipe saqut ast into your own tool, diff the optimized tree, or
build an LSP from saqut symbols alone.

A typical compiler exposes only its source input and its final output; the stages in between are not observable. saQut exposes each stage as a separate command that produces a named, machine-readable result:
saqut tokens hello.sqt # token stream (JSON)saqut ast hello.sqt # abstract syntax tree (JSON)saqut symbols hello.sqt # symbol table (JSON)saqut ir hello.sqt # 3-address IRsaqut run hello.sqt # compile & executeThe language is deliberately small and C-flavoured. It exists to give the inspectable pipeline something to compile.
--jit): translates the same instructions into native
machine code while the program starts. Loops and arithmetic run much faster.
It is experimental and must print exactly what the VM prints.free; --gc-stats shows what was collected.saqut lsp speaks it directly.saqut dap lets you pause a program and inspect its
variables.catch, with a code, a
message and a source location.import / export; reach files, paths, text encoding, dates, the terminal
and the process through fs, path, utf8, date, sys and friends.Write a file called hello.sqt:
int main() { print("Hello, saQut!"); return 0;}Run it (no class or boilerplate needed); execution starts at main:
saqut run hello.sqtLook inside at any stage you’re curious about:
saqut ast hello.sqt --jsonThe Getting Started guide covers building the compiler and writing longer programs.
Binary search over a sorted array. Each step halves the search range, so a 10,000-element array is decided in at most 14 comparisons.
int binarySearch(int[] values, int target) { int low = 0; int high = values.length() - 1;
while (low <= high) { // (high - low) / 2 instead of (low + high) / 2: // the second form can overflow int on a large array. int mid = low + (high - low) / 2;
if (values[mid] == target) { return mid; } if (values[mid] < target) { low = mid + 1; } else { high = mid - 1; } } return -1; // not found}
int main() { int[] sorted = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91];
print(binarySearch(sorted, 23)); // 5 print(binarySearch(sorted, 7)); // -1 return 0;}Functions, arrays, control flow, and print(). No mandatory scaffolding. The
same program produces the same output on the VM and on the JIT.
Glass box pipeline
Tokens, AST, symbol table, and IR are each a named, machine-readable
output. Pipe saqut ast into your own tool, diff the optimized tree, or
build an LSP from saqut symbols alone.
Procedural and explicit
C-family syntax. No OOP, no generics, no hidden type coercion. Integer and float mixing follows one stated rule. The compiler runs what you wrote.
Value and reference, no pointers
Primitives (int, float, bool, byte) copy. Compounds (string,
struct, array) share by reference. There is no user-visible & or *.
Nullable is opt-in
A value is non-null unless you declare it T?. The type checker
verifies that a non-null value cannot be null before you use it, so there is
no null check at run time.
Deterministic VM
The bytecode VM is the reference backend: the same input produces the same
output on every run. LLVM is intentionally not used. An experimental MIR JIT
runs via –jit and is required to match the VM byte for byte;
embedded-runtime AOT is planned next.
Catchable errors
try / catch / throw with struct-based
errors. Out-of-bounds access, divide-by-zero, and null derefs raise a
catchable error instead of terminating the program silently.
Source -> Tokenizer -> Parser -> AST -> Symbol Collector -> Type Checker -> Optimizer (constant folding + DCE) -> IR Generator -> Bytecode VM -> outputEach arrow corresponds to a CLI command you can run and inspect.
37542b1 | Threads arrive as a preview. thread { ... } starts a thread
that runs at once and gives you a Thread you can join(), stop() and
ask running(). Every thread has its own heap, its own garbage collector
and its own copy of the globals, so threads never touch each other’s
objects. The VM is the reference backend; the JIT is experimental.46f8c35 | New shared globals (int, float, bool, Pool, List)
pass data between threads. Reads and writes are atomic, and so are +=,
-=, ++ and --. A Pool is a bounded blocking queue, a List is an
append-only list; anything put into them is copied deeply.46f8c35 | lock a, b; takes locks in a fixed order, so two locks can
never deadlock each other, and releases them when the block ends. The
compiler checks the rules (E014–E019) and warns about a non-atomic
x = x + 1 on a shared variable (W008) and about blocking calls inside
a lock (W009).c5d2140 | Threads run on the VM and on the experimental JIT
(0b8eded), with examples and a short guide in docs/threading-guide.md.5fecf75 | print and file writes are emitted in one piece under a lock,
so lines from different threads do not tear.19b99aa | The debugger understands threads: it lists them, reports when
they start and exit, pauses every thread at a breakpoint, lets you inspect
a worker’s stack, and shows a Shared scope. A deadlock under the debugger
is reported instead of hanging.6578af2 | Programs that never use threads pay about 0.10% for all of
this, measured with hardware counters.72bd1ca | s[i] reads the i-th character of a string (zero-based, a
Unicode code point) as a one-character string, on the VM and the JIT alike.
Strings are immutable, so s[i] = ... is a compile error.72bd1ca | An array index must now be a non-null int, byte or
longint. d["x"], d[1.5], d[true] and int? used to be accepted and
did the wrong thing; indexing with an enum now needs as int.72bd1ca | The VM no longer cuts an array index to 32 bits:
d[4294967297] used to read and write d[1], while the JIT correctly
reported an out-of-range error. Both backends agree now.69913ef | 5 = 3;, 5 += 3; and 5 &= 3; used to compile. The left
side of an assignment must now be a variable, a struct field or an array
element (E027).8ae879d | int a = 1 ? 2 : 3;, 7 ? 5 and 5 ! 3 used to compile and
print 0. They are now a clear syntax error (E901).eb9937e | A variable named EOL no longer silently cuts the program
short.835f42e | The catch-all E003 is split into precise codes. E003 now
means only a type mismatch in an assignment, an argument or a return value;
the rest moved to E020–E027 (literals, operators, nullable, as,
switch, calls, method receivers, writable locations). Messages did not
change. Codes that tools may match on are listed in docs/compiler-errors.md.46f6026 | A command now rejects options it does not accept
(saqut tokens a.sqt --jit used to exit 0). The old file:, output: and
ast: prefixes are gone; use a plain path.360bad2 | saqut ast loads the whole module graph, so programs with
import no longer report a false E001/E007; it prints every unit.539885f | Completion is scope-aware and survives broken code: it
understands a.b.c, a[i], f(), enum names and the real element type of
a Pool/List, and falls back to the last good parse while you type.962785e | Project-wide index: find references and rename work across
files, workspace/symbol is supported, and completion offers exports you
have not imported yet and adds the import for you.3687e5c | Folding, symbol outline, greyed-out unused code, code actions,
code lenses (“References: N”) and inlay hints for parameter names.28f42c8 | The first project index of a 200-file import chain went from
2.84 s to 38 ms.2ce7a11 | Error squiggles now cover the offending token instead of the
whole line; rename covers struct types and fields; breakpoints move to the
next runnable line, stepIn enters the call, and stop-on-entry starts at
the first line of main.a5f5b46 | The VS Code extension (1.0.1) highlights shared, thread,
lock, unlock, wait, Pool, List and Thread, and ships thread
snippets.835f42e | The command list, option table, diagnostic codes, keyword
table, built-in method rules and opcode result types each now live in a
single place instead of several hand-synced copies, so adding one is a
one-file change.84495b8 | Release builds compile with zero warnings on GCC 14 and 16.34c22b5 | Programs that used to run with a silently wrong result now
stop with a clear error: missing operands (5 +), missing ;, unknown
escapes like "\x41", and the non-existent >>> operator.34c22b5 | Variables declared without a value start at a typed zero on
every loop iteration, on the VM and the JIT alike.34c22b5 | % works on float and double; compound assignments like
d *= k widen an int right-hand side correctly.34c22b5 | Endless recursion raises a catchable E_STACK_OVERFLOW instead
of hanging or crashing; the limit is set with --max-call-depth=N.34c22b5 | Accessing a field of a null struct raises a catchable E_NULL.34c22b5 | Modules have their own namespace: private names no longer clash
across files, import { x as y } works for source files, and a standard
library import is only visible in the file that imports it.34c22b5 | A decimal literal without context is a double, as documented.34c22b5 | The CLI rejects unknown options and stray arguments instead of
ignoring them; all runtime messages are in English.87068da | ** exponentiation, prefix ++x, and ++/-- on every
numeric type, array element and struct field.100979f | byte arithmetic wraps within 8 bits; an explicit as byte
cast still checks the range.1f4a119 | Optimization is on by default; --dont-optimize turns it off.b33d569 | One garbage collector shared by the VM and the JIT.2862123 | Strings use a single garbage-collected representation on both
backends.4ec9c2e | Runtime values shrank from 112 to 40 bytes.0a0847d | The JIT checks for errors only after instructions that can fail;
pure arithmetic became up to 57x faster than the VM.c54b157 | Array reads and writes in the JIT are direct memory accesses for
every packed element type.41efa5e | New standard library modules: path, utf8, process,
stdin/stdout/stderr, os and terminal.651b04f | readFile can read a slice of a file with seek and size.b7aa4d5 | Imports can be renamed: import { readFile as load } from fs;.a08c756 | try / catch / throw now work in the JIT backend with
the same output and exit code as the VM, including catchable runtime
errors like division by zero, failed casts, and out-of-range array
indexing. (The planned setjmp/longjmp design turned out to be
undefined behavior in a JIT; a per-instruction pending-error check is
used instead, and functions without a try still pay zero cost.)a68cad7 | Fixed a JIT garbage-collector bug: in recursive calls that
allocate, a live object could be swept and reused while still in use.
Shadow-stack frames now unwind properly per call.2095564 | The six test programs previously marked “JIT known broken”
are now required to match the VM byte for byte; parity is enforced
again in this area, with no exceptions.4568aac | The capability (permission) system has been removed
(ADR-043): host calls are open by default, and file operations now
work identically in the JIT.5aeec2b | String indexing and length() now count Unicode code
points instead of bytes.1cea6bf | upper() and lower() now do real Unicode case mapping.4bc1b2b | File contents can be handled as byte[] end to end.b2ed2f8 | 64-bit hex literals keep their exact bit pattern as
longint instead of being mangled.ec4174c | An uninitialized string now starts as "" instead of 0:
local variables, struct fields, and globals alike.b9f5c62 | A struct field with an array type now starts as an empty
array, so record.items.push(x) works without assigning a value first.690025b | A module-level int?, string?, or longint? you never
assigned now really reads as null. Before, x == null answered
false because the slot silently held 0.2469bbf | Ordering comparisons (<, >, <=, >=) on a nullable
value are now a compile error, same as arithmetic: pass the null gate
first (if (x != null) { x < 26 }). Equality (==, !=) is untouched.74296d6 | Using a built-in method without calling it (s.length
without parentheses) is now a compile error instead of silently
producing 0.dd8a493 | Statements at module scope (x = 5; outside any function)
are now rejected with a clear error, as in C++; the top level only
accepts declarations.030f4bd | Calling a function without its required arguments is now a
compile error; the language server also survives that input gracefully.907d904 | The JIT lowers hot array accesses to direct memory
operations and reuses compiled code, so array-heavy loops run faster.2645978 | The docs no longer advertise a ?: ternary operator the
language never had.2d6395c | --help now reads like a standard manual page, and
unimplemented commands were removed from the CLI entirely.ee102a3 | Fifteen error fixtures that existed on paper but were
never actually run are now wired into the test runner, so silent
failure classes get caught.c2266ad | An int or longint literal that does not fit the type is
now caught at compile time instead of silently wrapping to a different
value.d85c7ee | decimal comparisons now agree with decimal arithmetic,
so money math finally adds up.fd7d082 | Self-referential structs like Node { Node? next; } now
compile, and T? fields start out as null.7c8f678 | The JIT and the VM now give the same answer for decimal
values and the same message for runtime errors.3261321 | The VM stopped doing debugger bookkeeping on every single
instruction, so programs run about 1.9x faster.f4a60c8 | The parser reads each token once instead of six times.1a096ee | Source positions got three times smaller, so long file
paths no longer cost memory.76d2340 | bench now perturbs the program it measures by 1.4%
instead of 13%, and type checking shows up as its own stage.b576595 | Adds a harness that runs the same program through the AST,
IR, VM, and JIT and reports where they disagree.77e9276 | The parser now reports a clear error for a missing closing
), ], or } in 25 different situations.a01f46e | run, check, and ir now all return the same exit code
for the same kind of error, no matter which one you use.c7d1d94 | exec now reports parser errors instead of silently
continuing.fe90c0c | ast and symbols now report parser errors the same way
exec and run do.74e01a5 | saqut ir now shows every instruction kind in its output.c43e830 | When a function ends without an explicit return, saqut ir now shows that implicit return too.b48e0cf | saqut ir now only prints color codes when the output goes
to a real terminal, keeping piped or redirected output clean.f7a32f9 | The compiler now correctly recognizes when every branch of
a switch returns a value.54881c8 | Indexing a non-array value with [index] is now rejected
at compile time.7f871b7 | Adds a differential test harness that automatically checks
the VM and JIT agree on every result.2f31cb2 | Makes int32, longint64, float32, and double64 behave
identically on the VM and the JIT.🏆 saQut’s first official build happened here.
2b53460 | Ships the first version of the experimental MIR JIT
backend, enabled with the --jit flag.dcc8a04 | Full programs can now run end-to-end on the JIT backend.62651cf | The JIT can now compile and run float values.24acdc7 | The JIT can now compile and run string values.748f992 | 32-bit int overflow now behaves identically on the VM and
the JIT (defined, two’s-complement wraparound).574ce3f | Adds the saqut --version flag and a static release build.879a85e | saqut run --profile can now measure VM and JIT warmup and
run time separately.a785d2a | Adds FFI (foreign function interface) support: the ffi
keyword lets the language declare and call functions provided by the
host.2259bfb | Adds the fs module: read, write, check for, and remove
files.c3451f7 | Adds the sys module: generate random numbers, read
environment variables, sleep, and read command-line arguments.76c9bc6 | Adds the date type, which stores a UTC timestamp in
milliseconds.35790a2 | Adds the math::PI and math::E constants.5433cb3 | Adds the capability (permission) system for file, network, and
system access. With no --allow option all capabilities are enabled; when
present, --allow fs,net,sys acts as an explicit whitelist.1e0fbd3 | Adds caps::drop and caps::has: a running program can
voluntarily give up permissions or check which ones it currently has.04d90b5 | When a variable inside a block shadows one with the same
name from an outer scope, the compiler now resolves it to the correct
one.7c5d90a | The garbage collector (GC) now runs automatically once
memory use crosses a set threshold.587bb57 | Reworks how built-in functions are called: they can now be
called with dot syntax (value.method()) and are grouped into
namespaces.845588d | Adds the byte type: an 8-bit unsigned value, 0-255.025a667 | Rewrites the debugging protocol (DAP) correctly: editors
can now step through a saQut program, pause it, and inspect variables.e4abc6f | You can now actually pause a program while debugging it; it
waits without freezing (non-blocking).75e3dd1 | Stepping through code one line at a time now covers every
instruction and pauses on exactly the right line.d26575b | Makes autocomplete in the editor more reliable.11af652 | You can now safely rename variables and functions from the
editor, and see parameter hints while calling a function.b9ca166 | When two modules import each other in a cycle, the compiler
now reports it as an E_MODULE_CYCLE error.ad45bd5 | Lays the groundwork for editor support: syntax highlighting
(a TextMate grammar), a language server (LSP), and a debugging protocol
(DAP) scaffold.b66226b | The language server (LSP) can now compile the version of a
file currently open in the editor, without needing it saved to disk.f3a7853 | The language server now computes file positions
(line/column) far more reliably, including across multi-file projects.934a903 | Adds pastel coloring to the AST, symbol table, and IR
output.6651a91 | When a struct contains another struct as a field, all of
its nested fields are now set up automatically as soon as the variable
is declared.d6dd530 | Adds the saqut bench command.585b8eb | Commits to a second execution backend: a MIR-based JIT and
an embedded-runtime AOT compiler.8136773 | Adds enum type support.2af0bce | Adds the decimal type, fully supported by both the
compiler and the VM.878faa3 | Adds struct arrays (Point[]) and multi-dimensional
arrays.b9a6176 | Adds a built-in method system: E::method(args) syntax.4a84e59 | Adds a module system (import/export, multi-file builds) and
the exec command that goes with it.4a64812 | Tests the compiler against 37 real algorithms (sorting,
searching, and more).060b4a0 | Adds a hint to every error message showing how to fix the
problem.53823f1 | Rebuilds the garbage collector (GC): memory is now cleaned
up the same way, every time, right when a function returns.50c2615 | Struct fields declared as nullable (T?) are now parsed
and typed correctly.d7872f7 | Translates every user-facing message from Turkish to
English.b8c2d95 | Constant folding now runs without crashing the compiler.3baa5e3 | A function that returns a struct now has its return type
recognized correctly.8545ce4 | Bitwise operators (&, |, ^, <<, >>) are now fully
compiled to IR, with expanded test coverage.2bf52f5 | Finishes the lexer/tokenizer, which turns source code into
tokens. This is the compiler’s first stage.4839f37 | Completes the parser: expressions now build the correct
tree based on operator precedence.e488f29 | Runs fibonacci.sqt for the first time, through our own IR
generator and bytecode VM.d5a1b2c | Adds the type system and the engine that produces error and
diagnostic messages.c8ee926 | Adds the symbol table: resolves variable and function
names, and tracks scopes and references.36ad5d0 | Adds the optimizer: computes constant expressions at
compile time (constant folding) and removes code that can never run
(dead-code elimination).b02a7da | Adds string concatenation, try/catch/throw, and nullable
flow analysis.7b2258e | Adds array support, backed by an object model the garbage
collector (GC) can manage.fab82ee | Adds bitwise operators and IR generation for global
variables shared across a file.0.1.0 → 0.2.0 · 0.2.0 → 0.3.0 · 0.3.0 → 0.4.0 · 0.4.0 → 0.5.0 · 0.5.0 → 0.6.0 · 0.6.0 → 0.7.0 · 0.7.0 → 0.8.0 · 0.8.0 → 0.9.0 · 0.9.0 → 0.9.4 · 0.9.4 → 1.0.0