Composition
KynML has three opt-in features for reuse and experimentation: import, params/$ref, and sweep. They are applied as pre-passes before semantic validation — programs that use none of them are completely unaffected.
All three are handled in kynml/compose.py (apply_composition) and kynml/sweep.py (expand_sweep). The canonical entry point that runs them is compile_to_ir in kynml/pipeline.py.
params block and $name references
A params block declares named hyperparameters with default values. Any value position in any block accepts a $name reference, which the compiler replaces with the resolved value before semantic validation.
Syntax
params:
name = value
...
Values may be integers, floats, strings, booleans, or lists. The block must appear before the blocks that reference it (by convention).
$name reference positions
$name can appear anywhere a plain value is accepted:
params:
hidden = 64
lr = 0.001
bs = 32
epochs = 20
dataset D:
source = csv("data/x.csv")
target = "y"
model M:
input 10
dense $hidden relu # units = 64
dense 1 linear
train:
model = M
data = D
loss = mse
optimizer = adam(lr=$lr) # kwarg inside function call
epochs = $epochs
batch = $bs
Resolution order
- CLI
--param key=valueoverrides (highest priority) paramsblock defaults
An undefined $name (no matching key in either source) raises KynMLSemanticError:
Undefined parameter reference '$lr'. Known params: ['hidden']
CLI overrides
kynml compile model.kyn -o generated/model.py --param lr=0.01 --param hidden=128
kynml train model.kyn --param lr=0.01
--param values are coerced: tried as int, then float, then left as str.
Python API
from kynml.parser import parse_text
from kynml.pipeline import compile_to_ir
src = """
params:
hidden = 64
lr = 0.001
dataset D:
source = csv("data/x.csv")
target = "y"
model M:
input 10
dense $hidden relu
dense 1 linear
train:
model = M
data = D
loss = mse
optimizer = adam(lr=$lr)
epochs = 5
batch = 32
"""
prog = parse_text(src)
# Compile with defaults
module = compile_to_ir(prog)
# Compile with overrides (e.g. from a script or test)
module = compile_to_ir(prog, overrides={"hidden": 128, "lr": 0.005})
graph = module.graph("M")
# LinearOp.in_features=10, out_features=128
# LinearOp.in_features=128, out_features=1
import statements
Pull dataset and model blocks from another .kyn file. This lets you define shared data sources or base architectures once and reuse them across multiple training specs.
Syntax
import "relative/path/to/base.kyn"
- One
importper line. Multiple imports are allowed. - Must appear at the top of the file, before any block.
- Paths are resolved relative to the importing file (not
cwd).
What is imported
Only dataset and model blocks are merged. train, evaluate, export, params, and sweep blocks in the imported file are silently ignored — they are run-specific and belong to the top-level program only.
Example
shared/arch.kyn:
model BaseNet:
input 10
dense 64 relu
dense 32 relu
dense 1 linear
experiment.kyn:
import "shared/arch.kyn"
dataset D:
source = csv("data/x.csv")
target = "y"
train:
model = BaseNet
data = D
loss = mse
optimizer = adam(lr=0.001)
epochs = 10
batch = 32
Error cases
| Condition | Error |
|---|---|
| File not found | KynMLParseError: import: file not found |
| Circular import | KynMLSemanticError: Circular import detected |
| Name collision with importing file | KynMLSemanticError: Import conflict: dataset/model '...' is already defined |
import used without source_path |
KynMLParseError: import statements require a source_path to resolve relative paths |
Python API
resolve_imports requires source_path to compute relative paths:
from kynml.parser import parse_file
from kynml.pipeline import compile_to_ir
# compile_to_ir propagates source_path to resolve_imports automatically
module = compile_to_ir(parse_file("experiment.kyn"), source_path="experiment.kyn")
sweep block
A sweep block lists one or more parameter axes, each with a list of values. expand_sweep in kynml/sweep.py produces the Cartesian product of all combinations — one Program per combo with all $name references resolved.
Syntax
params:
hidden = 32 # default (used when axis not swept)
lr = 0.001
sweep:
lr = [0.001, 0.01]
hidden = [32, 64]
- Each axis value must be a non-empty list. A bare scalar raises
KynMLParseError. - Axes that are in
sweepbut not inparamsare valid — they act as overrides-only with no default. - Params not listed in
sweepuse theirparamsblock defaults.
Full runnable example
params:
hidden = 32
lr = 0.001
sweep:
lr = [0.001, 0.01]
hidden = [32, 64]
dataset D:
source = csv("data/x.csv")
target = "y"
model M:
input 4
dense $hidden relu
dense 1 linear
train:
model = M
data = D
loss = mse
optimizer = adam(lr=$lr)
epochs = 10
batch = 32
kynml sweep model.kyn expands this into 4 combinations (2 × 2) and runs them sequentially:
lr=0.001, hidden=32
lr=0.001, hidden=64
lr=0.01, hidden=32
lr=0.01, hidden=64
Generated per-combo scripts are written to generated/ (or --out-dir). Results are aggregated into generated/sweep_results.json, which collects the run_manifest.json from each run.
CLI
# Generate scripts + run all combos (default)
kynml sweep model.kyn
# Custom output directory
kynml sweep model.kyn --out-dir runs/
# Generate scripts without running
kynml sweep model.kyn --no-run
Python API
from kynml.parser import parse_text
from kynml.sweep import expand_sweep, generate_sweep_runner
prog = parse_text(src)
combos = expand_sweep(prog)
# combos: list of (dict, Program)
# e.g. [({'lr': 0.001, 'hidden': 32}, <Program>), ...]
for combo, resolved in combos:
print(combo)
# resolved is a fully-substituted Program — no ParamRef sentinels remain
from kynml.pipeline import compile_to_ir
module = compile_to_ir(resolved)
expand_sweep on a program with no sweep block returns a single-entry list [({}, resolved_program)] — useful for treating swept and non-swept programs uniformly.
# Generate and write a sweep orchestrator script
script_paths = [f"generated/run_{i}.py" for i in range(len(combos))]
runner_src = generate_sweep_runner(combos, script_paths, out_dir="generated/")
# runner_src is valid Python; write it to disk and execute
Interaction with Shape Inference
Composition passes run before shape inference. After substitute_params resolves all $name references, the resulting Program contains only concrete values (integers, floats, etc.). Shape inference then operates on those concrete values — so a $hidden that resolves to 64 produces exactly the same IR as if the user had written dense 64 relu directly.
Each combo in a sweep is independently compiled to IR, meaning each combo is independently shape-checked. A combo that produces a shape violation (e.g. bce with more than 1 output unit) raises KynMLShapeError for that specific combo, not for the sweep as a whole.
Formatter Support
kynml fmt (kynml/format/formatter.py) fully supports all composition constructs. The canonical output order is:
importstatementsparamsblocksweepblockdatasetblocksmodelblockstrainblockevaluateblockexportblock
$name references round-trip unchanged through the formatter.
LSP / Diagnostics
kynml.lsp.diagnostics.diagnose(source) runs the full pipeline — parse, validate, shape inference — and never raises. Composition errors (KynMLSemanticError for undefined params, KynMLParseError for missing import files) appear as severity="error" diagnostics.
from kynml.lsp.diagnostics import diagnose
diags = diagnose(source_text)
for d in diags:
print(f"{d['line']}:{d['col']} [{d['severity']}] {d['message']}")
See Also
- Language-Reference — full syntax reference including composition grammar
- Shape-Inference — how the IR inference pass works
- CLI-Reference —
kynml compile,kynml train,kynml sweep,kynml fmt,kynml lsp