Honey
Bro and Honey implement the same language; Bro is the prototype compiler used to bootstrap the self-hosted Honey compiler. the features below describe Honey's currently implemented subset. the expected development workspace places Bro at ../brolang; consult that project for the language grammar and documentation.
style: see the language guidelines.
build Honey from this directory:
bro build
the bootstrap must support contextual &[] literals for mutable slices; collection initialization and reset use these literals instead of a dedicated empty-slice helper. if the installed bro predates that feature, use ../brolang/build/brolang build after rebuilding Bro. zero-sized elements with nonzero logical lengths still use the memory module's specialized storage handling.
run a source file with the interpreter:
./build/honey run example.hon
run, emit-llvm and build also accept a directory. directory inputs compile its .hon and .bro files in bytewise filename order, excluding .test.hon/.test.bro and non-file entries; a file input still compiles only that file, not its siblings. declarations share a scope within each package, while dependencies have separate scopes and cannot supply the root package's main.
bare import "../library" and aliased library :: import "@library" declarations load dependencies. relative paths start at the importing package directory; @ starts at the nearest ancestor containing build.hon or build.bro, falling back to the entry package directory. package directories are canonicalized through symlinks and loaded once. cycles, missing/empty dependencies and invalid import paths produce source diagnostics. absolute paths and empty, escaped or control-containing import strings are rejected. production loading ignores test-only imports.
imports bind file-local namespaces, never dependency declarations in the importing package. bare imports use the canonical package directory's final name; directories without a valid identifier name require an explicit alias. sibling files can use the same alias for different packages, but an alias cannot conflict with a package declaration, builtin type or another import in its own file. local shadowing remains forbidden. library.add(...) selects a public declaration and uses ordinary signature checking, function values and IR calls without a runtime namespace value. qualified record types and constructors such as library.Point and library.Point { x = 1 } use the same declaration identity. imported namespaces are not re-exported; missing members are diagnosed. visibility attributes are not implemented; unsupported attributes produce syntax diagnostics.
add proc(left, right int) int {
return left + right
}
main proc() int {
answer :: add(19, 23)
return answer
}
this prints 42. both interpreted and Honey-compiled main must be a top-level function with no parameters; nonvoid results are printed, not used as process exit statuses.
compile the same supported language subset to a standalone Apple Silicon macOS executable:
./build/honey build programs/factorial.hon -o build/factorial -O2
./build/factorial
this prints 120. build requires -o <executable> and accepts -O0 (the default) or -O2 as its final option. Honey emits textual LLVM IR and invokes clang from PATH without a shell, targeting arm64-apple-macosx11.0.0. Apple Clang 21 is the verified toolchain; LLVM libraries, LLVM bindings and a C emission stage are not required. the temporary IR file is private and removed after compilation, including on failure.
inspect or compile the LLVM text separately:
./build/honey emit-llvm programs/factorial.hon > build/factorial.ll
clang -target arm64-apple-macosx11.0.0 -O2 build/factorial.ll -o build/factorial
emit-llvm writes LLVM text to stdout and source diagnostics to stderr. both emission commands return 1 for recoverable source diagnostics while still producing their output when emission and linking succeed. the resulting executable preserves demand-driven traps, reports all source diagnostics, and returns 1 even if unaffected code completes. runtime-generated diagnostics retain source locations; the original source file and compiler are not needed to run the executable. invocation or Clang failures return 2.
the backend (@source/llvm) consumes the existing typed IR, emits native functions and control flow, and uses entry-block stack slots for locals and instruction results, including non-SSA result IDs. LLVM can promote eligible slots during optimization. arrays and records use typed native aggregates with by-value copies; loop storage is allocated once per invocation. signed and unsigned arithmetic use checked overflow intrinsics, array accesses retain bounds checks, local initialization flags reset on scope re-entry, globals initialize lazily with cycle checks, and floating operations preserve IEEE behavior without fast-math flags. native calls use the native stack rather than the interpreter's heap-backed frames.
run the bootstrap suite with bro test. after bro build, run python3 tests/native.py for executable-level interpreter comparisons at both -O0 and -O2, including traps, source diagnostics, array ownership, calling conventions, and CLI error handling.
the scalar-function package acceptance gate is verified by the native suite: root and dependency sibling files, transitive/shared imports, same-spelled declarations with distinct signatures, file-local aliases and conflicts, missing packages/members, argument errors and dependency-file diagnostics. file-command checks cover run, emit-llvm and build, loading dependency siblings while excluding root siblings.
dependency provenance checks cover lexer, parser, resolver, checker and lowering diagnostics, plus overflow, bounds, local initialization and global-cycle traps through transitive calls. they assert originating filenames and line/byte-column positions, compare reached and untaken paths, and run generated executables with the source tree temporarily removed.
the native bootstrap accepts either a no-argument main or main proc(init process.Init). it supplies init.io and init.args from native startup argc/argv. startup scans each argument once into a heap-allocated array of borrowed zero-terminated slices (process.Args.values: [][;0]u8), without copying strings. the descriptor array and borrowed strings remain valid until main returns; startup then frees the descriptors, including on a fallible return. allocation failure exits with status 2. init.args.values.len includes the executable name, and init.args.values[index] directly accesses an argument after a length guard. callers can supply their own process.Args { values = slices[..] } for testing.
process.run(allocator, arguments) starts a child synchronously using Darwin posix_spawnp, searches PATH, and inherits the environment and standard streams. argument strings are borrowed zero-terminated slices; no shell parsing occurs. it releases temporary argument descriptors on every return, retries interrupted waits, and returns the exit code or 128 + signal for signal termination.
source loading uses the supplied io.Io for opening, reading, and closing files. io.open accepts a borrowed zero-terminated slice ([;0]u8), including init.args.values[index] without copying its storage. the bootstrap supports checked sentinel-slice construction with pointer[..length;0]. io.read_all(allocator, reader, maximum) reads to EOF into a caller-owned ArrayList(u8); release it with arraylist.deinit. it frees partial storage on error and consumes one extra byte to distinguish an exact-size input from a limit violation. the driver maps limit_exceeded to source_too_large at its u32 source-size limit.
@std/fs contains the Darwin filesystem operations used by source loading: canonical returns an owned symlink-resolved path (free with the same allocator), while kind and exists classify paths without exposing C mode bits. open_directory returns an owned handle released with close_directory; next returns a borrowed entry whose name expires on the next call or close. enumeration order is unspecified. read_file(allocator, runtime, path, maximum) reads a regular file through the supplied io.Io, closes it on success/error, and returns caller-owned bytes released with arraylist.deinit; exceeding the bound returns limit_exceeded. metadata and directory discovery use the host filesystem. the compiler's @source/loader retains project-root/import rules, production-file filtering, deterministic sorting and the u32 source-size limit, with no direct libc calls.
a recursive example is in programs/factorial.hon; ./build/honey run programs/factorial.hon prints 120.
an iterative example is in programs/iterative_factorial.hon; ./build/honey run programs/iterative_factorial.hon also prints 120.
the parser supports arithmetic expressions with nested parenthesized groups, such as a + (b - c) * 2 / 3. groups control precedence without adding AST nodes; inner expression spans exclude the grouping parentheses. groups may span lines in any expression position, including returns, initializers, arguments and guards. parsing accepts integer division syntax, but type checking requires float operands for /.
function calls are postfix expressions: f(), f(a, b), and factory()(x). calls bind tighter than unary and binary arithmetic and can appear in initializers or as expression statements inside function bodies. argument lists allow newlines and a trailing comma; outside parentheses, newlines remain statement boundaries. type checking validates callable values, argument counts and argument types.
generic procedures mark compile-time parameters with $, such as identity proc($T type, value T) T. pass all parameters explicitly (identity(i16, 42)) or omit every compile-time argument when runtime argument types determine them (identity(value)). inference follows pointers, arrays, slices, optionals and specialized record constructors, including array counts; conflicting or undetermined arguments are diagnosed. specializations are cached by declaration and compile-time arguments, retain nominal generated-record identity, and erase compile-time parameters from runtime calls.
Fixed proc($T type, $N usize) type {
return struct { values [N]mut T }
}
first proc($T type, $N usize, value @Fixed(T, N)) T {
return value.values[0]
}
main proc() i16 {
fixed Fixed(i16, 2) :: { values = [42, 0] }
return first(&fixed)
}
the specialization checkpoint supports type arguments and constant integer, boolean, string, callback and null value arguments; declared value-parameter types survive specialization, including narrow checked arithmetic and optional captures/unwrap. integer arguments are range-checked even when unused, and erased argument expressions still receive ordinary checking. immutable constant expressions may use unary signs, !, +, -, *, comparisons and logical operators. type-producing procedures accept ordinary parameters as well as $ parameters: every argument must be supplied explicitly and evaluate to a constant, and every input participates in specialization caching. for example, Fixed proc(T type, count usize) type { return struct { values [count]T } } accepts Fixed(i16, 2) and reuses its nominal type for equivalent inputs. use $ when a parameter must be visible in subsequent signature annotations. type-producing bodies support immutable locals, conditional returns, nested type-producing calls and generated record/array storage, including pointer-recursive records; mutable type-valued locals and loops in these bodies remain unsupported. runaway specialization is diagnosed at 128 nested evaluation/specialization calls or 4096 instances.
ordinary procedures may compute integer, boolean, float and string compile-time arguments and return nested records/tuples and fixed arrays from constant inputs, such as Fixed(i16, length(2)). calls reuse the ordinary checker, typed IR and interpreter, preserving local type inference, checked arithmetic, mutable locals, range/while loops, break/continue and recursive or specialized helper calls. aggregate arguments receive their declared context, including dependent [N]T parameters; cached specializations compare normalized values, not literal spelling or evaluation identity. returned aggregates become owned source literals before temporary interpreter storage is released, preserving by-value copies, exact integer types, string bytes, enum fields, optional fields and IEEE float bits.
$(expression) explicitly evaluates a constant expression, including an ordinary procedure call, even in a package without templates. use a typed expression when its shape cannot be inferred independently, such as $(Point { x = 40 }); integer literals retain contextual typing. the resulting value works in runtime expressions, bindings and array lengths, while the erased expression still receives ordinary checking. each interpreter evaluation is isolated and limited to 100,000 IR instructions/terminators and 128 active frames; failures retain their originating source locations. runtime parameters and mutable local/global dependencies cannot supply compile-time inputs. data pointers, slices, callbacks, unions and fallible results cannot escape temporary evaluation as generated constants; records/arrays containing these unsupported values are diagnosed rather than retaining interpreter handles. executed foreign calls remain interpreter traps. general type-valued execution, compile-time match/inline expansion, reflection, generated type/default arrays and existing generic-library acceptance remain open.
if $(condition) { ... } else { ... } evaluates a boolean guard before expanding or checking either body, including calls to ordinary constant-input procedures. concrete generic specializations also select branches whose guards use constants, immutable constant bindings, type comparisons or substituted compile-time parameters. only the selected body is specialized and checked; both bodies are still parsed and name-resolved. selected bodies retain their scope, cleanup and return behavior, and erased guards retain ordinary type checking and source diagnostics. ordinary calls in an implicit guard stay runtime; use $() to request their evaluation. optional captures remain runtime, and ordinary nongeneric if (true) still checks both bodies.
type == and != comparisons in compile-time expressions and specializations preserve aliases, nominal declaration identity and structural pointer/array/callback signatures. composed error-set identity and specialization caching ignore member ordering, grouping and repetition. for example, read proc($T type, value T) i16 { if (T == i16) return value else return value.count } checks the scalar or record path for each concrete call without requiring both operations on every T.
callback signatures use @proc(value i16) i16, including grouped parameter names and trailing commas. compatible named functions can initialize callbacks, which may be passed, returned, replaced and stored in records/arrays; Callback :: alias @proc(value i16) i16 names a reusable signature. parameter/result types match structurally, retaining nominal record identity and exact pointer mutability. callback fields call directly, such as allocator.vtable.alloc(context, size, alignment) once the remaining argument types are supported. @mut proc and *proc are invalid; @mut Callback instead points to writable callback storage.
true and false are boolean literals; bool is available for parameters, storage and results. ==, !=, <, <=, > and >= compare compatible numeric operands and produce bool; == and != also accept boolean operands. comparisons preserve contextual numeric inference without making their operands boolean. arithmetic binds tighter than relational comparisons, which bind tighter than equality; comparisons associate left to right. boolean arithmetic/ordering and string/function/void comparisons are invalid; there is no implicit numeric-to-boolean conversion.
!, and and or require boolean operands and produce bool. prefix ! and - bind tighter than arithmetic; equality binds tighter than and, which binds tighter than or. binary operators associate left to right, while prefixes nest right to left. operands evaluate left to right: false and rhs and true or rhs skip the right operand, including calls, lazy global initialization and runtime traps. both operands are still parsed, resolved and type-checked; errors in a skipped operand remain source diagnostics without forcing a runtime trap. this applies in initializers, assignments, returns, call arguments and conditions. programs/boolean_conditions.hon demonstrates combined predicates; ./build/honey run programs/boolean_conditions.hon prints true.
if/else supports braced arms, single statements and else if chains. conditions must have type bool. a braceless guard requires parentheses unless it is a function call: if (n <= 1) return 1 or if ready() return. else binds to the nearest if; after a braced arm it may start on the next line. branches are represented as AST blocks even when braceless.
name = expression updates a local := binding. :: bindings, parameters, named functions, builtin types and globals are not assignment targets. the established binding type is preserved: assignment inputs do not widen or infer that type, though numeric literals can adopt its float context. function-valued locals accept replacement functions with the same parameter and result types.
fixed arrays use [N]T for readonly elements and [N]mut T for mutable elements; literals use [a, b], including empty, nested and multiline forms with trailing commas. values [4]mut i64 := [1, 2, 3, 4] permits values[2] = 10. writable elements require a mutable local binding and mut at every traversed array level; parameters and globals remain readonly. whole-array assignment only requires a mutable binding, but source and destination types must match recursively, including counts and element mutability. initializations, assignments, arguments and returns copy arrays by value, including nested rows.
array lengths currently support nonnegative integer literals, unary signs, +, -, * and package-level immutable integer constants; local constants and other compile-time expressions are not implemented. [_] infers a binding's array length from its initializer. element annotations require concrete supported value types: use i64 or f64 rather than the int or float constraints. an unannotated array literal has readonly elements.
indices require usize; integer literals and flexible inferred values receive this context, while fixed signed integers do not convert implicitly. out-of-bounds access traps before loading or storing, including for empty arrays. reads evaluate the base before the index; indexed assignment evaluates its right-hand side before the destination, each once. leading nested assignment targets follow Bro's declaration ambiguity: write (matrix[1])[0] = 9, not matrix[1][0] = 9. programs/array_sum.hon uses a usize range bound to index, mutate and sum an array; ./build/honey run programs/array_sum.hon prints 17.
data pointers use @T for one value and *T for many values, with mut after the pointer marker for writable pointees. &value borrows addressable initialized storage; postfix ^ reads or writes the pointee. pointer mutability is independent of binding mutability: an immutable pointer binding or parameter can mutate its mut pointee. mutable pointers weaken to readonly pointers with the same child type. address-taking also accepts an expected many-pointer type, such as pointer *mut i16 :: &values[0]; arbitrary temporaries are not addressable.
pointer-to-record field selection dereferences implicitly. pointers to arrays support bounded indexing, .len, and .ptr; .ptr returns a many-pointer and retains both pointer and array-element mutability restrictions. many-pointer indices require usize and caller-provided valid storage. many-pointer pointer + offset requires a usize offset, advances by the pointee's storage size and preserves its type/mutability; single-pointer addition, reversed addition and pointer subtraction are invalid, matching Bro. operands evaluate left to right once; one-past pointers can be formed but not dereferenced. pointers preserve alias identity through calls, returns, globals and aggregate copies; dereferenced aggregates still copy by value. == and != compare locations. recursive records may contain pointers to themselves without recursive by-value layout. CLI results render pointer leaves as <pointer>. pointee lifetimes remain caller-managed; there is no borrow checker.
slices use []T or []mut T and borrow storage rather than copying elements. values[start..end] checks ordered, upper-exclusive usize bounds; either bound may be omitted for arrays, pointers to arrays and slices. many-pointer slices require an explicit end. empty and one-past slices are valid, but indexing at .len traps. .ptr returns a many-pointer and .len returns usize; copies, calls and aggregate storage preserve aliases. mutable slices can write through immutable bindings and parameters, while readonly views cannot strengthen implicitly. contextual &[] creates an empty mutable slice; borrowed backing storage must outlive every view.
sentinel slices use [;sentinel]T and checked construction such as pointer[..length;0]. the sentinel is a representable compile-time integer stored immediately after the logical elements, outside .len; construction checks both accessible bounds and its value. sentinel views can weaken to plain slices, not the reverse. string values expose readonly byte indexing, slicing, .ptr and .len, and convert to []u8 or [;0]u8; their module-owned backing includes an implicit NUL without changing the logical length.
optionals use ?T; values of T and null initialize optional storage, parameters, results and aggregate fields. == null and != null test presence. postfix value? unwraps or traps, while if value |payload| and while value |payload| bind a payload only inside the taken body; loop guards are re-evaluated and continue returns to the guard. optional data/function pointers use a nullable pointer representation; other optionals retain a separate presence flag and by-value payload. optional pointer weakening preserves aliases and readonly restrictions.
fallible values use explicit T ! E channels, where E is an enum, record or tagged union. success and error values initialize storage, arguments and returns; fallible values also work in arrays, records and callbacks. void ! E functions may fall through successfully. native unbacked enums and tagged unions compose with A | B, including grouped/imported error sets; compatible subsets widen without changing variant identities. immutable match subjects narrow within explicit/grouped arms and final else arms, allowing residual errors to propagate into smaller channels.
try expression evaluates once, extracts success or returns a compatible error from the enclosing fallible function. expression catch [|err|] fallback evaluates only the selected path; fallbacks may be expressions, terminal statement blocks or, for void success, blocks that fall through. inferred T! signatures and yielding catch blocks are not implemented. write builtin!(...) without a gap before !; in general expressions a spaced T ! (E) denotes a fallible type.
defer statement and errdefer [|err|] statement register reached cleanup in the current block; braces introduce explicit nested scopes. cleanup runs LIFO when its scope exits, including return, break and continue; return values are evaluated and copied before cleanup. errdefer requires a fallible function and runs only on a language-error return while its scope remains active. cleanup cannot return, propagate with try or transfer control out of itself. source errors remain ordinary values: catch does not intercept diagnostic traps or compiler allocation/IO failures, and traps do not unwind source cleanup. a fallible main prints its success value normally; an unhandled error prints nothing and exits with status 1, without creating a diagnostic.
sizeof!(T) and alignof!(T) report the supported target's native layout, including aggregate padding, slice descriptors and nullable pointers. ptrcast!(value) takes its full result pointer type from context; ptrcast!(T, value) states it explicitly. casts preserve the storage location and optional presence, permit byte reinterpretation, and cannot remove readonly qualification. constcast! changes pointer or slice mutability without changing the pointee type; it does not make genuinely readonly storage writable or extend its lifetime. anyopaque is a pointer-only pointee, not a storable or dereferenceable value.
memcopy!(destination, source) copies matching typed slices or pointers to arrays, checking equal lengths and rejecting nonempty overlap. memset!(destination, value) repeats complete typed values, including aggregate fields, rather than repeating a byte. destinations require writable elements; empty and zero-sized copies/sets do not access storage. interpreter byte views synchronize with the original typed storage so casts and memory operations preserve aliases.
native external declarations use malloc c_func(size c_ulong) ?*mut anyopaque, without a body. supported signatures contain concrete integer/float/boolean scalars, data pointers, optional data pointers and void results; aggregate C ABI and variadics remain outside this gate. Darwin c_char/c_uchar, c_short/c_ushort, c_int/c_uint, c_long/c_ulong and c_longlong/c_ulonglong map to the corresponding 8/16/32/64-bit integer representations. executable checks cover malloc, realloc, free and posix_memalign through allocator-style callbacks. foreign calls trap only when reached in the interpreter; arbitrary interpreter FFI is not supported. compiling the existing generic standard library remains a later gate.
records use Point :: struct { x i16, y i16 = 2 }, with comma- or newline-separated fields. named record declarations have nominal identity; two separately declared records are not interchangeable. construct them with Point { x = 40 } or a contextual keyed literal, such as point Point := { x = 40 }. omitted fields use their declared defaults; missing required fields, duplicate/unknown fields, incompatible values and recursive by-value layouts are diagnosed. explicit initializers run in source order, followed by omitted defaults in declaration order. an explicit value overrides its field's default without evaluating that default.
tuple types use struct { i64, bool }; tuple values use {42, true}, {42,} and {}. anonymous keyed records, such as { x = 40, ready = true }, infer structural types. fields use .x or canonical tuple ordinals such as .0, including chained selections. mutable local roots permit field updates; constants, parameters and globals remain readonly, and traversed arrays still require mutable elements. construction, binding, assignment, arguments and returns copy nested records/tuples and arrays by value. fields can be read from returned temporaries; records containing no scalar leaves retain their logical shapes. CLI results print declaration-order values as {value, value}.
enums use Kind :: enum { number, empty }, qualified members such as Kind.number, and contextual .number literals. unbacked enums use compilation-wide u16 variant identities; explicit integer values require a backing, as in Code :: enum(i16) { missing = -1, ready = 42 }, and must increase within that backing's range. enum identities remain nominal across packages. equality/inequality and explicit backing extraction are supported; enum arithmetic and ordering are rejected. reserved keywords are valid enum and tagged-union member names.
Id :: distinct u32 creates a nominal type with its backing's runtime representation. integer/float literals adopt distinct numeric context; arithmetic preserves the identity and checked-overflow rules. Id(value) explicitly constructs a value, applying checked conversion for numeric backing. nested distinct and aggregate backing require the immediate backing type; explicit extraction removes one layer. Name :: alias declaration preserves the original function/global/type identity, including imported declarations; aliases have no independent runtime storage.
untagged Bits :: union { integer u64, real f64 } overlays its members in shared storage. keyed construction selects exactly one member; reading another member reinterprets the same bytes. tagged unions use union(enum) or union(Kind); an explicit enum must contain every union variant. construct them with Choice { number = 42 }, contextual .number{42}, .point{x = 40, y = 2} for record payloads, or .empty for void payloads. field reads/writes validate the active tag. nested aggregates copy by value, including across calls. CLI union results print {payload} (or {} for a void payload); untagged results print the first member.
match value { .number |n|: n + 2, .empty: 0 } selects one arm after evaluating its subject once. matches support enums, tagged unions, composed error sets, numeric/boolean scalars, comma-separated patterns, numeric ranges, final else arms, arm-local payload captures, statement bodies and expression results. checking rejects missing coverage, duplicate/overlapping patterns, unreachable arms and incompatible captures/results. block arms can return or transfer loop control; yielding value blocks, pointer captures and inline specialization belong to later gates.
while repeats a block or single statement while its bool condition holds, re-evaluating the condition on every iteration. guards follow the same braced/braceless convention as if. loop bodies introduce child scopes; their locals, including locals in nested conditional arms, begin uninitialized on each scope entry.
for 0..10 |i| { ... } iterates an ascending, upper-exclusive integer range; ..= includes the upper bound. a capture is required: |i| binds an immutable value with the bounds' exact integer type in the body scope, while |_| discards it. bounds resolve in the enclosing scope and are evaluated left-to-right once, so changing a bound variable in the body does not change the range. reversed ranges are empty; equal bounds produce zero iterations with .. and one with ..=. inclusive ranges terminate safely at the maximum of each integer type.
range bounds that are compound or unary expressions require their own parentheses, such as (-2)..(limit + 1). a braceless body also requires parentheses around the entire iterable: for (0..10) |i| total = total + i. current execution supports direct integer ranges only; first-class range values, collection iteration, pointer captures and index captures remain unimplemented. programs/range_loop.hon skips 5 while summing 0..10; ./build/honey run programs/range_loop.hon prints 40.
break exits the nearest enclosing for or while; continue restarts a while condition or advances a for range through its guarded update. both are operand-free statements permitted within loop bodies and nested conditional arms; labels and values are unsupported. controls outside loops produce keyword-spanned diagnostics, including in untaken or unreachable code, and trap only if reached. code after a transfer is still resolved and type-checked but does not execute on that path. body locals are reset on re-entry after continue; loop transfers do not guarantee a function return. both controls lower to existing IR jumps, without new interpreter instructions or loop frames. programs/loop_control.hon skips an iteration and exits early; ./build/honey run programs/loop_control.hon prints 12.
name resolution (@source/sema) runs after parsing. it predeclares package names and each block's direct locals, allowing forward references and mutually recursive functions. parameters and top-level function-body locals share a scope; each conditional arm and loop body introduces a child scope whose names do not escape it. signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names; sibling scopes may independently reuse names.
the driver uses @source/compilation to own loaded source buffers, copied paths, one AST arena, the package graph and a shared identifier pool. add_source transfers its ArrayList(u8) allocation only on success, resetting the caller's list; infrastructure failure leaves the bytes caller-owned and rolls back the source registration, partial AST and source diagnostics. interned names may remain in the shared pool. deinit releases all retained frontend storage. metadata borrows are invalidated by registration growth, but source/package/node IDs remain stable within their compilation.
AST NodeId values are compilation-wide, so declarations in different files cannot alias. spans carry their originating diagnostics.SourceId; ast.source_files exposes borrowed paths and text. compilation.compile resolves each package independently, checks the complete arena and lowers one independently owned IR module, selecting an entry only from the requested root package. interpreter diagnostics and generated native traps retain the originating filename and local line/byte-column. borrowed ast.view trees must be refreshed after builder mutation and must not be passed to ast.deinit.
identifier interning is compilation-owned: initialize a strpool.StringPool, pass it to lexer.scan and sema.resolve before the diagnostic store, and release it after lowering. scanning interns identifier spellings once per pool; parsing preserves their IDs in compact AST storage, exposed through ast.name_id, while retaining source spans for diagnostics. name resolution keys scopes by those IDs and interns builtin names into the same pool, so identifier hashing and equality do not scan spelling bytes. IDs are meaningful only within their originating pool; independent compilations may assign the same numeric ID to different names. interning failures propagate as infrastructure errors rather than producing an invalid identifier ID. IR retains no pool dependency.
resolver scopes use @std/hashmap.AutoHashMap(K, V), which shares init, put, get and deinit with custom HashMap and StringHashMap. automatic keys support integers, booleans, enums, distinct types, fixed arrays and records recursively; record padding is ignored. slices, floating-point values, pointers, optionals and unions require explicit hashing and equality through HashMap (StringHashMap supplies content-based byte-string operations). keys and values are stored by value; referenced resources remain caller-owned. the builtin declaration count is inferred, and builtin ID storage derives its fixed stack-array length from that declaration array rather than repeating a count or initializer.
sema.resolve returns an owned binding table indexed by AST NodeId, without modifying the AST. sema.binding identifies a declaration, package namespace, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return .none. sema.resolve_packages additionally borrows package roots and import links, predeclares every package before resolving qualified uses, and keeps import aliases in their originating source file. selected declarations have the same binding identity as unqualified declarations. release the result with sema.deinit. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax and failed imports retain their existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration.
type checking (@source/sema/check) runs after name resolution. check.check borrows the AST and binding table and returns owned per-node types, pointer/container/record/enum descriptors and construction mappings; its result retains an AST borrow for callable-signature compatibility queries. query with check.type_of, check.pointer_type, check.array_type, check.record_type, check.enum_type, check.field_index and check.initializer_field, and release with check.deinit before releasing the AST. ArrayType.kind distinguishes fixed arrays, slices, optionals and fallibles; element records their element/success type, error_type the fallible error channel, with mutability and sentinel metadata for slices. declarations infer their initializer types, including forward references and function aliases; function types identify an AST declaration or explicit signature node supplying the callable signature. annotations support builtin types, data pointers, callback signatures, fixed arrays, slices, optionals, fallibles and local/imported records, enums, unions and distinct/alias types; type names are not ordinary runtime values. void is valid as a function result, success channel or tagged-union payload, not as a parameter or standalone stored value.
function fallthrough obligations are separate from body types. check.fallthrough_diagnostic(&result, function) accepts a function declaration's NodeId and returns its original missing-return diagnostic ID, or null when no obligation exists. nested body errors retain their original identities. lowering consumes this fact without interpreting diagnostic codes and places its trap only on a surviving fallthrough path; check.deinit releases the obligation storage.
the checker keeps union-find parent links and group sizes in a compact array separate from per-node inference metadata. root walks and path compression access only that link storage; both temporary arrays are released before check.check returns.
numeric types include i8, i16, i32, i64, u8, u16, u32, u64, isize, usize and f64. on the supported 64-bit target, isize and usize are signed and unsigned 64-bit types. current int/i64, uint/u64 and float/f64 annotations share their runtime types; other concrete types remain distinct even when their widths coincide. integer literals adopt numeric context through arithmetic, inferred bindings, arrays and calls before undemanded integers default to int. fixed values never convert implicitly between types. integer arithmetic checks the selected type's bounds; unsigned negation is invalid. / requires and supplies float context. implicit integer-to-float literal context requires exact representation: value f64 :: 9007199254740993 is diagnosed, while 9007199254740993.0 permits IEEE rounding.
the conversion policy is value-preserving by default: any future implicit widening must preserve every value of the source type, not merely the value observed at one use. narrowing, potentially inexact integer-to-float conversion, float-to-integer conversion, and signedness changes that cannot preserve the full source range require an explicit operation. integer arithmetic remains checked rather than wrapping; floating arithmetic retains IEEE rounding.
explicit numeric conversion uses T(value), such as u8(index), usize(byte) or f64(count), with exactly one numeric argument. integer conversions trap with integer arithmetic overflow if the value is outside the destination range; float-to-integer conversion truncates toward zero, then checks bounds, rejecting NaN and infinity. integer-to-float conversion permits rounding, including f64(9007199254740993). operands retain their independent types rather than being narrowed by the result context; a positive constant integer conversion operand above i64's maximum defaults to u64. boolean conversions, arbitrary-width integers, f32 and implicit typed-value widening are not implemented. native storage uses LLVM i8/i16/i32/i64, including array elements and function parameters/results.
@std/strconv.parse_float(allocator, text) parses a complete borrowed []u8 decimal into f64. it accepts an optional sign, decimal point, and exponent; whitespace, embedded NUL bytes, trailing input, hexadecimal, infinities, and NaNs return invalid_number. finite subnormals and signed zero are preserved; overflow and nonzero values rounded to zero return out_of_range. allocation failures propagate as out_of_memory, and temporary storage is always released. the Darwin backend uses libc's C-locale conversion without changing the process locale; rounding follows the caller's floating-point rounding mode. numeric lowering uses this interface, keeping exact integer-to-float validation compiler-specific.
type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions, arguments and nested bodies are still checked. inference cycles without a type anchor are diagnosed. function signatures are checked independently of their bodies. bare return and same-line return expression are supported inside function bodies and must match the declared result type. nonvoid return-path checking is structural: every path must return or terminate in a diagnostic trap, and both conditional successors are considered without constant folding. a missing else or a loop permits fallthrough, even for a literal true condition; a later return can close that path. statements after a return and untaken branches or loops are still type-checked but are not executed.
lower.lower turns the checked AST and binding table into an owned @source/ir module. functions, globals, local slots and instruction-result temporaries have explicit IDs; typed instructions execute in source evaluation order and basic blocks end in jump, conditional-branch, return or diagnostic-trap terminators. conditional arms join only when execution can continue; loops use condition/body/exit blocks and backedges. short-circuit expressions use branches and synthetic boolean local slots to merge results; call arguments are packed in the block reached after evaluating them. local slots and temporaries are function-wide, without SSA or phi nodes. literals are parsed and strings decoded during lowering; the module retains no AST, binding-table or source-buffer references. release it with ir.deinit; keep and reuse the diagnostic store used during lowering so diagnostic IDs remain valid. the IR is target-independent and is intended to feed later native backends as well as the current interpreter.
aggregate IR retains recursive array element types/counts/mutability and declaration-order record fields with flattened offsets. union descriptors additionally retain overlapping byte layouts and per-variant discriminants. typed construction, field/element addresses and load/store instructions use storage reserved per invocation; nested copies and accesses reuse that storage without per-iteration allocation. internal address values remain distinct from source pointer values: explicit bridge instructions preserve initialization checks, while pointer offsets retain typed element strides. native pointers occupy eight bytes; interpreter handles intern root storage identity and byte offset across frames, including union serialization. match lowering uses ordinary branches and local result slots; enum and distinct identities are checked before erasure to their runtime backing.
IR construction owns derived layout metadata. ir.add_array, ir.add_record and ir.add_union check shape capacity and compute their interpreter storage and native layouts; record/union shapes own copied field metadata. all aggregate kinds use ArrayId shapes and the existing backing segments. ir.add_parameter and ir.add_local create slots with disjoint aggregate backing. parameters precede other locals. ir.emit assigns fresh result IDs by default and maintains temporary counts, aggregate-result segments and address presence. hand-authored non-SSA IR may supply explicit result IDs; repeated aggregate results retain their shape and reuse the same segment. producers do not register backing separately or write derived counts, offsets or flags. construction failures leave logical contents unchanged, and no finalization pass or extra runtime allocation is required.
interpreter.run consumes only IR and returns an Outcome containing a value or diagnostic trap. returned aggregates own their escaped cell storage; release every outcome with interpreter.deinit before releasing its module. string values carry a module-local ir.StringId, not a slice; resolve bytes with ir.string_bytes(&module, id) before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. aggregate descriptors also borrow the module; ir.aggregate_item projects a borrowed field/element while preserving zero-cell child shapes. heap-backed invocation frames track their current block and resume it after calls or global initialization; loop iterations reuse the invocation's storage without allocating new frames. parameters are initialized on entry and local declarations execute in statement order. premature local reads and assignments trap; assignment evaluates its right-hand side before checking the destination's initialization. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. int/i64 use signed 64-bit storage, usize uses unsigned 64-bit storage, both with checked arithmetic; float/f64 use 64-bit IEEE arithmetic and comparisons, including NaN behavior. usize supports +, -, * and comparisons, but not unary negation or division. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support \\, \", \n, \r, \t, \0 and \xNN escapes.
recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is 0 for clean execution, 1 for source diagnostics or a runtime trap, and 2 for usage or infrastructure failure. compound assignment remains unimplemented.