Construct importer output in Python¶
Complete example:
loom/docs/examples/integration/python-importer/
Loom's Python construction API is the target half of an importer. A frontend owns its source language's parsing, traversal, diagnostics, and source model. It translates the facts that survive that traversal into ordinary Loom IR instead of adapting its foreign AST into a Loom-owned compatibility layer.
The example begins at that stable boundary. It imports no frontend package and
defines no source-language nodes: two buffer arguments, one exported kernel,
and a fixed launch shape are sufficient to construct and serialize a complete
module using the hrx-loom wheel.
Construct the kernel envelope and body¶
KernelModuleSpec describes the source-independent ABI and launch facts.
create_kernel_module creates the target record, kernel.def, configuration
region, and body arguments, then returns typed handles for filling the body.
from loom.importers import (
KernelArgumentSpec,
KernelLaunchConfigSpec,
KernelModuleSpec,
create_kernel_module,
kernel_module_ops,
print_loom_module,
)
from loom.ir import INDEX, OFFSET, Module
from loom.verify import verify_module
TARGET_PRESET = "reference"
MODULE_OPS = kernel_module_ops(TARGET_PRESET)
def build_copy_module() -> Module:
"""Builds a kernel that copies one byte per workitem."""
shell = create_kernel_module(
KernelModuleSpec(
target_preset=TARGET_PRESET,
export_symbol="copy_64_bytes",
callee="copy_64_bytes",
arguments=(
KernelArgumentSpec(ordinal=0, name="source"),
KernelArgumentSpec(ordinal=1, name="target"),
),
launch_config=KernelLaunchConfigSpec(workgroup_size=(64, 1, 1)),
)
)
source = shell.body_arguments_by_ordinal[0]
target = shell.body_arguments_by_ordinal[1]
with shell.builder.insertion_block(shell.body_block):
workitem_index = shell.builder.kernel.workitem_dispatch_id(
dimension="x",
results=[INDEX],
name="workitem_index",
)
byte_offset = shell.builder.index.cast(
input=workitem_index,
results=[OFFSET],
name="byte_offset",
)
value = shell.builder.buffer.load_i8_u(
source=source,
byte_offset=byte_offset,
name="value",
)
shell.builder.buffer.store_i8(
value=value,
target=target,
byte_offset=byte_offset,
)
shell.builder.kernel.return_()
verify_module(shell.module, ops=MODULE_OPS).raise_if_errors()
return shell.module
The generated builder methods expose each Loom operation with its named
operands, attributes, and results. In the copy body,
kernel.workitem.dispatch.id produces a logical index. Buffer addressing
uses the physical offset domain, so index.cast makes that semantic boundary
visible before the load and store.
kernel_module_ops returns the operation declarations required by the selected
target preset. Passing the same declaration set to verification, printing, and
bytecode serialization keeps all three consumers on the module's exact
dialect surface. The example uses the portable reference target; an importer
that has selected an exact device supplies its target preset instead.
Running the example with an installed SDK prints canonical Loom text:
target.generic<reference> @reference
kernel.def target(@reference) export("copy_64_bytes") @copy_64_bytes() {
%wg_count_x = index.constant 1 : index
%wg_count_y = index.constant 1 : index
%wg_count_z = index.constant 1 : index
%wg_size_x = index.constant 64 : index
%wg_size_y = index.constant 1 : index
%wg_size_z = index.constant 1 : index
kernel.launch.config workgroups(%wg_count_x, %wg_count_y, %wg_count_z) workgroup_size(%wg_size_x, %wg_size_y, %wg_size_z) : index
} launch(%source: buffer, %target: buffer) {
%workitem_index = kernel.workitem.dispatch.id<x> : index
%byte_offset = index.cast %workitem_index : index to offset
%value = buffer.load.i8.u %source[%byte_offset]
buffer.store.i8 %value, %target[%byte_offset]
kernel.return
}
Verify before crossing the serialization boundary¶
verify_module(...).raise_if_errors() rejects an invalid producer result while
the source-specific importer still owns the context needed to explain it. A
verified Module can then cross either of Loom's equivalent serialization
boundaries: canonical text for people and source control, or .loombc for
compact loading and composition.
The example test writes bytecode, reads it into a new module, verifies while reading, and compares the recovered canonical text with the same exact golden:
from loom.format.bytecode.reader import read_module
from loom.format.bytecode.writer import write_module
bytecode = write_module(module, op_decls=MODULE_OPS)
loaded = read_module(bytecode, op_decls=MODULE_OPS, verify=True)
Source locations fit the same ownership split. A frontend records locations
while traversing its source and passes location_id to generated builder
methods. Loom retains those locations through text or bytecode and uses them in
later diagnostics; it does not need to own the frontend's AST after import.