Skip to content

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:

python importer.py > copy_64_bytes.loom
loom-format --check copy_64_bytes.loom
copy_64_bytes.loom
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.