Link and package modules¶
loom-link combines text and bytecode modules, applies configuration bindings,
and either merges primary inputs or retains the dependency closure of selected
roots. Its inputs are explicit: it never searches a directory or pulls in
modules that the invocation did not provide.
Inputs and libraries¶
Positional inputs are primary modules. Repeated --library=path options add
library modules whose exported definitions and template implementations are
available to the primary program. The root below declares @project_layer
locally, while the invocation supplies the library that defines it:
// The declaration states the exact callable contract required from a library.
func.decl @project_layer(%value: i32) -> (i32)
func.def public @entry(%value: i32) -> (i32) {
%result = func.call @project_layer(%value) : (i32) -> (i32)
func.return %result : i32
}
loom-link root.loom \
--library=layer.loom \
--library=kernels.loom \
--root=@entry \
--mode=link \
--to=bc \
--output=entry.loombc
Inputs may mix .loom text and .loombc bytecode. --from=auto, the default,
detects their encoding independently. --to=text|bc controls the one linked
module written to --output or standard output.
The mode determines whether library bodies enter that output. --mode=merge
copies every non-stripped symbol from the positional inputs and leaves every
--library module separate. Pass several modules positionally when they are
deliberately being flattened into one reusable module. --mode=link starts
from roots in the positional inputs and may pull reachable providers from the
explicit library universe.
Paths are frontend inputs and diagnostic identifiers, not symbol namespaces.
loom-link does not canonicalize a path into program identity or search for
undeclared files. The LoomC API can construct the same source universe entirely
in memory.
Primary inputs jointly form the direct source merge, so their private definitions may satisfy one another's declarations. At most one direct definition may own a global name. When no direct definition exists, a separate library must export the exact definition and exactly one library may provide it. A direct definition is never replaced merely because a supplied library contains the same name; competing definitions within the applicable ownership class are errors rather than input-order choices. These roles govern linkage, not reachability: a symbol becomes live because it is a root or reachable dependency, not because its source was supplied. Library order likewise does not override template matching; contracts, signatures, facts, requirements, and explicit template priority determine eligibility.
Inspect before linking¶
List indexed symbols without materializing an output module:
Print the link plan for one root:
The plan shows why each symbol is live before the linker streams the selected definitions. This is the first place to inspect an unexpected dependency, missing declaration target, or template candidate set. It is cheaper and more direct than debugging target lowering after the wrong source program was selected.
List the configuration schema visible to the input program:
This exposes the declared keys, types, and constraints an application or build must bind without scraping source text.
Link one program¶
Link mode starts from explicit roots and retains their reachable program:
loom-link model.loom \
--library=kernel.loom \
--library=motif.loom \
--mode=link \
--root=@elementwise_transform \
--to=text \
--output=elementwise-transform.loom
This produces a closed program for the facts known at the link boundary. In a targetless link it selects the portable provider and discards the unresolved wave32 alternative. A deployment-product link can make its target facts available before provider selection:
loom-link model.loom \
--library=kernel.loom \
--library=motif.loom \
--mode=link \
--root=@elementwise_transform \
--target=amdgpu:gfx11-generic \
--to=bc \
--output=elementwise-gfx11.loombc
The profile uses family:selector syntax. It specializes every reachable
kernel entry in each selective-link analysis module, allowing target
requirements to prune providers before their bodies enter the output. The
result is still ordinary standalone Loom bytecode, now closed for that target
profile. Use the same selector when emitting the device artifact. Omitting the
profile remains the correct path for portable libraries, partial links, and
JIT boundaries that will receive target facts later.
The source-to-artifacts walkthrough runs this target-aware link as a checked example. In either mode, unrelated private symbols can disappear as soon as the chosen boundary no longer needs them.
Repeated --root=@symbol options select several roots from one catalog. Add
--include-input-exports when exported symbols from the requester inputs
should join the explicit root set. Library exports remain resolution candidates
and never become roots merely because they are public in their source module.
Output visibility follows root intent rather than provider visibility. A
public library definition selected only to satisfy the requester becomes a
private dependency in the linked output. Name it as another root or provide it
through --root-library when that definition should independently remain an
output. This prevents a reusable library's complete public catalog from being
re-exported through every product that consumes one definition.
Some products close before every reachable implementation. Portable command construction, for example, follows a kernel's contract and pure configuration without pulling its device body into the command artifact. Split command and kernel compilation follows that boundary through mixed local, linked, bytecode, and external kernels and shows how child source requests remain ordinary Loom modules.
Configuration bindings are applied to the composed analysis module before each reachability and template-selection step. This lets newly reachable code expose additional demands while value and target predicates eliminate provider alternatives that are impossible for the selected configuration:
loom-link root.loom \
--library=providers.loom \
--root=@entry \
--config=model.hidden_size=4096 \
--require-resolved-config \
--output=entry.loom
--require-resolved-config turns a remaining required configuration declaration
into a link failure. Without it, a partial module may retain unresolved config
for a later composition or JIT boundary.
Merge a reusable catalog¶
Merge mode preserves every non-stripped symbol from the positional inputs in deterministic input order and produces one flat module:
Use this mode for reusable catalogs that should retain several helpers, templates, kernels, or configurations for later root selection. The result is still one Loom bytecode module, not a container of separately named modules and not target-native code. Later link or compile invocations can select a much smaller reachable program from it.
Remove test- and benchmark-only symbols from a deployment module with
--strip-check:
Keeping checks is useful for an authoring or qualification package. Stripping them is useful when the embedding owns validation or when executable specifications should not ship in the runtime artifact. It does not change the production symbols' semantics.
Link transitive dependencies incrementally¶
An exact call or launch is legal in an independently verified module only when that module contains the compatible definition or declaration. Libraries may satisfy those declared dependencies when linked. They may not retroactively make an undeclared call valid.
Link mode considers compatible definitions from the explicitly supplied
universe. A closed link rejects a reachable exact declaration that
remains unresolved. Add --allow-unresolved to produce a reusable partial
artifact instead.
The checked composition example has three independently verifiable modules:
The layer library introduces its own kernel demand:
// A selected library definition may introduce its own unresolved dependency.
// Partial linking preserves the declaration in the relocatable output.
func.decl pure @scale(%value: i32) -> (i32)
func.def public @project_layer(%value: i32) -> (i32) {
%result = func.call @scale(%value) : (i32) -> (i32)
func.return %result : i32
}
Supplying only the layer resolves the root's declaration and retains the new transitive declaration in standalone bytecode:
loom-link root.loom \
--library=layer.loom \
--root=@entry \
--allow-unresolved \
--to=bc \
--output=partial.loombc
The generated partial module is ordinary Loom IR. It contains the selected layer body, the reachable root, and the unsatisfied kernel contract:
func.def @project_layer(%value: i32) -> (i32) {
%result = func.call @scale(%value) : (i32) -> (i32)
func.return %result : i32
}
func.def public retain @entry(%value: i32) -> (i32) {
%result = func.call @project_layer(%value) : (i32) -> (i32)
func.return %result : i32
}
// A selected library definition may introduce its own unresolved dependency.
// Partial linking preserves the declaration in the relocatable output.
func.decl pure @scale(%value: i32) -> (i32)
A later invocation can reload that artifact in a fresh process and supply only the remaining library:
loom-link partial.loombc \
--library=kernels.loom \
--root=@entry \
--to=text \
--output=linked.loom
func.def @project_layer(%value: i32) -> (i32) {
%result = func.call @scale(%value) : (i32) -> (i32)
func.return %result : i32
}
func.def public retain @entry(%value: i32) -> (i32) {
%result = func.call @project_layer(%value) : (i32) -> (i32)
func.return %result : i32
}
// This library satisfies the exact contract retained in the partial artifact.
func.def pure @scale(%value: i32) -> (i32) {
%factor = scalar.constant 2 : i32
%result = scalar.muli %value, %factor : i32
func.return %result : i32
}
The final module contains each reachable definition once and no longer needs a declaration. The first invocation did not search for the missing kernel library, and the root did not need to name the layer's transitive dependencies. A diamond of callers reaching the same global declaration still selects one compatible definition by symbol identity; path reachability does not clone one copy per route.
Run the exact formatting, partial-link, bytecode-reload, and final-link sequence with:
template.apply follows the parallel family-selection model. The using module
declares the family with template.decl; each explicitly supplied library may
repeat that declaration and contribute implementations. Specialization selects
an eligible implementation from that explicit universe. Source path and library
order never stand in for matching rules.
Choose the output for the next boundary¶
| Output | Use |
|---|---|
Linked .loom |
Review the selected source, inspect provider reachability, or feed a text-oriented tool. |
Linked .loombc |
Cache or distribute a compact closed program, optionally specialized for a deployment target. |
Merged .loombc |
Package a reusable catalog whose roots will be selected later. |
| Stripped full merge | Package production symbols without executable checks. |
The next boundary decides when remaining facts become known. A build may invoke
loom-compile immediately. A model loader may combine the bytecode with more
libraries and configuration. A JIT embedding may keep one indexed library
universe and select different roots and targets per workload.