machv

Target-neutral semantic machine IR

compiler
machine-ir
codegen
jit
moon add Milky2018/machv@0.8.2
Download zip
Author
Version
0.8.2
License
Apache-2.0
Last updated
2 days ago
Downloads
164
README

#MachV

MachV is a virtual-register machine IR. It represents a function after target instruction selection and before physical register allocation and machine-code emission.

MilkIR | | target lowering and instruction selection v MachV with virtual registers | | machv_regalloc v allocation locations and edits, or rewritten MachV | | machv_emit v machine code and relocation metadata

At this stage, high-level operations have become machine-oriented instructions, but most values still use virtual registers such as v0 and v1. Register allocation later decides whether each virtual register lives in a physical register or a spill slot.

#Start with one small function

The following example builds a 64-bit integer addition:

///|
test "build a 64-bit add function" {
let builder = FunctionBuilder::FunctionBuilder("add64")

let lhs = builder.add_param(Int)
let rhs = builder.add_param(Int)
builder.add_result(I64)

let sum = builder.new_vreg(Int)
builder.append(Add(true), uses=[Virtual(lhs), Virtual(rhs)], defs=[
{ reg: Virtual(sum) },
])
|> ignore
builder.terminate(Return([Virtual(sum)]))

let func = builder.finish()
inspect(
func.print(),
content=(
#|machv add64(v0:int, v1:int) {
#|block0:
#| v2 = add v0, v1
#| ret v2
#|}
#|
),
)
debug_inspect(func.get_result_kinds(), content="[I64]")
}

Read the generated MachV from top to bottom:

machv add64(v0:int, v1:int) { two integer-register parameters block0: execution starts in block0 v2 = add v0, v1 read v0 and v1, then define v2 ret v2 return the value in v2 }

The four builder operations correspond directly to the function shape:

  1. add_param(Int) creates an incoming virtual register in the integer register class.
  2. add_result(I64) records the full result kind used by ABI decisions.
  3. append(...) adds one machine instruction to the current block.
  4. terminate(...) gives the block its control-flow exit.

FunctionBuilder creates block0 automatically, so a straight-line function can emit instructions immediately.

#How MachV differs from MilkIR

MilkIR and MachV describe the same program at different levels:

MilkIRMachV
Represents target-independent program semantics.Represents instructions selected for a machine target.
Values have language-level types such as I32, F64, and Ptr.Registers have allocation classes such as Int, Float64, and Vector.
An instruction carries SSA operands and results.An instruction carries explicit register uses, definitions, and allocation constraints.
Control flow uses typed SSA block parameters and jump arguments.Control flow uses register-level block arguments and machine branch forms.
Most semantic optimization happens before instruction selection.Machine peephole optimization, register allocation, and encoding follow instruction selection.

MachV is not assembly text. Virtual registers, symbolic call targets, abstract stack locations, and allocation constraints still need later processing.

#Registers: virtual, physical, and writable

MachV uses four related types:

TypeMeaningExample
VRegA virtual register that still needs a location.v2
PRegA physical register identified by target register index and class.{ index: 0, class: Int }
RegEither Virtual(vreg) or Physical(preg).Virtual(sum)
WritableA register used as an instruction destination.{ reg: Virtual(sum) }

The Writable wrapper makes definitions visibly different from inputs. In the addition example:

uses = [v0, v1] defs = [v2]

This information is essential for liveness analysis and register allocation. The allocator needs to know where each old value is read and where each new value is written.

#Register classes and value kinds

RegClass answers an allocation question: which register bank can hold this value?

RegClassRegister bank
IntGeneral-purpose integer and pointer registers.
Float3232-bit floating-point registers.
Float6464-bit floating-point registers.
Vector128-bit SIMD registers.

ValueKind keeps information needed by function signatures and ABI lowering: I32, I64, F32, F64, V128, or Ptr.

Several value kinds can share one register class. Both I32 and I64, for example, use RegClass::Int. Function parameters print their register classes, while FunctionBuilder::add_result records result kinds separately for ABI decisions; get_result_kinds() therefore reports I64 in the example above.

#Instructions: opcode, uses, definitions, and constraints

