Skip to content

IR Reference ​

This document is the authoritative reference for the Ashes intermediate representation (IR). The IR is a flat, register-based instruction set defined in Ashes.Semantics/Ir.cs. The Lowering pass converts the typed AST into an IrProgram, which the LLVM backend consumes.


Program Structure ​

IrProgram ​

The root container for a compiled Ashes program:

FieldTypeDescription
EntryFunctionIrFunctionTop-level expression (_start_main)
FunctionsList<IrFunction>Lifted lambdas and named functions
StringLiteralsList<IrStringLiteral>All string constants with labels
ExternalFunctionsList<IrExternalFunction>Validated external declarations used by lowering and linking
ExternalOpaqueTypesSet<string>Opaque native types referenced by the program
UsesPrintIntboolWhether PrintInt is used
UsesPrintStrboolWhether PrintStr is used
UsesPrintBoolboolWhether PrintBool is used
UsesConcatStrboolWhether ConcatStr is used
UsesClosuresboolWhether closures are created
UsesAsyncboolWhether async/await is used
CapabilityHandlerGlobalsintNumber of declared capabilities (one handler-evidence global each)
TraitEvidenceTraitEvidenceAnnotationsStable dictionary-ABI and concrete-resolution facts for reports

The Uses* flags allow the backend to omit unused runtime helpers.

IrFunction ​

A single function with a flat instruction list:

FieldTypeDescription
LabelstringUnique name (e.g., _start_main, lambda_0)
InstructionsList<IrInst>Linear instruction sequence
LocalCountintNumber of local variable stack slots
TempCountintNumber of temporary registers
HasEnvAndArgParamsbooltrue for lambdas (implicit env+arg at slots 0, 1)
CoroutineCoroutineInfo?Non-null for async coroutine functions
LocalNamesIReadOnlyDictionary<int, string>?Optional source names for local slots
LocalTypesIReadOnlyDictionary<int, TypeRef>?Optional inferred types for local slots
OriginIrFunctionOrigin?Stable source/generated lineage for compiler reporting
LifetimesPlacedbooltrue once lifetime markers are placed; the program-wide pass skips these

Function origin metadata ​

Production lowering assigns an IrFunctionOrigin to the entry function and every lowered or synthesized IrFunction. This is immutable reporting metadata, not part of execution semantics:

  • GeneratedLabel is the unique emitted IR label.
  • Kind is a typed IrFunctionOriginKind, distinguishing source functions, closure helpers, reuse and parallel specializations, mutual-recursion dispatchers/wrappers, coroutines, external thunks, closure-environment normalizers, structural droppers, and deep-copy helpers.
  • Source, when present, is a SourceFunctionOrigin containing the declaration's source name, module-qualified name where known, declaration location, and deterministic combined-source offset.
  • ParentGeneratedLabel links a generated artifact to its immediate generated parent. Source-derived artifacts retain the same Source identity.
  • CompilerOwner gives shared artifacts without one source-function parent a typed program, type, external, runtime-layout, or mutual-recursion-group owner.
  • StableDiscriminator and GenerationLocation distinguish multiple generated artifacts of the same kind without making callers parse label suffixes.

FunctionOwnershipSummary uses the same SourceFunctionOrigin boundary, while its internal analysis lookup remains keyed by binder identity. This keeps source and qualified-name filtering independent of compiler-generated labels.

IR rewrites preserve Origin through record copies, including the optimizer and Perceus lifetime placement. The LLVM backend deliberately ignores the metadata, so adding or retaining origins does not alter generated code. Manually constructed IR used by tests or embedding callers may leave Origin unset.

CoroutineInfo ​

Metadata for coroutine functions generated from async blocks:

FieldTypeDescription
StateCountintNumber of states (N await points = N+1 states)
StateStructSizeintTotal size of the task/state struct in bytes
CaptureCountintNumber of captured environment variables

The entry function has HasEnvAndArgParams: false. Lambda functions have true, meaning slot 0 holds the closure environment pointer and slot 1 holds the argument.

IrStringLiteral ​

Maps a label to a string constant:

FieldTypeDescription
LabelstringReference name (e.g., str_0)
ValuestringThe string content

