milkir

Reusable Cranelift-like SSA intermediate representation

compiler
ir
ssa
optimization
moon add Milky2018/milkir@0.6.2
Download zip
Author
Version
0.6.2
License
Apache-2.0
Last updated
2 days ago
Downloads
153
README

#MilkIR

MilkIR is a reusable, target-independent intermediate representation for compiler middle ends. A frontend translates source operations into MilkIR, optimization passes simplify the MilkIR function, and a lowering package such as Milky2018/milkir_machv turns it into machine-oriented IR.

Target-independent means that MilkIR operations do not encode a particular instruction set or calling convention; it does not mean that every data width is target-configurable. MilkIR pointer/reference carriers are always 64-bit: Ptr, Ref, CallableRef, and OpaqueRef each have a fixed 64-bit representation. A backend for a non-64-bit target would require an explicit IR contract change rather than interpreting these types using the host pointer width.

frontend IR or bytecode | v MilkIR SSA -- verify and optimize here | v MachV IR -- select instructions and allocate registers later

#Start with one small function

Suppose the source program contains this function:

add_one(x: i32) -> i32 = x + 1

The following test builds the same function in MilkIR:

///|
test "build add_one" {
let builder = FunctionBuilder::FunctionBuilder("add_one")

let x = builder.add_param(I32)
builder.add_result(I32)

let one = builder.iconst_i32(1)
let answer = builder.iadd(x, one)
builder.return_([answer])

let func = builder.finalize()
inspect(func.verify(), content="()")
inspect(
func.print(),
content=(
#|function add_one(v0:i32) -> i32 {
#|block0:
#| v1:i32 = iconst 1
#| v2:i32 = iadd v0, v1
#| return v2
#|}
#|
),
)
}

Read the generated IR from top to bottom:

function add_one(v0:i32) -> i32 { function parameter v0 has type i32 block0: execution starts in block0 v1:i32 = iconst 1 create the constant 1 v2:i32 = iadd v0, v1 add x and 1, producing a new value return v2 return that value }

finalize() verifies the current function before returning it and raises VerifyError for malformed SSA, CFG, or generic instruction contracts. For extension instructions, generic verification checks only the dialect/opcode envelope and the explicit operand/result signature; the owning adapter validates dialect semantics. Finalization is a validation checkpoint, not a freeze operation: the returned Function remains mutable and does not carry a persistent "verified" state. get_function() remains the explicit escape hatch for in-progress construction and transformation code. Callers may continue transforming either value, but every consuming adapter must verify the function after its final mutation and immediately before lowering it.

The important detail is that x, one, and answer are not runtime integers in the MoonBit program that builds the IR. They are MilkIR Values: typed handles that name values in the function being compiled.

#The six concepts to know

ConceptMeaningIn add_one
FunctionOne compilable function, including its signature and blocks.add_one
TypeThe static type of an IR value.I32
ValueA typed name for a function parameter, block parameter, or instruction result.v0, v1, v2
BlockA straight-line sequence of instructions with one exit.block0
InstAn operation that may produce one or more values.iconst, iadd
TerminatorThe final control-flow operation in a block.return

FunctionBuilder is the normal construction API. It creates an entry block automatically and keeps track of the block that receives new instructions. For a straight-line function, the usual order is:

  1. Create a builder.
  2. Declare function parameters and result types.
  3. Emit instructions.
  4. End the current block with return_, jump, a branch, or trap.
  5. Call finalize, then verify the function.

Lower-level producers that already have a complete Signature can use Function::with_signature. It eagerly materializes every declared function parameter in Function.params; retrieve those values with func.param(index) before emitting instructions. Function::signature() returns a snapshot derived from the same explicit parameter and result arrays. Do not recreate parameters with new_value(): that method allocates instruction results, block values, and other values after the declared function parameters.

#Opcode families

Every instruction belongs to one semantic family. Opcode has no source-language instruction variants or target-machine operations of its own:

FamilyResponsibility
Scalar(ScalarOp)Constants, arithmetic, comparisons, conversions, selection, and copies.
Memory(MemoryOp)Full-width and narrow loads and stores over an explicit base and offset value.
Call(CallOp)Direct external-symbol calls and function-pointer calls with explicit contracts.
Vector(VectorOp)Language-neutral V128 lane, arithmetic, comparison, conversion, and effective-address memory operations.
GlobalValue(GlobalValue)A function-scoped, adapter-owned context field with an explicit type, stability, and alias region.
Ext(ExtOp, Signature)Typed operations whose semantics belong to a separately owned dialect.

Frontends must consume source-only metadata before constructing a core instruction. For example, a WebAssembly frontend resolves a SIMD memory index, alignment hint, and immediate offset while computing the effective address; MilkIR receives that address and the vector load/store semantics. A frontend uses Ext only when the operation genuinely requires dialect-owned validation and lowering.

#Embedding context fields

GlobalValue models a typed value loaded from an embedding-provided context, such as a linear-memory base pointer. Its declaration is interned in the Function; the instruction names that declaration and takes the context pointer explicitly. MilkIR does not know the field offset or runtime layout. The owning dialect validates the opaque ContextField, and its MilkIR-to-MachV adapter resolves it to a semantic EnvironmentField.

Every declaration also states whether the field is Stable for the whole function invocation or Mutable across calls, plus the abstract region reached by the resulting pointer. Stable fields remain explicit and cheap to rematerialize: general GVN and LICM do not turn them into function-wide live ranges. Mutable fields may be reused across unrelated heap stores but are invalidated by calls and unknown memory writes. Targets materialize the semantic occurrences they receive rather than applying a second reuse policy. These contracts permit local redundancy elimination without pinning a physical register or hard-coding an embedding layout into MilkIR.

#Semantic ownership

MilkIR records the optimizer-visible facts of each built-in opcode in one semantic summary: whether it may trap, whether it reads or writes memory, and whether it has another observable effect. Dead-code elimination, loop optimization, global value numbering, and e-graph admission derive their safety decisions from that summary instead of maintaining independent opcode lists. Unknown extension operations are conservatively treated as trapping and effectful.

Other concerns remain with the adapter that implements them. The verifier owns operand and result contracts, the printer owns textual syntax, MilkIR-MachV Lowering owns instruction selection, and the e-graph adapter owns which operations have an e-graph encoding. These are different responsibilities rather than duplicate semantic facts.

When adding a built-in opcode:

  1. Add its operand and result contract to the verifier.
  2. Classify its trap, memory, and observable-effect behavior in the opcode semantic summary.
  3. Add its textual representation to the printer.
  4. Add its instruction selection to MilkIR-MachV Lowering.
  5. Add an e-graph encoding only if the e-graph node language represents it.

The built-in family matches in those stages are exhaustive, so adding a new family or operation leaves a compiler error until its required behavior is supplied. E-graph admission is intentionally conservative: an operation without an explicit encoding remains outside the e-graph.

#Why values are called SSA values

SSA means static single assignment: each MilkIR Value is defined exactly once. An instruction never changes an existing value; it creates a new one.

For example, a source-language assignment such as x = x + 1 should not overwrite the MilkIR value for the old x. The frontend emits a fresh value instead:

v0:i32 = ... old x v1:i32 = iconst 1 v2:i32 = iadd v0, v1 new x

This makes data dependencies explicit. An optimizer can see that v2 depends on v0 and v1 without reconstructing the history of a mutable variable.

Every value belongs to one function and has one Type. The core types are I32, I64, F32, F64, V128, Ptr, Ref, CallableRef, and OpaqueRef. The four pointer/reference carrier types are fixed-width 64-bit values, not aliases for the host's native pointer type.

#Comparisons do not require extra blocks

Use select when both candidate values are already available. The next function returns the larger of two signed i32 values:

///|
test "build max_i32 with select" {
let builder = FunctionBuilder::FunctionBuilder("max_i32")
let lhs = builder.add_param(I32)
let rhs = builder.add_param(I32)
builder.add_result(I32)

let lhs_is_greater = builder.icmp_sgt(lhs, rhs)
let result = builder.select(lhs_is_greater, lhs, rhs)
builder.return_([result])

let func = builder.finalize()
inspect(func.verify(), content="()")
inspect(func.print().contains("icmp.sgt"), content="true")
inspect(func.print().contains("select"), content="true")
}

