Files
honey/README.md
T

82 KiB

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 and prefixed integer literals (0b, 0o, 0x); collection initialization uses contextual empty slices and AST flag masks use binary spelling. if the installed bro predates either feature, rebuild Bro with make -C ../brolang build and use ../brolang/build/brolang build. 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. declarations are public by default; @hide:file limits a declaration to its originating file, allowing independent private names in sibling files. other visibility 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 runtime function with no parameters; compile-time-only procedures cannot be entry points. 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 or an expected result type determine them (identity(value)). inference follows pointers, arrays, slices, optionals, fallible channels, callback signatures 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.

expected result types flow from typed bindings, assignments, returns, ordinary call parameters, aggregate fields/elements, defaults, match arms, value-block yields and cleanup handlers. they also reach explicit $() expressions, some! payloads and arithmetic/bitwise operands; shift counts keep their independent types. rigid runtime arguments constrain inference before result context and contextual literal defaults; result context also types nested generic calls. answer proc($T type) T { return 42 } can therefore be called as value i16 :: answer(). optional presence and nominal generated types remain exact, explicit arguments are never overridden, and type-producing procedures still require every input explicitly.

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. runaway specialization is diagnosed at 128 nested evaluation/specialization calls or 4096 instances.

type-producing bodies support mutable type and value locals, direct binding assignment, scoped blocks, conditional/early returns, integer range and while loops, break/continue and nested/recursive type-producing calls. for example, start with Result := T, repeat Result = [2]Result in a loop, then return Result to generate nested array storage. snapshots retain each local's current value without changing the source AST; scalar operations reuse checking, typed IR and interpreter execution, preserving exact types and overflow diagnostics. type-control execution is limited to 4096 statement, loop-guard and aggregate construction/validation steps shared with nested producers; individual scalar evaluations retain the interpreter limits below. only executed bodies are elaborated, while every body is still parsed and name-resolved. cleanup remains outside this checkpoint.

field/index assignments also update initialized, materializable aggregate locals in type-producing bodies, including nested record fields and mutable fixed-array elements. for example, counts [2]mut usize := [0, 0] followed by counts[1] = 2 can generate [counts[1]]i16 storage. array snapshots preserve earlier copies; scalar and ordinary record-field operations reuse checked IR. later loop iterations, generated lengths and defaults observe updates. ordinary mutability, index-type/bounds, field and exact value-type checks still apply, including immutable payload captures and narrow arithmetic overflow. imported producers and repeated compilation retain source diagnostics and nominal cache identity; pointer/slice-backed mutation remains open.

type-producing bodies also iterate materializable fixed arrays and strings with immutable by-value element captures and optional usize indices: for counts |count, index| { length = length + count + index } can compute generated storage. nested loops, aggregate captures, empty collections, break/continue and early returns share the type-control execution bound. array places retain their entry-time indices and observe later element updates; temporary arrays and strings are evaluated once. ordinary constant-input procedures additionally support slice and pointer captures through checked IR, provided only materializable results escape. type-producing loop bodies still reject storage-bearing captures rather than retaining temporary interpreter addresses.

inline for expands constant integer ranges, fixed arrays (including arrays of type), heterogeneous tuples and strings into separately checked runtime bodies. captures are immutable exact-type constants; collection indices are usize. for example, inline for [i16, bool] |T| { if (T == i16) { value T :: 42 } else { value T :: true } } checks only each iteration's selected branch. bounds/iterables are computed before expansion, empty loops skip body elaboration, and nested expansions share the 4096-step type-control bound. break and continue targeting the inline loop must be selected entirely at compile time; selected exits prune the remaining body and break skips subsequent iterations. nested ordinary loops retain runtime transfers, while runtime returns, errors and scoped cleanup retain ordinary semantics. runtime-dependent inputs, pointer captures and storage-bearing values that cannot escape constant evaluation are diagnosed. metadata collections and inline-match syntax remain open.