Referenced by LoadConstStr. The backend emits these as read-only globals using the ordinary String payload layout with the view bit set.

Text dumps and function selection ​

--emit-ir lowered and --emit-ir final use the same deterministic text shape. Lifted functions are rendered in their existing IrProgram.Functions order, followed by EntryFunction; no label sort or instruction ordinal is introduced. A function filter is an ordinal, ASCII-case-insensitive substring match over the emitted label and, when origin metadata exists, its generated label, source name, qualified source name, and immediate generated-parent label. An absent filter selects every function.

Each ordinary instruction prints its opcode in a 22-column field followed by its meaningful operands. Null, false, empty-string, and optional integer -1 values are omitted, while zero and long -1 remain visible. Collections print their stable element count rather than their contents. Source locations use path:line:column; labels are left-aligned as control-flow anchors. Trait dictionary and resolution annotations precede the functions in their stored order. The pure-Ashes implementation models this format exhaustively so its output does not depend on runtime reflection.


Registers and Locals ​

Instructions use integer indices to address values:

  • Temporaries (Target, Source, Left, Right) — virtual registers allocated per-function by NewTemp().
  • Locals (Slot) — stack slots allocated by NewLocal() for named bindings.

Each instruction that produces a value writes to a Target temporary. Each instruction that consumes values reads from Source, Left, Right, or named parameter temporaries.

Every instruction also carries an optional source Location for debug information. It is immutable metadata attached before the instruction enters a function and does not affect execution semantics.


Instruction Reference ​

Constants ​

InstructionFieldsDescription
LoadConstIntTarget, Value: longLoad integer literal
LoadConstFloatTarget, Value: doubleLoad floating-point literal
LoadConstBoolTarget, Value: boolLoad boolean literal
LoadConstStrTarget, StrLabel: stringLoad string literal by label
LoadProgramArgsTargetLoad command-line arguments list

Local Variables ​

InstructionFieldsDescription
LoadLocalTarget, SlotLoad value from local stack slot
StoreLocalSlot, SourceStore value into local stack slot

Environment and Memory ​

InstructionFieldsDescription
LoadEnvTarget, IndexLoad captured value from closure environment
StoreMemOffsetBasePtr, OffsetBytes, SourceStore value at [base + offset]
LoadMemOffsetTarget, BasePtr, OffsetBytesLoad value from [base + offset]
AllocTarget, SizeBytes, RuntimeManagedAllocate a raw payload in the scoped arena or, when runtime-managed, behind an RC header
SaveArenaStatecursor/end slotsRecord a scoped-region watermark
RestoreArenaStatecursor/end/pre-restore slotsReset to a saved watermark
ReclaimArenaChunkssaved/pre-restore end slotsReturn abandoned region chunks

Ownership and Lifetime ​

InstructionFieldsDescription
BorrowTarget, SourceTempCreate a non-owning compiler-tracked alias
RcDupTarget, SourceTemp, RuntimeManaged, MayBeEmptySplit ownership; increments the count for an RC value
RcDropSourceTemp, TypeName, OwnerSlot, RuntimeManaged, MayBeEmptyEnd one ordinary ownership path; runtime-managed forms perform type-directed RC release
RcIsUniqueTarget, SourceTempTest whether an RC value has count 1
IsReferenceCountedTarget, SourceTempTest whether a value of statically unknown representation lies in the reference-counted heap
CleanupResourceSourceTemp, TypeNameDeterministically close/reap a language resource; distinct from ordinary RC

PerceusLifetimePlacement consumes the OwnerSlot provenance on lexical anchors and places drops after last use or at dead branch entry. Constructor, match, closure, and TCO lowering emit additional shape-aware ownership operations. RuntimeManaged: false marks a compiler fact used by a scoped or specialized region; it is not an instruction to read an RC header.

MayBeEmpty records that the value's resolved type admits the empty-list representation, which is the null pointer and carries no reference-count header.

IsReferenceCounted asks about an address, not a header, so any word is a valid source and an empty, scalar, static, stack or arena value answers 0. A runtime whose reference-counted blocks do not live in one reserved region answers 0 for everything, which is why every consumer of the test must be written to copy when the answer is 0, exactly as it did before the test existed. Codegen then skips the count update instead of reading a header 16 bytes below address zero. Lowering computes the fact from the resolved type at the one place a marker is promoted to runtime RC; codegen never re-derives it, and the duplicate stays identity-preserving so an empty value is its own result.

