Source modules and canonical text¶
Example files: loom/docs/examples/elementwise-transform/
A .loom file is a complete, independently verifiable module. It may contain
functions, templates, kernels, checks, command programs, target descriptions,
configuration declarations, and other symbols, but it does not inherit hidden
symbols or settings from the directory around it.
In this chapter, you will learn:
- what a source module owns;
- how definitions, declarations, libraries, and roots compose;
- why
.loomand.loombcare two encodings of the same program; - what canonical formatting preserves; and
- how source names carry information that the compiler should not discard.
A module verifies on its own¶
loom-format --check parses and verifies the complete module before checking
its canonical spelling:
An exact symbol reference must resolve to a definition or a compatible declaration in that module. The formatter does not search neighboring files, scan a library directory, or infer a dependency from a missing call. This makes the source boundary honest before a linker or build system is involved.
The composition example's command module demonstrates the pattern. It declares the kernel signature it calls, then defines a command program using that exact symbol:
Source: loom/docs/examples/elementwise-transform/model.loom
// The declaration makes the exact dependency explicit in this source module.
kernel.decl @elementwise_transform_f32(%workload_element_count: index) launch(%device_element_count: index, %input: buffer, %output: buffer)
// The command program owns the reusable schedule while buffers remain
// replaceable at issue time.
command.program.def public @elementwise_transform() launch(%input: buffer, %output: buffer) {
%element_count = index.constant 1009 : index
kernel.launch @elementwise_transform_f32[%element_count](%element_count, %input, %output) : [index](index, buffer, buffer)
command.return
}
kernel.decl states the dependency
and its workload and launch signatures. It does not say where the definition is
stored. The link invocation supplies modules that may satisfy declarations;
the source itself remains meaningful when stored as text, packaged as bytecode,
or provided from memory through the C API.
The same rule applies to exact func.call
and func.decl pairs. Contract-based
func.apply is different: it asks the
visible provider set for an implementation of a named contract rather than
naming one exact function symbol.
Definitions, declarations, and roots¶
A definition owns a body. A declaration owns the signature expected from another module or runtime boundary. Linking matches compatible declarations and definitions among the inputs it was explicitly given.
A root states which symbol matters for one operation. Roots may be chosen by a tool invocation, exported by a module, or selected through an embedding API. Starting from those roots, linking retains the reachable declarations, definitions, configuration, target records, and provider candidates needed to form the requested program. Unreachable private helpers and unselected providers can disappear.
This has two important consequences:
- Passing a library to the linker does not make every symbol in that library live.
- Passing one module does not authorize the linker to discover or pull in the transitive world. A declaration that no supplied module satisfies remains a declaration in a partial link and must be satisfied before a consumer that requires a closed program can use it.
The path or input order of a provider module does not give its templates priority. Template matching uses contracts, signatures, facts, requirements, and explicit provider priority. Module placement is a packaging choice, not a hidden overload-resolution rule.
Modules are not target boundaries¶
Reusable modules normally contain no selected target. A compile invocation specializes the executable roots it is compiling for a target profile; it does not turn every helper or every other root in the module into that target.
The motif below contains a wave32 provider and a portable provider. The module itself is still target-independent:
Source: loom/docs/examples/elementwise-transform/motif.loom
// These targetless helpers and providers deliberately omit `public`. An
// explicitly linked library can satisfy reachable dependencies from them
// without adding their implementation symbols to the public module interface.
func.def inline @double(%value: f32) -> (f32) {
%doubled = scalar.addf %value, %value : f32
func.return %doubled : f32
}
func.template<guide.elementwise_transform> requires [#target.subgroup.size<32>] priority(20) @wave32_elementwise_transform(%value: f32) -> (f32) {
%result = func.call @double(%value) : (f32) -> (f32)
func.return %result : f32
}
func.template<guide.elementwise_transform> priority(1) @portable_elementwise_transform(%value: f32) -> (f32) {
%result = func.call @double(%value) : (f32) -> (f32)
func.return %result : f32
}
The exact helper and both providers are private: none carries the public
modifier. Explicitly supplying this module as a library makes them visible to
the linker for the requested composition without exporting their names from
the resulting module interface.
The specialized provider is eligible only when facts for the selected function establish a subgroup size of 32. The fallback carries no target requirement. This keeps one library useful across target families while allowing exact specialization at the leaf that needs it.
Text and bytecode preserve the same program¶
.loom is canonical human-readable source. .loombc is the compact bytecode
encoding of the same linkable module. Bytecode is not native code and does not
imply that target selection or specialization has already happened.
Convert a module in either direction with loom-format:
loom-format motif.loom --from=text --to=bc --output=motif.loombc
loom-format motif.loombc --from=bc --to=text --output=motif.loom
The converter verifies the complete input before writing output. A bytecode
library can therefore be indexed and linked without reparsing text, while a
round trip back to .loom recovers canonical source for inspection, review, or
migration.
Comments, SSA names, source syntax choices, and intentional blank-line grouping are part of Loom's source representation and survive supported text/bytecode round trips. Optimization and target lowering may of course produce a different program; format conversion alone does not.
Canonical formatting is the source contract¶
Canonical text removes whitespace and layout churn while retaining the source structure people use to understand a program. Check several files without writing them:
Rewrite only files that are valid but noncanonical:
Each input parses and verifies independently. Invalid source is reported and is not rewritten. In-place mode reports how many files changed, remained canonical, or failed.
Canonical formatting matters beyond aesthetics. Stable text gives reviews, source migrations, compiler-report paths, and automated iteration a common representation. When a source-format migration changes one construct, the diff can show that construct rather than unrelated indentation or line wrapping.
Names should preserve program meaning¶
SSA names do not affect execution, but they are valuable source facts for
people, diagnostics, reports, and agents. A value named %batch_size says why
the value exists. A value named %fivehundredtwelve merely spells its current
literal in words.
When a constant has a program role, use that role:
When the literal itself is the only available meaning, use the compact numeric fallback. A type suffix distinguishes otherwise identical literals when needed:
%c512 = index.constant 512 : index
%c0_i32 = scalar.constant 0 : i32
%c0_f32 = scalar.constant 0.0 : f32
Literal equality does not imply semantic identity. Two values that both happen
to be 16 may still deserve names such as %channels_per_group and
%lane_partition_width when they represent different constraints. Folding
those names into %c16 would discard information the author already had.
The same principle applies above SSA values. Contract names describe reusable semantic demands, configuration names describe product choices, and symbol names describe the role an embedding or another module depends on. Target mnemonics and one model's branding belong in those names only when they are actually part of the contract.
Follow the composition¶
The source-to-artifacts walkthrough
links motif.loom, kernel.loom, and model.loom, selects one command root,
and compiles the reachable kernel for GFX11. Its
run.sh prints every public command before executing it, and the documentation
build regenerates the displayed Low products from that same path.
Use Format and verify source for the complete
loom-format task contract and Link and package
modules for archive, selective-link, config,
and plan commands.
Continue with Values, types, and shapes, then follow
the guide through functions, memory, kernels, facts, checks, and command
programs as those ownership boundaries become relevant. For exact source
forms, use the generated func,
kernel, and
command references.