inline tuples may mix types and values, including nested aggregates, strings, optionals and tagged unions. parenthesize tuple literals in loop headers, such as inline for ({i16, usize(2), "ok"}) |item, index| { ... }; use a compile-time index condition to select operations for each field's exact type. constant helper results, tuple projections and erased generic tuple parameters use the same owned snapshots. the entire tuple must be materializable before iteration, even with discarded captures or an early break. type-producing bodies borrow local tuple places, observing later field updates without changing earlier copies or captures. named records are not tuple collections, and ordinary for still requires homogeneous sequence storage.

initialized fixed arrays of type live entirely inside compile-time execution, including nested arrays, explicit/inferred annotations, .len, checked usize indexing and collection captures. for example, types [2]mut type := [i8, bool], then types[1] = i16 and return [types.len]types[1], generates [2]i16 without runtime type metadata. whole-array and nested indexed assignments preserve shape and mutability at every level; copies and loop captures retain earlier values while borrowed array places observe later updates.

initialized records/tuples containing type fields live inside compile-time producers. local schemas, typed/contextual/anonymous construction, defaults, nested records/arrays, field/index reads and updates, whole-value replacement and by-value collection captures retain owned snapshots. for example, config struct { child type, count usize } := { child = i8, count = 2 }, then config.child = i16 and return [config.count]config.child, generates [2]i16. earlier copies and loop captures remain independent, while borrowed array places observe later updates. type leaves retain nominal identity; ordinary value leaves reuse checked constant execution. unknown/duplicate/missing fields, invalid types/defaults, readonly places, narrow overflow and dependency provenance retain diagnostics. record validation/construction shares the type-control bound. runtime type-valued records remain unsupported.

type-valued records/tuples may also start with undefined: config struct { child type, count usize } := undefined, then config.child = i16 and config.count = 2. nested record fields, arrays of records, tuple ordinals and field! updates use the same persistent snapshots as initialized storage; partial copies retain independent initialization state. defaults do not initialize undefined fields. initialized leaves and array lengths can be read while other leaves remain uninitialized, but premature leaf reads and incomplete procedure arguments/results diagnose read-before-initialization. fully initialized results cross compile-time procedure boundaries and supply erased generic inputs, generated storage and defaults. empty shapes are initialized vacuously; each expanded record field/array cell shares the construction bound.

optionals containing types also stay in owned compile-time storage: ?type, nested ??type, and optional records/arrays of metadata support null, contextual payloads, explicit some!, whole-value updates, immutable captures, null comparisons and checked unwraps. some!(null) preserves an absent inner optional inside a present outer one. constant-input procedures exchange these snapshots; erased generic inputs retain content-based specialization identity and nominal type leaves. if/while captures and conditional expressions select generated types or runtime bodies without runtime metadata. for generated array children, bind the unwrap first (T :: selected?, then [count]T); postfix ? after an array type unwraps the whole expression. undefined presence and incomplete payloads diagnose before observation or procedure boundaries. runtime metadata storage remains unsupported.

fallible channels containing types also execute entirely at compile time: type ! Error, metadata records/arrays in either channel, and nested optionals retain owned active payloads. every producer input is explicit and constant; erased generic inputs, whole-value replacement and saved copies preserve exact nominal leaves and content-based specialization identity. try checks the complete error subset before propagating; catch captures an immutable error snapshot and selects generated types or runtime handlers. named metadata structs and tagged unions compose with ordinary error enums without runtime reflection. returns snapshot before scoped LIFO defer/errdefer; fallible void also permits bare returns and successful fallthrough. incomplete storage, invalid channels/payloads, capture mutation and runtime inputs diagnose. imported private types survive source/session disposal and repeated compilation. runtime locals still cannot store metadata aggregates; keep those values inside compile-time producers or pass them directly to erased inputs.