Integer Arithmetic ​

InstructionFieldsDescription
AddIntTarget, Left, RightTarget = Left + Right
SubIntTarget, Left, RightTarget = Left - Right
MulIntTarget, Left, RightTarget = Left * Right
DivIntTarget, Left, RightTarget = Left / Right

Float Arithmetic ​

InstructionFieldsDescription
AddFloatTarget, Left, RightTarget = Left + Right
SubFloatTarget, Left, RightTarget = Left - Right
MulFloatTarget, Left, RightTarget = Left * Right
DivFloatTarget, Left, RightTarget = Left / Right

Integer Comparisons ​

InstructionFieldsDescription
CmpIntGeTarget, Left, RightTarget = (Left >= Right) ? 1 : 0
CmpIntLeTarget, Left, RightTarget = (Left <= Right) ? 1 : 0
CmpIntEqTarget, Left, RightTarget = (Left == Right) ? 1 : 0
CmpIntNeTarget, Left, RightTarget = (Left != Right) ? 1 : 0

Float Comparisons ​

InstructionFieldsDescription
CmpFloatGeTarget, Left, RightTarget = (Left >= Right) ? 1 : 0
CmpFloatLeTarget, Left, RightTarget = (Left <= Right) ? 1 : 0
CmpFloatEqTarget, Left, RightTarget = (Left == Right) ? 1 : 0
CmpFloatNeTarget, Left, RightTarget = (Left != Right) ? 1 : 0

String Comparisons ​

InstructionFieldsDescription
CmpStrEqTarget, Left, RightTarget = (Left == Right) ? 1 : 0
CmpStrNeTarget, Left, RightTarget = (Left != Right) ? 1 : 0

String Operations ​

InstructionFieldsDescription
ConcatStrTarget, Left, Right, RuntimeManagedTarget = Left ++ Right; the flag selects arena or RC allocation
ConcatStrTipTarget, Left, Right, reservation slots, RuntimeManagedAffine string append with geometric headroom; the RC form consumes Left

Closures ​

InstructionFieldsDescription
MakeClosureTarget, FuncLabel, EnvPtrTemp, EnvSizeBytes, ownership flagsAllocate a closure payload, optionally behind an RC header
CallClosureTarget, ClosureTemp, ArgTemp, RuntimeManagedArgumentFlagTempCall closure with an optional hidden ownership word
CallKnownTarget, FuncLabel, EnvTemp, ArgTemp, ownership word, EnvironmentIsStackAllocatedDevirtualized closure call with explicit environment lifetime provenance
LoadArgumentOwnershipTargetRead the hidden ownership word the caller passed

A closure payload is 32 bytes: [code, env, packed_env_size_and_ownership, dropper]. The packed word uses bit 63 for runtime-managed result ownership, bit 62 for RC-argument adoption, and the low 62 bits for the environment size.

Every lifted function takes a hidden third parameter, the ownership word, which a call passes through RuntimeManagedArgumentFlagTemp (zero when the call passes none). Bit 0 set means the caller transferred a retained runtime-managed argument, which an RC-normalizing entry adopts instead of copying. Bit 1 set means the caller cannot own a runtime-managed result: a generic body applying a closure parameter has no static layout for the result and its own caller deep-copies the whole result out later, so a callee whose result is reference-counted deep-copies it into the arena (ArenaResultBoundary) and releases the original before returning. The dropper releases moved resources or RC captures. Supported captured ordinary graphs also have code-label metadata for normalizing the complete environment when a closure crosses into RC ownership. CallClosure loads the code and environment pointers and calls code(env, arg, owns_arg). A normalizing direct-parameter entry adopts a transferred RC root when owns_arg is set and otherwise performs the defensive arena-to-RC graph copy. The caller retains a non-fresh root before transfer; fresh owned results can transfer their existing reference. Curried parameters captured in closure environments cannot consume this direct-argument flag. Indirect closure calls are native notail calls because their environment may live in the current frame. A devirtualized CallKnown is eligible for a native tail call only when EnvironmentIsStackAllocated is false; recursive source-level TCO remains the explicit IR back-edge transformation and does not depend on this backend optimization.