icmp_sgt produces an I32 condition. select(condition, when_nonzero, when_zero) chooses a value without changing control flow.

#Control flow: blocks and terminators

A block contains zero or more instructions and exactly one terminator. Once a terminator has been emitted, switch to a different block before emitting more instructions.

The common terminators are:

Builder methodMeaning
return_(values)Return values to the caller.
jump(target, args)Continue in another block and pass its arguments.
brnz(condition, then_block, else_block)Branch to the first target when the condition is nonzero.
brz(condition, then_block, else_block)Branch to the first target when the condition is zero.
br_table(index, targets, default)Choose one of several targets.
trap(reason)Stop execution abnormally.

Here is the shape of a conditional function before considering how values cross block boundaries:

+--------------+ | block0 | | test cond | +------+-------+ | +---------+---------+ | | v v +-----------+ +-----------+ | then_block| | else_block| +-----+-----+ +-----+-----+ | | +---------+---------+ | v +------------+ | join_block | +------------+

#Passing values between blocks

MilkIR uses block parameters instead of phi instructions. A block parameter is a value defined at the start of a block. Every jump to that block supplies the argument that the parameter receives on that edge.

The following function computes input + 1 when condition is nonzero and returns input otherwise:

///|
test "pass a value into a join block" {
let builder = FunctionBuilder::FunctionBuilder("add_if")
let input = builder.add_param(I32)
let condition = builder.add_param(I32)
builder.add_result(I32)

let add_block = builder.create_block()
let unchanged_block = builder.create_block()
let join_block = builder.create_block()
let result = builder.add_block_param(join_block, I32)

builder.brnz(condition, add_block, unchanged_block)

builder.switch_to_block(add_block)
let one = builder.iconst_i32(1)
let incremented = builder.iadd(input, one)
builder.jump(join_block, [incremented])

builder.switch_to_block(unchanged_block)
builder.jump(join_block, [input])

builder.switch_to_block(join_block)
builder.return_([result])

let func = builder.finalize()
inspect(func.verify(), content="()")
inspect(func.blocks.length(), content="4")
inspect(func.blocks[3].params.length(), content="1")
}

Focus on the three names around the join:

add_block --jump [incremented]--+ >-- join_block(result) -- return result unchanged_block --jump [input]----------+

result is defined by join_block, not by either predecessor. On the add_block edge it receives incremented; on the other edge it receives input. If a join block has multiple parameters, each incoming jump must pass arguments in the same order.

#Verification

Always verify a function after construction and after transformations that may change its structure:

let func = builder.finalize()
func.verify()

Function::verify checks core structural and local typing rules, including:

  • the function contains at least one block;
  • each referenced operand has been defined;
  • each block has a terminator;
  • core instructions have the expected operand arity;
  • operands that must agree have matching types;
  • comparison results have type I32.

Verification is a useful construction guard, not a complete proof of every frontend, dialect, dominance, calling-convention, or embedding invariant. Frontends and dialects should perform any additional semantic checks their input requires.

#Optimization

Optimization mutates a Function and returns an OptResult whose changed field reports whether any pass changed the IR.

///|
test "fold a constant expression" {
let builder = FunctionBuilder::FunctionBuilder("constant_answer")
builder.add_result(I32)
let ten = builder.iconst_i32(10)
let twenty = builder.iconst_i32(20)
let answer = builder.iadd(ten, twenty)
builder.return_([answer])

let func = builder.finalize()
let result = optimize_with_level(func, O1)

inspect(result.changed, content="true")
inspect(instruction_count(func), content="1")
inspect(func.print().contains("iconst 30"), content="true")
inspect(func.verify(), content="()")
}

The optimization levels are:

LevelIntended use
O0Minimal pipeline: removes dead code plus constant and unused block parameters.
O1Inexpensive simplification: mandatory cleanup, constant folding, alias canonicalization, and dead-code elimination.
O2The default pipeline: O1-style cleanup plus e-graph rewriting, budgeted global value numbering, and CFG simplification.
O3The O2 pipeline, loop-invariant code motion, checked counted-loop unrolling, strength reduction, and a final O2 cleanup.

Use optimize(func) for the default pipeline or optimize_with_level(func, level) when the caller chooses the level explicitly. Optimizers assume a valid SSA and block-parameter structure. Verify before optimization when the input comes from a frontend, then verify again after developing a new transformation.

#Counted-loop unrolling at O3

O3 unrolls only natural loops for which analysis produces a complete plan. The supported form has one preheader, one header comparison, one body path, one latch/back edge, and one exit; body and latch blocks must not introduce additional block parameters. Initial values, bounds, and steps must resolve to constants through any loop-external copy chain. The induction value may be I32 or I64 and may use signed or unsigned <, <=, >, or >= comparisons. Reversed comparison operands and inverted conditional-branch polarity are normalized before analysis. Increasing and decreasing updates use checked arithmetic, so a loop whose final update would wrap is rejected.

All header block parameters are treated as loop-carried state. Full unrolling is limited to at most eight source iterations, while larger proven loops use factor-two unrolling with one peeled iteration for odd trip counts. Both strategies cap newly cloned instructions at 64. Cloning assigns fresh instruction and value IDs, remaps zero-result and multi-result instructions, preserves metadata and effect order, and verifies correctly with calls, loads, stores, and traps.

The pass leaves the function unchanged when the CFG shape is unsupported, a bound or step is dynamic, the trip count exceeds the analysis limit, an operand or latch edge cannot be mapped, arithmetic may wrap, or code growth exceeds the budget.

#Calls, memory, and traps

The concepts below matter when a frontend moves beyond pure arithmetic into embedding and effect semantics.

#External calls

ExternalSymbol names a symbol outside the IR function. CallOp::Direct(symbol, signature) and CallOp::Pointer(num_args, num_results) are treated conservatively by core optimizations because calls may observe or change state. Direct calls carry operand/result signatures that the verifier checks; pointer calls carry explicit argument and result counts.

CallOp::Pointer has a generic operand contract:

  1. operand 0 is the function pointer;
  2. operands 1 and later are ordinary arguments;
  3. num_args counts every operand after the callee pointer.

An embedding adapter may assign roles such as VMContext to those arguments when it chooses a calling convention, but that role is not part of the core opcode contract.

#Traps and effects

Trap(reason) and TrapExit(reason) terminators end execution without normal results. Stores, calls, traps, and unknown extension operations are observable or potentially observable. Optimizations must not delete or reorder them unless a stronger analysis proves that doing so preserves behavior.

#Dialect-specific operations

Ext(ExtOp, Signature) represents dialect-specific operations. MilkIR stores only a dialect name, opcode name, integer immediates, and an explicit operand/result contract. Validator closures are not part of Function; the dialect package owns builders, semantic validation, decoding, and lowering.

Function::verify and FunctionBuilder::finalize validate generic IR structure without interpreting dialect semantics. At the adapter seam, Function::verify_with_dialect_validator receives the owned dialect name plus explicit instruction and global-value validators, rejects data owned by another dialect, and converts diagnostics into VerifyError::UnverifiableInstruction. Dialect lowering likewise requires explicit adapter validation, so validation behavior depends only on the adapter selected by the consumer, never on function construction history.

///|
test "validate a dialect opcode descriptor" {
let descriptor = ExtOpDescriptor::ExtOpDescriptor("demo", "checked_add", 1)
let opcode = ExtOp::ExtOp("demo", "checked_add", FixedArray::make(1, 32))

inspect(opcode.matches_descriptor(descriptor), content="true")
inspect(descriptor.expected_immediate_count(), content="1")
}

Core optimizations treat extension operations conservatively because MilkIR cannot infer whether an unknown operation reads memory, writes memory, traps, or depends on embedding state.

#Typical frontend workflow

A frontend using MilkIR normally follows this sequence:

  1. Map source types to MilkIR Types.
  2. Create a FunctionBuilder, then declare the function parameters and results.
  3. Translate each source basic block, keeping a map from source values to MilkIR Values.
  4. Represent merges and loop-carried values with block parameters and jump arguments.
  5. Finalize and verify the function.
  6. Optimize it at the desired level.
  7. Lower it to the next IR, for example with Milky2018/milkir_machv.