Each Inst contains:

  • an Opcode describing the operation;
  • uses, the registers read by the instruction;
  • defs, the registers written by the instruction;
  • optional constraints for operands that must use particular physical registers.

For integer arithmetic, the Boolean carried by opcodes such as Add, Sub, and Mul selects operand width: true means 64-bit and false means 32-bit. Therefore Add(true) prints as add, while Add(false) prints as add32.

The order of uses and defs follows the operand contract of the opcode. For example:

Add(true) uses [lhs, rhs] defs [result] Load(I64, 8) uses [base] defs [loaded] Store(I64, 8) uses [value, base] defs [] Move uses [source] defs [destination]

MachV also contains comparisons, conversions, calls, traps, stack operations, SIMD operations, and target-oriented instruction forms used by the lowering packages.

#Operand constraints

Most operands use Any, allowing register allocation to choose a location. FixedReg(preg) requires an operand to use a particular physical register, which is useful for calling conventions and instructions with fixed-register requirements.

///|
test "attach a fixed-register constraint" {
let builder = FunctionBuilder::FunctionBuilder("fixed_result")
let src = builder.add_param(Int)
let dst = builder.new_vreg(Int)
let required : PReg = { index: 1, class: Int }

builder.append(Move, uses=[Virtual(src)], defs=[{ reg: Virtual(dst) }], def_constraints=[
FixedReg(required),
])
|> ignore
builder.terminate(Return([Virtual(dst)]))

let func = builder.finish()
inspect(
func.blocks[0].insts[0].def_constraints[0] == FixedReg(required),
content="true",
)
}

Constraint arrays correspond positionally to uses or defs. An omitted entry behaves as Any.

#Blocks and terminators

A MachV block contains instructions followed by one terminator. create_block() returns the numeric block ID used by branch and jump terminators. Call switch_to_block(id) before emitting into another block.

///|
test "build conditional control flow" {
let builder = FunctionBuilder::FunctionBuilder("choose_path")
let condition = builder.add_param(Int)
let then_block = builder.create_block()
let else_block = builder.create_block()

builder.terminate(Branch(Virtual(condition), then_block, else_block))

builder.switch_to_block(then_block)
builder.terminate(Return([]))

builder.switch_to_block(else_block)
builder.terminate(Return([]))

let func = builder.finish()
inspect(func.blocks.length(), content="3")
inspect(func.blocks[0].terminator is Some(Branch(_, _, _)), content="true")
inspect(func.blocks[1].terminator is Some(Return([])), content="true")
inspect(func.blocks[2].terminator is Some(Return([])), content="true")
}

The main terminators are:

TerminatorPurpose
Jump(target, args)Transfer control to one block and pass register arguments.
Branch(condition, then_id, else_id)Choose between two blocks.
BranchCmp(...)Compare two registers and branch without materializing a Boolean value.
BranchZero(...)Branch on a zero or nonzero register value.
BranchCmpImm(...)Compare a register with an immediate and branch.
BrTable(index, targets, default)Dispatch through a jump table.
Return(values)Return registers to the caller.
Trap(payload)End execution with an embedding-defined trap payload.

Every block passed to register allocation or emission needs a terminator.

#Calls, ABI data, and stack state

MachV records the machine-level information needed around calls:

  • CallConventionLayout describes argument and result registers plus overflow stack layout.
  • EmbeddingABI groups the calling convention with reserved-register and context-layout data.
  • call opcodes record argument counts, result register classes, symbolic targets, and clobber classes.
  • max_outgoing_args_size records the largest outgoing stack-argument area needed by the function.
  • num_spill_slots records spill space assigned during register allocation.

Call targets can remain symbolic through MachV and machine-code emission. The embedding application resolves those symbols when installing generated code.

#Construction rules

FunctionBuilder records the instruction shape supplied by the caller; it does not infer an opcode's operands. A lowering implementation should maintain these rules:

  1. Give every block exactly one terminator.
  2. Supply uses and defs in the order required by the opcode.
  3. Use register classes compatible with the selected instruction form.
  4. Keep constraint arrays aligned with their corresponding operands.
  5. Use Function::print() in lowering tests so instruction and control-flow mistakes are easy to inspect.