tagged unions containing types use the same owned compile-time storage. Choice :: union(enum) { child type, empty void } supports typed/contextual construction, active payload reads and nested field/index updates, immutable match captures and whole-value variant replacement. direct payload writes require the selected variant to be active; they never switch its tag. saved copies, constant-input procedure arguments/results and erased generic inputs preserve payloads and nominal type identity while generating runtime storage/defaults. undefined unions have no active variant until a whole-value assignment initializes them, including inside records and arrays. explicit enum tags must contain every variant, but unused tag members do not become match cases. malformed tags, nominal initializer conflicts, inactive/uninitialized access, readonly updates and narrow overflow retain checked diagnostics. untagged unions containing types remain invalid: metadata has no overlapping runtime layout.

fixed type arrays may also start with undefined inside type/type-array producers: types [2]mut type := undefined, then fill types[index] = i16 in a loop. nested and whole-array updates preserve shape, mutability and independent initialization state in local copies; .len is available before elements initialize. reading an uninitialized type leaf, returning an incomplete array or passing one to a compile-time procedure diagnoses a read before initialization. empty shapes are valid, but every dimension must be explicit and nonnegative. construction charges each expanded cell against the shared execution bound, including nested zero-leaf shapes.

ordinary fixed value arrays use the same construction inside type/type-array producers: counts [2]mut usize := undefined, then fill indexed elements or replace whole rows/arrays. initialized leaves retain exact scalar, string, optional, record and tagged-union values; local copies retain independent partial initialization state. generated lengths/defaults and compile-time arguments can use initialized elements, while incomplete arrays cannot enter checked IR or procedure arguments. array .len and empty shapes do not read leaves. constant materialization of mutable slices or nonnull data pointers remains unsupported.

ordinary procedures also accept undefined as the initializer of a mutable local with a concrete runtime type, including nested records and fixed arrays. field/index writes initialize storage without reading its previous contents; aggregate copies retain independent contents and initialization state in the interpreter. ordinary compile-time calls can fill this storage and return materializable values, but observing or materializing an uninitialized leaf is diagnosed. native storage uses LLVM poison, not zero initialization: assign every leaf before observing it, and do not rely on native read-before-initialization traps for undefined storage. immutable bindings, globals, inferred array lengths and other expression uses remain invalid.

some!(value) constructs the present branch of an expected optional, evaluating its payload once and retaining exact payload typing. value ??i16 :: some!(null) is present with an absent inner optional; it differs from outer null. nested presence survives compile-time results, generic cache keys and generated defaults. optional-result procedures still require explicit returns on every path.

procedures returning fixed arrays or records/tuples containing type, such as types proc() [2]type { return [i16, bool] } or config proc() struct { child type, count usize } { return { child = i16, count = 2 } }, execute entirely at compile time. every argument is explicitly supplied, constant, cached and erased; nested/aliased results, empty shapes and dependent $N lengths retain exact shape and mutability. compile-time producers accept ordinary type-array/type-record inputs, and runtime generic procedures accept erased $ parameters with these shapes. direct projections generate signatures, constructors, storage and defaults; explicit $() calls inside producers retain owned values. cache identity uses canonical type leaves without merging distinct nominal types, and copies cannot mutate cached inputs/results. imported diagnostics, shared type-control execution limits and repeatable compilation work through interpreter/native execution. ordinary parameters remain excluded from dependent signatures; use $N for lengths. runtime type-array/type-record storage and reflection remain open.

ordinary parameters containing type also make a procedure compile-time-only, even when its result is a runtime value. for example, count proc(types []type) usize { return types.len } computes count([i16, bool]) without runtime metadata. every input must be explicitly supplied and constant; use $ parameters when later signature annotations depend on them. helpers can return owned scalar, aggregate and string values, recurse over metadata and use checked try/catch with scoped cleanup. plain and fallible void support bare returns and successful fallthrough. ordinary runtime dependencies and incomplete metadata arguments remain diagnostic; mutable metadata borrowing and general type-valued execution remain open.