For debugging, Function::print produces a readable textual view and CFG::to_dot produces Graphviz DOT for the control-flow graph. CFG also provides predecessors, successors, traversal orders, dominators, back edges, and loop discovery.

MilkIR focuses on target-independent SSA construction, verification, control-flow analysis, and optimization. Other compiler stages build on it through separate packages:

Compiler stagePackage
SSA construction, verification, CFGs, and optimizationMilky2018/milkir
Optional WebAssembly extension operationsMilky2018/wasm_milkir
Lowering from MilkIR to machine-oriented IRMilky2018/milkir_machv
Target instruction selection and ABI detailsMilky2018/aarch64_target and Milky2018/x64_target

#
VerifyError

pub suberror VerifyError {
MissingTerminator(block_id~ : Int)
EmptyFunction
UndefinedValue(value_id~ : Int)
ForeignValue(value_id~ : Int)
ForeignBlock(block_id~ : Int)
ForeignInstruction(inst_id~ : Int)
ForeignGlobalValue(global_value_id~ : Int)
DuplicateBlockId(block_id~ : Int)
DuplicateValueDefinition(value_id~ : Int)
DuplicateInstructionId(inst_id~ : Int)
UseBeforeDefinition(value_id~ : Int)
NonDominatingUse(value_id~ : Int, defining_block~ : Int, use_block~ : Int)
InstructionOperandMismatch(inst_id~ : Int)
InvalidBlockTarget(block_id~ : Int)
ArityMismatch(message~ : String)
TypeMismatch(message~ : String)
UnverifiableInstruction(message~ : String)
} derive(Eq,
Debug
)

impl Show for VerifyError

#
VerifyError::unverifiable_instruction

fn VerifyError::unverifiable_instruction(message : String) -> VerifyError

Construct a structured error for a consumer seam that cannot validate or lower an otherwise well-formed MilkIR function.

#
AliasRegion

pub(all) enum AliasRegion {
Heap(Int)
Table(Int)
Context
Other
} derive(Eq, Hash,
Debug
)

Disjoint abstract memory regions used by alias analysis.

#
Block

pub struct Block {
id : Int
params : Array[(Value, Type)]
instructions : Array[Inst]
terminator : Terminator?
// private fields
}

Block - represents a basic block in the control flow graph A basic block is a sequence of instructions with:
  • One entry point (the first instruction)
  • One exit point (the last instruction, a terminator)

The public fields are exposed for zero-copy inspection. Callers must not mutate params, instructions, or terminator directly. Use FunctionBuilder, append_inst, and set_terminator so ownership checks run, then verify the containing function before optimization or lowering.

#
Block::append_inst

fn Block::append_inst(self : Block, inst : Inst) -> Unit

#
Block::set_terminator

fn Block::set_terminator(self : Block, term : Terminator) -> Unit

Set the terminator for this block

#
CFG

pub struct CFG {
size : Int
num_blocks : Int
successors : Array[Array[Int]]
predecessors : Array[Array[Int]]
valid : Array[Bool]
} derive(
Debug
)

#
CFG::build

fn CFG::build(func : Function) -> CFG

#
CFG::compute_dominators

fn CFG::compute_dominators(self : CFG) -> Array[Int]

#
CFG::dominates

fn CFG::dominates(self : CFG, a : Int, b : Int) -> Bool

#
CFG::find_back_edges

fn CFG::find_back_edges(self : CFG) -> Array[(Int, Int)]

#
CFG::find_loops

fn CFG::find_loops(self : CFG) -> Array[Loop]

#
CFG::get_loop_preheader

fn CFG::get_loop_preheader(self : CFG, loop_ : Loop) -> Int?

#
CFG::get_predecessors

fn CFG::get_predecessors(self : CFG, block_id : Int) -> Array[Int]

#
CFG::get_successors

fn CFG::get_successors(self : CFG, block_id : Int) -> Array[Int]

#
CFG::is_entry_or_unreachable

fn CFG::is_entry_or_unreachable(self : CFG, block_id : Int) -> Bool

#
CFG::is_exit_block

fn CFG::is_exit_block(self : CFG, block_id : Int) -> Bool

#
CFG::is_valid

fn CFG::is_valid(self : CFG, block_id : Int) -> Bool

#
CFG::postorder

fn CFG::postorder(self : CFG) -> Array[Int]

Depth-first postorder over the reachable CFG, entry block first.

The depth of this walk is the length of a CFG path, which the input function controls, so it keeps its own stack on the heap. A chain of blocks is ordinary in generated code, and a native recursion turns one into a stack overflow inside the optimizer rather than a diagnosable error (ISS-380, ISS-401).

#
CFG::reverse_postorder

fn CFG::reverse_postorder(self : CFG) -> Array[Int]

#
CFG::to_dot

fn CFG::to_dot(self : CFG, func_name : String) -> String

#
CallOp

pub(all) enum CallOp {
Direct(ExternalSymbol, Signature)
Pointer(Int, Int)
} derive(Eq, Hash,
Debug
)

Generic call semantics. Pointer-call operands are laid out as the callee pointer followed by ordinary arguments.

#
ContextField

pub struct ContextField {
dialect : String
key : Int
parameters : FixedArray[Int]
ty : Type
} derive(
Debug
)

Opaque embedding-context field identity.

The dialect owns the meaning of key and parameters; MilkIR only relies on the declared type and optimizer-visible global-value contract.
impl Eq for ContextField

#
ContextField::new

fn ContextField::new(dialect : String, key : Int, parameters : FixedArray[Int], ty : Type) -> ContextField

#
ConversionOp

pub(all) enum ConversionOp {
IntReduce
SignedExtend
UnsignedExtend
FloatPromote
FloatDemote
FloatToSignedInt
FloatToUnsignedInt
FloatToSignedIntSaturating
FloatToUnsignedIntSaturating
SignedIntToFloat
UnsignedIntToFloat
Bitcast
} derive(Eq, Hash,
Debug
)

#
ExtOp

pub struct ExtOp {
dialect : String
opcode : String
immediates : FixedArray[Int]
} derive(
Debug
)

impl Eq for ExtOp
impl Hash for ExtOp

#
ExtOp::ExtOp

fn ExtOp::ExtOp(dialect : String, opcode : String, immediates : FixedArray[Int]) -> ExtOp

#
ExtOp::matches_descriptor

fn ExtOp::matches_descriptor(self : ExtOp, descriptor : ExtOpDescriptor) -> Bool

#
ExtOpDescriptor

pub(all) struct ExtOpDescriptor {
dialect : String
opcode : String
min_immediates : Int
max_immediates : Int
} derive(Eq, Hash,
Debug
)

#
ExtOpDescriptor::ExtOpDescriptor

fn ExtOpDescriptor::ExtOpDescriptor(dialect : String, opcode : String, immediate_count : Int) -> ExtOpDescriptor

#
ExtOpDescriptor::accepts_immediate_count

fn ExtOpDescriptor::accepts_immediate_count(self : ExtOpDescriptor, count : Int) -> Bool

#
ExtOpDescriptor::expected_immediate_count

fn ExtOpDescriptor::expected_immediate_count(self : ExtOpDescriptor) -> String

#
ExtOpDescriptor::with_immediate_range

fn ExtOpDescriptor::with_immediate_range(dialect : String, opcode : String, min_immediates : Int, max_immediates : Int) -> ExtOpDescriptor

#
ExtensionInstView

pub struct ExtensionInstView {
op : ExtOp
operand_types : ReadOnlyArray[Type]
result_types : ReadOnlyArray[Type]
} derive(
Debug
)

Read-only instruction shape exposed to an extension dialect validator.

#
ExternalSymbol

pub struct ExternalSymbol {
name : String
} derive(Eq, Hash,
Debug
)

#
ExternalSymbol::ExternalSymbol

fn ExternalSymbol::ExternalSymbol(name : String) -> ExternalSymbol

#
FloatBinaryOp

pub(all) enum FloatBinaryOp {
Add
Sub
Mul
Div
Min
Max
} derive(Eq, Hash,
Debug
)

#
FloatCC