Algebraic Data Types (ADTs) ​

InstructionFieldsDescription
AllocAdtTarget, Tag, FieldCount, RuntimeManagedAllocate ADT payload [tag, fields...], optionally behind an RC header
DropReuseTarget, SourceTemp, FieldCount, RuntimeManagedConsume a dead cell into a compatible reuse token, or return null after decrementing a shared RC cell
AllocReusingTarget, Tag, FieldCount, TokenTemp, RuntimeManaged, ListCellOverwrite a compatible tagged ADT or untagged list-cell token; runtime null falls back to fresh RC allocation of the same layout
SetAdtFieldPtr, FieldIndex, Source*(Ptr + 8 + Index*8) = Source
GetAdtTagTarget, PtrTarget = *(Ptr + 0)
GetAdtFieldTarget, Ptr, FieldIndexTarget = *(Ptr + 8 + Index*8)

ADT values are heap-allocated cells. The first 8 bytes hold an integer tag identifying the variant. Each field occupies 8 bytes. Total size is (1 + FieldCount) * 8 bytes.

Graph Normalization and Region Copies ​

CopyOutArena, CopyOutList, CopyOutClosure, and CopyOutTcoListCell all carry a required CopyOutPurpose:

PurposeContract
RcNormalizationConstruct an independently owned RC graph
ArenaScopeBoundaryPreserve scheduler/capability state across a scope reset
ArenaCallBoundaryPreserve scheduler/capability state across a call reset
ArenaTcoCompactionPreserve live state at a region-managed TCO edge
IndependentCloneExplicit deep copy, worker publication, or reuse defense
ArenaResultBoundaryA reference-counted result deep-copied into the arena for a caller that cannot own it; the original is released

AllocAdtToSpace and CopyOutArenaToSpace are separate instructions for the persistent Map/HashMap specialization and do not represent general ordinary-value lifetime.

Foreign calls ​

InstructionFieldsDescription
ToCStringTarget, StrTempProduce a null-terminated pointer for a call-scoped Str argument
AllocFfiOutTarget, ElementTypeAllocate and null-initialize a non-escaping opaque/pointer output slot
CallExternalTarget, symbol/library, argument temps, parameter types, return typeInvoke a declared native function using its target ABI
LoadFfiOutTarget, SlotTemp, ElementTypeLoad an output slot exactly once after its external call
CopyFfiStringTarget, PointerTemp, StringTypeValidate and copy a native NUL-terminated UTF-8 string into an Ashes Result
CopyFfiBytesTarget, PointerTemp, LengthTempBounds-check and immediately copy a foreign byte range into Result(Str, Bytes)

AllocFfiOut produces the address passed at the corresponding FfiType.Out position. Lowering materializes each loaded null as None and each non-null value as Some(value); the slot address never becomes a source-language pointer or survives the direct call. CopyFfiString scans at most 1 GiB, validates UTF-8, and copies before returning. For owned contracts it invokes the validated destructor exactly once for every non-null pointer, including conversion failures; nullable contracts map null to Ok(None) and never dispose null. CopyFfiBytes accepts null only for a zero-length range, rejects lengths above 1 GiB before touching the pointer, and materializes an owned byte buffer before control returns to source code.

Immutable binary construction ​

InstructionFieldsDescription
BytesAllocateTarget, LengthTempAllocate a checked, zero-filled owned buffer
BytesCopyRangeTarget, destination/offset, source/offset, length, ownership flagsReplace one checked byte range
BytesSetTarget, bytes, offset, value, ownership flagsReplace one checked byte
BytesSetU16Le / BytesSetU32Le / BytesSetU64LeTarget, bytes, offset, value, ownership flagsPatch a checked little-endian field

Update lowering normalizes the destination to reference-counted ownership and marks only a freshly produced, unaliased update temporary as reusable. Code generation forwards and patches that storage; a named, borrowed, or otherwise potentially shared destination is copied first, preserving aliases.

Console I/O ​