local metadata storage also makes a procedure compile-time-only, even with ordinary scalar inputs and results. explicit annotations (T type := i16) and inferred mutable type bindings (T := i16), metadata array/record/tuple literals, helper results, checked projections and yielding blocks use owned snapshots and erased calls. nested storage, partial initialization, checked updates, loops, recursion and scoped cleanup reuse type-control execution and its shared 4096-step bound. inferred arrays retain readonly elements unless their type explicitly permits element updates; whole-value replacement remains available for mutable bindings. every input remains explicitly supplied and constant; this is not partial evaluation of runtime inputs. ordinary local alias declarations and scalar projections from metadata helpers keep their runtime-call contract. mixed runtime/type-valued execution and mutable metadata borrowing remain open.

readonly type-valued slices ([]type and slices of metadata records/arrays) also cross compile-time procedure boundaries as owned snapshots. contextual array literals, including [], construct them; explicit views of immutable readonly arrays and existing metadata slices support checked usize bounds, empty end views, indexing and .len. nested slices, ordinary/inline by-value iteration, erased $ inputs and optional/fallible channels retain exact nominal leaves and content-based specialization identity. snapshots generate runtime types/defaults, but metadata itself has no runtime storage. mutable slice types, sentinels and views of mutable array places remain rejected rather than silently losing borrowing semantics; copy an array into an immutable binding before taking an owned view. construction shares the type-control step bound, and uninitialized leaves cannot cross procedure boundaries.

optional if/while captures also execute in type-producing bodies: if count |length| { return [length]i16 } generates storage from an optional usize. ordinary checked IR validates each executed guard; present payloads become immutable owned constants with exact scalar/aggregate types and strings, and absent optionals skip the body. while guards observe updated locals on each iteration; captures stay body-local across break/continue and early return. generated lengths/defaults, imported producers, cache identity and repeated compilation preserve these values. null data/function pointers materialize as null; callbacks retain source declaration identity, while nonnull data pointers and unsupported slice types retain the constant-materialization escape boundary.

type-producing bodies also execute scalar/enum match statements and type-valued match expressions in initializers, assignments and returns, such as return match wide { true: i16, false: i8 }. selected expression arms may themselves be matches. an ordinary checked IR match returns the selected arm index, preserving subject/pattern typing, range boundaries, overlap, unreachable-arm and exhaustiveness diagnostics without checking unexecuted bodies. statement arms retain scoped locals, assignments, early returns and enclosing-loop break/continue; expression selection shares the type-control step bound.

matches also select nested types, array lengths, generic arguments, callback signatures and field defaults in type-producing bodies. for example, return struct { values [2](match wide { true: i16, false: i8 }) } generates concrete storage without elaborating the unselected arm. snapshots preserve loop-local type/value bindings, selected literals retain their spelling and source provenance, and nominal results are cached before their recursive fields are expanded. deferred type selection shares the producer's execution bound; failed elaboration retains its original diagnostic without leaving unchecked generated types behind. general type-valued execution remains open.

tagged-union matches in type-producing bodies support immutable by-value payload captures in statements, expressions and nested type/storage positions. for example, match choice { .count |length|: return [length]i16, .empty: return bool } uses a captured usize to generate storage. grouped variants require identical nonvoid payload types; invalid captures in unselected arms are still diagnosed. selected payloads become owned typed constants, preserving narrow integers, nested aggregates and strings across generated lengths/defaults, loop snapshots and repeatable compilation. active payloads containing data pointers or unsupported slice types retain the constant-materialization escape boundary below.