pub(all) enum FloatCC {
Eq
Ne
Lt
Le
Gt
Ge
} derive(Eq, Hash,
Debug
)

Floating point comparison condition codes

#
FloatUnaryOp

pub(all) enum FloatUnaryOp {
Neg
Abs
Sqrt
Ceil
Floor
Trunc
Nearest
} derive(Eq, Hash,
Debug
)

#
Function

pub struct Function {
name : String
params : Array[(Value, Type)]
results : Array[Type]
blocks : Array[Block]
external_symbols : Array[ExternalSymbol]
global_values : Array[(GlobalValue, GlobalValueData)]
next_value_id : Int
next_inst_id : Int
next_block_id : Int
next_global_value_id : Int
// private fields
}

Function - a complete mutable IR function.

The public fields are exposed for zero-copy inspection. Callers must not mutate params, results, blocks, external_symbols, or the ID counters directly. Use FunctionBuilder and the checked Function/Block methods for structural changes. A function must be verified after its final mutation and immediately before optimization or lowering.

#
Function::add_param

fn Function::add_param(self : Function, ty : Type) -> Value

Add a parameter to the function

#
Function::add_result

fn Function::add_result(self : Function, ty : Type) -> Unit

Add a result type to the function

#
Function::declare_external_symbol

fn Function::declare_external_symbol(self : Function, name : String) -> ExternalSymbol

#
Function::declare_global_value

fn Function::declare_global_value(self : Function, data : GlobalValueData) -> GlobalValue

Declare or reuse an identical function-scoped global value.

#
Function::global_value_data

fn Function::global_value_data(self : Function, global_value : GlobalValue) -> GlobalValueData?

Return the declaration for an owned global-value handle.

#
Function::new_block

fn Function::new_block(self : Function, params : Array[Value]) -> Block

Create a new basic block

#
Function::new_block0

fn Function::new_block0(self : Function) -> Block

#
Function::new_empty

fn Function::new_empty(name : String) -> Function

#
Function::new_inst

fn Function::new_inst(self : Function, opcode : Opcode, args : Array[Value], results : Array[Value]) -> Inst

#
Function::new_value

fn Function::new_value(self : Function, ty : Type) -> Value

Create a new value with a unique ID

#
Function::param

fn Function::param(self : Function, index : Int) -> Value?

Return a declared function parameter by position.

#
Function::print

fn Function::print(self : Function) -> String

Print a function

#
Function::signature

fn Function::signature(self : Function) -> Signature

Return a signature snapshot derived from the explicit function contract.

#
Function::verify

fn Function::verify(self : Function) -> Unit raise VerifyError

#
Function::verify_core

fn Function::verify_core(self : Function) -> Unit raise VerifyError

Verify that a function is self-contained core MilkIR. Dialect-bearing instructions must be validated and lowered by their owning adapter.

#
Function::verify_with_dialect_validator

fn Function::verify_with_dialect_validator(self : Function, dialect : String, validate_extension : (ExtensionInstView) -> String?, validate_global_value : (GlobalValueData) -> String?) -> Unit raise VerifyError

Verify generic MilkIR structure, require every extension and context global to belong to the owning dialect, and apply that adapter's validators before crossing its seam.

#
Function::with_signature

fn Function::with_signature(name : String, signature : Signature) -> Function

Create a function whose declared signature is materialized as explicit parameter values and result types.

#
FunctionBuilder

type FunctionBuilder

FunctionBuilder - helps construct IR functions Tracks the current block and provides methods for emitting instructions

#
FunctionBuilder::FunctionBuilder

fn FunctionBuilder::FunctionBuilder(name : String) -> FunctionBuilder

#
FunctionBuilder::add_block_param

fn FunctionBuilder::add_block_param(self : FunctionBuilder, block : Block, ty : Type) -> Value

Add a block parameter (for SSA phi nodes)

#
FunctionBuilder::add_param

fn FunctionBuilder::add_param(self : FunctionBuilder, ty : Type) -> Value

Add a parameter to the function

#
FunctionBuilder::add_result

fn FunctionBuilder::add_result(self : FunctionBuilder, ty : Type) -> Unit

Add a result type to the function

#
FunctionBuilder::append_block_params_for_function_params

fn FunctionBuilder::append_block_params_for_function_params(self : FunctionBuilder, block : Block) -> Unit

Append block parameters matching the function parameter types.

#
FunctionBuilder::append_block_params_for_function_returns

fn FunctionBuilder::append_block_params_for_function_returns(self : FunctionBuilder, block : Block) -> Unit

Append block parameters matching the function result types.

#
FunctionBuilder::band

fn FunctionBuilder::band(self : FunctionBuilder, a : Value, b : Value) -> Value

Bitwise and

#
FunctionBuilder::bitcast

fn FunctionBuilder::bitcast(self : FunctionBuilder, ty : Type, a : Value) -> Value

Bitcast (reinterpret bits)

#
FunctionBuilder::block_params

fn FunctionBuilder::block_params(self : FunctionBuilder, block : Block) -> Array[Value]

Return the SSA values used as block parameters.

#
FunctionBuilder::bnot

fn FunctionBuilder::bnot(self : FunctionBuilder, a : Value) -> Value

Bitwise not

#
FunctionBuilder::bor

fn FunctionBuilder::bor(self : FunctionBuilder, a : Value, b : Value) -> Value

Bitwise or

#
FunctionBuilder::br_table

fn FunctionBuilder::br_table(self : FunctionBuilder, index : Value, targets : Array[Block], default_target : Block) -> Unit

Branch table (switch)

#
FunctionBuilder::brnz

fn FunctionBuilder::brnz(self : FunctionBuilder, cond : Value, then_block : Block, else_block : Block) -> Unit

Conditional branch (branch if non-zero)

#
FunctionBuilder::brz

fn FunctionBuilder::brz(self : FunctionBuilder, cond : Value, then_block : Block, else_block : Block) -> Unit

Conditional branch (branch if zero)

#
FunctionBuilder::bxor

fn FunctionBuilder::bxor(self : FunctionBuilder, a : Value, b : Value) -> Value

Bitwise xor

#
FunctionBuilder::call_pointer

fn FunctionBuilder::call_pointer(self : FunctionBuilder, func_ptr : Value, args : Array[Value], result_types : Array[Type]) -> Array[Value]

Call a function pointer with ordinary arguments.

#
FunctionBuilder::call_symbol

fn FunctionBuilder::call_symbol(self : FunctionBuilder, symbol : ExternalSymbol, result_ty : Type?, args : Array[Value]) -> Value?

#
FunctionBuilder::call_symbol_multi

fn FunctionBuilder::call_symbol_multi(self : FunctionBuilder, symbol : ExternalSymbol, result_types : Array[Type], args : Array[Value]) -> Array[Value]

#
FunctionBuilder::clz

fn FunctionBuilder::clz(self : FunctionBuilder, a : Value) -> Value

Count leading zeros

#
FunctionBuilder::copy

fn FunctionBuilder::copy(self : FunctionBuilder, a : Value) -> Value

Copy value (for register allocation)

#
FunctionBuilder::create_block

fn FunctionBuilder::create_block(self : FunctionBuilder) -> Block

Create a new block.

#
FunctionBuilder::ctz

fn FunctionBuilder::ctz(self : FunctionBuilder, a : Value) -> Value

Count trailing zeros

#
FunctionBuilder::current_block

fn FunctionBuilder::current_block(self : FunctionBuilder) -> Block

Get the current block

#
FunctionBuilder::emit_ext_inst

fn FunctionBuilder::emit_ext_inst(self : FunctionBuilder, ty : Type, opcode : ExtOp, operands : Array[Value]) -> Value

#
FunctionBuilder::emit_inst

fn FunctionBuilder::emit_inst(self : FunctionBuilder, ty : Type, opcode : Opcode, operands : Array[Value]) -> Value

Emit an instruction that produces a result

#
FunctionBuilder::emit_multi_ext_inst

fn FunctionBuilder::emit_multi_ext_inst(self : FunctionBuilder, result_types : Array[Type], opcode : ExtOp, operands : Array[Value]) -> Array[Value]

#
FunctionBuilder::emit_multi_inst