InstructionFieldsDescription
PrintIntSourcePrint integer with newline to stdout
PrintStrSourcePrint string with newline to stdout
PrintBoolSourcePrint boolean with newline to stdout
WriteStrSourceWrite string to stdout (no newline)
WriteErrorStrSource, AppendNewlineWrite string to stderr
ExitProcessSourceTerminate with controlled exit code
ReadLineTargetRead line from stdin → Maybe<String>
PanicStrSourcePrint error message and terminate

ReadLine returns a Maybe<String> ADT: Some(line) on success, None on EOF.

File I/O ​

InstructionFieldsDescription
FileReadTextTarget, PathTempRead file → Result<String>
FileReadAllBytesTarget, PathTempRead file → Result<Bytes>
FileMmapTarget, PathTempMap file → Result<Bytes>
FileWriteTextTarget, PathTemp, TextTempWrite file → Result<Unit>
FileWriteBytesTarget, PathTemp, BytesTempWrite bytes → Result<Unit>
FileExistsTarget, PathTempCheck existence → Result<Bool>
FileReplaceTarget, SourceTemp, DestinationTempAtomically replace file → Result<Unit>
FileMakeExecutableTarget, PathTempPrepare regular file for execution → Result<Unit>
DirectoryEntriesTarget, PathTempEnumerate sorted basenames → Result<List<String>>
DirectoryCreateAllTarget, PathTempRecursively create directories → Result<Unit>
DirectoryRemoveTreeTarget, PathTempRecursively remove without following symlinks → Result<Unit>
FileOpenTarget, PathTempOpen file → Result<FileHandle>
FileReadChunkTarget, HandleTemp, CountTempRead bounded chunk → Result<Bytes>
FileReadLineTarget, HandleTempRead line → Result<Maybe<String>>
FileCloseTarget, HandleTempClose handle → Result<Unit>

All file operations return Result ADTs: Ok(value) on success, Error(message) on failure.

Text Parsing ​

InstructionFieldsDescription
TextUnconsTarget, TextTempSplit front scalar → Maybe((Str, Str))
TextParseIntTarget, TextTempParse decimal integer → Result<Int>
TextParseFloatTarget, TextTempParse decimal float → Result<Float>

These instructions return the existing Maybe and Result ADTs.

Text Formatting ​

InstructionFieldsDescription
TextFromIntTarget, ValueTempFormat integer → Str
TextFromFloatTarget, ValueTempFormat finite float → Str
TextToHexTarget, ValueTempFormat integer as hexadecimal → Str

HTTP ​

InstructionFieldsDescription
HttpGetTarget, UrlTempHTTP GET → Result<String>
HttpPostTarget, UrlTemp, BodyTempHTTP POST → Result<String>

TCP Networking ​

InstructionFieldsDescription
NetTcpConnectTarget, HostTemp, PortTempConnect → Result<Socket>
NetTcpSendTarget, SocketTemp, TextTempSend data → Result<Unit>
NetTcpReceiveTarget, SocketTemp, MaxBytesTempReceive → Result<String>
NetTcpCloseTarget, SocketTempClose socket → Result<Unit>

Control Flow ​

InstructionFieldsDescription
LabelNameDefine a jump target
JumpTargetUnconditional jump to label
JumpIfFalseCondTemp, TargetJump to label if CondTemp == 0
ReturnSourceReturn value from function

Capabilities ​

Handler evidence for capabilities: one module global per declared capability (__ashes_capability_handler_<i>, created when IrProgram.CapabilityHandlerGlobals > 0) holds a pointer to the innermost installed handler frame for that capability, 0 when none. See Architecture for the frame layout and perform/handle sequences.

InstructionFieldsDescription
LoadCapabilityHandlerTarget, CapabilityIndexLoad the capability's current handler frame pointer
StoreCapabilityHandlerCapabilityIndex, SourceStore a handler frame pointer into the capability global

Async / Task ​