Downstream register-allocation and emission tests provide the strongest end-to-end check that a constructed function satisfies the selected target's requirements.

#Packages

PackagePurpose
Milky2018/machvFunction model, FunctionBuilder, common instruction types, and printing.
Milky2018/machv/abiVirtual and physical registers, operand constraints, calling conventions, and embedding ABI data.
Milky2018/machv/instrMachine instructions, calls, traps, and terminators.
Milky2018/machv/blockBasic-block representation.
Milky2018/machv/isaISA descriptors and target selection.
Milky2018/machv/isa/aarch64AArch64 register descriptions.
Milky2018/machv/isa/amd64AMD64 register descriptions.

#Integration

Target lowering packages produce MachV functions. Milky2018/machv_regalloc assigns their virtual registers and plans spills and moves, then Milky2018/machv_emit emits machine-code bytes and symbolic metadata.

#
Block

MachV block - a basic block in MachV

#
CallClobberClass

Call-site clobber class for register allocation and optimization barriers.

#
CallType

Call type - classifies instructions that behave like calls CallType enum for different call variants

#
CmpKind

Comparison kind for integer comparisons

#
CodeSymbol

Symbolic name for code owned by the embedding.

MachV treats this as an opaque relocation key. Frontends may use the scope and ordinal to map their own function identifiers to this generic code target without exposing those identifiers in MachV opcodes.

#
Cond

AArch64 condition codes (for conditional branches and traps) Condition codes for comparisons

#
ExtendKind

Extend kind - how to extend a value

#
ExternalName

Symbolic name for a runtime or external helper address.

MachV treats this as an opaque relocation key. Embedding packages own the mapping from names to native addresses.

#
FCmpKind

Comparison kind for float comparisons

#
FloatToIntKind

Float to Int conversion kind Encodes: source float type, destination int type, signedness

#
IndexExtend

AArch64 addressing-mode extend for register-offset loads/stores.

Mirrors Cranelift's AMode.{RegExtended,RegScaledExtended} options:
  • None: use X register offset (LSL option)
  • Uxtw/Sxtw: extend Wm to X, optionally scaled by access size (S bit)

#
Inst

MachV instruction - a machine-level instruction with virtual registers Operand constraints for fixed register allocation

#
IntToFloatKind

Int to Float conversion kind Encodes: source int type, destination float type, signedness

#
LaneSize

SIMD lane size for vector operations

#
MemType

Memory type for load/store

#
Opcode

MachV opcode - machine-level operation (target-independent subset)

#
OperandConstraint

Operand constraint for register allocation Fixed register constraint handling

#
PReg

using @Milky2018/machv/abi { type PReg }

Physical register - a real machine register

#
Reg

using @Milky2018/machv/abi { type Reg }

A register reference - either virtual or physical

#
RegClass

Register class - categorizes registers by their purpose

#
RelocTarget

Target for a relocatable code address or direct call.

#
SIMDCmpKind

SIMD comparison kind for integer vector comparisons

#
SIMDFCmpKind

SIMD floating-point comparison kind

#
ShiftType

Shift type for shifted operand instructions

#
Terminator

MachV terminator - how a block ends

#
VReg

using @Milky2018/machv/abi { type VReg }

Virtual register - an abstract register before register allocation

#
Writable

A writable register reference (for instruction destinations)

#
Function

pub struct Function {
name : String
params : Array[
VReg
]
results : Array[
RegClass
]
result_kinds : Array[ValueKind]
blocks : Array[
Block
]
next_vreg_id : Int
num_spill_slots : Int
param_pregs : Array[
PReg
?]
int_stack_params : Int
max_outgoing_args_size : Int
uses_context_cache_0_source : Bool
}

MachV function - a complete machine-level function in virtual-register form.
impl Show for Function

#
Function::Function

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

#
Function::add_param_preg

fn Function::add_param_preg(self : Function, preg :
PReg
?) -> Unit

Add a parameter physical register mapping (called by register allocator)

#
Function::add_result