fn FunctionBuilder::emit_multi_inst(self : FunctionBuilder, result_types : Array[Type], opcode : Opcode, operands : Array[Value]) -> Array[Value]

Emit an instruction with multiple results.

#
FunctionBuilder::emit_void_ext_inst

fn FunctionBuilder::emit_void_ext_inst(self : FunctionBuilder, opcode : ExtOp, operands : Array[Value]) -> Unit

#
FunctionBuilder::emit_void_inst

fn FunctionBuilder::emit_void_inst(self : FunctionBuilder, opcode : Opcode, operands : Array[Value]) -> Unit

Emit an instruction without a result

#
FunctionBuilder::fabs

fn FunctionBuilder::fabs(self : FunctionBuilder, a : Value) -> Value

Float absolute value

#
FunctionBuilder::fadd

fn FunctionBuilder::fadd(self : FunctionBuilder, a : Value, b : Value) -> Value

Float add

#
FunctionBuilder::fceil

fn FunctionBuilder::fceil(self : FunctionBuilder, a : Value) -> Value

Float ceiling

#
FunctionBuilder::fcmp

fn FunctionBuilder::fcmp(self : FunctionBuilder, cc : FloatCC, a : Value, b : Value) -> Value

Float comparison (returns i32 0 or 1)

#
FunctionBuilder::fconst_f32

fn FunctionBuilder::fconst_f32(self : FunctionBuilder, value : Float) -> Value

Emit an f32 constant Note: We pack the f32 bits into the Double's bit representation to preserve NaN payloads. Using value.to_double() would go through the FPU and convert signaling NaNs to quiet NaNs.

#
FunctionBuilder::fconst_f64

fn FunctionBuilder::fconst_f64(self : FunctionBuilder, value : Double) -> Value

Emit an f64 constant

#
FunctionBuilder::fcvt_to_sint

fn FunctionBuilder::fcvt_to_sint(self : FunctionBuilder, ty : Type, a : Value) -> Value

Float to signed int

#
FunctionBuilder::fcvt_to_sint_sat

fn FunctionBuilder::fcvt_to_sint_sat(self : FunctionBuilder, ty : Type, a : Value) -> Value

Float to signed int (saturating - NaN->0, overflow->max/min)

#
FunctionBuilder::fcvt_to_uint

fn FunctionBuilder::fcvt_to_uint(self : FunctionBuilder, ty : Type, a : Value) -> Value

Float to unsigned int

#
FunctionBuilder::fcvt_to_uint_sat

fn FunctionBuilder::fcvt_to_uint_sat(self : FunctionBuilder, ty : Type, a : Value) -> Value

Float to unsigned int (saturating - NaN->0, overflow->max, negative->0)

#
FunctionBuilder::fdemote

fn FunctionBuilder::fdemote(self : FunctionBuilder, a : Value) -> Value

Demote float (f64 -> f32)

#
FunctionBuilder::fdiv

fn FunctionBuilder::fdiv(self : FunctionBuilder, a : Value, b : Value) -> Value

Float divide

#
FunctionBuilder::ffloor

fn FunctionBuilder::ffloor(self : FunctionBuilder, a : Value) -> Value

Float floor

#
FunctionBuilder::finalize

fn FunctionBuilder::finalize(self : FunctionBuilder) -> Function raise VerifyError

Verify generic MilkIR structure and return the same mutable function.

Finalization is a validation checkpoint, not a frozen or persistent validated state. Extension semantics remain the owning adapter's responsibility. Any later mutation requires validation at the next consuming seam.

#
FunctionBuilder::fmax

fn FunctionBuilder::fmax(self : FunctionBuilder, a : Value, b : Value) -> Value

Float maximum

#
FunctionBuilder::fmin

fn FunctionBuilder::fmin(self : FunctionBuilder, a : Value, b : Value) -> Value

Float minimum

#
FunctionBuilder::fmul

fn FunctionBuilder::fmul(self : FunctionBuilder, a : Value, b : Value) -> Value

Float multiply

#
FunctionBuilder::fnearest

fn FunctionBuilder::fnearest(self : FunctionBuilder, a : Value) -> Value

Float nearest (round to nearest even)

#
FunctionBuilder::fneg

fn FunctionBuilder::fneg(self : FunctionBuilder, a : Value) -> Value

Float negate

#
FunctionBuilder::fpromote

fn FunctionBuilder::fpromote(self : FunctionBuilder, a : Value) -> Value

Promote float (f32 -> f64)

#
FunctionBuilder::fsqrt

fn FunctionBuilder::fsqrt(self : FunctionBuilder, a : Value) -> Value

Float square root

#
FunctionBuilder::fsub

fn FunctionBuilder::fsub(self : FunctionBuilder, a : Value, b : Value) -> Value

Float subtract

#
FunctionBuilder::ftrunc

fn FunctionBuilder::ftrunc(self : FunctionBuilder, a : Value) -> Value

Float truncate

#
FunctionBuilder::get_const_value

fn FunctionBuilder::get_const_value(self : FunctionBuilder, v : Value) -> Int64?

Get the constant value if a Value was defined by an Iconst instruction. Searches all blocks in the function to find the defining instruction. Returns None if the value is not a constant or not found.

#
FunctionBuilder::get_function

fn FunctionBuilder::get_function(self : FunctionBuilder) -> Function

Get the mutable function being built without validating it.

This is an explicit construction and transformation escape hatch. Consumers must verify the function after the final mutation and before lowering it.

#
FunctionBuilder::global_value

fn FunctionBuilder::global_value(self : FunctionBuilder, global_value : GlobalValue, context : Value) -> Value

Read a function-scoped global value from the explicit embedding context.

#
FunctionBuilder::iadd

fn FunctionBuilder::iadd(self : FunctionBuilder, a : Value, b : Value) -> Value

Integer add

#
FunctionBuilder::icmp

fn FunctionBuilder::icmp(self : FunctionBuilder, cc : IntCC, a : Value, b : Value) -> Value

Integer comparison (returns i32 0 or 1)

#
FunctionBuilder::icmp_eq

fn FunctionBuilder::icmp_eq(self : FunctionBuilder, a : Value, b : Value) -> Value

Integer equal

#
FunctionBuilder::icmp_ne

fn FunctionBuilder::icmp_ne(self : FunctionBuilder, a : Value, b : Value) -> Value

Integer not equal

#
FunctionBuilder::icmp_sge

fn FunctionBuilder::icmp_sge(self : FunctionBuilder, a : Value, b : Value) -> Value

Signed greater than or equal

#
FunctionBuilder::icmp_sgt

fn FunctionBuilder::icmp_sgt(self : FunctionBuilder, a : Value, b : Value) -> Value

Signed greater than

#
FunctionBuilder::icmp_sle

fn FunctionBuilder::icmp_sle(self : FunctionBuilder, a : Value, b : Value) -> Value

Signed less than or equal

#
FunctionBuilder::icmp_slt

fn FunctionBuilder::icmp_slt(self : FunctionBuilder, a : Value, b : Value) -> Value

Signed less than

#
FunctionBuilder::icmp_uge

fn FunctionBuilder::icmp_uge(self : FunctionBuilder, a : Value, b : Value) -> Value

Unsigned greater than or equal

#
FunctionBuilder::icmp_ugt

fn FunctionBuilder::icmp_ugt(self : FunctionBuilder, a : Value, b : Value) -> Value

Unsigned greater than

#
FunctionBuilder::icmp_ule

fn FunctionBuilder::icmp_ule(self : FunctionBuilder, a : Value, b : Value) -> Value

Unsigned less than or equal

#
FunctionBuilder::icmp_ult

fn FunctionBuilder::icmp_ult(self : FunctionBuilder, a : Value, b : Value) -> Value

Unsigned less than

#
FunctionBuilder::iconst

fn FunctionBuilder::iconst(self : FunctionBuilder, ty : Type, value : Int64) -> Value

Emit an integer constant

#
FunctionBuilder::iconst_i32

fn FunctionBuilder::iconst_i32(self : FunctionBuilder, value : Int) -> Value

Emit an i32 constant

#
FunctionBuilder::iconst_i64

fn FunctionBuilder::iconst_i64(self : FunctionBuilder, value : Int64) -> Value