ordinary procedures may compute integer, boolean, float and string compile-time arguments and return nested records/tuples, fixed arrays, tagged unions and explicit fallible results 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 and fallible arguments receive their declared context, including dependent [N]T parameters; cached specializations compare normalized values, not literal spelling or evaluation identity. returned constants become owned source literals before temporary interpreter storage is released, preserving by-value copies, exact integer types, string bytes, enum/optional fields and IEEE float bits. tagged unions retain their actual discriminant and only the active payload, including nested aggregates and void/zero-cell variants. fallible constants retain success/error identity, composed or payload-bearing errors and void success; catch can consume them in generated lengths, defaults and runtime expressions.

computed callbacks materialize as references to their original source declarations, never as temporary interpreter function IDs. choose proc() @proc(value i16) i16 { return increment } can supply $(choose()), compile-time arguments and generated defaults. optional callbacks preserve absence and nested presence; arrays, records, tagged payloads and both fallible channels retain exact callback signatures and specialization-cache identity. imported private targets remain bound to their originating declarations, including across repeated compilation and native emission.

readonly byte-slice results ([]u8 and [;sentinel]u8 for any byte sentinel) snapshot their bytes into owned literals before evaluation storage is released. ordinary compile-time helpers can return string subviews, views of locally filled arrays or immutable global arrays, including nested records/arrays, tagged payloads, optionals and both fallible channels. explicit $() expressions, generic arguments and generated defaults preserve embedded NULs, arbitrary byte values, exact declared slice types and content-based specialization-cache identity. nonzero-sentinel snapshots retain an owned trailing sentinel outside the logical length; empty views also retain it. snapshots survive evaluation/session disposal and repeated compilation; uninitialized source bytes diagnose read-before-initialization, and sentinel bytes are rechecked after evaluation.

readonly slices of other materializable element types also own their snapshots, including distinct bytes, wider integers, arrays, records, callbacks and nested readonly slices. generated backing has module lifetime, so returning a computed slice from a runtime helper does not borrow its stack. exact element types, logical lengths, zero-sized elements, full-width signed/unsigned sentinels, nested optional/tagged/fallible channels and content-based specialization identity survive $(), compile-time arguments, generated defaults and repeated compilation. snapshots recheck source initialization, address ranges and trailing sentinels; recursive expansion is bounded to 100,000 values and 128 evaluation/serialization levels. mutable slices, nonnull data-pointer elements and address-bearing untagged union elements retain the escape diagnostic.

untagged union constants snapshot every byte of their native-layout storage, including padding and bytes beyond a smaller overlapping member. scalar reinterpretation, nested records/arrays, optionals, tagged payloads, fallible channels and generated defaults preserve exact nominal types and by-value copies through $() and compile-time arguments. specialization keys compare the full snapshot, not one selected field. every storage byte must be initialized. because untagged storage has no active-member identity, all member layouts must be free of data pointers, callbacks, strings and slices, recursively; otherwise materialization diagnoses an escape rather than retaining interpreter handles, even if a numeric member was initialized.