fn Function::add_result(self : Function, class :
RegClass
) -> Unit

#
Function::add_result_kind

fn Function::add_result_kind(self : Function, ty : ValueKind) -> Unit

Add a result kind with full kind information for multi-value returns

#
Function::blocks

#
Function::calls_multi_value_function_for_call_conv

fn Function::calls_multi_value_function_for_call_conv(self : Function, call_conv :
CallConventionLayout
) -> Bool

Check whether this function calls anything whose result count exceeds the selected call convention's register-result capacity.

#
Function::clone_base

fn Function::clone_base(self : Function) -> Function

Clone the base structure of a function for regalloc transformations. Copies: name, next_vreg_id, int_stack_params, max_outgoing_args_size, uses_context_cache_0_source Empty: params, results, result_kinds, blocks, param_pregs Zero: num_spill_slots

#
Function::get_blocks

#
Function::get_max_outgoing_args_size

fn Function::get_max_outgoing_args_size(self : Function) -> Int

#
Function::get_name

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

#
Function::get_num_spill_slots

fn Function::get_num_spill_slots(self : Function) -> Int

#
Function::get_param_pregs

fn Function::get_param_pregs(self : Function) -> Array[
PReg
?]

#
Function::get_params

#
Function::get_result_kinds

fn Function::get_result_kinds(self : Function) -> Array[ValueKind]

#
Function::has_calls

fn Function::has_calls(self : Function) -> Bool

Returns true if this function contains any call-like instruction.

#
Function::mark_uses_context_cache_0_source

fn Function::mark_uses_context_cache_0_source(self : Function) -> Unit

#
Function::needs_extra_results_ptr_for_call_conv

fn Function::needs_extra_results_ptr_for_call_conv(self : Function, call_conv :
CallConventionLayout
) -> Bool

Check if this function needs an embedding-provided extra-results pointer.

#
Function::new_block

#
Function::params

#
Function::print

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

#
Function::push_param

fn Function::push_param(self : Function, vreg :
VReg
) -> Unit

Push a parameter vreg directly (used during regalloc reconstruction)

#
Function::set_int_stack_params

fn Function::set_int_stack_params(self : Function, n : Int) -> Unit

Set the number of integer stack parameters (called during lowering)

#
Function::set_num_spill_slots

fn Function::set_num_spill_slots(self : Function, n : Int) -> Unit

Set the number of spill slots (called by register allocator)

#
Function::should_reserve_context_cache_0

fn Function::should_reserve_context_cache_0(self : Function) -> Bool

Returns true if context cache 0 caching should be enabled for this function.

#
Function::should_reserve_context_cache_1

fn Function::should_reserve_context_cache_1(self : Function) -> Bool

Returns true if context cache 1 pointer caching should be enabled for this function.

#
Function::update_max_outgoing_args_size

fn Function::update_max_outgoing_args_size(self : Function, size : Int) -> Unit

Update max outgoing args size if the new size is larger (called during lowering)

#
Function::uses_context_cache_0_source

fn Function::uses_context_cache_0_source(self : Function) -> Bool

#
Function::uses_context_cache_1

fn Function::uses_context_cache_1(self : Function) -> Bool

Returns true if the function loads the module context cache 1 from embedding context. Used to reserve a dedicated register for caching the context cache 1 pointer.

#
FunctionBuilder

pub struct FunctionBuilder {
func : Function
current_block :
Block

}

#
FunctionBuilder::FunctionBuilder

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

#
FunctionBuilder::add_result

fn FunctionBuilder::add_result(self : FunctionBuilder, kind : ValueKind) -> Unit

#
FunctionBuilder::create_block

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

#
FunctionBuilder::finish

#
FunctionBuilder::switch_to_block

fn FunctionBuilder::switch_to_block(self : FunctionBuilder, block_id : Int) -> Unit

#
FunctionBuilder::terminate

#
ValueKind

pub(all) enum ValueKind {
I32
I64
F32
F64
V128
Ptr
} derive(Eq,
Debug
)

Machine-level value kind needed after MilkIR lowering.

Keep this in MachV instead of carrying @milkir.Type through register allocation and emission.

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io