Emit an i64 constant

#
FunctionBuilder::imul

fn FunctionBuilder::imul(self : FunctionBuilder, a : Value, b : Value) -> Value

Integer multiply

#
FunctionBuilder::ireduce

fn FunctionBuilder::ireduce(self : FunctionBuilder, ty : Type, a : Value) -> Value

Reduce integer width (e.g., i64 -> i32)

#
FunctionBuilder::ishl

fn FunctionBuilder::ishl(self : FunctionBuilder, a : Value, b : Value) -> Value

Shift left

#
FunctionBuilder::isub

fn FunctionBuilder::isub(self : FunctionBuilder, a : Value, b : Value) -> Value

Integer subtract

#
FunctionBuilder::jump

fn FunctionBuilder::jump(self : FunctionBuilder, target : Block, args : Array[Value]) -> Unit

Unconditional jump

#
FunctionBuilder::load_ptr

fn FunctionBuilder::load_ptr(self : FunctionBuilder, ty : Type, base : Value, offset : Value) -> Value

Load from raw pointer (no bounds checking) For trampoline code that operates on host memory

#
FunctionBuilder::load_ptr_narrow

fn FunctionBuilder::load_ptr_narrow(self : FunctionBuilder, result_ty : Type, bits : Int, signed : Bool, base : Value, offset : Value) -> Value

Load narrow value from raw pointer (no bounds checking) Loads 'bits' bits from memory and extends to result_ty

#
FunctionBuilder::popcnt

fn FunctionBuilder::popcnt(self : FunctionBuilder, a : Value) -> Value

Population count (count number of 1 bits)

#
FunctionBuilder::print

fn FunctionBuilder::print(self : FunctionBuilder) -> String

Print using FunctionBuilder

#
FunctionBuilder::return_

fn FunctionBuilder::return_(self : FunctionBuilder, values : Array[Value]) -> Unit

Return from function

#
FunctionBuilder::rotl

fn FunctionBuilder::rotl(self : FunctionBuilder, a : Value, b : Value) -> Value

Rotate left

#
FunctionBuilder::rotr

fn FunctionBuilder::rotr(self : FunctionBuilder, a : Value, b : Value) -> Value

Rotate right

#
FunctionBuilder::sdiv

fn FunctionBuilder::sdiv(self : FunctionBuilder, a : Value, b : Value) -> Value

Signed integer divide

#
FunctionBuilder::select

fn FunctionBuilder::select(self : FunctionBuilder, cond : Value, a : Value, b : Value) -> Value

Conditional select: cond ? a : b

#
FunctionBuilder::sextend

fn FunctionBuilder::sextend(self : FunctionBuilder, ty : Type, a : Value) -> Value

Sign extend (e.g., i32 -> i64)

#
FunctionBuilder::sextend16

fn FunctionBuilder::sextend16(self : FunctionBuilder, ty : Type, a : Value) -> Value

Sign extend from 16 bits (in-place, keeps the same type) Similar to ireduce(I16) + sextend(ty)

#
FunctionBuilder::sextend32

fn FunctionBuilder::sextend32(self : FunctionBuilder, a : Value) -> Value

Sign extend from 32 bits to 64 bits Similar to ireduce(I32) + sextend(I64)

#
FunctionBuilder::sextend8

fn FunctionBuilder::sextend8(self : FunctionBuilder, ty : Type, a : Value) -> Value

Sign extend from 8 bits (in-place, keeps the same type) Similar to ireduce(I8) + sextend(ty)

#
FunctionBuilder::sint_to_fcvt

fn FunctionBuilder::sint_to_fcvt(self : FunctionBuilder, ty : Type, a : Value) -> Value

Signed int to float

#
FunctionBuilder::smulh

fn FunctionBuilder::smulh(self : FunctionBuilder, a : Value, b : Value) -> Value

Signed multiply high (i64 only)

#
FunctionBuilder::srem

fn FunctionBuilder::srem(self : FunctionBuilder, a : Value, b : Value) -> Value

Signed integer remainder

#
FunctionBuilder::sshr

fn FunctionBuilder::sshr(self : FunctionBuilder, a : Value, b : Value) -> Value

Signed shift right

#
FunctionBuilder::store_ptr

fn FunctionBuilder::store_ptr(self : FunctionBuilder, ty : Type, base : Value, value : Value, offset : Value) -> Unit

Store to raw pointer (no bounds checking) For trampoline code that operates on host memory

#
FunctionBuilder::store_ptr_narrow

fn FunctionBuilder::store_ptr_narrow(self : FunctionBuilder, bits : Int, base : Value, value : Value, offset : Value) -> Unit

Store narrow value to raw pointer (no bounds checking) Stores the low 'bits' bits of value to memory

#
FunctionBuilder::switch_to_block

fn FunctionBuilder::switch_to_block(self : FunctionBuilder, block : Block) -> Unit

Switch to a different block for emitting instructions

#
FunctionBuilder::trap

fn FunctionBuilder::trap(self : FunctionBuilder, reason : String) -> Unit

Trap/unreachable

#
FunctionBuilder::udiv

fn FunctionBuilder::udiv(self : FunctionBuilder, a : Value, b : Value) -> Value

Unsigned integer divide

#
FunctionBuilder::uextend

fn FunctionBuilder::uextend(self : FunctionBuilder, ty : Type, a : Value) -> Value

Zero extend (e.g., i32 -> i64)

#
FunctionBuilder::uint_to_fcvt

fn FunctionBuilder::uint_to_fcvt(self : FunctionBuilder, ty : Type, a : Value) -> Value

Unsigned int to float

#
FunctionBuilder::umulh

fn FunctionBuilder::umulh(self : FunctionBuilder, a : Value, b : Value) -> Value

Unsigned multiply high (i64 only)

#
FunctionBuilder::urem

fn FunctionBuilder::urem(self : FunctionBuilder, a : Value, b : Value) -> Value

Unsigned integer remainder

#
FunctionBuilder::ushr

fn FunctionBuilder::ushr(self : FunctionBuilder, a : Value, b : Value) -> Value

Unsigned shift right

#
FunctionBuilder::v128_and

fn FunctionBuilder::v128_and(self : FunctionBuilder, a : Value, b : Value) -> Value

#
FunctionBuilder::v128_andnot

fn FunctionBuilder::v128_andnot(self : FunctionBuilder, a : Value, b : Value) -> Value

#
FunctionBuilder::v128_anytrue

fn FunctionBuilder::v128_anytrue(self : FunctionBuilder, a : Value) -> Value

#
FunctionBuilder::v128_bitselect

fn FunctionBuilder::v128_bitselect(self : FunctionBuilder, a : Value, b : Value, c : Value) -> Value

#
FunctionBuilder::v128_const

fn FunctionBuilder::v128_const(self : FunctionBuilder, bytes : Bytes) -> Value

v128_const - emit a V128 constant

#
FunctionBuilder::v128_extract16s

fn FunctionBuilder::v128_extract16s(self : FunctionBuilder, vec : Value, lane : Int) -> Value

#
FunctionBuilder::v128_extract16u

fn FunctionBuilder::v128_extract16u(self : FunctionBuilder, vec : Value, lane : Int) -> Value

#
FunctionBuilder::v128_extract32

fn FunctionBuilder::v128_extract32(self : FunctionBuilder, vec : Value, lane : Int) -> Value

#
FunctionBuilder::v128_extract64

fn FunctionBuilder::v128_extract64(self : FunctionBuilder, vec : Value, lane : Int) -> Value

#
FunctionBuilder::v128_extract8s

fn FunctionBuilder::v128_extract8s(self : FunctionBuilder, vec : Value, lane : Int) -> Value

Extract a lane from a v128 value

#
FunctionBuilder::v128_extract8u

fn FunctionBuilder::v128_extract8u(self : FunctionBuilder, vec : Value, lane : Int) -> Value

#
FunctionBuilder::v128_extract_f32

fn FunctionBuilder::v128_extract_f32(self : FunctionBuilder, vec : Value, lane : Int) -> Value

#
FunctionBuilder::v128_extract_f64

fn FunctionBuilder::v128_extract_f64(self : FunctionBuilder, vec : Value, lane : Int) -> Value