$(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, expression statements 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. nonnull data pointers, unsupported slice types and address-bearing untagged union layouts cannot escape temporary evaluation as generated constants; active payloads or aggregate fields containing unsupported values are diagnosed rather than retaining interpreter handles. executed foreign calls remain interpreter traps. general type-valued execution, broader storage-bearing materialization and remaining existing-library acceptance, including broader meta operations and io.print, 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.

compile_error!(message) requires a compile-time string and reports its owned text at the call site when that body is elaborated. unselected compile-time branches do not report it; ordinary runtime branches remain checked. messages from constant-input procedures retain their bytes through later evaluation, interpreter diagnostics and native executable emission.

match $(subject) { ... } selects an expression or statement arm before specializing or checking its body, including ordinary constant-input calls. concrete generic procedures also select matches on side-effect-free constant subjects, including substituted scalar/enum/tagged-union arguments and pure field/index projections. ordinary checked IR validates every pattern and capture declaration before selection; only the selected body is elaborated, while all bodies remain parsed and name-resolved. by-value payload captures retain exact types and owned strings, and selected bodies may use runtime parameters, nested matches, cleanup, returns and enclosing-loop transfers. ordinary call subjects, runtime parameters and mutable locals stay runtime, as do contextual aggregate bindings that might omit runtime defaults; ordinary nongeneric literal matches still check all arms. use $() to request explicit evaluation. unsupported storage-bearing payloads retain the escape boundary above.

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.

field!(base, name) selects a member using a compile-time string, without runtime reflection metadata. names may come from constants, constant-input procedures or erased generic parameters; only the name is evaluated at compile time, while the base retains ordinary selection semantics. records, tuples (canonical decimal names such as "0"), enum/error-set members, tagged payloads, callbacks and sequence "len"/"ptr" members reuse exact typing, single base evaluation, address-taking and writable-place checks. type-producing bodies also snapshot selector names for nested aggregate updates, preserving earlier by-value copies. unknown members, runtime/nonstring names and invalid calls are diagnosed; tagged payload reads still check the active field.

typeinfo!(T) returns the compile-time-only TypeInfo declared by @std/meta; import that exact path, optionally aliased, in the calling package. arrays expose child and len, distinct types expose their immediate backing type, and records/enums expose owned readonly fields collections. each field has name, exact type and ordinal index; enum indices follow declaration order, not numeric discriminants, and enum field types retain the reflected enum or composed error-set identity. records also expose their original optional name, is_tuple and .auto/.c layout; tuple names are canonical decimal ordinals. aliases preserve underlying names and nominal identities.

ordinary and inline metadata matches select bodies before runtime checking, including in nongeneric procedures. metadata collections support .len, checked indexing, by-value iteration and compile-time procedure inputs/results; copied field records can change without mutating the readonly collection. field names/types drive field!, generic calls, generated storage and defaults through interpreter/native execution, without runtime reflection objects or borrowed interpreter addresses. construction and expansion share the type-control bound. the existing std/meta.EnumFieldStruct and std/enummap.EnumMap, init and get work through interpreter/native execution, including sparse/aliased/composed enum keys, absent entries and aggregate copy isolation. generated-map arguments infer key/value types before checking contextual .tag arguments; conflicting rigid arguments and unresolved generics still diagnose. get explicitly returns null after its lookup. general storage-bearing constant materialization and runtime metadata storage remain unsupported.

struct_type!(layout, names, types, defaults) generates an ordinary record from constant fixed arrays or tuples of equal length. for example, struct_type!(.auto, ["value", "bias"], [i16, i8], [null, 2]) requires a value initializer and supplies an i8 default for bias. literal collections retain each field's context, including heterogeneous types and defaults; computed collections can come from helpers and loop-filled producer locals. names must be unique ASCII identifiers. each default is checked as ?Field: absent means required, present supplies an owned field value. generated fields retain ordinary exact typing, aggregate copy isolation, field selection and checked updates through interpreter/native execution. producer specializations cache nominal identity; imported producers and repeated compilation retain source provenance and fresh owned values. incomplete arrays and runtime dependencies are rejected, and construction shares the type-control execution bound.

layout accepts .auto or .c, including constant enum members such as Layout.c. C-layout records must be nonempty; fields may contain concrete scalars, data pointers, optional data pointers, explicitly backed enums, nonempty fixed arrays and nested C-layout records. distinct types and non-C representations are rejected. this does not add aggregate C calling conventions or storage-bearing constant materialization.

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.

compound updates += -= *= /= &= |= xor= <<= >>= <<|= preserve the target type and ordinary writable-place checks. the target address and old value are evaluated once before the right operand, then the result is stored through that same address; /= is float-only and xor= must be contiguous. field/index/pointer targets, cleanup bodies, ordinary compile-time calls and type-producing locals use the same checked operations.

integer ~, &, xor and | preserve exact operand types, including distinct integer identities; incompatible fixed integer types remain invalid. shifts <<, >> and saturating <<| preserve the left type and accept unsigned counts independently of its width. ordinary shifts reject known out-of-range counts and trap for runtime counts at least the left bit width. left shifts discard high bits; signed right shifts extend the sign. saturating shifts accept any count, preserve zero and clamp nonzero values to the signed minimum/maximum or unsigned maximum. shifts bind below arithmetic; &, xor and | share one left-associative level below shifts and above comparisons. parenthesize bitwise-OR header expressions before capture lists. type-valued A | B remains error-set composition.

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 support nonnegative integer literals, unary signs/complement, +, -, *, bitwise operators/shifts and package-level immutable integer constants; use $() for broader compile-time execution. [_] 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, yielding or terminal statement blocks or, for void success, blocks that fall through. inferred T! signatures 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, yield, break and continue; returned and yielded 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, not on a successful yield. cleanup cannot return, propagate with try or transfer control out of itself; a nested value block may yield locally. 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/function pointers, nullable pointers and void results. generated bindings may declare c_struct records, nominal pointer-only opaque types and *c_func(...) Result callback signatures. unused foreign declarations do not force ABI validation or native symbol emission; demanding a variadic or aggregate-by-value signature diagnoses unsupported operands. 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; c_double maps to f64, while demanded c_float and c_longdouble types remain unsupported. foreign calls trap only when reached in the interpreter; arbitrary interpreter FFI is not supported.

the existing std/arraylist and std/mem work without replacement bindings or library code. interpreter/native checks cover initialization, reserve/growth, append/pop/clear, allocation failure without losing the original list, zero-sized elements and deferred cleanup on error. native checks use the real mem.c_allocator and imported ffi/c declarations for aggregate element storage and over-aligned allocation/reallocation; interpreter allocation checks use a fixed-capacity test allocator, not foreign calls. broader metadata operations and formatted IO remain later acceptance gates.

existing std/hashmap.HashMap, AutoHashMap and StringHashMap execute without replacement library code. checks cover collision chains, zero-hash normalization, growth/rehashing, duplicate-key errors, absent lookups, aggregate copy isolation, allocation failure preserving the map and deferred cleanup. native checks use the real C allocator for integer boundaries, booleans, sparse/aliased enums, nested distinct/array/record keys (ignoring record padding), content-based strings and custom callbacks; interpreter checks use separate fixed-capacity typed buffers. a declared result supplies the initializer parameters: map hashmap.AutoHashMap(i16, i16) := hashmap.init(allocator); StringHashMap and custom HashMap results also infer their exact private/custom callbacks. explicit calls such as hashmap.init(i16, i16, null, null, allocator) remain supported.

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.

if (guard) then_value else else_value is a boolean value expression. if value |payload| then_value else else_value instead tests optional presence and binds the exact payload as an immutable by-value capture in the then expression; nested optionals preserve present-null payloads. the guard runs once and only the selected expression executes; else is required and binds to the nearest conditional. both runtime branches must fit the result context, including optional/fallible channels and aggregate copies. constant-input procedures, direct $() evaluation, generic branch specialization, explicit compile-time subject selection and generated type positions reuse checked match execution. boolean guards require parentheses unless they are calls or explicit $() expressions; optional captures also permit bare guards. parenthesize a conditional used as a match-arm value to distinguish it from an if statement.

value blocks contain ordinary statements and produce a result with yield expression:

answer i16 :: {
    value i16 := 40
    yield value + 2
}

yield exits the nearest enclosing value block, match arm or catch handler with a copied result. ordinary nested statement blocks and loops do not change its target; return, break and continue retain their usual targets. conditional branches also accept yielding blocks. result context reaches yielded expressions, including narrow integers, aggregates and optional/fallible channels. nonvoid fallthrough, incompatible yields and yields outside a value source diagnose; only reached invalid paths trap. constant evaluation and type-producing procedures support yielding blocks, generated storage/defaults and owned snapshots. empty, singleton and keyed record/tuple literals retain their existing syntax; statement-bearing braces select a value block. labeled yields and value-producing loops remain unsupported.

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 yield results, return or transfer loop control; pointer captures belong to a later gate.

a final inline |tag|: arm on an enum, or inline |payload[, tag]|: on a tagged union, expands into one ordinary arm for each variant not covered earlier. dispatch remains runtime and evaluates the subject once. enum captures and optional union tags are immutable compile-time enum values; payloads are immutable by-value captures with each variant's exact type, including a void value for empty variants. either capture may be discarded with _. static tag matches/conditions select variant-specific operations before checking, including generic calls, nested inline arms and type-producing procedure bodies. ordinary payloads remain runtime values, and return/break/continue/deferred cleanup retain ordinary match semantics. redundant/nonfinal inline arms, invalid captures and scalar/untagged subjects are diagnosed. expansion shares the 4096-step type-control bound.

composed error sets use the same inline arms, including through catch |err| match err { ... }. sets containing only void variants use inline |tag|:; sets with any payload use inline |payload[, tag]|:, with void values for their empty members. nested compositions, transparent aliases and repeated members expand each remaining variant once. named struct errors use their declaration name as the variant tag, and payloads retain their exact struct type and by-value isolation. for example, Missing | union(enum) { failed i8 } can use inline |payload, tag|: match tag { .failed: i16(payload), .missing: 0 }. constant inputs and type-producing bodies also select generated storage from composed payloads. pointer captures and metadata reflection remain open.

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.

a literal while true without a reachable break is terminal, including when only nested loops break. nonvoid functions need no fallthrough return after it; runtime guards and loops with reachable breaks still require a return.

unreachable is an operand-free statement that traps only when reached, preserving its source location in interpreted, native and compile-time execution. untaken and unused occurrences do not diagnose. it is a fatal trap, not a source error value or a cleanup transfer; it does not execute deferred cleanup.

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. first-class range values and range pointer/index captures remain unimplemented. programs/range_loop.hon skips 5 while summing 0..10; ./build/honey run programs/range_loop.hon prints 40.

collection loops accept arrays, slices, strings and single pointers to arrays: for values |value, index| { ... }. element captures are immutable by-value copies with the exact element type; optional indices have type usize, and either capture may be discarded with _. |@value| borrows an element through a single pointer, writable only when the source storage permits mutation. the iterable and its length are evaluated once, then each iteration reads its current element: array lvalues are borrowed, temporary arrays are retained once, and slice rebinding does not change the active loop's view. empty collections execute no body, including arrays of zero-cell elements. collection loops share range-loop cleanup, break/continue and return handling through the interpreter and native backend.

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.

AST nodes retain a compact 16-byte layout on the bootstrap target: a kind, three 32-bit payload words and one packed flag byte in existing padding. file-local, foreign-callable, variadic and opaque metadata therefore need neither per-node boolean fields nor another allocation. spans and variable-length payloads remain in parallel storage; cloning and replacement copy the flags with the node.

the AST renderer displays nested blocks, cleanup bodies/captures and compound operators alongside ordinary statements.

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.

compiler token/operator mappings, scalar type metadata and backend spellings use compile-time @std/enummap tables directly; state-dependent layout and control-flow dispatch remain ordinary code. IR exports integer_minima and integer_maxima maps for shared integer bounds, with absent entries for noninteger types. enummap.init_default(E, V, storage_default, fields) initializes unused value slots so sparse maps can become owned compile-time constants without changing which keys are present. enummap.get still performs runtime linear lookup; static construction is not a constant-time lookup guarantee.

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.

minval!(T) and maxval!(T) return exact concrete integer bounds, including distinct integer types. divtrunc!(left, right) requires matching integer types and truncates toward zero, with ordinary contextual literal inference and single left-to-right operand evaluation. integer division by zero and signed minimum divided by -1 trap before executing host/LLVM division; / remains floating-point division. these builtins also work in checked compile-time execution.

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