Multi-frontend IR (conceptual)
Multiple source languages lower to Core IR and share desugaring, verification, optimization, and native JIT/emission. Guarded matches use the same native pipeline as ordinary control flow.
Conditional and block values
Core IR represents value control flow explicitly. Conditional { cond, then_expr, else_expr } evaluates a Bool condition once and executes only the selected branch. Block { body, result } has a local scope and an optional trailing value; its return, break, and continue retain their enclosing function or loop targets. Branch values must have compatible types, while a branch that exits does not need to produce a value.
Rust and brace-form .in lower these expressions through the shared verifier and owned AArch64/x86_64 backends, including scalar, array, tuple, record, and scalar-vector values. Canonical .in output preserves block statements and the distinction between a trailing result and a discarded expression. Go source projection, Thumb, and INISA explicitly refuse structured value forms they cannot preserve.
For .icore version 2 or 3, expression objects use kind: "conditional" with cond, then, and else, or kind: "block" with body and optional result. For example, this expression produces 42:
{
"kind": "conditional",
"cond": true,
"then": {
"kind": "block",
"body": [{"kind": "let", "name": "base", "type": "Int", "value": 20}],
"result": {"kind": "binary", "op": "+", "lhs": {"kind": "ident", "name": "base"}, "rhs": 22}
},
"else": 0
}
Match values
MatchValue { scrutinee, arms } evaluates its scrutinee once, tries patterns and guards in source order, and yields the first selected arm's value. Each arm has its own pattern bindings. Bindings are captured before its guard executes; a failed guard continues to the next arm without evaluating the arm value. Native dispatch uses one branch sequence without duplicating later arms.
Value matches require compatible arm types and proven exhaustiveness. An unguarded wildcard, binding, or irrefutable typed aggregate pattern establishes coverage; both unguarded Bool literals also cover Bool values. Guards alone do not prove coverage. Fixed-length array patterns require a fallback because Core IR arrays do not retain a static length. Block arms retain enclosing returns and loop jumps.
Rust expression match and value-position if let, brace-form .in matches, and .icore v2/v3 kind: "match-value" use this representation. The JSON form contains scrutinee and arms; each arm has pattern, optional guard, and value. C/Go source projection, Thumb, and INISA refuse match values until those emitters preserve the same semantics.
Native symbol resolution
Native reads resolve declared local, parameter, global, and supported function-address bindings. Core IR symbol names are matched exactly, including names supplied by .icore that source-language parsers would not spell as identifiers. A spelling heuristic must not replace a declared value. Unresolved identifiers refuse lowering rather than becoming zero or allocating an uninitialized stack slot.
Function addresses and indirect calls
Declared functions used as values retain their definitions through optimization and use an Int address carrier. AArch64 resolves these addresses relative to the emitted code; out-of-range address sites refuse lowering. invoke(pointer), invoke1(pointer, value), and invoke2(pointer, first, second) use the low-level integer calling convention. The pointer and arguments evaluate once, in order, before the call; callback errors propagate through the existing error slot. Arity and argument carriers are checked, and directly named callback targets must match the integer signature.
These intrinsics do not provide typed function pointers or closure capture support. Opaque integer pointers require a valid code address with the matching signature; their provenance cannot be checked statically. Native targets without the runtime error slot refuse indirect calls that require it. Global function-address initializers still refuse native lowering until data initialization can relocate code addresses.
Lazy Rust dependency modules
Dependency lookup uses the registered crate root or existing source search roots, with Cargo-style src/lib.rs, src/module.rs, and src/module/mod.rs paths. Imported modules receive canonical crate/module names before caching and merging. Function calls and address values, global references, named schemas, and local type annotations keep those identities; lexical bindings and member names retain their own scopes. Identical imported declarations merge once, while conflicting definitions of the same canonical name refuse compilation. Dependency expansion runs before entry verification and optimization for both JIT and native requests. Executable discovery follows the selected entry through functions, globals, schemas, and address values; libraries without an entry retain full discovery. Trait implementation methods retain receiver and trait identities, and anonymous constants retain distinct internal identities. Reachable Rust dependency bodies are validated before caller-specific coercions; a C++ caller cannot turn an invalid Rust condition or assignment into accepted code. Native report-cache keys include the resolved module and emit context, and cache reuse checks the recorded output bytes. Dependency edits, reverting an edit, or another profile overwriting the same output therefore trigger a rebuild; unchanged inputs and outputs retain cache hits.
Contextual nominal names
Rust impl lowering resolves Self and Self::… against the owning type in signatures, constructors, closures, and patterns, including macro-lowered values. Nested modules preserve those identities without rewriting field names, string literals, generic parameters, or deferred crate paths. This does not add generic specialization or complete aggregate-vector runtime support. Pattern argument splitting respects quoted commas, brackets, and escaped quotes, so struct, tuple, and array patterns preserve their string values through normalization. The .in statement and value match parser balances struct-pattern fields before locating the arm body, preserving guards and optional separators. Match scrutinee headers respect quotes, parentheses, and brackets; parenthesize struct initializers and block expressions used as scrutinees. Canonical match output supplies these parentheses, preserving nested expressions and following standalone blocks. Statement if/while and conditional-value headers use the same boundary rule; canonical statement conditions are parenthesized to retain embedded blocks and struct initializers. Native preparation materializes aggregate and control-value scrutinees before arm dispatch, so supported scalar-array initializers run once even for wildcard arms. Native array storage and destructuring of struct elements remain incomplete.
Literal text across source lines
The .in normalization, declaration, and surface passes share quote state across physical lines. Braces, comment-looking lines, colons, whitespace, and blank lines inside strings retain their literal meaning. Canonical output retains global declarations and their mutability, types, and initializers. Typed String globals store pointers to aligned literal payloads in the writable data section. Data-to-data relocation metadata rebases these pointers for independent JIT mappings and native assembly; SCI retains its NUL-terminated payload layout. String equality compares contents across distinct payloads, including embedded NUL bytes in the hosted length-prefixed layout. Dynamic String concatenation uses the existing allocating runtime in the hosted 64-bit x86 JIT; freestanding, CString, 32-bit, and interrupt contexts reject it. Runtime-call global initializers remain a separate limitation. Unannotated globals infer their types after imported declarations and all global bindings are available; explicit annotations are preserved. Unresolved or cyclic initializers and empty arrays require explicit type annotations.
Method lookup uses declared global types alongside parameter, local, closure, and pattern bindings, and retries unresolved methods after local imports merge. Constant substitution respects the same lexical shadows so an outer constant cannot replace a receiver bound in an inner scope.
Int-to-String conversion in native Apple ARM64 executables returns an allocated length-prefixed String. It shares decimal encoding with integer printing, including zero and both signed 64-bit limits, and traps explicitly if allocation fails.
Unit values
UnitLit represents () with type Void, distinct from integer zero and an empty array. Rust, .in, canonical source, and .icore expression kind: "unit" retain this type through checking and lowering. Empty-tuple patterns match unit values, including the results of calls, and explicit unit returns evaluate their producers before returning.
Native lowering uses a zero carrier for unit locals and parameter reads. AArch64 unit arguments occupy no ABI words; x86 keeps its existing argument convention. Unit elements in native arrays, vectors, and struct fields remain refused until their zero-width layouts are supported. C/Go source projection and INISA also refuse unit literals.
Rust inline constants
Rust const { ... } lowers to the reserved #rust-inline-const call with one scoped Block argument. It must be evaluated within the bounded constant evaluator before native lowering; it is neither a closure nor a runtime block. Its environment contains constant globals and its own locals, never captured runtime bindings. Unsupported evaluation remains an explicit refusal.
Inline constants have their own control-flow boundary: return is invalid, and break or continue cannot target an enclosing runtime loop. A loop declared inside the constant block has its own jump targets, though executing such loops still requires constant-evaluator support. General Rust const-function evaluation remains outside this subset.
Historical textual SIL merge behavior (hybrid_sil)
parse_textual_sil keeps the legacy top-level function_id as whichever sil @name line appeared last in the input, and still exposes flattened block / instruction vectors for existing callers. It also records explicit per-function entries in SilArtifact::functions, each with its own blocks and instructions. extract_call_graph prefers those per-function records, then falls back to instruction_callers, then to the legacy function_id behavior for old artifacts. Emitters still typically place sil @main last so single-function views line up with main. For full detail see in-language.md.
UnifiedModule (current schema)
Rust type: in_cli::core_ir::UnifiedModule.
| Field | Meaning |
|---|---|
decls: Vec<Decl> | Top-level declarations in source order. |
.in also has a parser-side surface fact helper for import, capability, and extern declarations. Local relative .in imports merge imported declarations into the current UnifiedModule; std.io / std.fs / std.http / std.json / std.process / std.cli imports synthesize bounded extern-style declarations; imports and capabilities are exposed through in agent JSON, and extern requires contracts produce agent diagnostics when required capabilities are missing. Extern bindings lower into empty Function declarations today so call graph extraction can observe explicit .in calls without expanding the Core IR schema.
Decl variants
| Variant | Fields | Notes |
|---|---|---|
Struct | name, fields: Vec<(String, Typ)> | .in v0.2 supports multiline field blocks; Tree-sitter and dedicated fronts fill fields where their current extractor supports them. |
Function | name, params, ret, body | .in, icoreVersion: 2, dedicated Rust/Go/V fronts, and selected Tree-sitter fronts fill bounded statement lists; lowering follows lower_core. |
Typ: Int, String, Bool, Void, Named(String).
Compile-time values and stored globals
Decl::Global.constant_value distinguishes a value copied at use from runtime storage. Rust const items use this flag; .in globals keep their existing storage semantics. Shared lowering resolves pure constant expressions, including literal strings, arrays, and struct fields, with lexical shadowing and qualified module names. Unresolved constant uses are refused rather than initialized with fabricated data. Native and C emission omit compile-time declarations from runtime global storage.
Rust indexed assignment uses #index-assign-value-first(base, index, value) in statement position. Shared operand spilling evaluates the assigned value before the place, evaluates each operand once, and skips place evaluation when the value throws. Primitive indexed compound assignments use #index-modify-value-first(base, index, value, operator); the old element is read after the right-hand operand has completed, and the same saved place receives the result. Numeric field compounds use #field-modify-value-first(base, field, value, operator) and retain the original object storage rather than updating a copied struct. A throwing RHS skips the update.
.in numeric field compounds use #field-modify-place-first(base, field, value, operator). Shared lowering retains the destination across effectful operands, reads the old field before RHS effects, and writes back through that same place. Pure updates reuse ordinary binary evaluation without extra capture slots. Rebinding the source array during the RHS does not redirect the update. A failed old-field read skips the RHS; a throwing RHS skips the final store.
Compiler-generated ordered stores and repetition use reserved # callee identities. Legacy helper spellings resolve to these operations only when no module or lexical binding shadows them. Declared functions and local closures retain ordinary call behavior; generated syntax keeps its operation identity alongside them. Canonical .in indexed compounds retain compound syntax, while value-first operations retain explicit private calls to preserve their evaluation order.
Array repetition and collection loops
Rust [value; count] lowers to the reserved Core IR operation #array-repeat(value, count). The shared pass evaluates an ordinary value expression once, including when the count is zero, and copies that value into an array. It preserves surrounding argument order, short-circuit branches, and repeated while-condition evaluation. Generated bindings avoid source names. Empty arrays retain the element type for checking unreachable loop bodies.
Counts currently support checked integer constant arithmetic and immutable integer globals. Expansion is bounded to 4096 items; backend frame and element-layout limits still apply. Unresolved, negative, and larger counts are refused. Inline constants must first resolve under the bounded constant evaluator described above. Inferred counts, general const-function evaluation, and Rust Copy trait checking remain outside this subset.
.in collection loops use the same lowering as Rust collection loops, alongside existing bounded and open ranges. conformance/control-flow/array-repeat-effects.in covers single evaluation, zero length, short-circuit effects, and conditions reevaluated after continue.
Fields of produced struct values
x86 global scalar struct fields use the same writable data-section slots for reads and writes. Local bindings shadow global field paths. Both native backends copy, return, and pass complete global structs with scalar fields using the existing internal struct ABI. Whole-global assignments stage the complete source before committing fields, including constructors, local or global values, function results, and zero reset. Assignment respects local and parameter shadowing.
Constant folding projects a constructor field only when every initializer is a scalar literal. Calls, reads, and possible failures in unselected fields remain in the IR; unsupported runtime global initializers stay refused.
Field reads from calls and struct constructors materialize their base once through shared operand spills. This evaluates every constructor field, preserves surrounding operand order, and keeps producers inside short-circuit branches. Native backends read the resulting local struct through their existing field paths. x86 hosted 64-bit results with scalar fields use owned heap storage and an internal data-pointer ABI; bindings and assignments copy all fields into their local layout. JIT storage lives in the runtime arena, and native storage uses the existing process-lifetime allocation policy.
Call argument values and order
x86 stages arguments in source order before loading ABI registers and outgoing stack slots. Known scalar-field struct value parameters capture their fields before later arguments run; receiver parameters retain their existing pointer path. Incoming stack arguments account for Harden's extra saved register. AArch64 evaluates producers of empty struct arguments even when the internal ABI assigns them no words. Nested constructor fields use the existing flattened slots recursively, including calls returning a nested scalar-field value.
Receiver addresses and argument positions
AArch64 named self parameters occupy one pointer word regardless of field count; the callee saves that pointer separately from its local field copies. Register and incoming stack arguments are saved before reading receiver fields, and null/failure sentinels return failure before reads. Supported global receiver layouts use their data-section addresses. x86 empty locals reserve stable address storage separately from value fields, so their receiver addresses do not add an ABI value word or trigger a fake field copy. Scalar receiver field reads and writes, forwarding, and value copies use the saved object pointer; ordinary value parameters remain independent copies. Whole receiver replacements stage the source before committing fields. AArch64 also resolves nested receiver addresses and materializes nested struct field values; known nested aggregate replacements stage every field before stores. Shared lowering materializes call and constructor results once for known value-layout receivers, preserving source argument order and conditional execution. Addressable receiver and field-assignment bases retain their identity when later operands spill. .in chained calls keep the terminal call. Scalar vectors support construction, growth, extension, cloning, capacity queries, and clearing; borrowed headers and aggregate elements still require additional ownership and drop semantics.
Writable array literal results
AArch64 materializes array literal results in writable storage instead of returning pointers into read-only code. Elements can be dynamic and retain their evaluation order; a saved cursor survives element calls. JIT storage belongs to the runtime arena, while native hosted artifacts use the existing process-lifetime heap policy. The internal AArch64 data-pointer/length array ABI remains unchanged. Array-returning functions initialize local literal arrays in owned storage, so returns through aliases and other calls survive frame release. Inline arrays materialized elsewhere copy their current elements into owned storage. Direct indexing of literals evaluates elements before the index and uses the same pointer/length bounds checks as call results. Direct literal and call-result array arguments use this same materializer; argument staging preserves earlier values across later calls.
For the native shared-array representation, variable assignment replaces that binding; earlier aliases keep their original backing storage. Element mutation remains visible through aliases to that storage. AArch64 uses pointer/length bindings for local alias groups containing a reassignment, while unrelated nonescaping literal arrays retain inline storage. Rebinding can select an array of a different length without changing the earlier object.
Flat record arrays
The 64-bit AArch64 and x86 native backends support fixed arrays of known, nonempty records whose direct fields are Int, Bool, Float, or String. Each element slot points to its own declaration-ordered payload. Repeating one record value in a literal produces independent elements; copying an indexed record produces an independent value. Array aliases share element mutations. Whole-element replacement stages every field before copying into the existing payload, preserving receiver addresses and leaving the outer replacement uncommitted if its RHS throws. Explicit RHS side effects remain observable.
Indexed field reads and writes, record value arguments and results, array parameter forwarding, indexed self receivers, and hosted array returns use this typed layout. Base and index are evaluated once before a store's RHS; bounds and payload checks precede the final store. Static arrays retain ordinary data pointer and String relocations. Hosted payloads follow the existing JIT-arena or process-lifetime heap policy. AArch64 plans simultaneous nested staging before reserving its frame; sequential operations reuse that space.
Nested record fields, empty or unknown schemas, record fields containing arrays or vectors, nested record-array containers, and 32-bit record arrays remain unsupported. Whole-array global reassignment retains its existing refusal. Vector aggregate storage has a separate layout; record-array slots cannot be used as scalar values, raw vector iterator elements, or aggregate vector-extension storage. Existing frame and argument ABI limits still apply.
Array bounds failures
AArch64 and x86 native array reads and writes return 1 from the current function on a negative or out-of-range index. They preserve effects evaluated before the bounds check and skip later statements. Raw integer-pointer indexing remains unchecked. This is the existing native failure-return convention; callers receive the result and can continue.
Indexed compound update order
Assignment scans share the character-aware operator scanner with expressions, respecting nested delimiters, escaped strings, and Unicode identifiers. Comparisons inside an assignment RHS retain their expression meaning.
.in supports &=, |=, and ^= for Int and Bool values through the existing bitwise IR operations. Boolean updates evaluate their RHS eagerly.
.in indexed compound assignments share the indexed-store spill machinery. The array/base, index, and old element are saved once before RHS effects, then the update stores through the saved place. This retains .in read-before-RHS order while eliminating duplicate index evaluation and unsupported call operands. Rust's value-before-place order remains distinct in the same lowering path.
Harden dispatcher scopes
Control-flow dispatcher states keep a local declaration with its last read or write, preserving lexical scope without moving initialization effects. A single scan computes the use boundaries, and the synthetic program counter avoids existing names. Hardened programs retain valid bindings across assignments and returns. State dispatch uses a balanced decision tree with exact state guards, keeping synthetic nesting logarithmic for wide function bodies.
Signed shifts
Canonical Int64 >> and >>= preserve the sign bit at runtime, matching shared constant folding and C projection. .in shift compound assignments use the existing assignment desugaring, retaining the complete multi-character operator. Explicit logical-shift instruction encoders remain available for bit-oriented runtime helpers. Represented C/C++ and Rust integer widths are retained during source normalization; other frontends still use their existing canonical integer carriers.
Floating width casts
Rust as f32 and as f64 preserve explicit precision conversion for canonical floating values. f32 conversion rounds to IEEE single precision, then widens into the internal Float64 word; f64 conversion preserves that word. Both native CPUs and C projection use this representation. Rust f64 annotations normalize to the canonical floating type, and explicit f32/f64 literal suffixes retain their precision, including decimal literals without a point. Overflowing source literals remain refused; floating-width conversion itself can produce infinity or subnormal values.
Owned Rust floating bindings and represented arithmetic retain their source precision until normalization erases the private width metadata. The shared width operations require canonical Float operands rather than guessing the representation of named types.
Saturating floating-to-integer casts
Rust numeric casts retain their target until typed normalization can resolve the source, including fields, indexed vector values, and function results. Canonical floating operands cast to signed or unsigned 8-, 16-, 32-, or 64-bit integers use float-saturate-* unary IR operations. They truncate toward zero, clamp to the destination range, and map NaN to zero. Unsigned 64-bit results retain their bits in the existing Int64 word. The operand is evaluated once. Constant folding, C projection, and the AArch64/x86_64 native emitters share these semantics; 32-bit x86 emission remains refused.
Raw integer-width operations still require integral operands. C's existing float-to-int operation retains its finite, in-range contract. Complete Rust overflow/panic semantics remain outside this conversion contract.
Rust associated function lookup
Concrete source-owned calls such as Factory::value() resolve against the receiver's nominal identity and the caller's visible traits before IR function facts are collected. Inherent methods take priority. A unique applicable trait implementation retains its exact receiver/trait namespace, including through receiver type aliases and trait import aliases. Trait positions reject type aliases and require a declared trait member; competing visible candidates are rejected rather than selected by name or iteration order. Known-false cfg methods are omitted.
Conditional applicability, unrepresented generic implementations, local ownership, trait defaults without emitted implementations and incomplete source catalogs remain explicit refusal boundaries. Concrete explicit UFCS resolves the owned receiver/trait pair to the same emitted implementation namespace without requiring a trait import.
Integer-to-floating casts
Rust integer-to-floating casts retain the source signedness and destination precision. Canonical int-to-float / uint-to-float produce Float64; int-to-float-f32 / uint-to-float-f32 round directly from the signed or unsigned 64-bit integer to Float32 before widening into the Float64 carrier. This avoids double rounding through Float64. Unsigned sources retain their complete word bits. These operations require resolved integer operands and evaluate them once.
Floating bit conversions
Rust primitive f32 / f64 from_bits and to_bits methods lower to four unary operations:
| Operation | Input | Result |
|---|---|---|
float-to-bits-f32 | Float64 carrier | zero-extended Float32 bits in Int |
bits-to-float-f32 | low 32 bits of Int | Float32 value in the Float64 carrier |
float-to-bits-f64 | Float | identical raw bits in Int |
bits-to-float-f64 | Int | identical raw bits in Float |
Float32 NaNs embed their sign and fraction bits directly in the Float64 carrier. Bit conversions and source-value transfers preserve the quiet/signaling bit and payload without numeric NaN conversion. Transfers compose the two Float32 bit operations, evaluating the source once. Same-width Rust casts preserve the value; cross-width numeric casts promise NaN classification rather than payload identity.
from_bits requires the exact Rust unsigned primitive (u32 or u64), with contextual inference for unsuffixed integers. It does not coerce signed integers, a different width, or usize. to_bits retains its exact unsigned result type until integer normalization. Proven reference receivers support dot-method autodereference; by-value associated calls retain their ordinary argument contract. Nominal methods and shadowed primitive paths do not acquire primitive behavior.
Constant folding, bounded static evaluation, scalar/array/flat-record global data and C projection preserve raw bits. Non-finite constants remain integer bit constructors rather than non-finite Float literals. Native runtime conversions use the existing 64-bit carriers; unsupported target layouts remain refused. C projection uses alias-safe bit copies, exact finite hexadecimal constants and explicit infinity/NaN constants. Source identifiers that collide with compiler builtins are escaped consistently across declarations and references.
Rust integer casts
Direct unsuffixed integer literals infer their source kind from an integer cast, including unary operators and block result edges. Out-of-range or unsigned-negative sources are rejected before execution. Independently inferred bindings and arithmetic keep their own source types. Explicitly typed sources retain ordinary narrowing behavior.
Explicit Rust casts to signed and unsigned 8-, 16-, 32-, and 64-bit integers truncate low bits and then sign- or zero-extend into the canonical Int64 word. Boolean operands become zero or one. isize/usize resolve to the selected target word. Shared bit operations implement these conversions for both native backends and C projection, with one evaluation of the operand. Rust integer declarations and suffixed literals retain their exact widths through contextual literal inference, calls, fields, and container values. Unsigned division, remainder, comparisons, and right shifts use the shared integer operations; narrow bitwise and shift results retain their source width. Thumb supports the represented 8-, 16-, and 32-bit casts and refuses wider values. Thumb binary scratch storage follows the transformed expression depth, including cast expansion and Harden expressions; lazy operators do not reserve an extra pair. Functions exceeding the existing 508-byte local-frame encoding remain refused. Primitive wrapping_add, wrapping_sub, and wrapping_mul use the same width-preserving operations at runtime and in bounded constant evaluation, after user-defined method resolution. Constant overflow in ordinary arithmetic and invalid constant shift counts refuse before width conversion; dynamic overflow/panic behavior remains incomplete. Existing untyped runtime builtins retain their canonical integer compatibility path.
C-style numeric casts
C/C++ (type)value casts to primitive numeric types lower through shared typed conversions. Explicit signed/unsigned char, short, and int casts retain their 8-, 16-, and 32-bit destinations on supported targets. Unsigned conversion keeps the low bits; signed conversion sign-extends those bits using the targets' two's-complement policy. Nested casts, boolean truth conversion, qualifiers, and comments inside type spellings preserve operand evaluation once. Floating operands truncate toward zero before integer conversion; only finite values whose truncated result fits the destination are within the defined C conversion subset.
The result uses the canonical target-word carrier. Plain char signedness and target-dependent long widths remain incomplete. Unsigned 32-bit comparisons and integer arithmetic preserve unsigned bits on both supported word sizes; unsigned-to-floating conversion on 32-bit carriers remains incomplete. Pointer, reference, object, typedef, void, and extended-precision casts remain refused.
C-like numeric conversions
Fixed-width C/C++ scalar declarations preserve signed/unsigned 8-, 16-, and 32-bit destinations until typed normalization inserts conversions. Global and local initialization, assignment, function arguments and returns, and scalar field initialization/stores use their destination width. Desugared compound assignments and increments use the same assignment conversion. Explicit and implicit conversions share single-evaluation mask/sign-extension lowering. A scope-aware type traversal then erases the temporary width aliases from signatures, schemas, local annotations, and nested expressions before native emission.
C/C++ expressions promote signed/unsigned byte and short operands to signed int, and combine represented signed/unsigned int operands using unsigned 32-bit arithmetic when required. Addition, subtraction, multiplication, bitwise operations, unary negation/complement, and left shifts retain the low 32 bits of unsigned results. Comparisons use unsigned ordering; division, remainder, and right shift use shared unsigned Core IR operations. Literal radix and U suffix preserve provable 32-bit kinds, including through signs and generated temporaries. Mixed unsigned expressions involving unrepresented literal ranks or long widths are refused rather than assigned an inferred rank.
Core IR binary udiv, urem, and lshr operate on unsigned target-word bits while returning the canonical signed carrier. AArch64, x86, and Thumb use unsigned division/logical-shift instructions; division and remainder retain existing checked zero-divisor failure behavior. Folding uses the target word width and does not fold zero divisors or out-of-range shift counts. Native target-word <<, >>, and lshr mask runtime counts to the word size (32 bits for Thumb/i386 and 64 bits for AArch64/x86_64). Source-language shift rules and the refusal to fold out-of-range counts remain unchanged. The C projection uses unsigned 64-bit carriers and ordered operand staging. Checked unsigned division/remainder preserve current-function failure exits for Int, Bool, and Void returns; other return ABIs are explicitly refused. Unsigned global initializers must fold to constants before C emission. Rust/Go source projections explicitly refuse these operators.
Storage and call carriers retain the existing target-word layout; this does not establish packed C layouts or an external C ABI. Mixed floating arithmetic and comparisons promote integer and boolean operands to Float64. Floating-to-integer conversion truncates toward zero; only finite values whose truncated result fits the destination are within the defined C subset. Wider integer ranks, unsigned-to-floating conversions on 32-bit carriers, implicit array-element narrowing, target-dependent declaration widths, and Float32 arithmetic remain incomplete.
C-like post-test loops
do loops lower through the shared loop representation with a hygienic first-iteration flag. The flag skips the initial condition check; later loop headers evaluate the condition after the preceding body, including after continue. The body retains its own scope so local shadowing cannot change which binding the condition reads. Breaks and returns skip the next condition check, and nested-loop continues keep their original target.
C++ while declaration conditions
A while declaration condition lowers to a declaration at the start of every loop iteration, followed by the converted condition and a break on false. Its binding remains scoped to the body, and continue reevaluates the initializer. The existing shared loop and boolean lowering handle returns and breaks. Declaration conditions in for headers remain unavailable in the installed C++ grammar.
C-like decimal floating literals
Decimal floating literals, exponent forms, and valid digit separators lower to canonical Float. An f suffix rounds the literal as an f32 value before widening into the existing f64 representation. Shared constant folding handles literal floating comparisons with NaN and signed-zero semantics, including static boolean initializers. Hexadecimal floating literals, long-double suffixes, non-finite overflow literals, and full source-width arithmetic remain unsupported. Ordinary literals borrow their digit text; only separators require a copy.
AArch64 argument staging
Known native calls save every ABI argument word in the reserved caller frame before evaluating later arguments. Register arguments are reloaded only after evaluation completes, and stack arguments are copied from those saved words into a 16-byte-aligned outgoing area. Scalar arguments and flattened struct words share this staging, including the 32-word call limit. Nested calls reserve disjoint portions of the existing 64-word temporary pool and refuse exhausted capacity rather than reusing live argument slots.
C-like scalar boolean conversion
C-like boolean declarations retain Bool in Core IR. Typed normalization converts numeric conditions and logical operands with value != 0, preserving single evaluation and the conditional right operand of && and ||. The same conversion handles numeric values stored or passed as booleans; boolean arithmetic and integer destinations use explicit bool-to-int promotion. Function returns, arguments, locals, globals, and field or index stores use their expected types. Global types participate in normal primitive normalization. C++ if initializers and declaration conditions preserve their evaluation and scoped bindings before conversion. C type keywords are matched as keywords, preserving nominal names. Unsupported named boolean conversions and reference values are refused. Other frontends retain strict boolean operands; pointer, object, and general numeric casts remain outside this conversion.
AArch64 assignments reuse scalar parameter slots instead of growing the frame after reservation. Calls align outgoing stack arguments to 16 bytes, including odd numbers of argument words.
C-like local declarations
Every initialized identifier in a local C-like declaration lowers to an ordered Stmt::Let, using the declaration's common type. Later initializers can read earlier bindings, and unused bindings retain initializer side effects. The same extraction handles multiple bindings in for initializers while preserving their loop scope. Uninitialized, pointer, and aggregate declarators remain refused; they are not silently discarded when another declarator is supported.
C-like integer literals
C-like integer literals retain hexadecimal digits and use their source radix: hexadecimal, binary, octal, or decimal. Traditional U/L suffix combinations are validated separately, and digit separators are accepted only between valid digits. The frontend still uses canonical signed 64-bit Int; suffixes do not yet preserve source width or unsigned arithmetic. Values outside that representation, unsupported suffixes, and malformed literal forms are refused instead of truncated or reinterpreted.
Value-first variable updates
The reserved statement operation modify-value-first(target, value, operator) lowers through the shared value-spill pass. It evaluates value once into a hygienic temporary, then reads and updates the original identifier target. C-like compound assignments use this operation, including loop updates, so a right operand that modifies the target cannot leave a stale value in the calculation. Invalid operation shapes remain explicit refusals.
C-like global storage
Top-level C and C++ variable definitions use Decl::Global and the existing shared native data section. Integer literal definitions, zero initialization, multiple identifier declarators, and static storage preserve reads and writes across functions. const definitions retain immutable storage. External declarations do not fabricate module-owned storage; unresolved external variable reads are refused. Pointer, array, and reference declarators are currently refused, and runtime initializers retain the native backend's existing refusal.
C-like counted loops
C and C++ for statements lower to a scoped initializer and a Core IR loop. The condition is reevaluated before each iteration, and the update executes after the body or before a continue targeting that loop. Nested-loop continues retain their own target; break and return skip the update. Omitted conditions form an unbounded loop. Identifier increment, decrement, and compound assignment updates retain their stores. Unsupported initializer, condition, or update expressions produce an explicit unsupported operation instead of dropping the loop.
Boolean bitwise operations
Core IR &, |, and ^ preserve the operand type: two Bool operands produce Bool, and two Int operands produce Int. Mixed types and floating operands are refused. Boolean bitwise operations evaluate both operands in order; && and || retain conditional evaluation. Verification, native type inference, and constant folding share this distinction. C-like frontends insert explicit bool-to-int promotions where bitwise operands or integer call parameters require integer values.
Guarded matches
Stmt::Match arms contain a pattern, an optional guard: Expr, and a body. The subject is evaluated once. A guard runs only when its pattern matches; a false guard continues to the next arm. The first successful arm runs its body, and later guards and bodies are skipped. Guard conditions must be boolean.
Rust preserves pattern if condition => value. .in accepts pattern if condition { statements }. Version 2/3 .icore accepts a match statement with scrutinee and arms; each arm has a string pattern, an optional expression guard, and a statement-array body. Expression operands use the parser's lhs/rhs fields. See the JSON fixture.
Shared lowering saves the subject and uses one completion flag with one conditional gate per arm, avoiding duplication of the remaining arms. Generated names avoid user globals, parameters, bindings, and captures. Native and C emitters also prepare raw guarded Core IR through this lowering.
Boundary IR (icoreVersion: 3)
Rust types: in_cli::boundary_ir::BoundaryModule, CompileArtifact.
| Field | Meaning |
|---|---|
abi_version | Must match IN_ABI_VERSION in shared/abi/in_abi.h. |
module | Stable boundary module id (for example sample.person). |
layouts | Struct/enum layouts with C repr, size, align, stride, and per-field transfer mode. |
symbols | Exported ABI symbols with ownership and calling convention metadata. |
allocators | Allocator ids (host_arena, module heap) used by buffers and borrow tokens. |
layout_hash | Canonical blake3-… hash over layouts + symbols; empty means compute on ingest. |
compiler::icore::parse_icore_artifact verifies boundary modules before lowering. Level-5 language fronts may attach the same BoundaryModule through dedicated extractors (Nim/Odin comment JSON today; Rust/Zig extractors planned) without expanding Core IR statement forms.
ParserId (extensible)
| Id | Source | Entry |
|---|---|---|
In | .in files (and #!in parser=in) | in_lang_parse → UnifiedModule → compiler::driver / lower_core; parser-side surface facts feed agent effects / capabilities |
Icore | .icore files (and #!in parser=icore) | compiler::icore v1 declarations, v2 bounded body JSON, or v3 Boundary IR (boundary section + optional semantic decls) → CompileArtifact → same lowering |
swift, go, ocaml, haskell, c, cpp, java, python, … | Known extensions or #!in parser=<slug> | Tree-sitter polyglot — compiler::tree_front grammar-backed AST → UnifiedModule. All 37 languages share the same pipeline. |
Resolution order for in build
See in-cli/src/parser_registry.rs (resolve_parser_id) and parser-surface.md. In short:
--parser in/--parser icore— force the.inor.icorefront.- Magic first line —
#!in parser=in|auto|<slug>. IN_PARSER=in/IN_PARSER=icore.- Extension map —
.in,.icore,.java,.cpp, … → Core IR path (full parsers for.in/.icore; Tree-sitter for other wired extensions). - Unknown extensions: fail closed with an
.icorehint.
All 37 languages now route through Core IR. Swift was unified into the Tree-sitter pipeline (the old swift_subset/native_swift_sil/sil_emit modules have been deleted).
Related
-
general-compiler.md — multi-language driver, icore, roadmap.
-
parser-surface.md — extension + magic-line routing, Tree-sitter vs full fronts.
-
in-language.md —
.inv0.2 grammar andhybrid_silnote. -
README · Core Commands — CLI flags and sample commands (no duplicate install steps here).
Local .in file imports retain canonical file identity until merging completes. Shared dependencies and path aliases contribute declarations once, in deterministic import order; cycles terminate across parallel parser workers. Distinct files with colliding declaration names still fail validation.
The public JIT accepts macOS AArch64 and x86_64 hosts independently of native artifact support. Hosted x86 JIT calls resolve explicit bodyless stdlib declarations through the same wrapper table as undeclared builtin spellings; user-defined function bodies retain precedence. Native emission retains external import records.
Stored scalar and String globals may initialize from earlier compatible globals in declaration order. Each initializer takes an initial-value snapshot in independent storage; later assignments do not change the copied global. Type inference can resolve forward names, but stored forward reads, cycles, runtime calls, and aggregate-reference initializers remain refused. Constants excluded from runtime storage must be folded before native data emission.
AArch64 and x86 native data emission also accepts named global struct constructors with flat Int, Bool, Float, and String fields. Fields occupy schema-order slots regardless of constructor order, and String fields use the existing deferred payload relocations. Field values follow the scalar initializer rules above, including snapshots of earlier compatible globals. Missing, duplicate, unknown, or mistyped fields, nested aggregates, and runtime calls remain refused. Direct field reads and writes need no aggregate call or allocation; passing or returning whole structs retains the target's existing ABI and runtime requirements.
Package binding materialization preserves explicit zero-argument signatures, including their return types, when a symbol shares a name with a standard-library function. Legacy named imports without signature metadata retain their catalog ABI.
Both native backends validate cataloged scalar stdlib calls against the selected runtime wrapper before staging arguments. Explicit declarations must match the catalog ABI, actual arguments must match its types and arity, and ARM wrapper staging rejects more than eight arguments. Defined functions retain precedence over wrapper and inline-builtin matching; reserved atomic names retain their redeclaration checks.
Known underscore-to-kebab function references normalize before optimization and in direct backend entry paths. Exact declarations and lexical bindings retain precedence, keeping reachability, Harden symbol rewriting, ABI checks, and calls on one function identity. Pattern bindings accept underscores while a lone _ remains a wildcard.
Stored String arrays accept literal elements, including nested literal arrays. Element pointers use the same deferred payload relocation as scalar Strings, while nested array headers retain relative offsets. Runtime calls and stored-global references inside array initializers remain refused.
Catch patterns and for iterator headers locate their bodies with the shared quote-aware scanner. Braces and escaped quotes in String literals survive parsing and canonical roundtrips, including multiple catch arms and function-call iterators.
Component metadata
Native objects and SCI images emit the same canonical .component-metadata.json sidecar when the source declares a component. It preserves named capabilities and operations, imports, exports, target, and checkpoint metadata. Replacing a component artifact with a component-free module through these emission paths removes the old generated sidecar. SCI header capability bits retain their existing ordinal encoding; platform packagers apply their own policy to the named declarations.