#
FunctionBuilder::v128_load_lane_with_addr

fn FunctionBuilder::v128_load_lane_with_addr(self : FunctionBuilder, opcode : VectorMemoryOp, effective_addr : Value, vec : Value) -> Value

SIMD load lane with effective address and existing vector

#
FunctionBuilder::v128_load_with_addr

fn FunctionBuilder::v128_load_with_addr(self : FunctionBuilder, opcode : VectorMemoryOp, effective_addr : Value) -> Value

SIMD load with effective address (for complex SIMD loads)

#
FunctionBuilder::v128_not

fn FunctionBuilder::v128_not(self : FunctionBuilder, a : Value) -> Value

Bitwise operations on v128

#
FunctionBuilder::v128_or

fn FunctionBuilder::v128_or(self : FunctionBuilder, a : Value, b : Value) -> Value

#
FunctionBuilder::v128_replace16

fn FunctionBuilder::v128_replace16(self : FunctionBuilder, vec : Value, val : Value, lane : Int) -> Value

#
FunctionBuilder::v128_replace32

fn FunctionBuilder::v128_replace32(self : FunctionBuilder, vec : Value, val : Value, lane : Int) -> Value

#
FunctionBuilder::v128_replace64

fn FunctionBuilder::v128_replace64(self : FunctionBuilder, vec : Value, val : Value, lane : Int) -> Value

#
FunctionBuilder::v128_replace8

fn FunctionBuilder::v128_replace8(self : FunctionBuilder, vec : Value, val : Value, lane : Int) -> Value

Replace a lane in a v128 value

#
FunctionBuilder::v128_replace_f32

fn FunctionBuilder::v128_replace_f32(self : FunctionBuilder, vec : Value, val : Value, lane : Int) -> Value

#
FunctionBuilder::v128_replace_f64

fn FunctionBuilder::v128_replace_f64(self : FunctionBuilder, vec : Value, val : Value, lane : Int) -> Value

#
FunctionBuilder::v128_shuffle

fn FunctionBuilder::v128_shuffle(self : FunctionBuilder, a : Value, b : Value, lanes : FixedArray[Int]) -> Value

Shuffle lanes from two v128 values

#
FunctionBuilder::v128_splat16

fn FunctionBuilder::v128_splat16(self : FunctionBuilder, val : Value) -> Value

#
FunctionBuilder::v128_splat32

fn FunctionBuilder::v128_splat32(self : FunctionBuilder, val : Value) -> Value

#
FunctionBuilder::v128_splat64

fn FunctionBuilder::v128_splat64(self : FunctionBuilder, val : Value) -> Value

#
FunctionBuilder::v128_splat8

fn FunctionBuilder::v128_splat8(self : FunctionBuilder, val : Value) -> Value

v128_splat - broadcast a scalar to all lanes

#
FunctionBuilder::v128_splat_f32

fn FunctionBuilder::v128_splat_f32(self : FunctionBuilder, val : Value) -> Value

#
FunctionBuilder::v128_splat_f64

fn FunctionBuilder::v128_splat_f64(self : FunctionBuilder, val : Value) -> Value

#
FunctionBuilder::v128_store_lane_with_addr

fn FunctionBuilder::v128_store_lane_with_addr(self : FunctionBuilder, opcode : VectorMemoryOp, effective_addr : Value, vec : Value) -> Unit

SIMD store lane with effective address and vector (void)

#
FunctionBuilder::v128_swizzle

fn FunctionBuilder::v128_swizzle(self : FunctionBuilder, a : Value, b : Value) -> Value

Swizzle lanes using indices from another v128

#
FunctionBuilder::v128_xor

fn FunctionBuilder::v128_xor(self : FunctionBuilder, a : Value, b : Value) -> Value

#
GlobalValue

pub struct GlobalValue {
id : Int
// private fields
}

Function-owned handle to an interned global-value declaration.
impl Eq for GlobalValue
impl Hash for GlobalValue

#
GlobalValueData

pub(all) enum GlobalValueData {
ContextField(ContextField, GlobalValueStability, AliasRegion)
} derive(Eq, Hash,
Debug
)

Function-scoped global-value declaration.

#
GlobalValueData::context_field

fn GlobalValueData::context_field(field : ContextField, stability : GlobalValueStability, result_region : AliasRegion) -> GlobalValueData

#
GlobalValueStability

pub(all) enum GlobalValueStability {
Stable
Mutable
} derive(Eq, Hash,
Debug
)

Whether a context field may change while the function is executing.

Stable grants reuse permission for one invocation. Mutable requires invalidation at calls and writes that may affect the context region.

#
Inst

pub struct Inst {
id : Int
results : Array[Value]
opcode : Opcode
args : Array[Value]
operands : Array[Value]
metadata : Array[Metadata]
// private fields
}

Instruction - an SSA instruction that produces a value.

The public fields are exposed for zero-copy inspection. Callers must not mutate results, opcode, args, operands, or metadata directly. Construct instructions through FunctionBuilder or Function::new_inst and add metadata through add_metadata.
impl Eq for Inst
impl Hash for Inst

#
Inst::add_metadata

fn Inst::add_metadata(self : Inst, metadata : Metadata) -> Unit

#
Inst::all_results

fn Inst::all_results(self : Inst) -> Array[Value]

Get all results of this instruction

#
Inst::first_result

fn Inst::first_result(self : Inst) -> Value?

Get primary result of this instruction (first result or None)

#
IntBinaryOp

pub(all) enum IntBinaryOp {
Add
Sub
Mul
UnsignedMulHigh
SignedMulHigh
SignedDiv
UnsignedDiv
SignedRem
UnsignedRem
And
Or
Xor
ShiftLeft
SignedShiftRight
UnsignedShiftRight
RotateLeft
RotateRight
} derive(Eq, Hash,
Debug
)

#
IntCC

pub(all) enum IntCC {
Eq
Ne
Slt
Sle
Sgt
Sge
Ult
Ule
Ugt
Uge
} derive(Eq, Hash,
Debug
)

Integer comparison condition codes

#
IntUnaryOp

pub(all) enum IntUnaryOp {
Not
CountLeadingZeros
CountTrailingZeros
PopulationCount
} derive(Eq, Hash,
Debug
)

#
Loop

pub struct Loop {
header : Int
blocks : Array[Int]
back_edges : Array[(Int, Int)]
} derive(
Debug
)

#
Loop::contains

fn Loop::contains(self : Loop, block_id : Int) -> Bool

#
MemoryOp

pub(all) enum MemoryOp {
Load(Type)
Store(Type)
LoadNarrow(Type, Int, Bool)
StoreNarrow(Int)
Vector(VectorMemoryOp)
} derive(Eq, Hash,
Debug
)

Generic memory semantics over an explicit address and offset operand.

#
Metadata

pub(all) enum Metadata {
SourceLoc(String)
Comment(String)
} derive(Eq,
Debug
)

#
Opcode

pub(all) enum Opcode {
Scalar(ScalarOp)
Call(CallOp)
Memory(MemoryOp)
GlobalValue(GlobalValue)
Ext(ExtOp, Signature)
Vector(VectorOp)
} derive(Eq, Hash,
Debug
)

Opcode - the operation performed by an instruction
impl Show for Opcode

#
OptLevel

pub(all) enum OptLevel {
O0
O1
O2
O3
}

Optimization level
  • 0: No optimization
  • 1: Inexpensive optimizations (constant folding, alias cleanup, DCE)
  • 2: Default optimizations (adds e-graph, global GVN, and CFG transforms)
  • 3: Aggressive optimizations (includes loop optimizations)

#
OptLevel::from_int

fn OptLevel::from_int(n : Int) -> OptLevel

Parse optimization level from integer

#
OptResult

pub struct OptResult {
changed : Bool
} derive(Eq)

Result of an optimization pass

#
OptimizationMetricsSink

type OptimizationMetricsSink

Optional embedding hook for optimization metrics.

The embedder owns the clock and recorder so MilkIR remains independent of any runtime, operating system, or metrics output format.

#
OptimizationMetricsSink::new

fn OptimizationMetricsSink::new(now_us : () -> Int64, record : (OptimizationPassMetric) -> Unit) -> OptimizationMetricsSink

