Documentation
Changelog
All notable changes to Eskiu are documented in this file.
The format is based on Keep a Changelog.
Versions follow MAJOR.MINOR.PATCH-stage (e.g. 0.0.9-alpha).
[0.9.2] - 2026-09-29
A full-project audit (codegen, type checker, self-host parity, stdlib, front end, driver
and docs) found about a hundred latent bugs that the existing corpus did not reach. All
are fixed lockstep in the C++ and self-hosted compilers unless noted, each with a
regression test. A second pass added three fuzzers (a C oracle, a negative corpus and an
ASan fuzzer for the stdlib parsers), which found about twenty more, and resolved the two
known limitations left from 0.9.1 (R and S). Seven more blind audit rounds followed, each
on a frozen tree and each fixed the same way, for about 500 fixes in total. The last
rounds concentrated on the C ABI, generics, volatile and the HTTP servers under hostile
input. What is still open is listed under Known issues.
Added
#pragma link("name")links the executable with-lname. The driver also adds the libraries a program implies: the stdlib's own pragmas (libmon Linux,pthread,ws2_32), the C++ exception runtime when the program throws or catches, andpthreadwhen it callsthread_create.-lm,-lc++/-lstdc++and-lpthreadare no longer needed.--no-default-libsturns all of this off.- Inline methods on generic structs (
struct Box<T> { T get() {...} }), instantiated per struct instance on first call. - Dot-calls on generic instances:
l.push(8)on aList<int>isList_push<int>(&l, 8), andch.send(v),m.get(k, &out)work the same way. The type arguments come from the receiver, then the call's arguments. The call is lowered as an ordinary call, so arguments convert, struct results usesretand a call insidetryunwinds. - C callbacks with structs by value: a top-level function passed to C as
(*void)fthat takes or returns a struct by value is reached through a thunk with the C convention. - Fuzzers:
tests/fuzz/c_oracle.py(programs compiled by both Eskiu compilers and by clang as C, outputs compared),tests/fuzz/neg_fuzz.py(one injected error per program, both compilers must give a located diagnostic) andtests/fuzz/stdlib_fuzz.py(the stdlib parsers under ASan). Each has a CI gate, andtests/selfhost/cabi_parity.shcompares the self-host's lowered C signatures with the C++ ones per target. #if/#elifwith C integer constant expressions (defined(X),defined X, arithmetic, shifts, comparisons,&&,||,?:). An identifier left after macro expansion counts as0, as in C.- Interface values. An interface is now a real
{data, vtable}value, so an interface-typed local, struct field, return value or assignment works (these compiled and then crashed before). The value refers to a struct through&x. constreceivers. A method declared withconst T* selfcan be called on aconstvalue.- C ABI for structs passed or returned by value across
extern: AArch64, x86-64 SysV, Windows x64 and 32-bit ARM, in both compilers (the self-host picks the convention from--target). New predefined macros__aarch64__,__x86_64__and__arm__follow the target. - Test runner: a
runor smoke test may have a C companiontests/NAME.cthat is compiled and linked in;tests/warnings/NAME.eskis a new lint kind that asserts the exact-Wallwarnings (// EXPECT-WARNING:lines). - The compiler builds against LLVM 23 as well as LLVM 22. The minimum is now LLVM 21.
- Generic async functions (
async T f<T>(...)), in both compilers. (void)exprevaluates an expression and discards its value, which also silencesmust_use.<http2_server>: theH2Serverengine (flow control, SETTINGS validation, stream states) serves both h2c and TLS connections.<future>gainsfree_future_polledfor a future whose waker was installed by hand, and<http>gainshttp_content_length.<json>and<regex>limit nesting to 512 levels (JSON_MAX_DEPTH,RE_MAX_DEPTH), so deep input fails cleanly instead of exhausting the stack.
Changed (may reject or change the behavior of existing programs)
- A non-constant global initializer is a compile error.
int g = f();, or a read of a non-constglobal, used to compile to a silent0. Constant expressions (arithmetic, casts, ternaries,sizeof, enum members,constvalues,&global) fold correctly. - Narrow integer operands promote to
intbefore arithmetic and comparison (the C integer promotions).(uint8)200 + (uint8)100is300, anduint8 200 > int8 -1is true. - Conversion to
boolis!= 0for integer, float and pointer sources. It used to keep only the low bit, so(bool)2was false. ptr - ptrcounts elements, not bytes (C++; the self-host already did).- Passing a struct by value where an interface is expected is an error; pass
&x. - Interface conformance checks method signatures (return type, arity, parameter types), not only method names.
- Duplicate and conflicting declarations are errors: same-scope locals, globals,
parameters, fields, enum members, structs with different fields, a second
default:, one name used for a function and a global (or a struct, or a method), and a prototype whose signature differs from its definition. - Struct literals are type-checked: field types, literal ranges, too many initializers, a field given twice. Omitted fields are zero-filled.
sizeof(variable)gives the size of the variable's type; an unknown name is an error.- Methods called on a
constvalue must declareconst T* self, and&of aconstvalue no longer converts to a non-constpointer. matchon a classic enum whose members share a value rejects a second arm for the same value.- Preprocessor: unknown directives,
#include(useimport), a stray or duplicate#else/#elif/#endif, a conditional left open, and#or##in a macro body are located errors instead of being silently dropped. - Numeric literals: an invalid digit in an octal literal (
08,019) is an error (it was read as decimal), as are0xwith no digits and any suffix (0b101,1_000,3.5f). - Strict stdlib parsers:
json_parsefollows RFC 8259 (exact literals, no trailing garbage,\uXXXXdecoding);base64_decoderejects bad padding and truncated input; a malformed or overflowingContent-Lengthgets a 400;String_to_intsaturates and accepts a leading+;http_reasonreturns the class name (or an empty string) for an unknown code instead of "OK". - C's usual arithmetic conversions apply to mixed-sign operands: a signed and an
unsigned operand of the same width give an unsigned result, a shift has the type of
its promoted left operand, unary
-and~promote a narrow operand toint, and two different narrow ternary arms meet asint. An integer literal too wide forintis anint64. Constant initializers fold with exactly these rules. - An
externparameter of fn type is a C function pointer. The call passes a top-level function by name ornull; a lambda or a fn-typed variable there is an error (its environment cannot cross into C). It used to pass the{fn, env}closure. - Bitfields follow the target's C layout (ABI change for structs mixing bitfield
types). On SysV/AAPCS targets a bitfield shares bytes with its neighbours while it
fits in an aligned unit of its declared type, so
uint8 a:4; uint32 w:12;is now 4 bytes. Windows targets use the MS layout, including under#pragma pack(N). - Each generic instantiation is type-checked with its concrete type arguments
(known limitation S), so an error that exists only for some arguments is reported as
... (in instantiation of f<Box>). The self-host now checks method-call argument types (known limitation R). - A local in a function's outermost block may not reuse a parameter name (the parameters and that block share one scope, as in C).
- Range loops:
for (i in A..B)takes the bounds' common integer type (it was alwaysint, so anint64bound wrapped), a non-integer bound is an error, and a bound that names the loop variable reads the outer variable of that name. - Duplicate
caselabels are found by value, socase 7:andcase (4 * 2) - 1:, or aconstname and its value, collide. - The self-host rejects bad operands, calls, returns and global types the way the C++ compiler does.
- A lambda may not assign a captured variable (
n = 1,n += 1,n++): the closure holds a copy, so the write was silently lost. Write through a pointer, or use a global or astaticlocal, which are not captured. - Conditions take a
bool, a number or a pointer (stringand?*Tincluded). A float condition is!= 0.0, so NaN is true, and!=on floats is unordered, as in C. - Aggregates (structs, arrays, slices, closures, interface values) have no built-in
==,<or truth value; define an operator overload. An overload needs at least one user-type operand. - Interface boxing takes a pointer to a struct with exactly one
*; a?*Tmust be null-checked first, and a pointer toconstonly boxes into an interface whose methods all takeconst T* self. - More constant checks: array sizes must be positive, a constant shift count must be
inside the operand width, a
casevalue must fit the subject type, enum member values must fitint, and constant array indices and slice bounds are folded and checked (lo <= hion any base). - Read-only rules:
constparameters can't be assigned, aconstarray can't be sliced, ands.lenof a slice is read-only. - A struct literal can't mix positional and named fields; a second
_arm in amatchis an error; returning the address of a local's field, element or slice is an error (astaticis fine);#pragma packtakes 1, 2, 4, 8 or 16. - A top-level function called with dot syntax needs a pointer
self;async mainand a lambda that can fall off its end without returning a value are errors. - A lambda written directly in
thread_createis owned by the thread, which frees its environment when it finishes. - Globals get C linkage, so an
externnext to its definition refers to the same symbol. An assignment evaluates its target before its value, as the other compound forms already did. - Self-host diagnostics go to stderr, as the C++ compiler's do.
- Pointer conversions between unrelated pointee types need a cast (
*intto*Big).*void,nulland the byte pointers (string,*char,*int8,*uint8) stay implicit, andint/int32anduint/uint32spellings of the same type match. - A global initializer or a function body may not name a global defined later in the file, as in C (it compiled to a zero address before).
- A by-value recursive enum (
enum L { Cons(int, L), Nil }) is an error, like a struct that contains itself.Box<void>,void[]and a non-constant array size are errors. - A lambda may not write a field or element of a captured struct or array (
p.a = 5,arr[0] = 9); writes through a pointer, a slice or a string are allowed. - A bare nullary variant of a generic enum needs its type arguments (
None<int>()), and a variant constructor's payload is checked against the declared instance. ?*Tnarrowing ends at any call orawaitfor a global, and an address-taken variable is never narrowed. A call in a condition (also in a&&right operand, a ternary condition or an early-exit guard) or in the branch that falls through ends a global's narrowing from that point on.- A lambda that captures a non-
escapingclosure parameter and outlives the call (returned, stored, passed to anescapingparameter or tothread_create) needs the parameter markedescaping. - A pointer to a sum type converts only to and from a pointer to the same sum type (or
*void), and a pointer to an array or slice (&arr) is not a pointer to a struct. - A
\-newline inside a string or char literal on a directive line splices the next line, as in C, so a#definebody's literal may span lines. ?needs an integer orboolokfield; a union literal names one member; an inline method can't declare a parameter namedself, and an operator overload must have the operator's arity.va_startneeds a variadic function (...), andva_start,va_argandva_endtake oneva_list;va_argof a struct, union, sum type or array is an error.thread_createtakes afn()->void,thread_joinits*voidhandle, andfree_closurea closure.alloc_withneeds an integer count and an allocator type with anallocmethod (it was a codegen error without a location).- A sum type is not a bitfield type, and a named bitfield may not have zero width
(
int x : 0), as in C.p - qneeds pointers to the same type. - An enum bitfield whose enum has no negative member reads back unsigned, as in clang
and GCC (MS layout keeps it signed):
Col col : 2holdingB = 2read back as -2. - A bitfield whose values all fit an
intreads asint, as in C:u - 1of auint32 u : 3holding 0 is -1 (it was 4294967295), anduint64 a : 20promotes too. A postfixb.f++keeps the declared type, as in clang. - A
voidcall is not an operand of&&,||or a comparison, nor an argument passed through...(printf("%d", f())); these were codegen crashes or invalid IR.return f();of avoidfin avoidfunction, andc ? f() : g()withvoidarms as a statement, compile and run. - A user operator (
s + s,s[i],-s,s += s, an overloaded==in a condition) is a call, so it ends a global's?*Tnarrowing like a plain call. A lambda body does not see a global's narrowing from where the lambda is written (it runs later), and astaticlocal follows the global rule (a call or a lambda may null it). alloc_withneeds a pointer to the allocator (alloc_with(b, T, n)with a by-valueballocated from a copy), an alloc method of the shape*void T_alloc(*T self, int64 size)(another return type or parameter list emitted invalid IR), and a known, sized element type (an unknown type orvoidcrashed the C++ compiler).*T[N]is an array of N pointers in the type checker too, as codegen always lowered it:*int[3] p = &arrand*pof such an array are type errors (they compiled to invalid IR). The address of an array is typedT[N]*.- The handle
thread_createreturns is a*voidin the self-host too, soint t = thread_create(...)is an error in both compilers. Like any*voidit converts to another pointer,stringincluded (astringis a byte pointer). - The arms of a
?:of two unrelated pointers have no common type in the self-host either (*A p = c ? &a : &b); it accepted any two pointers. va_arg<T>of a type the default argument promotions widen is an error that names the type to read:floatarrives asdouble, andbool,char,int8,int16,uint8anduint16asint(it read the wrong bytes).- Without
-o,eskiuc-eskwrites the objectFILE.olikeeskiuc(it printed the IR);--test-codegenprints the IR. -
A
returnor a?inside afinallyblock is an error, in both compilers: it would discard the pending exit, including an exception being unwound (C++ hung compiling it, the self-host crashed).breakandcontinueinside afinallykeep working, and a lambda written there is its own function. -
!of a void value (!v(), also through a method or interface call),throw v()of a void call and dereferencing a*void(*p,*p = v()) are type errors in both compilers; they compiled (the self-host emitted invalid IR for*p;). - Inside a lambda, taking the address of a captured variable's storage (
&n,&p.a,&arr[0]) or slicing a captured array (arr[0..2]) is an error, like assigning to it: the address is the closure's copy, so a write through it was silently lost. alloc_with(a call to the alloc method),thread_createandthread_joinend a global's?*Tnarrowing like a call, and the rest of a block after an early-exit guard is not narrowed when the branch that falls through assigns the variable (if (p == null) { return 0; } else { p = null; } return p.v;was accepted).- Inferred type arguments no longer come from the first argument alone. A binding
from a composite parameter (
*T,List<T>*) wins; by-valueT a, T barguments that deduce different integer types meet at their common type (maxof(1, big)withint64 bigismaxof<int64>; it wasmaxof<int>and truncatedbig), and any other disagreement (maxof(1.5, (float)2.5)) is a located error in both compilers. -
Constant operands are checked through their folded value. Integer division or remainder by zero, the most negative value divided by
-1, a shift count out of range, a floating constant cast to an integer type that cannot hold it and a constant index out of bounds are errors when the operand folds through aconstname, a fixed-sizesizeof, a cast or arithmetic (5 / Zwithconst int Z = 0,x / (int)0.5,1 << sizeof(int64) * 8,(int)Dwithconst double D = 1e10,M / -1withMthe most negativeint,a[sizeof(int)]on anint[4]). Only the literal forms were checked; the others emitted poison. Both compilers; testserrors/const_*,errors/float_cast_*,errors/index_oob_sizeof,const_fold_checked. -
String and char escapes follow C. An octal escape
\NNN(one to three digits) is the byte it denotes ("\101"isA,"\012"a newline;"\012"was NUL then12),\a,\band\?are recognized, and any other escape (\q), an octal escape above\377and a\xwith no hex digit are lexer errors located at the backslash (an unknown escape used to be the character itself). Both compilers and the#ifevaluator; testsescape_octal,errors/escape_unknown,errors/escape_octal_range,errors/escape_hex_empty. -
A type nests at most 1000 levels (each pointer level, array dimension, template argument list and fn type counts one): deeper is a located
type nesting too deeperror in both compilers, alongside the 100000-level statement and expression limit. Testerrors/type_nesting_too_deep,tests/deep/gen.sh(types_deep_999,generic_too_deep).
Deprecated
- Stdlib modules built around a struct now use
Type_methodnames, as the naming convention says:Rng_*(<random>),Regex_search/Regex_free/Match_*(<regex>),Heap_*(<sysheap>),EventLoop_*(<eventloop>),Executor_*(<executor>),Chan_*(<channel>),HpackDecoder_*andHpackHuff_build/HpackHuff_free(<hpack>;hpack_huff_code,hpack_huff_len,hpack_huff_decode,hpack_huff_encodeandhpack_huff_encoded_lenkeep their names),H2Conn_*/H2Stream_*(<http2>),DateTime_to_epoch/DateTime_format_iso(<time>). Factories keep their names (el_new,executor_new,chan_new,regex_compile). The old names remain as wrappers and will be removed in a later release.String_is_spaceis deprecated in favor ofis_spacefrom<ctype>.
Fixed
Miscompiles of valid code
- A
letin a nested block overwrote the outer variable of the same name for the rest of the function (including insideforbodies and acrossawait). - Compound assignment (
a[f()] += 1,a[i++] += 5,getf().lo += 2on a bitfield) evaluated its left side twice. - An unbraced
defer(if (c) defer ...;, acasebody, a loop body) ran even when it was never reached; an unbraced body is now its own scope. - A ternary with a literal wider than 32 bits or an unsigned arm was truncated, and a
nullarm crashed LLVM (thenullarm in C++ only). - A lambda body inherited the enclosing function's defers, loop targets and unwind
block, and a generic first instantiated inside
tryproduced invalid IR (C++). finallynow runs when acatchhandler leaves byreturn,breakorcontinue.- A
staticlocal captured by a closure is shared (not copied), and an uninitializedstaticstarts at zero. - A range loop's upper bound (
for (i in 0..n())) is evaluated once. - Operator overloads inside generic bodies,
for (row in m)over a 2D array, and denormal or overflowing float literals (C++). - Member, index and method access on rvalues (
mk().a[1],a.add(b).add(c));!on pointers and floats and a unaryoperator !overload;case K:with aconstor a constant expression. - Unions have C alignment; bitfield
++/--works, bitfields use C storage words, and their signedness follows the dealiased type. - Async:
do/while,deferand lambdas capturing locals work inside anasync fn; a local namedfrno longer collides with the frame pointer, and shadowed names inside lambda bodies are renamed correctly. - Self-host: expression temporaries no longer allocate stack on every loop iteration
(long loops overflowed the stack); pointer arithmetic, promotions and ternaries get the
right type; ADT payloads with arrays or nested enums, and bitfield structs, have the
same size as in the C++ compiler; negative and non-
i32global constants keep their value; calling a returned closure directly works; anexternfollowed by its definition no longer emits two definitions; chained assignment yields the converted value. - Found by the C oracle: a shadowing initializer (
int64 x = x + 1;) reads the outer variable; a deferred statement uses the names visible where it was written, even at an exit where a later declaration shadows one; a braceddeferbody run on an early exit no longer crashes codegen (C++);continueinside aswitchcase runs only the loop's defers (self-host); aconstinitializer usingsizeof(struct)folds to the real size (self-host folded 0); literals andsizeofkeep their type in mixed-sign operations (self-host). - Built-in operators inside generic bodies get their instance types, so unsigned division and comparison in a generic function are unsigned (C++).
- Names synthesized by the async transform and by range loops (
__fr,st,__end_iand the rest) no longer collide with user identifiers. - An
externprototype next to the program's own definition keeps the Eskiu convention, so by-value structs and fn parameters agree between the call and the definition (C++).
Type checker
- Missing-return analysis is sound for labeled
break,breakbefore areturn, andswitchfall-through, and no longer flags a case that falls into a returningdefault. ?*Tnarrowing is flow-sensitive (a reassignment or a shadowing declaration ends it) and covers&&,||,!, early-exit guards (if (p == null) return ...;), loop conditions and ternaries. A narrowed pointer can be passed or assigned as*T.- Calls through
fnvalues, fields and interface methods, and generic calls, check the argument count and types; a generic call also checks its type-argument count and reports a type argument it cannot infer.break/continueescaping a lambda is rejected. - Located errors instead of compiler crashes for cyclic type aliases,
voidvariables, parameters and fields, float, pointer or over-wide bitfields, a struct containing itself by value, unknown types and wrong template argument counts. An unknown type no longer triggers a cascade of conversion errors. - Errors that only codegen used to report (assigning to an rvalue,
&of an rvalue or a bitfield, invalid indexing and casts, a non-constantcase,breakoutside a loop, constant slice bounds out of range) now come from the type checker with a location. Integer literals in compound assignments and ternary arms are range-checked. must_useapplies to method-call syntax. An assignment target is no longer reported as an uninitialized read. A leading-star pointer to a generic instance (*List<int>) is accepted as a declared type.-Wallno longer flags methods used through dot calls, interfaces or operators, or the parameters of prototypes (an unused lambda parameter is still reported, like any other parameter); the unused-parameter warning points at the parameter. Diagnostics spell operator functions asoperator +(V, V).- Self-host: dot calls to free-function methods (
s.trim()) are accepted; many invalid programs it used to accept (C-style array declarators,++on a float, a mismatched?, misplaced array literals) are rejected like the C++ compiler; its diagnostics carryline:col. int32 main()is accepted as an entry point. Generic arguments are inferred through aconst Box<T>* selfreceiver, and the self-host infers every type parameter of a nested generic argument (HashMap_get(&m, k, &out)with no<K, V>).
Front end, driver and tooling
- A C-style local of function type (
fn(int32)->int32 h = f;) parses (C++). x < y >> 1parses (a backtracked generic parse left>>split), and(*p)parses as a dereference rather than a cast.- Macro arguments keep string and character literals whole, nested macro calls in
arguments expand, and an apostrophe in a comment no longer stops expansion. CRLF
sources and
\continuations work. - Imports are deduplicated by canonical path, so diamond and circular imports work.
- Every diagnostic is
file:line:col, including parse and lexer errors; an error inside an imported file names that file; parse errors recover at the next declaration. - The
--test-*modes predefine the same macros as a real build and exit non-zero on errors;--definition-atresolves by scope. runexits with128+signalwhen the program is killed, drops a--separator and accepts flags with values before the script.-onaming an input file, a directory input and-O4are refused.--helplists the Eskiu options and subcommands without LLVM's internal ones.$CCmay carry arguments.- Deeply nested or very long input no longer overflows the compiler's stack. Binary
operators parse by precedence climbing, operator and
else ifchains are walked by loops in every pass (no chain limit), the compiler runs on a 1 GB stack, and more than 100000 nesting levels is a locatednesting too deeperror. - Faster builds:
-O0no longer runs the optimizing backend passes, and symbol lookup takes constant time at any scope depth. fmtindents code after a closing*/and preserves CRLF line endings.- Self-host driver: spawns clang without a shell (safe with any argument), accepts the
full flag set (
-Wall/-Wextraare accepted and ignored: lint warnings come only from the C++ compiler), finds the stdlib through$ESKIU_ROOT, and reports a missing import as an error. - VS Code server: full-document sync, unsaved buffers resolve relative imports, and the
version comes from
package.json(extension 0.0.27, which also highlights octal and\a\b\?escapes).
Standard library: memory safety
tls_read_frame/tls_read_frame_asynccheck the peer's frame length (heap overflow).- HPACK: a table-size update above the SETTINGS limit is rejected (heap overflow), a literal naming an evicted entry no longer reads freed memory, and integer and string decoding stay inside the header block.
- A dropped
Chan_recvfuture no longer leaves a dangling waiter. fs_read_allworks on pipes and stdin;String_concat(&s, &s)andBytes_append(&b, &b)no longer read freed memory; theMaphash can't go negative.- HTTP/2 response headers are sized to fit and split into CONTINUATION frames.
Standard library: logic
- Regex: repeated groups (
(a){8}) match, capture memory is bounded,a{3,1}is an error, and a leading]in a class is literal. Json_intwrites the fullint64, the builder escapes control characters, and a failed parse frees its partial tree.- HTTP: header values are trimmed of optional whitespace and error bodies are valid JSON.
- Multipart no longer matches
name=insidefilename=and handles empty fields;net_write_asyncno longer leaks;EventLoop/Executorclose their descriptors and the timer table grows. FirstFit(and so<sysheap>) coalesces adjacent free blocks; freeingnullis a no-op.String_from_int(INT_MIN),String_init(&s, 0), POSIXdirname/basenameedge cases, negative ISO years andenv_get_intfallbacks on non-numeric values.- HTTP: a body above 2 GiB is copied and its
Content-Lengthwritten in full (int64throughout). - The internals of 34 stdlib modules use the current language (dot-calls,
defer,const, range loops) with no API change;tools/gen_hpack_huffman.pyemitsswitchtables for the Huffman code.
Second audit round
- Exceptions: a
deferruns when an exception unwinds through its block; a thrown value keeps its type and a genericthrowmatches itscatch; calls through a closure, a vtable or a generic instance unwind insidetry; athrowinside adeferbody ends that exit path cleanly;?inside adeferbody is an error. - Closures and fn values: calls through a fn value parse nested fn types, convert their
arguments and use
sretfor struct results; arrays of fn values work; lambda parameters can be reassigned; a non-escaping closure parameter can be passed on to another non-escaping parameter; a named function used as a ternary arm decays to a closure (self-host). - Generics: a prototype before its definition, interface arguments boxed at generic call sites, generic instance methods in interface vtables, arrays and slices of generic instances, a method call on a source-form instance, an operator overload on a generic instance inside a template body, a generic struct containing itself by value (now an error), and a type alias cyclic through a type argument (now an error).
- Pointers:
stringarithmetic and*voidarithmetic step by bytes;for-inand member access work through a*structpointer. ?*Tnarrowing ends at an assignment inside a condition and follows pointer arithmetic on the narrowed pointer. A nullable non-pointer type is an error.- Front end: imports see the macro table as of their import line,
__FILE__is escaped as a string, macro comments and arity errors are handled, rescanning reaches a call that spans the expansion,#ifarithmetic wraps and short-circuits like C, an out-of-range float constant cast is an error, and 64-bit integer literals and exponent digits are checked. Every top-level and use-site diagnostic carries a location. - Driver: nothing is printed on a successful compile; test modes take every input the
way a build does; importing a directory is an error;
_WIN64is defined for arm64 Windows triples;fmtkeeps line numbers and literal bytes. - Self-host: temp
.llfiles are created withmkstemps,runaccepts--before the script,--test-codegentype-checks first, parse errors are located, and sema matches the C++ checks on lambda bodies, conditions, operands, assignability, match arms, variant constructors, array literals, indexes, members,for-in, constraints, alias targets,awaitoperands and writes through a pointer toconst. alloc_withreturnsnullwhenn * sizeof(T)overflows.- Stdlib: HTTP/2 frames on a half-closed stream are stream errors and SETTINGS values
are validated; async servers retry a failed accept;
Chan_freedetaches a parked receiver; the executor's self-pipe wakes coalesce; the event loop handles a zero capacity and frees callback environments; multipart reads the boundary parameter and CRLF delimiters; HTTP/1.1 rejects a truncated body, conflicting lengths, bare LF and a NUL in the body; the ALPN list is bounds-checked; HPACK sizes its scratch buffer, rejects a late table-size update and a field that overflows;jsonfrees its tree iteratively;StringandMapuse-after-free, substring clamping andint64epochs are fixed; allocator sizes can't overflow and a tinyFirstFitbuffer is handled.
Third audit round
- Bitfields: a narrower signed value stored into a wide bitfield keeps its sign, and a
global or
staticinitializer of a bitfield struct, a union or a packed struct keeps its values (it was zero-filled). Union literals, including a global union set through a pointer member, produce valid IR. - Async functions: a
staticlocal keeps its value across calls, andfor-inover a slice,try/catch, array literals andmatchon an enum local work. - Generics: an instance reached only through a call result (
flip(n).a,unbox(bx(7)),bx(2.5).get()) is instantiated. - Interfaces: arrays of interface values, method calls through
*I, a global interface value and a closure taking an interface argument work; returning a large array usessret. - A struct field whose type is declared later in the file gets the right layout (it was
laid out as
i32). Member access throughconst *Tworks. - Lambdas can return a struct, a pointer or a generic instance;
(*pf)(x)calls through a pointer to a closure; arrays of closures and pointers to closures work in the self-host; unary+compiles. - A function-like macro call can span lines,
#pragmais accepted inside a function or struct body, andfmtleaves the bytes of a multi-line string literal alone. - Self-host: rejects casts from an interface, a brace initializer for a non-array, a mistyped global initializer, a struct literal with wrong type arguments and a free function with the wrong signature for a constraint, as the C++ compiler does.
- Stdlib: regex loops whose body can match empty follow RE2 (
(a?|b)*on "b" is 0,0), a repeat count above 1000 is an error, HTTP/2 rejects CR, LF and NUL in header fields, RST_STREAM on an idle stream, a short GOAWAY and a stream that depends on itself, andhttp_replyaccepts anullbody.
Fourth audit round
- A
finallyruns when acatchhandler throws (directly or from a call); the new exception then propagates. - A
boolbitfield at a nonzero bit offset reads back what was stored (it used ani1storage unit, so it read false or trapped at-O2). for (x in E)evaluatesEonce: a call, or an index by a variable, was evaluated again for every iteration (synchronous and async loops). The C++ checker accepts a pointer to a generic list returned by a call as the iterable.- The C++ checker accepts arithmetic, comparisons,
++/--and unary operators on a value typed by an alias through a field, element, return value or pointee (type u8 = uint8; s.a + 1). - Awaiting a future of a struct or a generic instance no longer prints a spurious
0:0: cannot converterror, and an error in the lowered async code now fails the build instead of being ignored. - Self-host: rejects
free_closureof a non-closure, a sum type as a bitfield type, reading or assigning an inline method as if it were a field (p.sum), a string slice into a non-charslice, andalloc_withwith a non-integer count or an allocator withoutalloc(each emitted invalid IR or garbage before). - Async: dropping a
select2,join2,select2vorjoin2vfuture before it resolves drops its inputs (their producers are cancelled and their wakers unhooked). A later completion of an input used to write into the freed combinator, for example when an outer timeout cancelled a task that was awaiting aselect2. - Sockets: a send to a peer that has closed returns an error (
EPIPE) instead of raisingSIGPIPEand killing the process.net_sendpassesMSG_NOSIGNAL(Linux, macOS), macOS sockets fromnet_accept/net_tcp_connect/net_tcp_listengetSO_NOSIGPIPE, the async write path sends throughnet_send, and a TLS connection on Linux setsSIGPIPEto ignored (OpenSSL writes withwrite()). - HTTP/1.1 (
http_recv,HttpRequest_parse) follows RFC 9112 framing: a chunked body is decoded (it was left unread),Transfer-EncodingwithContent-Lengthis 400, a coding other thanchunkedis 501, and whitespace before a header colon, an obsolete line fold, a missing or repeatedHostin HTTP/1.1 and a request line without a validHTTP/x.yversion are 400 (HTTP/2.0 on the HTTP/1 path is 505). A method must be a token. multipart_partfinds the part by thenameparameter of itsContent-Dispositionheader only (aname=inContent-Typematched before), and parameter and header names are case-insensitive.url_query_getdecodes each key before comparing it, soa%20b=1anda+b=1match the keya b.- Regex: an invalid bracket range (
[z-a],[a-\d]) is a compile error as in RE2, and\D\W\Sinside a bracket class are the complemented shorthands (they were read as the letters). - A cast in a constant expression truncates and sign-extends as in C, so
case (int8)259:andcase 3:are duplicate labels anda[(int8)257]isa[1]. fmtkeeps a line after a\continuation byte for byte (it re-indented it, changing a spliced macro body).- A variant constructor's integer literal argument must fit its payload type
(
A(300)forA(int8)), andnull[0]is a type error (it was a codegen error or segfault). - An interface arm and a struct-pointer arm of
?:meet as the interface in both compilers (C++ codegen crashed, the self-host rejected it). - Self-host: rejects
matchon a pointer or a struct, casts to and from a sum type, a struct literal of an enum, calling an enum member, an array or a non-closure field, a method used as a value, a variadic function assigned to a number, a payload variant without arguments,sizeofof a function, variant or later global,++on an enum, a by-value cycle through a type alias, an array size naming a laterconst, and an uninitialized read in a generic instance, as the C++ compiler does.
Fifth audit round
List_freesetsdatato null, so a push after it regrows the list and a second free (also throughString_split_free) is a no-op; it was a use after free and a double free.- HTTP/2: a malformed request (RFC 9113 §8.1.1, §8.2.2, §8.3) is reset with
RST_STREAM PROTOCOL_ERROR and the handler is not called; it was answered 200. That
covers an unknown or response pseudo-header, a repeated one or one after a regular
field, a missing
:method,:schemeor:path(CONNECT::authorityand no:scheme/:path),connection,keep-alive,proxy-connection,transfer-encodingandupgrade,teother thantrailers, a content-length the DATA frames do not add up to, and a pseudo-header in trailers. Frames the peer had in flight on a stream the server reset are ignored instead of drawing a second RST_STREAM or a GOAWAY. - HTTP/2 responses drop the handler's connection-specific fields and its
content-length(it was sent next to the real one). - A 1xx or 204 response has no Content-Length (RFC 9110 §8.6) in
HttpResponse_render,http_replyand HTTP/2, and no body. - HTTP/1: a NUL in a header value or the request target makes
HttpRequest_parseandhttp_parse_headfail (RFC 9110 §5.5); a header lookup stopped at it. - Regex: escapes follow RE2.
\b\B(ASCII word boundaries),\A\z,\xHH,\x{HH}, octal (\0,\012,\101) and\aare supported, and any other escaped letter or digit (\q,\1,\Z,[\b]) is a compile error; they all matched the literal letter. In a class, a-after a shorthand is a literal ([\d-z]is digits,-andz, as in RE2); the fourth round made it an error, wrongly citing RE2. sizeof(var)in a generic body measured the variable as anint(4 bytes) whatever its type (T[4] loc,B<T> b, a lambda's local);catch (T e)andcatch (Err<T> e)in a generic body never matched, so the exception terminated the program.- A slice or array of a struct with an
operator []indexed through that operator in the self-host (s[i]on aV[], andfor (v in s)): the operator's mangled name dropped the brackets, soV[]named likeV. Brackets are now part of the name in both compilers. - Self-host: a lambda whose type names a type parameter was rejected in a generic instance
(
return T() {...},fn(T)->T d = T(T x) {...}); a call to a generic variadic function was emitted non-variadic (garbage arguments); an array of closures as a global orstaticgave its lambdas the array as their return type (invalid IR); a union constant whose member is a struct or an array was rejected without a location. - A struct field, a pointee or an array of structs typed through an alias of an array
(
type AI = int[3]) compiles in C++ (q.b[2]and(*p)[2]with*AI pwere codegen errors, a field alias of a struct array crashed), and*AIis a pointer to the array in the self-host (it lowered as an array of pointers). A struct holding itself through such an alias (type AR = R[2]; struct R { AR a; }) is rejected by C++ as by the self-host (it compiled with a 4-byte field). - When the target is an interface, each arm of a
?:is boxed with its own struct's vtable, soI i = c ? &a : &bwith different conforming structs works in declarations, assignments, returns and arguments (C++ rejected it, and the self-host called the first struct's method on the second). - An array dimension is an integer constant expression folded like a
const: casts truncate as in C (int[(uint8)258]has 2 elements), and arithmetic over numbers,constints and enum members works (int[N + 1]). Both compilers rejected anything but a number or a single name. The self-host resolves an enum member as a struct field's dimension (int[B] a, it was invalid IR).
Sixth audit round
- HTTP/2:
h2_fill_requestleaked 16 bytes per request (String_fromon the already allocated version String). - HTTP/1:
HttpRequest_parse, behindhttp_serveandhttp_serve_async, now frames a request by RFC 9112 with the codehttp_recvuses: the body is exactly Content-Length bytes (it was every byte after the head), an invalid, listed or repeated differing Content-Length is rejected, and a request with no blank line after its head or a body shorter than its Content-Length is incomplete instead of accepted. The newHttpRequest_parse_statusreturns 0, -1 (more bytes needed) or the status (400, 413, 501, 505). Both servers read until the request is complete (HttpConnBuf_feed), so a request split across TCP segments reaches the handler whole; a peer that closes early gets 400, a head over 64 KiB 400 and a body over 1 MiB 413. - A Host value must be uri-host [":" port] (RFC 9110 §7.2):
Host: a b,Host: a, b, a bad port or%escape is 400 inhttp_recvandHttpRequest_parse. - A 304 response has no body and no Content-Length, and a HEAD response keeps the
Content-Length of its body but sends no body, in HTTP/1 (
HttpResponse_render_head, used by both servers) and HTTP/2; both sent the body. - HTTP/2 trailers follow the header-field rules: an uppercase name, a CR, LF or NUL in a
value, a connection-specific field or
teresets the stream with PROTOCOL_ERROR, as a pseudo-header already did. - Regex:
\sis[\t\n\f\r ]as in RE2 (it also matched\v, and\Sand[^\s]missed it). POSIX classes in brackets ([[:alpha:]],[[:^digit:]], the 14 RE2 names) are supported; they were read as a set of letters and a stray]. A{that does not start a{n},{n,}or{n,m}repeat is a literal ({,a{,a{,2}), as in RE2; it was an error. sizeof(T)in an array dimension is an integer constant, as in C (uint8[sizeof(Header)],int[sizeof(S) / 4]): a fixed-size scalar folds in the type checker, any other type's size comes from the target layout in codegen. Both compilers rejected it.- The self-host accepts
<,<<,<=,>and?:in an array dimension (int[K << 1],int[K < 4 ? 2 : 1]); its type-spelling scanners read the<as a generic argument list ("unknown type"). - An array of an array alias (
type Arr = int[3]; type Mat = Arr[2];) is 2 arrays of 3 in the self-host (it laid out 3 arrays of 2), and C++ indexes three levels of such aliases (Cube c; c[3][1][2]was a codegen error). - A nested brace initializer whose rows are an array alias (
type A = int[2]; A[2] m = {{1, 2}, {3, 4}};, also a global) compiles in C++; it was rejected. alloc_withover an instance of a generic allocator works in both compilers, through its inlineallocmethod or a top-levelG_int_alloc(*G<int> self, int64 n): C++ failed in codegen without a location, and the self-host rejected both.- The self-host lays out 32-bit x86 targets (
--target i686-...): 4-byte pointers, and 4-byte alignment ofint64anddoubleon SysV (8 on Windows). It used the 64-bit layout, sosizeofand struct sizes differed from C++.cabi_parity.shcompares the sizes oftests/target_sizes.eskper target. - C++ iterates
for (x in *p)withp: *A4(type A4 = int[4]); it was rejected. - The self-host rejects a cast of a void value (
(int)v()), of a{...}literal (((S){1}).a) and, in a generic instance, of an int to a struct type argument ((T)0with T = S), and a type argument that makes a parametervoid(id<void>(v())), as C++ does. - x86-64 SysV C ABI: an eightbyte holding a union is as wide as its widest member, as in
clang.
union { int; double; }passed or returned by value is ani64(it was ani32, losing the high half),union { char; int; }ani32,union { float; double; }adouble, andstruct { float; union { int; float; double; } }is{ float, i64 }. Both compilers; testc_abi_union(+.c), also incabi_parity.sh. - A type alias is its target for every shape check, in both compilers: the expression
types the type checkers and codegens reason about never name an alias (C++
getExpressionTypeandgetExprEskiuType, self-hostsema_infer_typeandcg_etype). An alias of an interface boxes (type Sh = Shape; Sh s = &q;was a bus error in C++, anShparameter a codegen error) and dispatches (s.area(),mk().area()); an alias of a pointer derefs (PSq[2] ps; ps[0].s) and dot-calls (pq.area(), also on generic instancesBI/PB); a field or return typed by an alias of a fn type calls (s.f(1),getf(5)(1); invalid IR or a crash in the self-host); amatchon an alias of a classic enum is accepted and checked for exhaustiveness (C++ rejected it, the self-host accepted a missing arm); an operator over alias operands (operator +(VV, VV)) and a*VV selfreceiver resolve in the self-host;.lenof an element ofIS[2](type IS = int[]) works, andIS[2]/*ISkeep the slice as their element in the self-host (they collapsed toint[2]/int*). Testalias_shapes. - A struct pointer boxes into an interface as a variant payload (
Som<Shape>(&a),W(&b)forenum Wr { W(Shape) }; C++ segfaulted, the self-host emitted invalid IR) and as a lambda's return value (Shape() { return &g; }, a C++ verifier error). Testiface_payload_lambda. - A member of an overloaded
[]result (w[2].v) is read from the call's value (a C++ codegen error, invalid IR in the self-host). Testindex_result_member. - A global (or
static) initialized with a function name (Op g = add;, also in an array or struct literal) is a constant closure, as in C; both compilers rejected it as not a compile-time constant. Testglobal_fn_value.
Seventh audit round
- HTTP/2: request bodies were buffered without limit (every DATA frame appended while
WINDOW_UPDATE gave the credit back, 400 MB over 128 streams). The
H2Serverengine now buffers at mosts.max_bodybytes per request (default 1 MiB, as HTTP/1.1) ands.max_bufferedacross a connection (defaultH2_MAX_BUFFERED, 4 MiB); a request past either, or with a content-length pastmax_body, is answered 413 without the handler and its stream reset with NO_ERROR. Testhttp2_body_limit. http_serve_asyncsent each answer with a blockingnet_send, so a client that asked for a large body and did not read it stalled the event loop and every other client. The connection socket is now non-blocking and the answer goes out throughnet_write_async.net_set_nonblocking(and the executor's self-pipe setup) did nothing on arm64 macOS:fcntlis variadic and was declared with a fixed third parameter. Testhttp_async_slow_reader.HttpConnBuf_feedreparsed the request from its first byte on every read, so a chunked request fed in small pieces cost time quadratic in its size. It now parses the head once and decodes the chunked body as it arrives; a chunk-size or trailer line overHTTP_CHUNK_LINE_MAX(8192 bytes) is 400. Testhttp_conn_feed_incremental.- Response header injection:
HttpResponse_headeraccepted a CR or LF in a value (and any bytes in a name), so a handler echoing input could add a header line. It now returns 0 and adds nothing for such a pair (1 otherwise), and the HTTP/2 encoder drops a hand-written line with a non-token name or an LF or NUL. A Content-Length the handler sets is no longer sent next to the automatic one in HTTP/1 (a 1xx, 204 or 304 keeps it). Testhttp_response_header_inject. http_chunked_stepspun forever on a chunk size nearINT64_MAXwhen the limit allowed it (size + 2overflowed in the room check). Testhttp_chunk_size_max.&eevaluates its operand once in C++:&buf[i++],&A[f()],&getp().x,&*gp()and afor-inover a List elementls[f()]ran the side effect twice. Testaddr_of_once.- A cast to an alias of an unsigned type (
(u8)-1) or a call returning one zero-extends in the self-host (it sign-extended, so(u8)-1was -1 andf() + 10withfreturningu8250 was 4). Testalias_unsigned. - The self-host checks every argument of an inferred generic call against the
instantiated parameter types, as C++ does: only a bare
Tparameter was checked, soswp(&i64, &s.a)against*Tcorrupted memory andList_push(&l, 1.5)on aList<int>compiled. Testserrors/generic_ptr_arg_mismatch,errors/generic_struct_param_arg,generic_infer_common,errors/generic_deduce_conflict. fmtfollows the preprocessor's view of strings and comments, in both drivers: a stray"in a skipped#if 0branch or inside a/* */opened on a#defineline made it think a string was open and reindent a later multi-line string, changing the program. Each branch of a conditional starts from the string state at its#if, a#line is a directive even inside a multi-line string, and a directive that leaves a comment open opens it for the following lines. Testfmt_cases/pp_string_state.- A field or element of a temporary (
mk().x,arr()[0],mp().v) is not storage: taking its address or assigning to it is a located type error, and slicing a temporary array (mp().v[0..2]) is "cannot slice a temporary array" in both compilers (C++ failed in codegen without a location, the self-host accepted&mp().x). Testserrors/slice_temporary_array,errors/addr_of_rvalue_member,errors/assign_rvalue_element. - The self-host rejects a binary operator on a struct operand when no overload accepts
the operand types (a pointer argument converts only through
*void), as C++ does:v + &g.aforoperator +(V, *int64)compiled to invalid IR. Testerrors/operator_ptr_arg_mismatch. - Narrow integers cross the C boundary extended, as clang does: an
extern'sint8,int16,uint8,uint16,boolandcharparams and results carrysignext/zeroext, and an Eskiu function returning one (a callback, or a function C calls by name) extends its result. AArch64 Darwin and x86-64 C code relies on it; a C callee read garbage high bits.charfollows the target's Cchar. Both compilers; testc_abi_narrow(+.c), also incabi_parity.sh. - A
va_listpassed to a C function (vprintf,vsnprintf) goes the way the target's Cva_listdoes: by address on x86-64 System V and AArch64 Linux, as itschar*on Darwin AArch64, Windows x64 and 32-bit ARM. It was passed as a 32-byte aggregate (a crash on x86-64). Both compilers; testva_list_c. - Every load and store through a
volatilevariable is volatile:*p,p[i],p.f,++/--and compound assignment, for locals and globals. C++ marked only the variable's own loads (a volatile global not even those), the self-host ignoredvolatile. Testvolatile_access(IR checked inrun.shandcg_parity.sh). - The self-host emits inline assembly (it dropped
asm(...)statements), with inputs and clobbers like C++. Testinline_asm_ext. - An async function whose
matcharms ortry/catch/finallybodies declare locals compiles: the lowering hoists them to frame fields (C++ failed with "has no member", the self-host emitted invalid IR). Testasync_arm_locals. - The self-host instantiates a generic enum with an alias type argument (
Opt<F>,type F = int) as the target's instance (invalid IR). Testgeneric_enum_alias_arg. - The self-host dot-calls through a pointer to an alias (
IL* l; l.len()withtype IL = List<int>), as C++ does. Testalias_ptr_dotcall. - A closure param of an async function is retained by the coroutine frame, so the
call's lambda gets a heap environment even when the body only calls it. C++ rejected
the program with an internal error. Test
async_closure_param.
Eighth audit round
- Returning the address of a local through a pointer cast or a
?:arm (return (*void)&x;,return c ? &x : &y;) is the dangling-pointer error, likereturn &x;. Both compilers; testserrors/dangling_cast,errors/dangling_ternary. - A struct that holds itself by value through an alias of a generic instance
(
type RA = W<RB>; struct RB { RA a; }) is rejected; C++ gave it a wrong layout. Both compilers; testerrors/struct_cycle_generic_alias. - The self-host declares a prototype that the program never defines, so its object links
with the one that does (separate compilation failed).
driver_parity.shseparate/prototype. - The self-host rejects assigning an array literal (
a = {4, 5, 6};) and casting a function name to a non-pointer ((int64)f), as C++ does. Testserrors/assign_array_literal,errors/cast_fn_name_to_int. - The self-host driver accepts
-O 2(a separate value), likeeskiuc. - A leading UTF-8 byte order mark is skipped instead of rejected, in both compilers.
Test
utf8_bom. - The HTTP/2 engine bounds a request's decoded header list: over
H2_MAX_HEADER_LIST(64 KiB, asSETTINGS_MAX_HEADER_LIST_SIZEcounts it, now advertised) or past whats.max_bufferedleaves is 431, without keeping the fields, and open streams' header lists count towardmax_buffered. Only the field count was limited, so a large dynamic-table entry referenced many times turned 13 KB of HPACK into 34 MB. Testhttp2_header_list_limit. - HTTP/2 requests follow the HTTP/1.1 Host rules: an invalid host or
:authorityis 400, two host fields or a host that differs from:authorityis a stream error, and without a host field the handler sees:authorityas Host (it was dropped, and any host fields with any value were accepted). Testhttp2_host_rules. http_recvreads a chunked trailer section incrementally: it rescanned it from the last chunk on every read and kept it all (quadratic CPU). A trailer section overHTTP_CHUNK_TRAILER_MAX(64 KiB) is 400 there and in the servers. Testhttp_recv_trailers.HTTP_CHUNK_LINE_MAXalso holds for a complete chunk-size or trailer line, so the verdict no longer depends on how the request was split into reads (a 9000-byte chunk extension was accepted in one read). Testhttp_chunk_line_split.HttpConnBuf_feeddrops chunked input it has decoded, so a body in tiny chunks (6 raw bytes per body byte) is no longer 413 belowHTTP_SERVE_MAX_BODYand the buffer stays small. Testhttp_chunk_tiny_chunks.- The spec's
#definecontinuation example declaresprintf. - A multidimensional array type in generic code keeps C order:
T[2][3]in a generic function or struct was re-spelledint[3][2]on substitution, so a non-square local crashed C++ codegen, a field was laid out with swapped dimensions (writes out of bounds) and an in-range constant index was rejected. Testgeneric_multidim. - A generic struct instance keeps its bitfields and packing:
struct G<T> { T v; uint8 f : 3; }had full-width fields in both compilers, and the self-host ignoredpacked/#pragma packon a generic struct. The instance is laid out like the plain struct of its concrete field types. Testgeneric_struct_layout(+.c). - The self-host's
sizeof(x)of an async function's local or parameter, and of an enclosing local used only bysizeofinside a lambda, is the variable's size (it was 4). Testsizeof_var_frame. - A dot-call through a leading-star pointer to an alias (
*Counter pc; pc.bump(1),type Counter = Cnt) resolves the aliased struct's methods in C++, as the trailing-star form did. Testalias_ptr_dotcall. - An array or slice type argument (
Box<int[3]>,Box<int[]>) instantiates a generic struct in C++ ("unknown type 'Box_int'"). Testgeneric_array_targ. - The self-host accepts returning a slice of a slice parameter (
return s[1..s.len];) and the address of an element of a slice parameter or slice field (&s[1],&p.sl[2]); they were rejected as dangling. Testslice_return_nonlocal. - A struct literal accepts a trailing comma (
P{ x: 5, y: 6, }), like an array literal. Teststruct_lit_trailing_comma.
Final additions
- A generic struct's
*Tfield with an array or slice type argument (Box<int[3]>,Box<int[]>, or an alias such astype IS = int[]) is a pointer to that array or slice: the C++ type checker rejectedBox<int[3]>{ p: &a }and the self-hosted compiler emitted invalid IR forBox<IS>{ p: &s }(it read the field as a slice of pointers). AT[2]field withT = int[3]is two arrays of three in both. Testgeneric_alias_slice_ptr. - A struct declared under
#pragma pack(N)with N of 2 or more is aligned to the smaller of N and its largest field alignment, as C does, also as a field or array element of another struct, a union member, next to bitfields and as a generic type argument (it was aligned to 1 there, so the outer struct could be smaller than clang's; a layout change for such structs). The type checker foldssizeofthe same way, and the C ABI lowering uses that alignment and skips the padding of a struct laid out by hand. Testspack_nested_c(+.c, also incabi_parity.sh),sizeof_pack_n_fold. - On AArch64 Linux (and other non-Darwin, non-Windows AArch64 targets) a variadic
function written in Eskiu reads its arguments right:
va_arg<T>walks the AAPCS64va_list(general-register and FP/SIMD save areas, then the stack) as clang does, where LLVM'sva_arginstruction read it as a Darwinchar*list. Testsvariadic_regs(+.c), andvariadic,generic_variadicunderlinux_docker.sh. -Wallwarns about a scalar local that may be used uninitialized: a read that some path throughif/else, loops,switchfall-through,match,try/catch/finally,defer, early exits or a lambda capture reaches without an assignment (&xassigns; aggregates set field by field are not tracked). A read no path assigns stays an error. C++ compiler only. Testswarnings/maybe_uninit,warnings/maybe_uninit_ok.- The stdlib HTTP servers (
http_serve,http_serve_async,http2_serve_async, the TLS servers) disconnect a client that sends nothing, trickles its bytes or stops reading: a request head (or the HTTP/2 preface and SETTINGS) must arrive withinHTTP_HEADER_TIMEOUT_MS(10 s), a body withinHTTP_BODY_TIMEOUT_MS(60 s), an idle HTTP/2 connection gets GOAWAY afterHTTP_IDLE_TIMEOUT_MS(60 s), an answer must be taken withinHTTP_WRITE_TIMEOUT_MS(30 s), and the async servers hold at mostHTTP_MAX_OPEN_CONNS(1024) connections. A cut HTTP/1.1 request is answered 408.HttpLimitsand the*_withserver variants change them;<net>gainsnet_set_timeoutsand<net_async>deadline reads and writes. Testshttp_timeouts,http2_timeouts,http2_tls_timeouts. - A declaration may follow
case N:ordefault:directly, without braces. The switch body is one scope, as in C: the variable is visible in the later cases, and two cases may not declare one name. Testscase_decl,async_case_decl,errors/case_decl_redefined,errors/case_decl_out_of_scope. - A
T[]parameter infersTfrom a slice argument (sum(a[0..4])). Testgeneric_slice_infer. sizeof(expr)is the size of the expression's type, not evaluated (sizeof(*p),sizeof(a[0]),sizeof(s.f));sizeof(*p)used to measure a pointer. The type checker foldssizeofof a pointer, array, struct or union with the target's layout, so the constant checks (zero divisor, index out of bounds, duplicatecase) see it. Testssizeof_expr,errors/index_oob_sizeof_struct,errors/switch_dup_sizeof_ptr,errors/const_div_zero_sizeof_expr.- An enum member value may be an integer constant expression over literals, earlier
members,
constints andsizeof(B = A << 2), folded like any constant. Testsenum_value_expr,errors/enum_value_not_const,errors/enum_value_expr_range. - A bitfield read or written through a
volatilevariable (r.mode = 5,r.mode++) uses volatile loads and stores of its storage word, and a volatile variable's initializing store is volatile. Testvolatile_bitfield(IR count checked inrun.shandcg_parity.sh). - A nullary variant written with parentheses (
B()) is an error located at the call, "variant 'B' takes no arguments" (writeB). Testerrors/variant_nullary_parens. - A function-like macro whose name is followed by a newline (and blanks or comments)
before its
(is expanded, as in C. Testmacro_call_newline. - Deep types, member chains and nested ternaries compile in linear time: type spellings
are parsed and rendered in one pass (C++
ty::Type, parser), a member chain is typed once (self-host sema and codegen; C++ codegen's volatile-root walk is memoized), and a ternary finds its:from a table built once (both parsers).tests/deep/gen.shgainsmember_chain_50kandternary_then_5k. - The blocking TLS server's deadlines (
tls_accept_with,tls_read_full_deadline,http2_tls_serve_conn_with) hold against a peer that trickles bytes inside one TLS record. They usedSO_RCVTIMEO, which restarted with every byte OpenSSL's record loop read, so one byte every 200 ms kept a handshake or preface open forever. The socket now runs non-blocking and the calls wait withpoll(WSAPollon Windows) on what is left of the deadline; writes taketls_write_all_deadline.<net>gainsnet_wait_ready. Testtls_trickle_deadline(+.c, an OpenSSL stand-in with a record layer). - An HTTP/2 response must be all sent within the new
HttpLimits.write_total_ms(HTTP_WRITE_TOTAL_MS, 5 min) of when it was ready.write_mscounts from the last data sent, so a peer granting one byte of window every 150 ms kept a 2 MB response and its buffer alive forever. Past the cap the stream is reset with CANCEL and its response freed; the connection stays open (H2Server_expire,H2Server_timed_out_with, used by the h2c and both TLS servers). Testhttp2_write_total. <net>'s socket timeouts pass astruct timevalof two Clongs with its own size, so 32-bit ARM Linux gets the 8-byte layout it expects (it was always 16 bytes). Testrun_cmd/net_timeval(IR checked byrun.shfor armv7 and x86-64).- An object-like macro that expands to the name of a function-like macro is invoked when
the
(is on a following line (#define G F, thenGand(5)on the next line), as in C. A line inside a multi-line string literal is string text in both preprocessors: a#undefor other#line there is not a directive and no macro expands in it;fmtkeeps such a line's bytes. Testspp_string_lines,fmt_cases/string_hash_line. - The self-hosted compiler types each
?:of a deeply nested one once, in the type checker (memoized per node while no binding, scope or narrowing changes) and in codegen (stamped bottom-up when the outermost one is emitted): 5000 levels took about ten seconds to type-check, 20000 now compile in about a second.tests/deep/gen.shgainsternary_mixed_20k. - Inline
asmtakes output operands, in both compilers:asm("op" : "=r"(y) : "r"(x))with register outputs (=r,=&r, several at once), read-write+r, and memory=m/+m, numbered like clang (outputs first) and lowered as clang does. An output must be a writable lvalue of a non-bool integer, floating-point or pointer type (not a bitfield). Testsinline_asm_out(x86-64 IR checked byrun.shandcg_parity.sh),errors/asm_output_*,errors/asm_input_output_constraint. externstruct and union arguments and results follow the 32-bit x86 C ABI (cdecl) in both compilers, as clang lowers them: an argument of at most 16 bytes made only of 32/64-bit scalars is passed as those scalars, anything else byval; Linux returns every aggregate through sret, while Darwin, Windows and the BSDs return one of 1, 2, 4 or 8 bytes in registers (a lonefloat/doubleor pointer as itself, except a floating one under MSVC).cabi_parity.shcovers the i686 targets and compares both compilers with clang's IR for the C companions. Testc_abi_x86_32(+.c).-
http_servesends an answer under onewrite_msdeadline from its first byte and retries a partial send within it, so an answer is no longer cut short by an early partial send. -
A slice or array compares by its element type in the C++ type checker, whatever the spelling (
int*[]is*int[]), and aT[]parameter bindsTfrom a slice of a generic instance or of a trailing-star pointer. Testslice_elem_spelling. - The self-host calls a local or parameter of fn type named like an ADT variant (
A(1),B()) instead of building the variant, as C++ does. Testvariant_name_shadow. - A keyword after
.is a member name, soj.int(5)andj.bool(1)dot-call the<json>builder'sJson_intandJson_bool. Testjson_keyword_methods. - An enum member value may use
?:,&&,||and!(short-circuit, as in C) and aconstwhose initializer uses them. Testsenum_value_logic,errors/enum_value_ternary_not_const. nullconverts to an interface value ({null, null}) in a declaration, an assignment, a return, an argument and a field, and an interface value compares withnulland is a condition (if (i),!i,i && ...). Testsiface_null_value,errors/iface_compare_ptr.- A pointer to a nullable pointer is spelled
*?*T(&pof a?*Tis one);?*T*is still a nullable pointer to*T. Its pointee must be checked before a second dereference, and it does not convert to**T. Testsnullable_ptr_to_ptr,errors/nullable_ptr_to_ptr_deref,errors/nullable_ptr_to_ptr_drop. - The type checker folds
sizeofof a struct under#pragma pack(N)with N of 2 or more, with or without bitfields, per target as code generation lays it out, so enum values, array dimensions andcaselabels may use it. (A named: 0bitfield stays an error.) Testsizeof_pack_n_fold. - A type alias inside a composite type is its target in the C++ compiler too:
Box<F>isBox<int>withtype F = int,List<Vec>isList<V>,fn(int)->IFisfn(int)->int. Type arguments are canonicalized before an instance is named, so both spellings share one monomorph in both compilers (unbox<F>no longer adds anunbox_F). Testsalias_in_composite,errors/alias_in_composite_mismatch. - A generic variant takes its type arguments from the expected type: in a declaration,
an assignment, a
returnand an argument of a non-generic function,Some(5)builds the expectedOpt<int64>, and a bareNoneor an under-determiningLeft(4)is accepted (Opt<int> n = None;). The payload is checked against those arguments; the explicit forms still work. Testsvariant_expected_type,errors/variant_expected_payload. - The two compilers report a diagnostic at the same line and column: the self-hosted parser
stamps each node with the token the C++ parser uses (an operator at its operator, a cast
at its
(, a member access at its., a declaration at its name, a statement at its keyword), and its checker points at the same node (a callee, a struct literal field's value, a range bound, a method).tests/selfhost/tc_parity.shnow also compares the line:col of the expected error with the C++ one over the error corpus. Athrowstatement is located at its keyword (it was at the;), andstaticon a global is reported by the self-hosted checker, not as a parse error. - The self-host emits a double literal too large for double as infinity in LLVM hex form
(
0x7FF0000000000000), as C++ does; a global initializer1e400was rejected by clang.float_literal_rangenow covers the global case, socorpus_parity.shchecks it. a * b;anda * g();withaa local or parameter are expression statements in both compilers, in a block or after acaselabel (C++ read them as declarations, the self-host reada * b;after acaseas one). Testlocal_times_stmt.sizeofof a generic struct instance, a sum type, a struct with bitfields (SysV/AAPCS or MS rules, per target) and a struct holding an interface value folds in the type checker, so a duplicatecase sizeof(T):is a located error instead of an LLVM or clang failure. Testssizeof_case_layouts,errors/switch_dup_sizeof_{generic,adt,bitfield,iface}.awaitworks in any position of anasyncfunction, in both compilers: inside atrybody and acatchhandler (an exception thrown before or after a suspension reaches the right handler;finallyruns exactly once on every exit), amatcharm, aswitchsubject, afor-initerable, a range bound, a compound assignment (its target evaluated once, before the await), a condition, a call argument and any larger expression (operands with side effects keep their order;&&,||and?:evaluate an awaiting operand only when it runs). This replaces the located errors added earlier in this cycle in this release. A future dropped while suspended now runs thedefers andfinallyblocks pending at its await once. Anawaitin afinallyis a located error. Testsasync_try,async_try_cancel,async_await_positions,async_await_edges,async_generic_try,errors/await_in_finally,run_cmd/await_in_defer,run_cmd/await_in_generic_match.<regex>reads patterns and texts as UTF-8, with the semantics of Go's regexp (RE2):., classes and literals match whole code points (match offsets stay byte offsets, and a byte that is not valid UTF-8 reads as U+FFFD),\x{..}goes up to\x{10FFFF}, and it gains\p{..}/\P{..}(the general categories, their one-letter groups, the scripts andAny, Unicode 15 tables generated from Go's bytools/gen_regex_unicode.gointo<regex_unicode>), the flags(?i)(simple case folding),(?m),(?s)and(?U)for the rest of a group or scoped as(?flags:...), non-capturing groups(?:...), named groups(?P<name>...)and(?<name>...), and literal text\Q...\E. A differential against Go's regexp over 60 000 generated patterns and texts agrees on every result. Testregex_unicode; the stdlib fuzzer's regex dictionary covers the new syntax.<eventloop>on AArch64 Linux:struct epoll_eventwas declared packed as on x86-64, so each event read the wrong descriptor and the async servers spun at full CPU (thehttp2_*tests hung). It is packed only on x86-64 now.tests/linux_docker.shruns a Linux smoke of a release from a Mac: it cross-compiles the run and smoke tests and the self-hosted drivers for AArch64 (or x86-64) Linux, links and runs them inubuntu:24.04with gcc, runs the self-hosted type checker and code generator over the corpus, and checks the bootstrap fixpoint on Linux. A pre-release check, not a CI gate.- A native build targets the target's baseline CPU unless
--mcpuis given, as clang does (apple-m1on arm64 Apple targets,genericelsewhere). It used the host CPU's name without its feature list, which could select instructions a VM or a masked host does not support (a SIGILL), and made the binary depend on the build machine. -
-Walllints only the program's own files, not the modules it imports, as C compilers treat system headers. Importing<http>alone used to print 54 warnings about the stdlib, labelled with the user's file name. -
A pointer to an array or slice alias as a generic type argument (
List<*A3>,Box<*A2>withtype A3 = int[3],type A2 = int[2][3]) names its own instance again (the C++ type checker reportedunknown type 'List_int_A3', a regression from the alias-in-composite change; the self-hosted compiler emitted invalid IR).*T[2]withTan array type is an array of two pointers to that array (int[3]*[2]) in both compilers. Testgeneric_alias_array_ptr. - Enum values, array dimensions and
caselabels fold with C's typed integer rules in both compilers: the usual arithmetic conversions, 32-bitintwraparound, unsigned comparison, division and right shift, the arithmetic right shift of a signed value, andsizeofas an unsigned size (((uint)3 - (uint)5) / 2 > 100is 1,(1 << 31) >> 31is -1,-1 < (uint)0is 0), as aconstglobal, a local and clang give. Testconst_fold_typed_c(+.c). #pragma pack(N)(andpack(1)) applies to a union: each member's alignment is capped at N, so#pragma pack(2) union U { char[5] c; int x; }is 6 bytes, 2-aligned, as C (it was 8 and 4-aligned, a layout change for such unions), in the layout, thesizeofthe type checker folds and the C ABI lowering. Testpack_union_c(+.c, also incabi_parity.sh).-Wallmaybe-uninitialized follows&&,||and!in a condition: an assignment in the right operand ofa && (x = f())assignsxwhere the whole is true (if (b && (x = f()) > 0) return x;andif (!b || (x = f()) < 0) return 0; return x;no longer warn), inif,while,forand?:. Testswarnings/maybe_uninit_ok,warnings/maybe_uninit.return null;in a generic function instantiated with an interface type returns the empty interface value (the C++ compiler failed LLVM verification). Testgeneric_iface_null_return.- Expected-type variant inference reaches a nested variant (
Opt<Opt<int64>> x = Some(Some(5))), a struct-literal field, an array-literal element, an argument of an explicit generic call (get<int64>(Some(8))) andreturn None;in a genericOpt<T>function (writtenNone<T>()there, which every instance shares), in both compilers. Testvariant_expected_more. - The self-hosted compiler builds a bare generic variant (
Some(v)) in a generic body as each instance's enum (a second instance reused the first one's type, invalid IR). Testgeneric_variant_bare_inst. - A generic dot-call on an rvalue receiver (
mk().get(0), a call result) passes the address of a temporary holding it, as a struct method call does (the C++ compiler failed with an unlocated error, the self-host emitted invalid IR). Testgeneric_dot_call_rvalue. - An inline asm operand naming a local or parameter of an
asyncfunction reads and writes its frame field (the async lowering left the name, an internal error in the C++ compiler and invalid IR in the self-host). Testasm_async_locals. - A method call on a non-struct value (
x.foo()withxanint) reports one error,undefined method 'foo' on type 'int', at the same line and column in both compilers (the C++ checker also reported a member-access error before it).
Known issues
These are open in 0.9.2. None of them miscompiles a valid program.
- An
awaitinside afinallyor adeferis rejected: both run without suspending when a cancelled future is dropped. In a generic async function anawaitis also rejected in amatcharm that binds a payload, after an operand with a side effect in the same expression, and inside a?:arm. Bind the awaited value to a local first. - A method that mutates a captured value inside a lambda acts on the closure's copy (captures are by value); write through a pointer to share state.
- The type checker does not fold
sizeofof a struct with an unnamed: 0bitfield, so constant-expression checks do not see it (codegen gets its size right). - The self-hosted compiler generates code for deeply nested binary expressions
(
a + (a + (...)), thousands of levels) in time quadratic in their depth; the C++ compiler is linear.
[0.9.1] - 2026-09-09
Fixed
A correctness campaign (a multi-front bug hunt) closed a set of latent miscompiles and type-rule gaps. All are fixed lockstep in the C++ and self-hosted compilers unless noted.
- Global and
static-local constant initializers. These were silently mis-folded in several forms: a 64-bit literal was truncated to 32 bits (C++), astructglobal came out zeroed, and aconst/enum/sizeof/~/!value folded to0; astringglobal came outnulland a hex literal produced invalid IR (self-host). The constant folder now handles all of these.staticlocals also accept any of those constant forms (they previously required a bare literal), and an over-full global array literal is rejected. - Signed/unsigned integer semantics now follow C.
float→unsigned casts usefptoui(a value above the signed max no longer saturates); a right shift's kind follows the value shifted, not the count's signedness; and a mixed-rank signed/unsigned op is unsigned only when the unsigned operand's rank is at least the signed one's. switchbreakno longer runs enclosingdefers twice (it unwound to the wrong cleanup depth), and a bitfield write through a*Structnow reaches the pointee instead of the pointer's own slot (both C++).- Scientific-notation float literals (
3.4e38,1e6,2.5e-3) now lex and compile. --safeslice construction bounds-checks0 <= lo <= hi <= len: a valid empty end-slice (s[len..len]) no longer traps, and an out-of-range upper bound is caught.- Async (self-host): the awaited value's type is taken from the
letvariable, so awaiting a future from a method call, a variable, or a template call resolves correctly (was read through a wrongintoverlay); and anawaitthe transform can't place (in a condition or a larger expression) is a clean compile error instead of a crash or silent miscompile.
[0.9.0] - 2026-09-08
Added
- Labeled
breakandcontinue. A loop can be named with a leading label (outer: for (...) { ... }), andbreak outer/continue outeract on that loop from inside a nested one, instead of only the innermost. The label must name an enclosingwhile,do/while,for, orfor ... inloop, otherwise the program is rejected at compile time. A labeled jump runs the samedefer/errdefercleanups as the unlabeled forms (every deferred statement between the jump and the target loop runs, innermost first), may not escape adeferbody, and is not supported inside anasync fn. Implemented lockstep in both the C++ and the self-hosted compiler.
Fixed
- Global array initializers are no longer dropped. A global (or
staticlocal) array literal such asint[3] G = {10, 20, 30};was silently zero-filled: only scalar globals kept their value, and an array came out all zeros with no warning. The constant folder now builds the array constant from the initializer, with C-style zero-fill for a partial list (int[3] = {7}gives{7, 0, 0}), nested arrays, and numeric casts inside the initializer (float[3] = {(float)0.485, …}, which also emit the correct LLVM f32 constant form). Fixed in both compilers. Locals were unaffected.
[0.8.0] - 2026-08-21
Fixed
- The release binaries are self-contained.
eskiucpicked up z3 and zstd as dynamic libraries from the build machine (versioned/opt/homebrew/...paths on macOS;libz3.so, absent on a stock Linux, on Linux), so the shipped tarballs failed to load elsewhere. Both platforms now bundle those libs intolib/depsand reference them relatively (macOS via@loader_path, Linux via an$ORIGINRUNPATH), with a build guard that fails if the rewrite did not take. (The compiler still shells out toclangto link native output, so a C toolchain is a runtime requirement, as before.) - The async stack runs on Windows.
<eventloop>gained aWSAPollreactor (a user-space pollfd set rebuilt each wait, sinceWSAPollkeeps no kernel state) alongside kqueue/epoll, and<net_async>gained a Winsock backend (ioctlsocket(FIONBIO)for non-blocking mode,recv/send,WSAGetLastErrorwithWSAEWOULDBLOCK) beside thefcntl/read/writepath. A WindowsSOCKETis not a small sequential fd (handle values in the hundreds are normal), so the loop's fd-indexed slot table now grows to fit the largest handle instead of silently dropping registrations past a fixedmax_fds. Validated end to end on a native Windows runner: a non-blocking socket round-trip completes over the reactor. POSIX behavior is unchanged (small dense fds never trigger a grow). - Windows platform shims for the OS-level stdlib.
<sysheap>now carves pages withVirtualAlloc/VirtualFreeon Windows (it has nommap), and<time>uses the Win32 clocks (GetTickCount64for the monotonic clock,GetSystemTimeAsFileTimefor wall time,Sleepforsleep_ms) instead ofclock_gettime/nanosleep(whosetimespec.tv_nsecis a 32-bitlongon LLP64 Windows and would not match).<threading>already links against winpthreads. Validated end to end on a native Windows runner (sysheap+ aMutex+ the monotonic clock in one program). Linux/macOS behavior is unchanged. - Exceptions work on Windows (mingw).
try/throw/catchpreviously emitted the Itanium/DWARF EH personality (__gxx_personality_v0) unconditionally, so a Windows object failed to link. The personality is now selected by target: mingw x86-64 uses the SEH personality (__gxx_personality_seh0); Linux/macOS keep__gxx_personality_v0. A native build with no--targetfollows its host. The rest of the EH lowering (landingpad IR, the__cxa_*runtime,_ZTIPv) is unchanged. Validated end to end on a native Windows runner (windows.yml): atry/catchprogram compiles, links with the mingw C++ runtime, and catches the throw.
Added
matchon classic (payload-less) enums. A plainenum Dir { N, E, S, W }can now bematched with exhaustiveness checking, the same guarantee algebraic enums already had: every variant must be covered (or a_default), and adding a variant turns every unhandledmatchinto a compile error. It lowers to a switch on the enum's int value (cases respect explicit= N). Previouslymatchwas ADT-only and a plain enum could only be dispatched with a non-exhaustiveif/switch. Landed lockstep in both compilers. Test:tests/enum_match.esk.- Operator overloading. A struct can define
V3 operator +(V3 a, V3 b) { ... }(and the same for- * / % == != < > <= >= & | ^ << >>, unary- ! ~, and subscript[]), soa + breads as algebra instead ofv3_add(a, b). Resolution is fully static: an operator compiles to a normal function under a canonical mangled name, anda op bon non-built-in operands resolves to it by operand types at compile time (zero runtime cost, no vtable), so overloads coexist by operand type (V3 * V3andV3 * double). Compound assignment (v += w) desugars to the overloaded binary op. Structural: a type gets+simply by declaringoperator +, noimplblock. Landed lockstep in both compilers, including the numeric-argument coercion at the call site (V3 * 2.0findsoperator *(V3, float)). Test:tests/operators.esk. - Self-host mirrors of the two safety features. The Eskiu-written compiler now implements the two features that had shipped C++-only, reaching full feature parity:
--saferuntime bounds checks (self-hostcodegen.eskemits the same slice/array index trap-on-out-of-range as the C++ back-end; off by default, opt-in via--safe).- the
?*Tchecked nullable pointer (self-hostparser.esk/sema.eskreject an unchecked deref, narrow throughif (x != null), allow*T->?*Twidening and reject?*T->*Twithout a check;codegen.esklowers?*Texactly like*T). Each is gated in CI (tests/selfhost/safe_parity.sh,tests/selfhost/nullable_parity.sh).
Changed
alloc<T>now zero-initializes.<mem>'salloc<T>(n)returns memory that is zeroed (hosted mode callscallocinstead ofmalloc; the--freestandingesk_alloccontract is now "return zeroed memory", honored bykernel/alloc.esk). A freshly allocated*Tis null, anintis0, aListis a valid empty list, so reading a field before assigning it is defined rather than a garbage read. This matches C++'snew T()and removes an uninitialized-read crash class that only surfaced on platforms whose raw heap is not already zero (Linux), while working on those whose is (macOS).
[0.7.0]
Added
externvariables.extern <type> <name>;declares a global defined in another translation unit (a C global), emitting an external-linkage declaration with no initializer. Reads and writes resolve at link time, so Eskiu can share state with C libraries (e.g. a libctru global). Previouslyexternaccepted only function prototypes. Landed in lockstep across both compilers.- Cross-compile to Windows (x86-64).
--target x86_64-pc-windows-gnuemits a COFF object with the Microsoft x64 calling convention (LLVM lowers it from the triple), and the preprocessor predefines_WIN32(plus_WIN64on a 64-bit arch). Link it withlld-linkand an import library generated byllvm-dlltool(both ship with LLVM, so no Windows SDK is needed to link); the resulting.exeruns on Windows or under wine. - 32-bit ARM backend, cross-compile to the Nintendo 3DS. The ARM target is now
registered alongside AArch64 and x86, and three new flags drive it:
--mcpu(e.g.mpcorefor the 3DS's ARM11),--mattr(LLVM feature string, e.g.+vfp2), and--reloc(staticfor the.3dsxloader, which applies static relocations and has no dynamic loader for a GOT). A hard-float ARM triple (ending inhf, e.g.armv6k-none-eabihf) selects the hard-float ABI, so the emitted object carries theTag_ABI_VFP_argsbuild attribute and links against hard-float libraries like libctru. An eskiuc object built this way links cleanly into a.3dsxhomebrew via the devkitARM toolchain.
Fixed
- Float arguments narrow at the call site. A
doubleliteral (Eskiu's default float literal type) passed to afloatparameter was left asdouble, so the argument type disagreed with the callee's signature and the reference compiler rejected the call at LLVM verification. Direct and method calls now run every argument through the shared numeric-coercion path, matching assignment and return (the self-hosted compiler already did). This unblocks calling C float APIs, common in 3DS graphics code. - Bare-metal targets no longer inherit the host platform macro. An explicit
non-hosted triple (OS
none, e.g. the 3DS'sarmv6k-none-eabihf) now predefines neither__APPLE__nor__linux__; previously it fell through to the build host's macro. Native builds and explicitlinux/appletriples are unchanged. - Hard-float ABI reached the object emitter. The
--target …hfhard-float ABI was applied when building the module and running the optimizer but not in the object-emitting path, so the written object droppedTag_ABI_VFP_argsand a hard-float linker rejected it. The three code-emitting paths now share oneTargetMachinebuilder, which also aligns the x86 backend-init guards (the static Apple release links without x86).
[0.6.2]
Added
- Cross-compilation targets the right platform. The
--targettriple now drives the predefined platform macro (__APPLE__/__linux__), soeskiuc --target x86_64-linux-gnuon macOS selects the Linux stdlib paths (epoll, Linuxsockaddr_in) instead of emitting unresolved BSD symbols. With no--targetit still follows the build host.
Fixed
- Event-loop timer callbacks are initialized.
el_newallocated the timer array but only zeroed each timer'sactiveflag, leaving theon_fireclosure as heap garbage (allocdoes not zero). Firing is guarded byactive, but an uninitialized function pointer is exactly the shape of the intermittent Linux CISIGILLin the HTTP/2 tests (a garbage closure invoked on a fresh, non-zero heap). It now initializes every field, completing the equivalenton_readfix from v0.3.1, so the event loop holds no uninitialized function pointer. - The async transform preserves
escapingparameters. Lowering anasync fnto its coroutine constructor dropped the per-parameterescapingflags. Because the ctor stores each parameter into the heap coroutine frame (which outlives the call), an escaping closure argument could then be stack-allocated at the call site, leaving the frame with a dangling env. The lowered constructor now carries the original flags, and the post-lowering soundness check no longer spuriously flags the frame-stored callback. - Interface (vtable) dispatch now coerces arguments. Calling an interface method
with an argument that needs widening (e.g. an
int32where the method declaresint64) emitted a call whose argument type did not match the vtable slot's signature. The reference compiler rejected it at LLVM verification (code generation failed); the self-hosted compiler emitted mistyped IR. Both now widen the argument to the method's declared parameter type, exactly like a direct call. Fixed in lockstep across both code generators.
[0.6.1]
Added
\xNNhex escape in string and character literals: one or two hex digits decode to a raw byte, so byte-precise strings are possible (e.g."\xC3\x91"isÑin UTF-8,'\x41'is'A').\xwith no following hex digit falls back to a literalx. In lockstep across both lexers.- Slices from a raw pointer.
ptr[lo..hi]on a*Tnow yields aT[]slice over the pointer's memory, so heap buffers (alloc<T>(n)) can become slices, not just fixed arrays. Write-through the slice aliases the backing buffer, and.len/ indexing /for-inwork as on any slice.
Fixed
- Escape sequences in string and character literals.
\0now decodes to a NUL byte instead of the character'0'(a silent trap), character literals accept\r(and\f,\v), and string and character literals share one escape set (\n \t \r \f \v \0 \\ \" \' \xNN); an unrecognized escape still yields the character itself. Fixed in lockstep across the C++ and self-hosted lexers. - Compiler crash slicing a raw pointer.
ptr[lo..hi]on a pointer base crashed code generation (a null element type in the slice-construction path); it now builds the slice correctly (see the slice-from-pointer support above). - Global lambda crashed when called. A non-capturing lambda assigned to a global
(
let f: fn(int)->int = int(int x){ return x*2; };) compiled to a null closure, so calling it (f(6)) segfaulted; it now folds to a constant closure{ @lambda, null }and works, matching a lambda written inside a function. Fixed in lockstep across the C++ and self-hosted code generators (the self-host also gained direct calls through a global closure variable).
Changed
- Documentation accuracy pass (from an internal audit): corrected AST node counts and
added the
defercleanup-stack to the internals docs, regenerated the real--test-lexer/--test-parsersample output, fixed inline-asm operand syntax ($0, not%0) and noted that output operands are unsupported, added anintrinsicspec section and a--safeflag row, and corrected glossary token-type names.
[0.6.0]
Added
<sort>stdlib module. Generic in-placesort<T>(a, n, cmp)(heapsort, a guaranteed O(n log n) worst case) andbsearch<T>(a, n, key, cmp)over a*Tarray, driven by acmp(&x, &y)function in the C qsort/bsearch convention. Works directly on aList<T>'s backing data.<url>stdlib module. RFC 3986 percent-encoding (url_encode/url_decode) and form-query lookup (url_query_get, decoding+as space), all writing into a caller'sString.<uuid>stdlib module. RFC 4122 version-4 UUIDs (uuid_v4), formatted in the canonical 8-4-4-4-12 hex form. Built on<random>, so it is not cryptographically secure.- UTC civil calendar in
<time>.DateTimeplustime_to_utc/time_from_utc(exact days<->civil conversion over the whole int64 range) andtime_format_isofor ISO 8601YYYY-MM-DDTHH:MM:SSZoutput.<time>now imports<string>for the formatting helpers. <random>stdlib module. A seedable pseudo-random generator (xoshiro256**) with no global mutable state:rng_seed,rng_next(raw 64-bit),rng_belowandrng_range(unbiased bounded integers),rng_double([0.0, 1.0)),rng_bool, andrng_fill. A given seed always reproduces the same stream. Not cryptographically secure.<regex>stdlib module. A regular-expression engine built as a Thompson NFA run as a Pike VM, so matching is linear in the input with no catastrophic backtracking. Supports literals,., classes[a-z]/[^...], the shorthands\d \w \s(and\D \W \S), quantifiers* + ? {m} {m,} {m,n}(greedy or, with a trailing?, lazy), alternation|, capturing groups( ), and the anchors^ $. API:regex_matchfor a quick yes/no, orregex_compile+regex_search+match_groupto extract captures.deferstatement.defer stmt;runsstmt(a statement or block) when the enclosing block is left, in LIFO order, on every path out: fall-through,return,break,continue, and?-propagation. Block-scoped, so adeferin a loop body runs each iteration. It is the ergonomic way to pair an acquisition with its release (*uint8 b = alloc<uint8>(n); defer free(b);) without leaking on early exits. A defer body may notreturnorbreak/continueout of itself.errdeferstatement. Likedefer, but runs only on the error exit path (when the function leaves via?-propagation), not on a normalreturnor fall-through. For undoing partial work when a fallible step fails while keeping it on success (Conn c = open()?; errdefer close(c); handshake(c)?;).- Slice type
T[]. A fat pointer (data + length) that views a contiguous run ofT. Construct by slicing a fixed array with a half-open range (a[lo..hi]); it aliases the backing array. Supportss[i](read/write),s.len(int64), passing by value, andfor (x in s). Because the length travels with the slice, a function takingT[]needs no separate count argument. Lowers to{ ptr, i64 }. - Checked nullable pointers (
?*T). Opt-in null safety: a?*Tcannot be dereferenced, indexed, or member-accessed until it is proven non-null, andif (x != null) { ... }narrows it to non-null in that branch. A*Twidens to?*T; the reverse needs a check.*Tstays nullable (C-faithful), so nothing existing changes. Same representation as*T, checked entirely at compile time. (Self-host mirror is on the promotion track.) must_usefunction qualifier. Prefixing a function withmust_usemakes discarding its result a compile error, catching leaked allocations and dropped return values. The stdlib'sallocis nowmust_use, so a barealloc<T>(n);is rejected.--safemode. A new compiler flag that inserts runtime safety checks: slice and fixed-array indexing is bounds-checked, and an out-of-range index traps (aborts via@llvm.trap) instead of reading or writing past the end. Off by default, so release builds are unaffected. (Self-host mirror of--safeis deferred to the promotion track; the shipped C++ compiler carries it.)
Fixed
finallywas skipped on an earlyreturn/break/continuefrom inside atrybody. The cleanup only ran on fall-through and exception unwind; an early exit jumped straight out, leaking whatever thefinallywas meant to release. It now runs on those paths too (the same scope-exit cleanup mechanism that powersdefer).
[0.5.0]
Fills a set of basic C constructs the language was missing, so idiomatic C ports compile without workarounds. Each lands in lockstep across the C++ and self-hosted compilers and follows C semantics.
Added
do/whileloops.do { ... } while (cond);runs the body once before testing the condition, as in C.- Increment and decrement operators. Prefix and postfix
++/--on integer and pointer lvalues; postfix yields the old value, prefix the new, and a pointer steps by one element. - Array-literal initializers.
int[N] a = { e0, e1, ... };initializes an array in place; a short list zero-fills the remaining elements, and an over-long list is rejected. staticlocal variables. Astaticlocal has a single instance that persists across calls (C storage semantics). Its initializer must be a compile-time constant;staticon a global is rejected.- Multidimensional arrays.
T[N][M]is N arrays of M in C order (the leftmost bracket is the outer dimension). Indexing peels one dimension at a time and each index is bounds-checked against its own dimension. Nested brace initializers (int[2][3] a = { {1,2,3}, {4,5,6} }) zero-fill at every level. - Ternary conditional
cond ? a : b. Evaluates exactly one arm; the arms take a common type (two numerics promote C-style). Right-associative, soa ? b : c ? d : echains. It coexists with the postfix?Result-propagation operator: a?with a matching same-level:ahead is a ternary, otherwise propagation (parenthesize to propagate inside a ternary arm).
Fixed
switchon a sub-intsubject. Aswitchover achar(or other narrow integer) with wider case constants failed LLVM verification; the subject and case constants are now widened to a common type before lowering.
[0.4.0]
A correctness and type-strictness release. A four-front bug hunt (behavioral differential, sema soundness, synthesized-default audit, feature edges) across the C++ and self-hosted compilers found a batch of miscompiles, crashes, and soundness holes; this release fixes them and tightens the type system. The shipped (C++) compiler carries every fix below; the self-hosted compiler's matching sema checks are tracked for the promotion track.
Fixed
- A catch-less
try/finallyaborted when an exception passed through it. With an in-flight exception, thefinallyblock was skipped and the runtime aborted (std::terminate) instead of running the cleanup and propagating the exception. The exceptional path now runsfinallyand re-raises via__cxa_rethrow(as an invoke when an enclosingtrycan catch it), sotry { throw } finally { cleanup }inside a caller'stry/catchruns the cleanup and is caught. - A lambda nested inside another lambda miscompiled when it captured a variable two
scopes up. The capture was recorded only on the innermost lambda, so the enclosing
lambda did not thread it through its environment and codegen referenced a value from
another function (
Referring to an instruction in another function). Captures are now propagated to every enclosing lambda they are outer to. - Self-hosted back-end: several codegen bugs on edges the corpus never exercised.
Logical-not (
!) and bitwise-not (~) were emitted as no-ops (returning the operand unchanged), silently inverting control flow; hexadecimal and octal integer literals were passed through as raw source text (invalid LLVM IR for hex, wrong value for octal); an integer literal above 2^63 overflowed the width scan and was emitted as a truncated i32; and an array sized by a namedconst(int[CAP]) copied the name into the LLVM array type instead of the value. All now match the C++ back-end. (The C++ compiler was already correct on these.) - Self-hosted back-end: an async
for-inover a genericList<T>(T != int) miscompiled. The loop-desugar defaulted the element type tointwhen the iterable was a generic container, sofor (v in xs)over aList<double>/List<int64>truncated each element (and emitted invalid IR forList<Struct>). The desugar now resolves the element type by substituting the container's type argument, matching the C++ back-end. (Residual R1 inPROMOTION_PLAN.md.) - Self-hosted back-end: exceptions were mishandled. A catch-less
try/finallyswallowed an in-flight exception (running cleanup but then continuing as if caught), and athrowfrom inside a catch handler that had to cross a function boundary was lost (the runtime terminated). The self-host now runsfinallyand re-raises via__cxa_rethrowon the exceptional path, and names a rethrown value's type from its LLVM type when it cannot be resolved structurally, matching the C++ back-end. - Comparison operators did not type-check their operands.
==,!=,<,>,<=,>=were accepted for any pair of types (pointer vs int, struct vs struct, string vs int), so the checker reported success and codegen emitted a malformed comparison (an assertion under an assertions build, a miscompile otherwise). They now require mutually comparable operands: both numeric, both pointer-like (includingnull), or the same non-aggregate type. Enum, pointer, andnullcomparisons are unaffected. - Incompatible non-numeric variable initializers were only a warning.
int x = f();wherefreturnsvoidlet a non-value into codegen and hung the back-end;int x = "hello";crashed at run time. Both are now compile errors, matching the assignment path. - Taking the address of a
constproduced a mutable pointer.const int N; *int p = &N; *p = 99;silently mutatedN.&of a const location now yields a pointer to const, so writing through it (or assigning it to a plain pointer) is rejected. floatcompared againstdoubleorintfailed to compile. The comparison operators did not promote a mixed float/int (or float/double) pair to a common float type, so LLVM rejected thefcmp. They now promote like the arithmetic operators do.- Integer literals in [2^31, 2^32) were truncated to i32 and sign-extended. A
decimal literal such as
3000000000assigned to anint64materialized as a 32-bit constant (high bit set), then sign-extended to a negative value (-1294967296). The codegen now widens any literal that does not fit a signed i32 to i64 (matching the self-host), so large literals keep their value. unsigned -> floatconversions used signedSIToFP. A high-bit-set unsigned value (for exampleuint32 4000000000) converted to a negative float. Integer to float conversion now selectsUIToFPvsSIToFPby the source's signedness, in both back-ends (routed through a singleintToFloathelper on the C++ side).- A generic (template) function could fall off the end without returning. The
missing-return analysis skipped template bodies, so a generic like
T pick<T>(T a, int c) { if (c > 0) { return a; } }compiled (returning a synthesized zero under the C++ back-end, crashing under the self-host). The definite-return check now runs on template bodies too.
Changed
- Assigning a floating-point value to an integer needs an explicit cast.
int x = 3.9;(or anyfloat/doubleinto an integer) is rejected, because it silently drops the fraction. Write the cast to keep it:int x = (int)3.9;. Integer-width narrowing (int n = strlen(s);,int64intoint,intintouint8) and float-width narrowing (float f = aDouble;) stay implicit, matching C. This is the one numeric conversion C itself flags under-Wall. - An integer literal that does not fit its target type is a compile error.
int8 x = 300;(out ofint8's range) is rejected rather than silently wrapping to 44. - Division or remainder by a literal zero is a compile error.
x / 0andx % 0are rejected at compile time instead of trapping at run time. - A constant array index proven out of bounds is a compile error.
int[3] a; a[5](or a negative constant index) is rejected. - Reading an uninitialized scalar local is a compile error.
int x; return x;(and calling a fn-pointer that was never assigned) is rejected by a conservative definite-assignment check over the function's straight-line prefix; taking&xor assigningxfirst clears it, and branchy code is never a false positive. - Returning the address of a local is a compile error.
*int f() { int x; return &x; }is a dangling pointer and is rejected (&(*ptr)/&ptrParam.field, which point into caller memory, are still fine). - Redefining a function is a compile error. Two definitions of the same name are rejected (a body-less forward declaration alongside one definition is still allowed).
mainmust returnint. Its return value is the process exit code, sovoid main()(which left the exit code as an undefined, platform-dependent register value) is rejected; writeint main() { ... }.- Falling off the end of a non-void function is now a compile error. A non-void
function that returned on no path still compiled before: the C++ back-end
synthesized an implicit
ret 0/ret null, so `int add(int a, int b) { int r = a - b; }
silently returned 0 rather than the computed value, and the last expression was never the result. The self-hosted back-end emittedunreachablefor the same code, so the two compilers disagreed and the self-host build crashed (SIGILL) on a program the C++ build ran cleanly. Both now reject it in sema withmissing return in non-void function, via a definite-return analysis.voidfunctions may still fall off the end,asyncfunctions complete their future implicitly, and a body that provably cannot fall through needs no trailingreturn(anif/elsewhere both branches return, an exhaustiveswitch/matchwhere every arm returns, or an infinitewhile (1)with nobreak`).
[0.3.1]
Fixed
*T[N]now parses as an array of pointers, not a pointer to an array. The type-string parser (ty::Type::parse) peeled a leading*before the trailing[N], so*Node[7]became a pointer toNode[7]and lowered to a single opaqueptr, while codegen'sIndexExprlowering assumed the array reading. The two disagreed: indexing such a value emitted an invalid two-index GEP into a scalar pointer, silently corrupting locals and crashing the compiler outright on a module-level array (a constant-folded GEP tripped an LLVM assertion). The trailing[N]now binds outermost, so*Node[7]is an array of 7 pointers consistently across the type checker and codegen. (This entry used to say a pointer to an array is spelledNode[7]*; that form does not parse, so point at the first element with a*Nodeinstead.) Found porting a C program whose central data structure was a module-levelActividad *agenda[7].-
<eventloop>: initialize theon_readclosure of every fd slot.el_newzeroedactive/gen/isWritebut left theon_readfat pointer asallocgarbage. Dispatch is guarded byactive, so this was latent, but any stray read would invoke a garbage function pointer, a SIGILL on Linux/x86-64 (fresh allocations aren't zero there as they happen to be on macOS). Now defaulted to a no-op. Defensive hardening for the intermittent HTTP/2-test SIGILL under investigation (see theproject-flaky-http2note). -
A lambda's return type is reconciled with its target
fn(...)->Rtype. Assigning a lambda whose header return type disagrees with the declared closure type (e.g.let f: fn(int)->float = int(int x) { return (float)x * k; }) emitted a function returning the header type (int): anfptositruncation plus an int/float return- register mismatch against the closure's call ABI. It was correct at-O0by luck but a silent0.0miscompile under-O2. Sema now sets the lambda's return type to the target'sRso itsreturncoerces through the normal path. Surfaced by an-O0-vs--O2differential over the whole test corpus (closures.eskwas the only divergence). - Incompatible function-type assignments are now rejected. Assigning a function
value to a
fn(...)slot with a different signature (e.g. anint-returning function to afn(int)->float) was silently accepted: a fn value has a fixed call ABI (param and return registers), so there is no implicit adapter, and reinterpreting it miscompiled (wrong even at-O0,0.0under-O2).isValidAssignmentnow requires fn types to match exactly, and a mismatched fn initializer is a hard error rather than a warning. Lambda literals in alet x: fn(...)->R = ...still coerce (their return type is malleable); only non-adaptable fn values are rejected. Testerrors/fn_return_mismatch.
Changed
- New CI gate:
-O0-vs--O2behavioral differential (tests/opt_differential.sh). Compiles the whole corpus at both levels and fails on any exit/stdout divergence, guarding against optimization-path miscompiles now that-Oexists (it caught the float-closure bug above). The intermittent HTTP/2 async tests are excluded to keep the gate deterministic. - Self-hosted parser parity now covers the full corpus (51 → 121).
parse_mainpreprocesses the top-level file (matching how the C++--test-parserfolds preprocessing into the lexer), soparse_parity.sh --fullno longer excludes files whose import closure touches the preprocessor (#ifdef/#define/__FILE__/__LINE__/shebang). A prerequisite for promoting the Eskiu-written compiler (seeselfhost/PROMOTION_PLAN.md, R2).
[0.3.0]
The self-hosting milestone: the whole compiler (lexer, preprocessor, parser, semantic
analyzer, and code generator) is reimplemented in Eskiu (selfhost/), reaches a 3-stage
bootstrap fixpoint, and the self-hosted codegen is feature-complete against the C++
corpus (a full feature sweep is clean). All parity/self-host/bootstrap gates are CI-wired.
Fixed
&&and||now short-circuit. Code generation evaluated both operands eagerly and emitted a logical-and/or, so the right-hand side always ran; a guarded dereference likep != null && p.fieldcould fault. They now lower to a conditional branch + PHI, evaluating the RHS only when the LHS doesn't decide the result. Found dogfooding the self-hosted type checker. Regression testtests/short_circuit.esk.--test-parserno longer crashes on a forward-declared function. The AST printer dereferenced a nullFunctionDecl::body(a prototype likeint f(int);), segfaultingeskiuc --test-parser. It now omits theBody:section for a body-less function (asReturnStmtalready does for a null value). Found dogfooding the self-hosted parser. Affected only the debug printer, not codegen.- C
size_texterns now useint64, notint. The<mem>/<string>declarations formemcpy/memset/memmove/memcmp/memchr(size argument) andstrlen(return) wereint(i32). That is an incorrect ABI on LP64 targets and truncates sizes above 4 GB. They are nowint64, matchingsize_t. Behavior is unchanged for the common sub-4 GB case (the value zero-extends into the argument register either way).
Added
-Ooptimization levels.eskiuc -O1/-O2/-O3now runs the LLVM middle-end pipeline (mem2reg/SROA/instcombine/inlining/GVN/...) over the module before code generation.-O0(the default) keeps the prior behavior: naive IR straight to the backend. On real code this collapses the per-local stack traffic the front-end emits (theine_decoderdemo drops from 514 allocas to 25 at-O2); its Makefile now builds with-O2.- Clearer diagnostic for a keyword used as a name. Using a reserved word (
fn,in,match, a type name, ...) as a variable, parameter, or field name now reportsexpected a name, found keyword 'fn'at the cause, instead of a misleading downstream error (Expected ';',Expected expression). This was the most recurring self-host papercut. The self-hosted parser mirror is tracked inselfhost/PROMOTION_PLAN.md(R3). String_freeclearsdata. It setsself.data = nullafterfree, so a reused or doubly-freedStringcan no longer hand a dangling pointer tofree.Stringlength/capacity are nowint64.%Stringwent from{ ptr, i32, i32 }to{ ptr, i64, i64 }; the length/index/capacity arithmetic inside<string>isint64throughout. Matchessize_t, removes the 2 GB object-size cap, and avoids width casts at the libc boundary. Public accessors that return a length (String_len,String_index_of) now returnint64; callers that store into aninttruncate exactly as before.- Self-hosted codegen is now feature-complete against the C++ corpus. A systematic feature
sweep (every feature-bearing
tests/*.eskthroughcg_parity.sh) is clean: each program, compiled by the Eskiu-written codegen, runs identically to the C++ build. Closing it required 11 root-cause fixes the sweep exposed (the bootstrap fixpoint had only ever exercised the subset the compiler's own source uses): var-decl/struct-init coercion + signedness-aware integer widening; full integer semantics (binary-op operand-width unification, unsignedudiv/lshr/icmp, >32-bit literals asi64, vararg small-int promotion); type-alias resolution; function-as-value decay (a__fnptrenv-dropping thunk +{thunk,null}closure, or a bare@fnfor a pointer cast); generic ADT enum monomorphization; a self-host parser fix (a parenthesized struct literal(P{…})was mis-parsed as a cast); primitive constraint dispatch (a.m(b)on a primitive →m(a,b)); thealloc_with/thread_create/thread_join/free_closurebuiltins; user-defined variadic functions +va_list/va_start/va_arg<T>/va_end; packed structs (packed/#pragma pack(N)); and the?error-propagation operator. All parity/self-host/bootstrap gates stay green throughout. - Self-hosted codegen: ADT payloads wider than one slot. An ADT enum's tagged-union
payload area (
{ i32, [N x i64] }) is now sized by bytes with field alignment (cg_layout_size, mirroring the C++ DataLayout), not by field count, so a variant carrying a struct-by-value larger than 8 bytes (e.g.Line(Vec3)whereVec3is 12B) fits instead of overflowing its slot. Construction +matchextraction already viewed the payload as the variant's{ fields }struct, so only the area sizing needed the fix. Testadt_big_payload. cg_parity 56/56. (A subsequent feature sweep found this was NOT the last gap; see below.) - Self-hosting feature coverage audited + closed out. Verified the async combinators
select2/join2/spawnall work through the self-hosted codegen (addedasync_select/async_spawnto the parity corpus). The self-hosted drivers (esk_main/cg_main) now route diagnostics to stderr (aneprinthelper overwrite(2, …)+sprintf) so an error can never contaminate the.lltext on stdout. cg_parity 55/55, cg_selfhost 68/68, bootstrap fixpoint. (NOTE: a later systematic feature sweep, pushing the C++ test corpus through the parity oracle, showed self-host codegen is NOT yet feature-complete; ~8 root-cause gaps remain, tracked inselfhost/BACKEND_PLAN.md. The bootstrap only exercises the subset the compiler's own source uses, so it missed them.) - Self-hosted codegen: more of the language. Beyond the bootstrap subset, the
self-hosted code generator (
selfhost/codegen.esk) now also lowers: floating point (fadd/fsub/fmul/fdiv,fcmp,fneg, and int↔float casts viasitofp/fptosi/fpext/fptrunc, with mixed int/float promotion);switch(a real LLVMswitchwith C-style fall-through +break); and ADT enums +match(the tagged-union layout{ i32 tag, [N x i64] payload }, variant construction, andmatchlowered to a tag switch with payload bindings). Each is behaviorally parity-tested (cg_parity.sh). Return values are now coerced to the function's return type (e.g. aboolresult zero-extended toint). It also lowers closures/lambdas (free-variable capture into a stack env + a fat pointer{ fn, env }, higher-order functions, indirect calls) and exceptions (the Itanium ABI:invoke/landingpad,__cxa_allocate_exception/__cxa_throw/__cxa_begin_catch, type-namestrcmpcatch matching,finally; programs withthrow/trylink-lc++abi) and atomics (atomic_load/store/swap/cas→load atomic/store atomic/atomicrmw xchg/cmpxchg). The self-hosted compiler reproduces its own IR throughout (bootstrap fixpoint stays green). - Self-hosted codegen: unions, bitfields, and dynamic trait dispatch. The
self-hosted code generator now also lowers: unions (every member overlaps at
offset 0; the type is
{ [N x i8] }sized to the largest member, member access GEPs to the shared storage); bitfields (declared widths packed intoi32words: readslshr+and+optionalsext, writes read-modify-write the word with a cleared mask, struct-init fills bit slots, and a normal field in a bitfield struct uses its real word index); and interfaces / dynamic trait dispatch (an interface value is a fat pointer{ data, vtable }; passing a struct pointer where an interface is expected boxes it, building a per-(interface, struct)vtable global of the struct's method implementations, and a call through an interface value dispatches by loading the method pointer from its vtable slot and calling it withdataas the implicit receiver). Each is behaviorally parity-tested; the bootstrap fixpoint stays green. - Self-hosting async: all 19 async tests pass.
for-inover an await now lowers too (desugared to a countedfor: arrayT[N]indexesxs[i]/length N; a list-like struct usesxs.data[i]/xs.size), completing the async transform. (Was 18/19.) - Self-hosting async: 18 of 19 async tests pass. Closures' environments are
heap-allocated (an escaping closure, e.g. an event-loop callback or a future combinator,
called after its creating frame returns, no longer dangles its captured variables); struct
literals work as rvalues (
x = P{…}, not just var-decls); and the async constructor emits theon_dropcancellation closure (future_dropon a suspended task cascade-drops the awaited future). Together these unblock the event-loop / socket / timer / combinator (select/join/spawn) / cancellation async tests. (Only asyncfor-inremains: it needs iterable element-type resolution.) - Self-hosting back-end: the async/await lowering pass, in Eskiu (
selfhost/async_lower.esk, a port ofsema/async_transform.cpp, run between parse and codegen). Eachasync fnis rewritten into a frame struct + awhile(1){if st==0…}state-machine resume function + a constructor that returns aFuture<T>*; eachawaitsplits a state (evaluate the future,future_pollit, suspend, resume reading the value), with completion viaatomic_swapand a waker closure, built on the now-available closures, atomics, and generics. Covers sequential bodies, control flow around an await (if / while / for with break/continue, lowered into the state graph), and a desugar pass (return await E/await E;/x = await E→ let-bound).async_basic/async_if/async_for/async_multi/async_break/async_return_awaitrun to parity. (Tests using generic-argument inference (chan_recv(ch)without<T>) or async for-in/switch, or closure→fn-pointer extern callbacks, await those orthogonal features.) Code generation also now de-duplicates extern declarations (the sameexternin two imported stdlib files no longer emits twodeclares). - Generic-argument inference. A generic function called without explicit type arguments
(
chan_recv(ch),unwrap(&b),add(7, 5)) now has its type parameters solved by unifying each parameter's pattern (Chan<T>*,Box<T>*,T) against the actual argument types, then monomorphized like an explicitchan_recv<int>. Pointer spellings are normalized (Box<T>*≡*Box<int>) andcg_etypenow types literals. This unblocks the channel-based async tests (async_channel,async_elseif) and any generic call relying on inference. - Unified self-hosted compiler driver + a three-stage bootstrap.
selfhost/esk_main.eskruns the full pipeline in Eskiu (preprocess → parse → type-check → code-gen), rejecting ill-typed input without emitting IR. The new gatetests/selfhost/cg_bootstrap.shperforms the canonical bootstrap: the C++eskiucbuilds the driver (cc0), cc0 builds it (cc1), and cc1 builds it (cc2); cc1 and cc2 emit byte-identical IR for the compiler and for a sample program, and cc2 compiles a runnable binary, i.e. stage2 ≡ stage3, a true self-hosting fixpoint. Wired into CI. - Self-hosting reached a bootstrap fixpoint (Phase D). The self-hosted code
generator emits valid LLVM IR for the entire self-hosted compiler, and
cg_maincompiled by itself reproduces the C++-built code generator's IR byte-for-byte over all 45 inputs (the wholeselfhost/tree + the corpus). All five drivers (lexer/preprocessor/parser/typechecker/codegen), compiled by the self-hosted codegen, produce output identical to the C++-built ones. New gatetests/selfhost/cg_selfhost.sh(emit-validity + fixpoint, wired into CI). Eleven codegen bugs were found and fixed at the root by dogfooding the compiler's own source. - Self-hosting back-end, Phase C: the code generator, in Eskiu.
selfhost/codegen.esk(drivercg_main.esk) generates textual LLVM IR (assembled + linked byclang, no LLVM library). Covers scalars/arithmetic/control-flow, structs + methods + pointers, arrays, plain integer enums, generics via monomorphization (List<T>,Box<int>→%Box_int,id<int>→@id_int, withsizeof/cast so the stdliballoc<T>instantiates), global variables, struct-by-value (sret), pointer arithmetic, and short-circuit&&/||. Behavioral oracle: emit.ll→clang→ run → compare exit code + stdout to the C++-built binary, over a synthetic corpus (tests/selfhost/cg_parity.sh, wired into CI). Deferred (unused by the compiler's own source): floats, ADT enums/match, closures, exceptions, async, atomics. - Imported files are now preprocessed. The self-hosted parser ran
imported files straight into the lexer, skipping the preprocessor, so an import's#ifdefkept both branches (e.g.stdlib/mem.eskemitted a duplicatefree).do_importnow runspp_runper imported file, mirroring how the C++ folds preprocessing into the lexer. - Self-hosting back-end, Phase B: the type checker, in Eskiu.
selfhost/sema.esk(drivertc_main.esk, full pipeline preprocess → parse → check) now catches all 19 semantic error classes intests/errors/: name resolution, argument counts, undefined types & fields, async/await rules, switch/match exhaustiveness & duplicates, const-correctness (value, field, and pointer-pointee), interface satisfaction for bounded generics, the?operator's return type, and closure-escape soundness. Gatetests/selfhost/tc_parity.sh(wired into CI): the verdict matcheseskiuc --test-typecheckeron all 121 positive corpus files with zero false rejections, and every sema negative test is rejected with the right diagnostic. Types flow as strings (no structured Type IR), matching the C++ checker's codegen boundary. - Self-hosting back-end, Phase A: AST enrichment. The self-hosted parser's AST
(
selfhost/{ast,parser}.esk) now captures what it previously parsed-and-discarded, so it can feed a future sema/codegen: generic type-params + constraints, struct methods, enum ADT payloads, bitfield widths, the async modifier, and full interface method signatures. Validated by a lockstep extension of both the C++ast/ast_printer.cppand the self-hosted printer (newTypeParams:/Methods:sections,Name = tag(t1, t2),type name : N,(async),ret name(t1, t2)), keeping--test-parserbyte-identical (corpus 50/50, synthetic 11/11). Tooling; the production compiler change is the additive, debug-only printer extension. - Self-hosting milestone 3: the preprocessor, in Eskiu.
selfhost/preprocessor.eskportslexer/preprocessor.cpp: object- and function-like#define/#undef,#ifdef/#ifndef/#else/#endifconditionals,#pragmapassthrough,#error, backslash line splicing, recursive identifier-aware macro expansion, and the predefined__FILE__/__LINE__. Validated through the lexer (pp_mainpreprocesses + lexes; gatetests/selfhost/pp_parity.sh): byte-identical to the C++--test-lexerover the wholetests/+stdlib/corpus (156/156, no exclusions), clean and directive-using files alike. Wired into CI. Tooling; the production compiler is untouched. List_set/List_removein<list>: set an element by index, and remove one (shifting the tail). Surfaced by the preprocessor's macro table (redefine = set,#undef= remove); generally useful.- Self-hosting milestone 2 complete: the parser, in Eskiu. The self-hosted
parser (
selfhost/{ast,parser,parse_main}.esk) now covers the full grammar ( expressions, statements, declarations, templates/generics, lambdas/async) and is byte-identical to the C++--test-parserAST dump over the import-free corpus (42/42 realtests/*.esk+ synthetic). Gatetests/selfhost/parse_parity.sh(--full) is wired into CI. Dogfood/tooling; the production compiler is untouched. (v0.2.5 shipped this milestone in progress; it is now finished.) - Self-hosted parser follows
import. It now resolves and recursively parses imports (<name>→stdlib/name.esk, relative"path"against the importing file's directory), merging the imported decls in declaration order, with dedup and shared type-name registration across files (mirroring the C++ parser). Parity now covers everytests/*.eskwhose transitive import closure is preprocessor-free (corpus 43 → 50); files whose closure touches the preprocessor stay excluded until the preprocessor is self-hosted. The harness computes the closure to gate inclusion.
[0.2.5]
Added
<ctype>stdlib module. Pure-Eskiu ASCII character classification (no libc, freestanding-safe):is_space,is_digit,is_hex,is_alpha,is_alnum,is_ident_start,is_ident_cont. Introduced for the self-hosted lexer; usable by any program viaimport <ctype>;.- Self-hosting milestone 1: the lexer, in Eskiu (
selfhost/+stdlib/ctype.esk). A full lexer written in Eskiu, byte-identical to the C++--test-lexerover the entire preprocessor-free corpus. Parity gatetests/selfhost/lex_parity.sh(--full→ 114/114; compiles the driver once to a native binary, ~seconds) is wired into CI. Dogfood/tooling; the production compiler is untouched. Seeselfhost/README.md. - Self-hosting milestone 2: the parser, in Eskiu (
selfhost/{ast,parser,parse_main}.esk), in progress. Builds a recursive AST (tagged heap structs) from the self-hosted lexer's tokens and prints it byte-identical to--test-parser; gatetests/selfhost/parse_parity.sh. Done so far: the full expression layer (precedence chain, unary, postfix call/index/member/?, casts,sizeof, all literal kinds). Statements/declarations/templates next.
Hardening
- Resolver consistency check (
ESKIU_RESOLVER_DEBUG). Codegen's type derivation is split out (deriveExprEskiuType) so that, under the env flag, the single-resolver table is cross-checked against the structural derivation on every hit; a semantic disagreement (the v0.2.4 two-evaluator miscompile class) is printed. Across the whole corpus the two agree on every behavior-affecting type (the only difference is a benign enum-as-int representation), confirming the v0.2.4 reconciliation holds. Behavior-preserving (golden IR 26/26). - Fuzzer: backslash-newline in comments & strings. New generators with a self-checking expected-output oracle (the O0/O2 differential is blind to a uniformly mis-lexed program), guards the comment-continuation footgun fixed below.
Fixed
- Preprocessor: a
//comment ending in\no longer swallows the next source line. Backslash-newline line continuation was applied unconditionally, so a comment whose last character was a backslash spliced the following line into the comment, silently deleting areturn, anelsebranch, or any statement, with no diagnostic. Continuation now fires only when the trailing\is genuine code (not inside a//or/* */comment or a string/char literal); legitimate#definecontinuation is unaffected. Found by dogfooding the self-hosted lexer. Regression test:tests/comment_backslash_continuation.esk.
[0.2.4]
Type unification: making the type checker the single resolver, so codegen stops re-deriving types independently (closing the two-evaluator risk). Internal soundness work; the only user-visible effect is three latent miscompiles it surfaced and fixed.
Single type resolver (kills codegen's independent re-derivation)
- The type checker is re-run on the post-AsyncTransform AST and its resolved
per-expression types are handed to codegen, which now consumes them instead of
re-deriving via
getExprEskiuType. This eliminated the structural condition behind the earliercheckConstraintsbug, and immediately surfaced and fixed three latent miscompiles where the two evaluators silently disagreed (benign only because the affected test values were small): - Float literals are
doublein sema too (were typedfloat; codegen always emitted adoubleconstant, so generic inference likemax(1.5, 2.5)now monomorphizes todouble, matching the emitted value). - Pointer-arithmetic deref width:
*(arr + n)forarr: *intloadedi8in some paths (reading 1 byte of a 4-byte int); now correctlyi32. charwidening signedness: acharwidened tointusedsextin some paths;charis unsigned in Eskiu, so it is nowzext.- Plus a
for-inconsumption fix (the resolved iterable type arrives asstruct:List_int; the loop lowering now strips thestruct:decoration). - One grammar interpreter.
codegen'sgetTypeFromStringnow dispatches onty::Type::parse, the same parser the type checker uses, instead of its own hand-rolled string matching. The type-string grammar is interpreted in exactly one place across both phases. Behavior-preserving (golden-IR identical; existing quirks preserved).
[0.2.3]
Completing bounded generics, plus a typed internal type representation that replaces ad-hoc type-string surgery: the foundation for keeping the compiler sound as it grows.
Bounded generics: primitives can satisfy a constraint
- A primitive type now satisfies a constraint through a free function named
like the interface method whose first parameter is that primitive, e.g.
int cmp(int, int)makesintsatisfyinterface Ord { int cmp(Self) }. Inside a generic body a constrained callt.cmp(x)on such atlowers tocmp(t, x). Somax<T: Ord>(int…)and a constraint-bounded map overintkeys now compile. Closes the method-only seam; the fn-pointerHashMap<K,V>remains for explicithash/eq. Sema and codegen are gated in lockstep (scalar primitives only).
Typed Type representation (soundness foundation, no behavior change)
sema/type.{h,cpp}: a structuredty::TypeIR (a tagged value withparse/str/substitute/nominalName), the typed replacement for thestd::stringtype surgery that was scattered across ~175 call sites. Types still travel as canonical strings at the boundaries; the IR is the manipulation form, so invariants are enforced by the type instead of by convention (which is what produced the earliercheckConstraintsstruct:-ordering bug).- Migrated onto it:
substType(nowType::substitute) and the duplicated "stripstruct:+ pointers → bare name" surgery at six sema sites, including the exactcheckConstraintsstrip that caused that bug, which no longer exists. - A golden-IR oracle (
tests/type_zoo/snapshot.sh) snapshots the emitted IR over a 24-program type-zoo and asserts byte-identical output across the migration. Every step above is verified behavior-preserving by it, plus the suite, sanitizers, and the differential fuzzer. A standalone round-trip unit test (tests/type_roundtrip/) checksparse(s).str() == sover the grammar. - The registry-coupled
normalizeType/unifyTypeParamand the cross-phase consolidation (codegen consuming resolvedTypes rather than re-deriving from strings) are intentionally left for a later step: they are not string surgery.
[0.2.2]
Exclusively hardening + traits, no new stdlib surface. The goal: make the compiler trustworthy and close the generics gap before the self-hosting arc.
Compiler hardening
- A fuzzer (
tests/fuzz/eskiu_fuzz.py): mutates the test corpus and generates programs biased toward the known bug classes, using the LLVM IR verifier and sanitizers as oracles (a crash, a verifier failure, or an asan/ubsan abort is a finding). Wired into CI as a bounded, fixed-seed job. Its first run found four real bug classes, now fixed: - Unreachable code after a block terminator (
return/break/…) was emitted into the terminated block → "terminator in the middle of a basic block". Codegen now stops at the terminator and drops the dead code. - Integer arguments on indirect / closure calls weren't widened to the callee's parameter type → an IR-verifier mismatch. Now coerced (the same family as the 0.2.1 sret-argument fix).
switchwith duplicate case values reached codegen and produced an invalidswitch→ now a clean type error.- A negative array dimension degraded the variable to a bare pointer that was then array-indexed → invalid GEP. Now rejected with a clear message.
Traits / constraints (bounded generics)
<T: Iface>(and<T: A + B>) on function and struct type parameters, built on the existing structuralinterface. At instantiation the concrete type argument must satisfy every listed interface (define its methods); otherwise it's a type error at the use site (type 'X' does not satisfy constraint 'I') instead of a confusing downstream failure. Inside a generic body, a constrained method call resolves to the concrete type's method. Works for bothT f<T: Ord>(…)andstruct Box<T: Ord> { … }. (Constraints are method-based, so a constrained type must be a struct with the methods; primitive keys still use the fn-pointerHashMap<K,V>from 0.2.1.)
Hardening + maintainability (0.2.2 re-cut, no behavior change)
- Fuzzer: O0-vs-O2 differential oracle. The fuzzer now also builds each
verifier-clean generated program at
-O0and-O2with clang and compares the runtime output; a divergence is a miscompile the IR verifier can't catch (valid IR, wrong semantics). Plus four more generators (ADT enums +match, capturing closures, async/await across control flow, nested structs) and mutation of generated programs. Wired into CI. - Compiler source modularized. The three ~2,000-line files were split, with no
behavior change:
sema/type_checker.cpp→typecheck_{decl,stmt,expr,type}.cpp;codegen/codegen_expr.cpp→codegen_{call,closure,adt}.cpp;parser/parser.cpp→parse_{decl,stmt,expr}.cpp; the lexer's preprocessor pass intolexer/preprocessor.cpp; andmain.cpp's driver utilities intomain_support.cpp. No source file exceeds ~760 lines.
[0.2.1]
Hardening and ergonomics, shaken out by building a real service (an INE-QR HTTP API) on 0.2.0.
Compiler
codegen.cppsplit intocodegen_{module,type,scope,decl,stmt,expr}.cpp: the 3,400-line file became six; no behavior change (same 243 tests).- Sanitizer hardening gate:
tests/run.shgainsSANITIZE=asan|ubsan(compiles every positive test with the instrumentation and fails on a sanitizer abort), and CI runs the whole suite under both. A newloop_localsstress test locks in the entry-block-alloca fix. - Fix: integer arguments to sret-returning functions were matched against the wrong parameter: the coercion loop didn't skip the hidden sret pointer at index 0, so the first scalar argument was checked against the sret pointer and an
intliteral was left unwidened (an IR-verifier error for a >16-byte-returning function called with int literals). Now offset-correct. - Fix:
substTypesubstitutes type parameters inside function types (fn(*K)->uint64→fn(*int)->uint64), so generic APIs with callback parameters type-check and monomorphize.
Standard library
<bytes>:Bytes, a growable binary-safe byte buffer (*uint8+ length; embedded NULs are real data, unlikeString's NUL-terminated*char):Bytes_init/_free/_push/_append/_append_raw/_slice(non-owning view)/_eq/_from_str/_cstr, plus base64 convenience (Bytes_from_base64/Bytes_to_base64).<http>gainsHttpReq_body: a non-owningBytesview of the request body.HashMap<K,V>(<map>): a hash map over any value-type key: you passhash/eqfunction pointers at init (Eskiu has no trait system to synthesise them, the systems-language answer, like C'sqsortcomparator), with built-inint_hash/int_eq. The string-keyed, key-owningMap<V>is unchanged.
[0.2.0]
Backend-services phase: async/await, the full HTTP/2 stack (framing, HPACK with
Huffman, streams and flow control, a multiplexed server, and TLS/ALPN), sum types
with match, and a broad async/networking standard library, built on the v0.1.0
systems foundation.
Language
alloc/freeare no longer keywords: heap allocation moved to the<mem>stdlib as the generic functionsalloc<T>(n)andfree(p). Under--freestandingthey targetesk_alloc/esk_freevia the predefined__ESKIU_FREESTANDING__macro.const: immutable typed bindings, usable as array sizes; field/element mutation of aconstvalue is rejected.- Raw C function pointers: casting a top-level function to a pointer type (
(*void)cmp) yields its bare C function-pointer address rather than the{fn, env}closure fat pointer, so an Eskiu function can be handed to a C API as a callback (qsort, OpenSSL's ALPN selector, signal handlers, …). Test:c_callback. - Pointer constness:
const T*is a pointer to const T (the pointee is read-only, the pointer is rebindable);T* constis a const pointer (the binding is read-only, the pointee is writable); the two compose asconst T* const. Reading through and rebinding aconst T*are allowed; writing through it (*p,p[i],p->field) is rejected, as is any conversion that drops a const qualifier: at initialization, assignment, call arguments and returns. Adding const (T*→const T*) is always allowed. const has no ABI effect and is stripped before code generation. Works in locals, parameters, struct fields and return types. - Integer ranges in
for-in:for (i in A..B)iterates the half-open range[A, B)(a new..operator). Desugars to a countedfor, so it composes withbreak/continueand the async transform. - User-defined variadic functions: a fixed parameter list followed by
..., read inside the body withva_list/va_start(ap)/va_arg<T>(ap)/va_end(ap)(C default promotions apply: read float arguments withva_arg<double>). Lowers to the LLVMva_argmachinery; works on arm64 and x86-64. - Preprocessor diagnostics:
__LINE__(current source line) and__FILE__(current file path) expand in place, and#error messageaborts compilation (honoring#ifdefbranches). The VS Code grammar now also highlights all preprocessor directives. - Algebraic enums +
match: anenumvariant may carry a payload (enum Shape { Circle(float), Rect(float, float), Unit }), making the enum a tagged union. Variants are constructed by name (Circle(2.0), bareUnit) and destructured withmatch s { Circle(r) -> …; _ -> … }, which binds payload fields per arm. Classic integer enums are unchanged.matchis checked for exhaustiveness (every variant covered, or a_default) and rejects duplicate arms. Algebraic enums may be generic (enum Option<T> { None, Some(T) },enum Either<A,B> { Left(A), Right(B) }) monomorphized per instantiation like template structs; generic variants infer their type arguments from the payload when it determines them (Some(5)→Option<int>), or take explicit ones otherwise (None<int>(),Left<int,string>(x)). #pragma pack(N)for N > 1: caps each field's alignment at N (v0.1.0 honoredpack(1)only). The layout matches the C#pragma pack(N)ABI: padding is inserted to N-capped field offsets and the total size rounds up to the struct's alignment (min(max-field-alignment, N)).pack(push, N)/pack(pop)/pack()still apply.alloc_with(&allocator, T, n): explicit-allocator built-in.volatile let: qualifier-first form (likeconst int), in local and top-level declarations.- Multi-argument
fntypes (fn(A,B)->R); function pointers usable as values, parameters, return types, and struct fields, and capturable in closures. intrinsic: function qualifier for compiler-lowered prototypes (distinct fromextern): a call lowers to inline IR, not a call to a C symbol, and emits nodeclare. Gated on import, so the names stay un-reserved otherwise.escaping+free_closure: closures now escape correctly. A non-escaping closure (only called / passed to a non-escapingparameter) keeps its environment on the stack (zero cost); an escaping one (returned, stored, or passed to anescapingparameter) gets a heap environment that survives. Theescapingparameter qualifier marks closure-retaining functions; using a non-escapingclosure parameter beyond a direct call is a compile error.free_closure(f)releases an escaping closure's heap environment. (Previously, a closure that outlived its creating function read a dangling stack environment.)- Fixed: a cast to a type imported from another file (e.g.
(FutureHdr*)p) was misparsed; imported type names are now visible for cast detection. Follow-up fix: when that type's defining import was deduplicated (already imported via another path), its name was still missing from the importing file's parser, so the cast misparsed again. Declared type names are now shared across all sub-parsers (like the import set), so a cast parses regardless of import order. async/await:async T f(...)whose call yields*Future<T>, and theawait Eexpression (E: *Future<T>→T), legal only inside an async function. A pre-codegen pass lowers an async function to a resumable state machine (frame struct + resume + constructor). Single and multiple awaits run end-to-end (fast path and suspend-over-reactor), includingreturn awaitandasync void. Cancellation works, every control-flow construct around await is supported (if/while/C-stylefor/switch/for-in, includingbreak/continue), and the runtime is leak-free (verified withleaks). Fixed: a frame-hoisted local used inside a struct literal /alloc_with/free_closureafter an await was not renamed to its frame field (the transform's expression walkers skipped those nodes). All expression nodes now share one child enumeration (ast/ast_walk.h).- Capturing closures inside generic functions: a lambda declared in a template function body now captures correctly. Capture analysis runs on template bodies (type-independent), and a capture typed by the type parameter (e.g.
Tor*Future<T>) has its environment field substituted per instantiation. (Previously such a lambda got an empty capture list and miscompiled.)awaitalso now accepts a*Future<T>returned by a generic function (its type arrives already instantiated). Removed the deadthread/spawn/mutexreserved words (the lexer reserved them but nothing used them), freeing those identifiers. spawn<T>/select2<A,B>/join2<A,B>: generic future combinators with a typed, cast-free call API (spawn(task),await select2(a, b),await join2(a, b), type args inferred).spawndetaches a fire-and-forget task;select2completes with the index of whichever of two futures finishes first (loser dropped);join2completes once both finish. Thin wrappers over one shared type-erased core (*_hdr), so no per-instantiation bloat; leak-free (verified withleaks).
Compiler fixes
- Unsigned widening cast:
(int)(uint8)255(anduint16/uint32/char/bool→ wider) now zero-extends (→ 255), matching the source's unsignedness, instead of always sign-extending (→ -1). Fixes byte-level decoding (parsers, the HTTP/2 frame codec, etc.). - Allocas are hoisted to the entry block. A local declared inside a loop body emitted its
allocain the loop block, so the slot was re-reserved every iteration and never reclaimed until the function returned. A long-running loop with locals (e.g.<base64>encoding a multi-hundred-KB buffer) overflowed the stack and crashed. All stack-slot reservations now live in the function's entry block (stores stay at their original point; only the reservation hoists). Fixes<base64>on large inputs and any temp-heavy hot loop.
Standard library
<http2>: HTTP/2 (RFC 7540):- Stage 1: the frame-header codec (
h2_write_header/h2_read_header, big-endian length/type/flags/31-bit stream id), frame-type + flag constants (H2_HEADERS,H2_SETTINGS, …), and the connection preface. - Stage 2, connection lifecycle: a SETTINGS codec (
h2_write_settings/h2_apply_settings) over the six standard parameters plus anH2Settingsmodel with RFC defaults; the SETTINGS ACK; PING/PONG (h2_write_ping/h2_send_pong); GOAWAY (h2_write_goaway/h2_read_goaway/h2_send_goaway) with the §7 error codes; anH2Connconnection-state struct; and async I/O over the event loop:h2_read_full_async(partial-read loop),h2_read_frame_async, andh2_server_handshake_async(preface validation → read + apply the client's SETTINGS → reply with ours + an ACK). Tested byhttp2_conn(codecs) andhttp2_handshake(the async handshake over a socketpair). - Stage 5, TLS / ALPN (
<tls>): OpenSSL (libssl) by FFI: a serverSSL_CTX(tls_server_ctx) that loads a cert/key and installs an ALPN callback selectingh2, blockingtls_accept/tls_read_full/tls_write_all/tls_close, andhttp2_tls_serve_connrunning the HTTP/2 frame protocol over the encrypted stream. Plus an async TLS server (http2_tls_serve_async): a non-blocking SSL pump (tls_accept_async/tls_read_async/tls_write_all_asyncretryingSSL_*onWANT_READ/WANT_WRITEover the reactor) runs many TLS connections on one thread; verified with 3 concurrentcurl --http2connections. The ALPN selector is passed to OpenSSL as a raw C function pointer (the new(*void)fncast). Verified end-to-end againstcurl --http2(ALPN → h2,HTTP/2 200); seeexamples/http2_tls_server.esk. Also fixed: a server must not advertiseSETTINGS_ENABLE_PUSH=1(§6.5.2). HTTP/2 is now complete (stages 1–6). - Stream multiplexing: the async server routes interleaved request frames to per-stream slots (
H2PendingStream) and completes each on its END_STREAM, so a client can run many concurrent streams on one connection (responses stay serialized on the single connection writer: no concurrent socket writes). Verified with an interleaved two-stream test (http2_multiplex). - Send-side flow control: the async server (
h2_respond_async) now spends the per-stream and connection send windows per DATA frame and parks for WINDOW_UPDATE when a window is exhausted, applying connection-level WINDOW_UPDATE in the dispatch loop too, so responses larger than the 65535-byte default window flow correctly (verified delivering 1 MB bodies overcurl). - Stage 6, server API (
<http2_server>):http2_serve_async(lp, fd, handler, max_conns)(andhttp2_serve_conn_async), an h2c server mirroring<http_async>and reusing<http>'sHttpRequest/HttpResponse. Handshake → frame-dispatch loop → HPACK-decode the request → handler → encode the response (HEADERS:status/content-length/headers + DATA, END_STREAM); SETTINGS/PING answered, GOAWAY/EOF ends it. Tested end-to-end over a socketpair (http2_server). Streams are multiplexed (interleaved request frames routed per-stream), non-blocking, and flow-controlled; cleartext h2c (TLS is in<tls>). - Stage 4, streams & flow control: the per-stream state machine (
H2Stream: idle → open → half-closed → closed viah2_stream_on_recv/h2_stream_on_send), credit-based flow control over per-stream and connection windows (h2_can_send/h2_account_sent/h2_account_recv/h2_grant_window), the HEADERS/DATA/WINDOW_UPDATE/RST_STREAM codecs, and async HEADERS+CONTINUATION reassembly (h2_read_header_block_async). Tested byhttp2_stream. <hpack>: HPACK header compression (RFC 7541), stage 3 (complete): prefix-integer coding (§5.1), string literals (§5.2), the 61-entry static table, a dynamic table with size-based eviction, the §6 decoder/encoder (indexed; literal with/without/never indexing; dynamic table size update), and Huffman coding (§5.2 / Appendix B): a decode trie + bit-packed encode whose 257-symbol table is generated from the RFC text bytools/gen_hpack_huffman.py(→stdlib/hpack_huffman.esk), never hand-transcribed. Verified against the RFC's own vectors, §C.1.1, §C.3.1, §C.4.1 (Huffman), plus encode→decode round-trips (hpack). Seedocs/dev/http2-design.mdfor the full stack.<alloc>: a toolkit of explicit allocators (Bump,Arena,Pool,FirstFit) over a buffer you own, viaalloc_with(the Zig allocator model). Pure pointer arithmetic, no externs. Not a globalmallocreplacement: the defaultalloc<T>stays libcmallocin hosted mode; these manage a region and are the building blocks for a--freestandingheap.<sysheap>: a general-purpose heap that sources OS pages viammapand allocates withFirstFit, so the allocation path never calls libcmalloc(the Zigpage_allocator/ jemalloc / Go-runtime approach).heap_init/heap_alloc/heap_free/heap_destroy; opt-in (the defaultalloc<T>is unchanged).<time>:time_now_ms,time_now_s,time_monotonic_ms,sleep_ms.<env>:env_get,env_has,env_get_or,env_get_int.<base64>: RFC 4648base64_encode/base64_decodeover buffers.<json>: JSON builder (Json) plus a recursive-descent parser (json_parse→JsonValue).<threading>:Mutex,Cond,Semover pthread.<http>: HTTP/1.1 request parser, response builder, and a threaded worker pool (http_serve). Also a binary-safe request reader,HttpReq+http_recv(loopsrecvuntil the full Content-Length body arrives, into a*uint8body; for uploads a single-recv String body would truncate/corrupt), withHttpReq_header,http_reply, andhttp_reply_error.<multipart>: extract a named part from amultipart/form-databody over raw bytes:multipart_boundary(parse the boundary from Content-Type) andmultipart_part(body, len, boundary, name, …)(returns a slice into the body).<map>:Map<V>, a string-keyed hash map (open addressing, linear probing, grows at 0.75 load):Map_init/Map_at(get-or-insert, returns a*Vslot)/Map_get/Map_free. Keys are strings (arbitrary-Khashing can't be synthesised generically).<net>:net_accept_addr(fd, *uint32 peer_ip)accepts a connection and reports the peer's IPv4 address (the plainnet_acceptdrops it), enabling per-client rate limiting and logging.<string>:starts_with,ends_with,trim,split(List + streaming token iterator).<path>:path_join,path_basename,path_dirname,path_extension,path_is_absolute.<eventloop>: readiness reactor over kqueue (macOS) / epoll (Linux):el_new,el_add_read,el_add_write(write-readiness:EVFILT_WRITE/EPOLLOUT; a fd waits for read or write at a time),el_del,el_run,el_stop,el_free. Callbacks arefn(EventLoop*, int)->void. Foundation for async I/O and the HTTP stack. Fixed: a one-shot callback's heap env leaked when its fd was re-armed from inside the callback (a sequential read loop on one fd).el_runnow frees the env it invoked using a per-registration generation counter, not the post-callbackactiveflag.<atomic>:atomic_load,atomic_store,atomic_swap,atomic_casover anintcell, lowering to LLVM atomics (acquire / release / acq_rel). Foundation for the async runtime's lock-freeFuturestate handshake.<executor>/<net_async>: async runtime foundation: anExecutor(event loop + thread-safe ready-queue + self-pipe wakeup, so wakers always resume on the home thread) and leaf futures (net_read_async,net_accept_async,net_write_async: non-blocking write that parks on writability and resumes until the whole buffer is sent) plusnet_set_nonblocking, over<eventloop>. The async/await de-risk gate validated the runtime model end-to-end (reactor read, cancel, cross-thread resume). The async HTTP/2 server (<http2_server>) is now fully non-blocking: responses go out vianet_write_async, so a slow client can no longer stall the loop (verified: a 40 KB body overcurl --http2-prior-knowledge).<http_async>: a non-blocking, concurrent HTTP/1.1 server over<eventloop>: an async accept loopspawns a detached handler per connection (so a slow request doesn't block the others), with a<channel>wait-group joining all handlers for clean bounded shutdown. The Phase-2 async-HTTP goal, end to end.<timer>:timer_after(lp, ms), a leaf*Future<int>that completes aftermsof monotonic time.<eventloop>gained a timer wheel (el_add_timer/el_del_timer);el_runnow blocks only until the nearest deadline. Enables real read-with-timeout:select2(net_read_async(...), timer_after(lp, ms)).<futureval>: value-returning async combinators built on sum types:select2v<A,B>resolves with the winner's value asEither<A,B>(loser dropped),join2v<A,B>resolves with both values as aPair<A,B>. Leak-free. (Also fixed: aPair<A,B>{...}-style template struct literal inside a template body now substitutes the enclosing type params instead of mangling to a bogusPair_A_B.)<either>: the standard sum types built on generic algebraic enums:Option<T>(None/Some) andEither<A,B>(Left/Right), with helpers (opt_is_some,opt_unwrap_or,either_is_left, …).matchworks inside these generic helpers (codegen resolves the enum instance from the subject's type under the active type substitution).<channel>: an async message channel over theFutureruntime:chan_recv(ch)is a*Future<T>that completes with the next item (immediately if buffered, else it parks until achan_sendhands one off and wakes it);chan_send(ch, v)enqueues or hands off directly. v1 is a bounded ring with a single outstanding receiver and non-blocking send. Generic and cast-free; leak-free.
Tooling
eskiuc run file.esk [args...]: compile to a temporary executable, run it (forwarding[args...]), then delete it; the program's exit code is propagated. Compiler flags precede the script, program arguments follow it.- Shebang scripts: a leading
#!/usr/bin/env eskiuc runline is ignored by the preprocessor (line numbers preserved), so a.eskfile can be made executable (chmod +x) and run directly. eskiuc fmt [--check] file.esk …: a conservative, comment-preserving source reindenter: normalizes leading indentation (4 spaces per brace level), trailing whitespace, blank-line runs and the final newline, while preserving every line's content (operators, inner spacing, comments, strings) verbatim. Idempotent and meaning-preserving; braces inside strings/comments are ignored.--checkreports unformatted files (exit non-zero) without writing.--asan/--ubsan: real sanitizer instrumentation via the LLVM pass manager.--asanruns AddressSanitizer (heap/stack/global memory errors) and links the matching LLVM compiler-rt runtime;--ubsaninserts trapping bounds checks (no runtime needed). Both compose witheskiuc run.-Wextra: extra warnings layered on top of-Wall: comparison between signed and unsigned integers; off by default.
Documentation
docs/lang/grammar.md: a formal EBNF grammar derived from the parser: lexical structure, preprocessor, declarations, the type grammar, statements, and the full expression-precedence chain.docs/dev/abi.md: the C-ABI contract: scalar/pointer lowering (const has no ABI effect), struct/packed/bitfield/union layout, ADT tagged-union shape, the >16-byte sret rule, varargs +va_list, fat pointers for closures/interfaces, and template name mangling.__FILE__/__LINE__reference: spec §18 now documents all predefined macros (__LINE__,__FILE__, OS macros,__ESKIU_FREESTANDING__) and the shebang interaction.
Compiler correctness (consistency audit)
- Mutable parameters. Every function parameter now gets a stack slot, so a parameter can be reassigned in the body like a local (
int f(int n) { n = n + 1; … }). Previously this miscompiled (a store into a non-pointer SSA value, caught by the IR verifier). Consequently the receiver resolution for method calls and interface dispatch was made representation-independent: a value-struct receiver passes its address, a pointer receiver passes the pointer it holds, and an interface value (a pointer to its{data, vtable}fat struct) is loaded from its slot, fixing a latent bug in calling a struct method through a pointer parameter. Tests:param_reassign,interfaces. bool→ wider integer zero-extends. Returning (or implicitly widening) aboolor comparison result into anintnow yields1, not-1. The i1 was being sign-extended.return a < b;from anintfunction is correct.- Async
else-ifwithawait. In anasyncfunction, anif / else if / elsechain whose branches containawaitand whoseelseterminates (e.g.return) no longer miscompiles. The state-machine lowering treated a plain terminating branch as fall-through and appended a state transition after thereturn, yielding invalid IR ("terminator in the middle of a basic block"). The lowering now recognizes terminating statements. Test:async_elseif. - Unsigned integer types use unsigned div/rem/shift/compare; 64-bit integer literals no longer truncate; mixed-width ops sign/zero-extend by signedness; variadic call args get the C default-argument promotions.
- Member access on a struct-valued temporary;
(Type)/(Type*)/alias/enum casts; type alias as a local pointer/array;List<StructType>through helper functions;T*(trailing-star) pointer deref; nested template close>>; closures no longer capture module globals by value.
[0.1.0]
Eskiu is a systems language built to replace the C + Go + C++ + Python stack that compute-intensive backend services typically require. C-style syntax, structural interfaces, monomorphic templates, and explicit memory, compiled to native via LLVM with no garbage collector and no runtime.
This is the first release. It includes everything needed to write real backend services and bare-metal systems code, validated against a cryptographic pipeline running at 74 ms on arm64 (2.5× faster than the reference C implementation) and a bare-metal ARM64 kernel booting in QEMU without libc.
Closures and threads
fn(T)->Ris a fat pointer{fn_ptr, env_ptr}; variables from the enclosing scope are captured by value automaticallythread_create(fn()->void)andthread_join(*void)are language keywords; the fat pointer maps directly to pthread's(start_routine, arg): no trampoline- Link with
-lpthreadon Linux
int base = 10;
let add: fn(int)->int = int(int x) { return x + base; }; // captures base
*void t = thread_create(void() { printf("hello\n"); });
thread_join(t);
Exception handling
try { } catch (T name) { } finally { }with multiple catch clausesthrow expr: any Eskiu value- LLVM
invoke/landingpadwith__gxx_personality_v0; unhandled exceptions re-thrown viaresume - Link with
-lc++(macOS) or-lstdc++(Linux)
try {
throw "division by zero";
} catch (string e) {
printf("caught: %s\n", e);
} finally {
printf("cleanup\n");
}
Language completeness
sizeof(T): compile-timeint64constant for any type including structsunion: all fields share offset 0; size = sizeof(largest field)- Typed pointer arithmetic:
p: *int; p + 1advances 4 bytes, not 1 *ptr = valuethrough pointer parameters works correctly
Forward declarations and declaration order
- Declaration order no longer matters: a function may call any function defined later, and mutual recursion works
- Body-less forward declarations (
int is_odd(int n);) parse and are honoured - Codegen declares every function prototype in a pre-pass before emitting any body, so call-before-define resolves cleanly instead of failing
Compound assignment
- Bitwise compound assignment operators:
&=,|=,^=,<<=,>>=(alongside the existing+= -= *= /= %=)
Template ergonomics
- Template struct literals:
Pair<int, float> { first: 7, second: 3.14 }(named and positional) - Type-argument inference: type parameters are inferred from argument types by structural unification: both when a parameter is the bare type parameter (
max(3, 5)) and when it appears inside a composite type (List_get(&nums, i)infersTfrom theList<T>*parameter). Explicitmax<int>(3, 5)still works - Mixed-width literal arguments are coerced to the substituted parameter type at the call site (e.g. a
doubleliteral passed whereT = float)
Iteration, error propagation, and richer strings
for-inloops:for (x in iterable)iterates over fixed-size arrays (T[N]) and over List-like structs (any struct withint sizeand adatapointer, includingList<T>). The loop variable is a per-iteration copy;break/continuebehave as in a counted loop (it desugars to one, socontinuestill advances the index)?error-propagation operator: postfixexpr?on aResult<T, E>returns the result early if it isErr, otherwise evaluates to the unwrappedT. Permitted only inside a function that returns the sameResulttype; rejected with a clear diagnostic otherwise- Richer mutable
String: addedString_push,String_char_at,String_set,String_clear,String_index_of,String_eq,String_eq_cstr,String_reverse,String_substring,String_from_int, andString_to_inttostdlib/string.esk, all maintaining the owned, NUL-terminated buffer invariant
Compiler robustness and fixes
- The parser now reports declaration errors and returns a failure instead of silently dropping unparseable declarations and reporting success;
--test-typecheckerexits non-zero on type errors - Fixed: writing a
floatmember of aunionstored a fulldoublewithout truncating, so reads returned garbage - Added a regression test suite (
tests/run.sh) with exact-output and expected-error checks covering arithmetic, control flow, recursion, pointers, strings, structs, interfaces, templates, closures, lambdas, exceptions, forward declarations, and compound assignment
One-step linking
eskiuc file.esk -o prognow compiles and links in one step, producing a runnable executable:eskiucinvokes the system C toolchain ($CC, thencc/clang/gcc), the same wayrustcandclangdo internally-l<lib>,-L<path>, and--link-arg=<arg>are forwarded to the linker (e.g.eskiuc app.esk -o app -lpthread)- Object-only when the
-ooutput ends in.o, when-cis passed, or under--freestanding(bare-metal links itself); a C toolchain is the only build dependency besides LLVM
Enums, type aliases, bitfields, and the preprocessor
- Enums:
enum Color { Red, Green = 5, Blue }: members areintconstants (implicit or explicit values), the enum type maps toi32, and they work inswitch, comparisons, and as parameter types - Type aliases:
type u8 = uint8;: a name for any existing type, including pointers and templates; resolved to the underlying type everywhere - Bitfields:
struct F { uint32 a : 1; uint32 b : 3; }: packed into storage words with masked read-modify-write access; signed fields sign-extend; works with struct literals - Preprocessor: object-like and function-like
#define/#undef(with recursive expansion and backslash-continued multi-line macro bodies) plus#ifdef/#ifndef/#else/#endifconditional compilation, run as a text pass before lexing; the macro table is shared acrossimport/multi-file builds, and source line numbers are preserved - Packed structs: the
packed structqualifier removes inter-field padding (back-to-back layout) for matching on-the-wire/on-disk formats and C__attribute__((packed))structs.#pragma pack(push, N)/pack(pop)is honoured for C source compatibility (maintains an alignment stack;pack(1)packs subsequent structs); both set the same flag and compose with bitfields andsizeof
Tooling
- Multi-file compilation:
eskiuc a.esk b.esk -o progcompiles several files together, merging their declarations (order-independent thanks to the prototype pre-pass) -Wall: lint-style warnings: unused variables, parameters, and functions, plus assignment used as a condition (if (x = 0)); off by default
Standard library
import <name>: angle-bracket imports resolve stdlib modules from the installation;import "path"for local files<fs>, file I/O:fs_open,fs_close,fs_read,fs_write,fs_seek,fs_tell,fs_size,fs_read_all,fs_write_all,fs_eof,fs_error<net>, TCP sockets over the POSIX BSD socket API:net_tcp_listen,net_accept,net_tcp_connect,net_send/net_recv/net_send_str,net_close, plus the rawexterns and a portablesockaddr_in. Pure stdlib; sockets need no compiler support beyond the C FFI andpacked struct. Includesexamples/http_server.esk(a working HTTP/1.1 server) andexamples/tcp_echo_server.esk
Networking-enabling language features
- Function-as-value: a top-level function used as a value (not called) decays to a
fn(T,...)->R, passable tothread_createand callbacks without wrapping it in a lambda. The compiler synthesizes an adapter so the function fits the{fn_ptr, env_ptr}closure ABI - Predefined OS macros: the compiler predefines
__APPLE__(macOS) or__linux__(Linux), so stdlib and user code can#ifdefper platform; used by<net>to pick the correctsockaddr_inlayout
Release
- GitHub Actions workflow builds static binaries for macOS arm64 and Linux x86-64 on every
v*tag - Tarball:
bin/eskiuc+lib/eskiu/stdlib/
Systems milestone
Bare-metal ARM64 kernel written in Eskiu boots in QEMU (-M virt) and prints to serial without libc or a C runtime.
- Inline asm UART driver, bump allocator in
--freestandingmode - Cross-compiled with
--target aarch64-unknown-none-elf volatileMMIO,asm(...)with GCC-compatible constraints
[0.0.14-alpha]
Added
Inline assembly
- asm("cli");: simple form; passes the string verbatim to the assembler with no inputs, outputs, or clobbers
- asm("outb %0, %1" :: "a"(val), "Nd"(port) : "memory");: extended form with GCC-compatible constraint syntax; supports input operands and clobber lists (no output operands)
- Lowers to LLVM inline asm nodes; "memory" clobber emits a compiler barrier
Freestanding mode
- --freestanding CLI flag: redirects alloc to call esk_alloc and free to call esk_free instead of the libc malloc/free
- User provides both symbols in their kernel or bare-metal runtime; the compiler emits declare stubs and the linker resolves them
volatile qualifier
- volatile let reg: *uint8 = (uint8*) ADDR;: marks all LLVM loads and stores through the pointer as volatile, preventing the optimiser from caching, reordering, or eliminating MMIO accesses
Cross-compilation
- --target TRIPLE CLI flag: sets the LLVM target triple for the output object file
- Both AArch64 and X86 LLVM backends are included in the Eskiu build
- eskiuc file.esk --target x86_64-pc-linux-gnu produces an ELF x86-64 object file
v0.1 prerequisites complete - All compiler prerequisites for the kernel-on-QEMU milestone are implemented and tested
[0.0.13-alpha]
Fixed
- Negative literals:
-1,-3.14etc. now parse as negative literal values directly, not as unary minus applied to a positive literal. Global variable initialisers with negative values (e.g.int x = -1;) previously compiled to 0; this is now correct.evaluateConstantExprfolds unary minus on numeric constants as a fallback.
[0.0.12-alpha]
Added
Lambdas and function pointer types
- fn(T,...)->R function pointer type syntax: first-class function types usable in variable declarations, struct fields, and parameter lists
- Anonymous function expressions: int(int x) { return x * 2; }, C-like syntax producing a function pointer value
- Lambda variables: let double_it: fn(int)->int = int(int x) { return x * 2; };
- Higher-order functions: lambdas can be passed as arguments to functions expecting fn(...) parameters
- LambdaExpr AST node; full visitor chain (parser, type checker, codegen, AST printer)
- Codegen: each lambda expression is lowered to a uniquely-named private llvm::Function; the expression value is the function pointer
VS Code extension: real-time diagnostics, hover, and go-to-definition
- --hover-at LINE:COL CLI flag: prints the inferred Eskiu type of the expression at the given source position; consumed by the VS Code extension for hover tooltips
- --definition-at LINE:COL CLI flag: prints the file:line:col of the definition of the symbol at the given position; consumed for go-to-definition
- VS Code extension (editor/vscode/) upgraded from syntax-only to a full language client: spawns eskiuc --test-typechecker on save for real-time error squiggles; hover provider calls --hover-at; definition provider calls --definition-at
- TextMate grammar already present from v0.0.11 provides syntax highlighting
switch/case type checking
- The type checker now validates that each case value is compatible with the switch subject expression type; mismatched case types are reported as errors at the case token position
[0.0.11-alpha]
Added
Decoder rewritten in Eskiu (no C pipeline code)
- ine_decoder/crypto.esk (541 lines): AES-256-CBC + RSA-8192 pipeline, hex/base64 decoders, PKCS#1 stripper, 6-bit decoder, WebP reconstruction, run_no_so_pipeline(); all in Eskiu calling OpenSSL via extern
- ine_decoder/output.esk (186 lines): Spanish character table, field splitter, growable JSON buffer, decode_to_buffers(); pure Eskiu
- ine_decoder/crypto.c and output_decode.c removed: replaced entirely by Eskiu
- Only C remaining: qr_extract.c shim (12 lines) + qr_extract_impl.cpp (CoreGraphics + zxing-cpp)
- Runtime: 80ms on arm64, identical output to reference
String literal adjacent concatenation
- "abc" "def" on consecutive lines (or the same line) are now concatenated into a single string at parse time; enables readable multi-line constant definitions
Fixed (compiler bugs found during Eskiu crypto port)
ptr - ptr→int64: pointer subtraction for computing byte offsets in buffers- Integer widening in arithmetic (
+ - * /):i8 - i32now ZExts the narrower operand before emitting the instruction string[i]→char: indexing astringtyped value now returns the correctchar(i8) element instead of computing type"strin"- Assignment store coercion:
arr[i] = valwhere the array element type is wider thanvalnow ZExts/truncates to match (e.g.int64[0] = 0was storing i32 into an i64 slot) - Return value coercion: functions that return
int64but the expression evaluates toi32now automatically extend the return value - GlobalVariable initializer coercion:
uint8 X = 0x52was creating a global with mismatched i32/i8 initializer; now casts to the declared type validateStructTypemulti-level pointers:**charwas triggering "undefined struct '*char'"; now strips all pointer levels before checking the base type
[0.0.10-alpha]
Added
Global variables
- VarDecl at module scope now emits llvm::GlobalVariable instead of alloca
- Constant initializers folded at compile time: int, float, double, bool, char, string, null
- Non-constant initializers zero-initialize the global (assign in main() for complex expressions)
- globalVarTypes map tracks Eskiu type strings for globals (complement to function-scoped varTypeStack)
- evaluateConstantExpr(): folds literal expressions to llvm::Constant*
- visit(IdentExpr*) now loads from llvm::GlobalVariable as well as AllocaInst
- IMAGE_PATH, OUT_JSON, OUT_WEBP in ine_decoder/main.esk moved to module scope
sret (large struct return)
- needsSret(type): returns true for aggregates > 16 bytes (arm64 register limit)
- visit(FunctionDecl*) rewrites large-return functions: prepends hidden ptr sret.ptr parameter, changes return type to void, tracks struct type in funcSretTypes
- visit(ReturnStmt*) stores result to currentSretParam and emits ret void for sret functions
- Call sites (regular + template): alloca sret buffer, prepend as arg 0, call, load result
- currentSretParam saved/restored across template instantiation
Integer argument widening at call sites
- Automatically SExt/Trunc integer arguments to match function parameter widths
- Fixes i32 1712 passed to int64 param (was LLVM verification error)
Fixed
*intvssize_t *inextern.esk:run_no_so_pipelineanddecode_to_buffersnow useint64for length params to match C'ssize_t(was writing 8 bytes to a 4-byte stack slot → heap corruption)- ine_decoder
pipeline.esk: stage signatures updated toint64for all size parameters
Planned
argv/argcsupport: programs can accept CLI arguments natively
[0.0.9-alpha]
Added
ine_decoder/, INE QR decoder port: full pipeline running at 74.4 ms total (QR: 71.7 ms + crypto: 2.8 ms + output decode: <1 ms) vs. 188.9 ms reference C and 3–5 s original target; 2.5× faster than hand-written Ctypes.esk:QRPair,NoSoKeys,IneResult,IneFieldsstructsextern.esk: libc + OpenSSL EVP (AES-256-CBC / RSA-8192) +ine_qr_extract()declarationsstage1_qr.esk: QR extraction wrapperstage2_crypto.esk: 3-round AES-256-CBC + RSA-8192 via OpenSSLstage3_output.esk: pipe-delimited plaintext → JSON + WebP extractionmain.esk: orchestration, timing, outputqr_extract.c/qr_extract_impl.cpp: C/C++ shim using CoreGraphics + zxing-cpp 3.xMakefile,README.md
Fixed
- Integer width mismatch in comparisons:
uint8 == int(e.g.plaintext[i] == 124) crashed LLVM with "Both operands to ICmp instruction are not of the same type"; nowZExts the narrower operand to match the wider before emitting any of the six comparison operators - Mixed int/float arithmetic:
i64 * double(e.g. timing calculation(t1 - t0) * 1000.0) generated invalid IR; now detects int/float type mismatch in+,-,*,/and promotes the integer operand withSIToFPbefore emitting the floating-point instruction
[0.0.8-alpha]
Added
import "file.esk": multi-file support with paths resolved relative to the importing file's directory; recursive imports with deduplication (a file imported more than once is parsed only once);Parser.basedir+importedFilesset propagated to sub-parsers- Interface vtable dispatch:
interface I { void method(); }generates%I_vtable = type { ptr, ... }and%I_fat = type { ptr data, ptr vtable }LLVM types; structs are auto-boxed at call sites viaboxAsInterface(); method dispatch loads the vtable pointer from the fat pointer and calls indirectly;getExprEskiuTypeextended to handleUnaryExpr("&", ...)andUnaryExpr("*", ...)for boxing detection String.append()andString.concat(): added tostdlib/string.eskusingmemcpy+ pointer arithmetic
Fixed
- Pointer comparison (
p == null,ptr1 == ptr2) incorrectly usedFCmpOEQ(float equality); now usesICmpEQ; all six comparison operators updated to checkisFloatingPointTy()first i1 → i32widening usedSExt(sign-extendstrue→-1); now usesZExtfori1operands so comparisons correctly store0or1
[0.0.7-alpha]
Added
- Bitwise operators
- Binary:
&,|,^,<<,>>, new precedence levels in parser (bitwiseOr → bitwiseXor → bitwiseAnd → equality → shift → comparison → additive) - Unary:
~(bitwise NOT), added to unary operator list in parser andinferUnaryExprType - Codegen:
CreateAnd,CreateOr,CreateXor,CreateShl,CreateAShr - Hex literals:
0xFF,0x0F; lexer reads hex digits after0x/0Xprefix;stoll(..., 0)for auto-base detection in codegen - Compound assignments:
+=,-=,*=,/=,%=; new tokens in lexer; desugared inparseAssignment()tox = x op y continuestatement:ContinueStmtAST node; full visitor chain;continueTargetsaved/restored aroundWhileStmtandForStmtbodies (points to loop condition and step block respectively)- For-loop with declaration init:
for (int i = 0; i < n; i += 1), parser wraps the declaration in aBlockStmt; type checker processes init items directly inForStmtscope, avoiding the inner-scope pop that previously madeiundefined in the condition, step, and body - Pointer arithmetic:
ptr + n→GEP(i8, ptr, n),ptr - n→GEP(i8, ptr, -n)invisit(BinaryExpr*)
Fixed
&address-of returned a loaded value instead of the alloca pointer, breaking all pointer-argument call patterns- Unary
-for floats used integerCreateNeg; now usesCreateFNegfor floating-point operands !logical NOT on integers used bitwiseCreateNot(incorrect fori32); now usesCreateICmpEQ(x, 0)for non-i1types~unary NOT was missing from the parser's unary operator list, causing a parse failure for~0and similar expressionsinferUnaryExprTypedid not handle~; reported "invalid operand" for bitwise NOT on integersinferBinaryExprTypedid not handle bitwise or shift operators; reported "invalid operands" fora & b,a << n, etc.- For-loop init declarations were scoped to an inner
BlockStmt, making the loop variable undefined in the loop condition and body
[0.0.6-alpha]
Added
- Source locations in errors:
ASTNodenow carriesline/col; parser stamps all expression and statement nodes; errors now reportfile.esk:line:col:instead offile.esk:0:0:; filename taken from the CLI input path &address-of operator: correctly returns the lvalue pointer (alloca), enablingfn(&localVar)patterns and template method calls viaselfpointer- Template member access on pointer types: trailing
*stripped before struct field lookup;self: List<int>*now correctly resolves tostructFields["List_int"]in bothvisit(MemberExpr*)andgetExprEskiuType() stdlib/list.esk:List<T>withList_init,List_push,List_get,List_len,List_freeas template functions; tested end-to-endstdlib/string.esk:Stringstruct withString_init,String_from,String_cstr,String_len,String_free- Interface structural check:
isValidAssignmentnow verifies that a struct satisfies an interface's method signatures (vtable codegen deferred to 0.0.8) isPointerTypeunified: now detects both leading*Tand trailingT*conventions
Fixed
&operator returned a loaded value instead of the address, breaking all pointer-argument patterns- Template method parameter types with pointer suffix (e.g.
List<T>*) were incorrectly mangled toList_int_*instead ofList_int* getExprEskiuTypefor member chains through a pointer-to-template-struct now resolves correctly
[0.0.5-alpha]
Added
switch/case:SwitchStmtAST node withCase { value, stmts }list; full visitor chain; parser handlesswitch (expr) { case val: stmts break; default: stmts }with fallthrough support; codegen emits LLVMswitchinstruction withConstantIntcase values;breakbranches toswitch.endvia existingbreakTargetmechanism- Function templates:
fn Name<T, E>(T x) -> RetType<T,E> { ... };FunctionDecl.typeParamsfield;TemplateCallExprAST node parsed inparsePostfix(); lazy instantiation viatypeParamOverridemap that intercepts all type lookups during template body emission; context save/restore preserves insert point andcurrentFunctionwhen instantiating inside another function's body;substTypehandlesName<T,E>nested substitution recursively InterfaceDecl:interface Speakable { void speak(); }fully parsed and stored ininterfaceDecls; codegen generates no IR (vtable dispatch deferred)- Stdlib base (
stdlib/) result.esk:struct Result<T,E>+Ok<T,E>/Err<T,E>template constructorsmath.esk:sqrt,fabs,pow,floor,ceil,abs(extern to libm)io.esk:printf,fprintf,sprintf,scanf,puts,getchar,putcharmem.esk:memcpy,memset,memmove,memcmp,strlen,memchr
Fixed
- Parser:
int fn<T>(T x)at top level was silently dropped;parseDeclaration()now detects<after a name as a function template - Type checker: template function bodies were visited with unresolved type params; now guarded with
typeParams.empty()check substType: did not substitute type params insideName<T,E>template strings; now handles nested template types recursively
[0.0.4-alpha]
Added
- Template struct declaration:
struct Result<T, E> { ... };StructDeclgainstypeParamsfield; template declarations are stored in a separate registry and not emitted to LLVM until first use - Template type references:
Result<int, string>parsed inparseType()viaIDENT < TYPE, ... >lookahead; stored as"Result<int,string>"in the AST - Lazy instantiation in type checker:
normalizeType("Result<int,string>")detects<, looks up the template, substitutesT→int/E→stringin all field types, and registers"Result_int_string"as a concrete struct - Lazy instantiation in codegen:
getTypeFromString("Result<int,string>")callsensureTemplateInstantiated()which creates%Result_int_string = type { i32, i32, ptr }on first use - Name mangling:
Result<int,string>→Result_int_stringin LLVM IR - Type substitution:
substTypehandles*T→*int,T*→int*,T[N]→int[N], and nested template types recursively varTypeStacknormalization:VarDeclstores the mangled name soMemberExprresolution finds instantiated struct fields correctly
Fixed
- Parser:
int fn<T>(...)at top level was silently dropped;parseDeclaration()now detects<after a function name as a template parameter list - Type checker: template bodies were visited with unresolved type parameters; now guarded so only concrete instantiations are type-checked
[0.0.3-alpha]
Added
alloc(T, N): newAllocExprAST node; emitscall @malloc(i64 N * sizeof(T));sizeof(T)resolved viaDataLayoutinitialized at the start ofgenerateCode()free(ptr): parsed as a regular call;freepre-registered in the type checker as variadic; auto-declared in the LLVM module on first use, no explicitexternrequiredgetOrDeclareFunc(): codegen helper that lazily declares C runtime functions (malloc,free) into the module without requiring explicitexterndeclarations
Fixed
validateStructTypedid not strip the leading*Tpointer prefix, causing false "undefined struct" errors on*uint8,*Point, etc.inferBinaryExprTypedid not handle the=operator for non-numeric types; assignment to pointer fields and struct fields now type-checks correctlyisValidAssignmentnow allows any-pointer-to-any-pointer assignment for C interop
[0.0.2-alpha]
Added
- Struct codegen:
llvm::StructType::createperStructDecl;alloca %StructTypefor locals; field read/write viagetelementptr - Fixed-size array fields:
uint8[858]→[858 x i8]; parser now captures the array size (was previously discarded);IndexExprcodegen via GEP for bothT[N]and*T - Struct literal initialization: named (
Point { x: 1.5, y: 2.5 }) and positional (Point { 1.5, 2.5 }); fills alloca directly with per-field type coercion - Method calls: methods emitted as
StructName_methodName(ptr self, ...)mangled functions;p.method(args)detects and prepends an implicitselfpointer StructInitExprAST node, full visitor chain: parser, type checker, codegen, AST printerBreakStmtcodegen:CreateBr(breakTarget); target saved/restored around loop bodiesemitObjectFile(): native.ovia LLVMTargetMachine+legacy::PassManager; full pipelineeskiuc file.esk -o file.oproduces a linkable object file- Float arithmetic:
+,-,*now emitfadd/fsub/fmulfor floating-point operands (previously always emitted integer instructions)
Fixed
- Type checker: method bodies were not registered and type-checked in the first pass
- Type checker:
*T(leading-pointer) was not auto-derefed on member access; now strips the leading*before struct field lookup - Type checker:
isValidAssignmentnormalizes both sides so"Point" == "struct:Point" - Type checker: function parameter types were stored as parameter names (e.g.
"a") instead of type strings (e.g."int") - Type checker: variadic functions incorrectly rejected call sites with more arguments than fixed params
- Codegen:
varTypeStacknow scoped alongside the symbol table for correct struct type resolution across nested scopes
[0.0.1-alpha]
Added
- Phase 0: Build system and CLI. CMake build with LLVM 17+ integration;
--versionflag;--test-lexer,--test-parser,--test-typechecker,--test-codegenmodes;file:line:colerror reporting - Phase 1: Lexer. Complete tokenizer with line/col tracking; all Eskiu keywords (
int,float,uint8–uint64,int8–int64,struct,interface,enum,alloc,free,extern,thread,try/catch/finally, and more);COLONtoken for type annotations - Phase 2: Parser. Recursive-descent parser producing a visitor-based AST; functions, variables (
let x: int = 5and C-styleint x = 5both accepted), structs with fields and methods,externdeclarations, full control flow (if/else,for,while,break,return), expressions with correct precedence, cast expressions(TYPE)expr - Phase 3: Codegen. LLVM IRBuilder backend; arithmetic, comparison, and logical operators;
if/else,while,for; function calls; integer and float literals; type coercion on initializers; correct lvalue/rvalue split (evaluateLValue) so assignments emitstoreto anallocarather than to a value - Phase 4: Type checker. Scope-aware analysis; type inference for all binary and unary operators; struct field validation; function signature checking;
MemberExprmember-type resolution; parameters registered before the validation pass - Types:
uint8/uint16/uint32/uint64andint8/int16/int32/int64as first-class types mapped to LLVMi8–i64;bool→i1;char→i8;string→i8* - Pointer types: both leading
*Tand trailingT*syntax accepted throughout lexer, parser, and type checker - Examples:
examples/hello.esk,examples/test_struct.esk,examples/test_struct_error.esk
Fixed
- Lexer:
COLONtoken was not recognized, breakinglet-style type annotations - Type checker: function parameters were not registered before the body validation pass, causing false "undeclared identifier" errors
Known limitations (resolved in later releases)
- Struct codegen not wired (Phase 5);
MemberExprtype-checks but does not emit IR - No heap allocation (
alloc/free), stack only (Phase 6) - No interfaces or templates (Phase 5)
- No standard library or
Result<T,E>(Phase 7) - No lambdas, threads, or async (Phase 8+)
eskiu