InstructionFieldsDescription
CreateTaskTarget, ClosureTemp, StateStructSize, CaptureCount, FrameDropperLabel, LoopResetEligibleAllocate task/state struct from closure
CreateCompletedTaskTarget, ResultTempAllocate pre-completed task (state = -1)
AwaitTaskTarget, TaskTempAwait a sub-task inside a coroutine
RunTaskTarget, TaskTempSynchronously drive a task to completion
AsyncSleepTarget, MillisecondsTempCreate a sleep task (state = -2)
SuspendStateStructTemp, NextState, AwaitedTaskTemp, SaveVarsState machine suspend point
ResumeStateStructTemp, ResultTemp, RestoreVarsState machine resume point

AwaitTask appears in the IR before the state machine transform. The transform replaces each AwaitTask with a Suspend/Resume pair that saves and restores live temps and locals across the await point. A suspend hands its saved values to the frame and the matching resume clears each word as it restores it, so exactly one owner holds each reference: while the task is parked the frame owns them and FrameDropper releases them on cancellation; once resumed the body owns them again and its ordinary lifetime markers do. Perceus lifetime placement runs on that pre-transform body, where the await is still an ordinary control-flow edge; an owner whose placed RcDrop follows an await is therefore live across it and enters the transform's save/restore set.

Task/state struct layout (TaskStructLayout):

OffsetFieldDescription
0-40coreState, coroutine, result, awaited task, task link, sleep duration
48-88leaf waitTwo I/O arguments, wait kind/handle, and two wait scratch slots
96FrameSizeBytesFull frame size including captures and live slots
104-112private arenaDetached root cursor and end
120-136scheduler linksReady-next, waiter, and arena owner
144LoopResetOkWhether an async TCO restart may reset its region
152FrameDropperFrame teardown helper, or 0 when the frame owns nothing
160+captures/live varsCaptures followed by variables live across suspension

Lowering Overview ​

The Lowering class transforms the typed AST into IR:

  1. Lowering.Lower(Expr) is the entry point. It walks the expression tree recursively, appending instructions to a flat list.

  2. Each LowerExpr() call returns (int Temp, TypeRef Type) — the temporary holding the result and its inferred type.

  3. Lambdas are lifted into separate IrFunction entries. Free variables are captured into a heap-allocated environment via Alloc + StoreMemOffset, then accessed inside the lambda body via LoadEnv.

  4. Pattern matching generates a series of GetAdtTag checks, JumpIfFalse branches, and GetAdtField extractions, with labels for each arm and a join point after the match.

  5. Let bindings allocate a local slot (StoreLocal) and make it available in the body scope (LoadLocal).


Backend Consumption ​

The LLVM backend (LlvmCodegen) processes each IrFunction:

  1. Pre-creates LLVM BasicBlock entries for every Label instruction.
  2. Iterates through instructions, calling EmitInstruction() which pattern-matches on the IrInst variant and emits corresponding LLVM IR builder calls.
  3. Temps and locals are mapped to LLVM alloca stack slots.
  4. The Uses* flags on IrProgram control which runtime helpers (print routines, string concatenation, etc.) are included.

Memory Layout Summary ​

StructurePayload addressed by value pointer
String / Bytes[length_and_view_flag:i64][bytes...]
BigInt[sign_and_limb_count:i64][limbs...]
List cons[head:i64][tail:i64]; nil is zero
Closure[code:i64][env:i64][packed_env_size_and_ownership:i64][dropper:i64]
ADT / record[tag:i64][field0:i64]...[fieldN:i64]
Tuple / environment[word0:i64][word1:i64]...

Runtime-managed values have [reference_count:i64][allocation_size:i64] immediately before the payload. Small RC cells use a dense per-thread region plus exact-size free-list reuse; large cells use direct OS allocation. Scoped arena instructions remain for proven scratch and explicit scheduler/specialized regions. See the architecture memory model for ownership and boundary invariants.

Semantic lowering describes these payloads with one cycle-guarded ordinary heap-layout capability. ADT child offsets are relative to the public value pointer shown above, so the same constructor-specific descriptor drives recursive drops, TCO normalization, and runtime-reuse cleanup independently of the optional RC header.

A TCO back edge may reuse an older runtime-owned List(record) graph as the normalization destination when the replacement is fresh and every record field is copied inline. It first checks equal spine length and uniqueness of every old cons cell and record head; only a complete successful preflight permits field overwrite. Otherwise the ordinary graph normalization and recursive drop path remains in effect.