#
OptimizationPassMetric

pub struct OptimizationPassMetric {
name : String
duration_us : Int64
before_insts : Int
after_insts : Int
changed : Bool
egraph_classes : Int?
egraph_nodes : Int?
egraph_rule_apps : Int?
work_done : Int?
budget_exhausted : Bool?
}

Metrics emitted for one optimization pass.

#
ScalarOp

pub(all) enum ScalarOp {
IntConst(Int64)
FloatConst32(UInt)
FloatConst64(UInt64)
IntBinary(IntBinaryOp)
IntUnary(IntUnaryOp)
IntCompare(IntCC)
FloatBinary(FloatBinaryOp)
FloatUnary(FloatUnaryOp)
FloatCompare(FloatCC)
Convert(ConversionOp)
SignExtendFrom(Int)
Select
Copy
} derive(Eq, Hash,
Debug
)

#
Signature

pub struct Signature {
params : Array[Type]
results : Array[Type]
} derive(Eq, Hash,
Debug
)

#
Signature::Signature

fn Signature::Signature(params : Array[Type], results : Array[Type]) -> Signature

#
Terminator

pub(all) enum Terminator {
Jump(Int, Array[Value])
Branch(Value, Int, Array[Value], Int, Array[Value])
Brz(Value, Int, Int)
Brnz(Value, Int, Int)
BrTable(Value, Array[Int], Int)
Return(Array[Value])
Trap(String)
TrapExit(String)
} derive(
Debug
)

Terminator - how a basic block ends

#
Type

pub(all) enum Type {
I32
I64
F32
F64
V128
Ptr
Ref
CallableRef
OpaqueRef
} derive(Eq, Hash,
Debug
)

IR type system for scalar, vector, pointer, and reference-typed values.

Ptr, Ref, CallableRef, and OpaqueRef are fixed-width 64-bit carriers. Their representation does not vary with the host pointer width.
impl Show for Type

#
Value

pub struct Value {
id : Int
ty : Type
// private fields
}

IR Value - represents a virtual register in SSA form Each value is defined exactly once and can be used multiple times
impl Eq for Value
impl Hash for Value

#
VectorBitwiseOp

pub(all) enum VectorBitwiseOp {
Not
And
AndNot
Or
Xor
Bitselect
} derive(Eq, Hash,
Debug
)

#
VectorConversionOp

pub(all) enum VectorConversionOp {
TruncSatF32ToI32(VectorSignedness)
TruncSatF64ToI32Zero(VectorSignedness)
ConvertI32ToF32(VectorSignedness)
ConvertLowI32ToF64(VectorSignedness)
DemoteF64ToF32Zero
PromoteLowF32ToF64
} derive(Eq, Hash,
Debug
)

#
VectorExtension

pub(all) enum VectorExtension {
None
Signed
Unsigned
} derive(Eq, Hash,
Debug
)

#
VectorFloatBinaryOp

pub(all) enum VectorFloatBinaryOp {
Add
Sub
Mul
Div
Min
Max
PseudoMin
PseudoMax
} derive(Eq, Hash,
Debug
)

#
VectorFloatCompareOp

pub(all) enum VectorFloatCompareOp {
Eq
Ne
Lt
Gt
Le
Ge
} derive(Eq, Hash,
Debug
)

#
VectorFloatLane

pub(all) enum VectorFloatLane {
F32
F64
} derive(Eq, Hash,
Debug
)

#
VectorFloatUnaryOp

pub(all) enum VectorFloatUnaryOp {
Abs
Neg
Sqrt
Ceil
Floor
Trunc
Nearest
} derive(Eq, Hash,
Debug
)

#
VectorFmaOp

pub(all) enum VectorFmaOp {
Add
NegatedAdd
} derive(Eq, Hash,
Debug
)

#
VectorHalf

pub(all) enum VectorHalf {
Low
High
} derive(Eq, Hash,
Debug
)

#
VectorIntBinaryOp

pub(all) enum VectorIntBinaryOp {
Add
Sub
Mul
AddSaturating(VectorSignedness)
SubSaturating(VectorSignedness)
Min(VectorSignedness)
Max(VectorSignedness)
AverageUnsigned
ExtMul(VectorHalf, VectorSignedness)
Dot16To32Signed
Q15MulrSaturating
} derive(Eq, Hash,
Debug
)

#
VectorIntCompareOp

pub(all) enum VectorIntCompareOp {
Eq
Ne
Lt(VectorSignedness)
Gt(VectorSignedness)
Le(VectorSignedness)
Ge(VectorSignedness)
} derive(Eq, Hash,
Debug
)

#
VectorIntLane

pub(all) enum VectorIntLane {
I8
I16
I32
I64
} derive(Eq, Hash,
Debug
)

#
VectorIntShiftOp

pub(all) enum VectorIntShiftOp {
Left
Right(VectorSignedness)
} derive(Eq, Hash,
Debug
)

#
VectorIntUnaryOp

pub(all) enum VectorIntUnaryOp {
Abs
Neg
Popcnt
Extend(VectorHalf, VectorSignedness)
ExtAddPairwise(VectorSignedness)
} derive(Eq, Hash,
Debug
)

#
VectorLane

pub(all) enum VectorLane {
I8
I16
I32
I64
F32
F64
} derive(Eq, Hash,
Debug
)

#
VectorMemoryOp

pub(all) enum VectorMemoryOp {
LoadExtend(VectorIntLane, VectorSignedness)
LoadSplat(VectorIntLane)
LoadZero(VectorIntLane)
LoadLane(VectorIntLane, Int)
StoreLane(VectorIntLane, Int)
} derive(Eq, Hash,
Debug
)

Vector memory operations consume an already checked effective address. Source-language memory indices, alignment hints, and offsets belong to the frontend.

#
VectorOp

pub(all) enum VectorOp {
Const(Bytes)
Splat(VectorLane)
ExtractLane(VectorLane, VectorExtension, Int)
ReplaceLane(VectorLane, Int)
Shuffle(FixedArray[Int])
Swizzle
Bitwise(VectorBitwiseOp)
Predicate(VectorPredicateOp)
IntUnary(VectorIntUnaryOp, VectorIntLane)
IntBinary(VectorIntBinaryOp, VectorIntLane)
IntShift(VectorIntShiftOp, VectorIntLane)
IntCompare(VectorIntCompareOp, VectorIntLane)
Narrow(VectorIntLane, VectorSignedness)
FloatUnary(VectorFloatUnaryOp, VectorFloatLane)
FloatBinary(VectorFloatBinaryOp, VectorFloatLane)
FloatCompare(VectorFloatCompareOp, VectorFloatLane)
Convert(VectorConversionOp)
Relaxed(VectorRelaxedOp)
} derive(Eq, Hash,
Debug
)

Language-neutral V128 operations normalized by semantic family.

#
VectorPredicateOp

pub(all) enum VectorPredicateOp {
AnyTrue
AllTrue(VectorIntLane)
Bitmask(VectorIntLane)
} derive(Eq, Hash,
Debug
)

#
VectorRelaxedOp

pub(all) enum VectorRelaxedOp {
Swizzle
TruncF32ToI32(VectorSignedness)
TruncF64ToI32Zero(VectorSignedness)
Fma(VectorFloatLane, VectorFmaOp)
LaneSelect(VectorIntLane)
Min(VectorFloatLane)
Max(VectorFloatLane)
Q15MulrSigned
Dot8To16Signed
Dot8To32AddSigned
} derive(Eq, Hash,
Debug
)

#
VectorSignedness

pub(all) enum VectorSignedness {
Signed
Unsigned
} derive(Eq, Hash,
Debug
)

#
clear_optimization_metrics_sink

fn clear_optimization_metrics_sink() -> Unit

#
install_optimization_metrics_sink

fn install_optimization_metrics_sink(sink : OptimizationMetricsSink) -> Unit

#
instruction_count

fn instruction_count(func : Function) -> Int

Count IR instructions in a function.

#
optimize

fn optimize(func : Function) -> OptResult

Run default optimizations (Cranelift-aligned O2 path).

#
optimize_with_level

fn optimize_with_level(func : Function, level : OptLevel) -> OptResult

Run optimizations based on level