Compiler and execution

From DSL to executable operator

The DSL builder does not execute operators directly. It first produces a nkdsl.core.operator.SymbolicOperator, which can then be lowered by the compiler.

The default flow is:

  1. build symbolic terms into nkdsl.ir.program.SymbolicOperatorIR

  2. validate symbol scope and update-program consistency

  3. run DSL linting diagnostics (symbol/index/connectivity quality checks)

  4. normalize and fingerprint the IR

  5. look up a cached compiled artifact

  6. on a cache miss, run analysis and fusion planning

  7. lower to a concrete executable operator target (default: nkdsl.core.compiled.CompiledOperator)

This structure is visible directly in the source tree under nkdsl.compiler.

Default passes

The default pipeline created by nkdsl.compiler.defaults.default_symbolic_pass_pipeline() contains five passes.

Pre-cache passes

  • nkdsl.compiler.passes.validation.SymbolicValidationPass

  • nkdsl.compiler.passes.diagnostics.SymbolicDiagnosticsPass

  • nkdsl.compiler.passes.normalization.SymbolicNormalizationPass

Post-cache passes

  • nkdsl.compiler.passes.analysis.SymbolicMaxConnSizeAnalysisPass

  • nkdsl.compiler.passes.fusion.SymbolicFusionPass

Diagnostics policy and strictness

Diagnostics are configurable through nkdsl.SymbolicCompilerOptions:

  • diagnostics_enabled

  • diagnostics_min_severity

  • fail_on_warnings

  • max_diagnostics

  • lint_state_sample_size

  • lint_branch_sample_cap

  • lint_max_exact_hilbert_states

For full details, see Linting. For the per-code lint catalog with examples and fixes, see Lint Messages.

Caching

The compiler can cache compiled artifacts in an in-memory store. The relevant public pieces are:

  • nkdsl.compiler.SymbolicCompiler

  • nkdsl.compiler.SymbolicCompilerOptions

  • nkdsl.compiler.SymbolicCompilationSignature

  • nkdsl.compiler.SymbolicCacheKey

  • nkdsl.compiler.defaults.default_symbolic_artifact_store()

Direct compiler usage

from nkdsl import SymbolicCompiler

compiler = SymbolicCompiler()
artifact = compiler.compile(symbolic_operator)
compiled = artifact.operator

Convenience compilation from the builder or symbolic operator is also supported:

compiled = SymbolicDiscreteJaxOperator(hi, "hop").for_each_site("i").emit(...).compile()

Compiled operators

Compiled objects are normal executable operators. The default target exposes get_conn_padded and is represented by nkdsl.core.compiled.CompiledOperator. Custom lowering targets can bind the generated kernel to a different method name (for example _get_conn_padded).

By default, compiled connectivity kernels deduplicate connected states with the same x' target, sum their matrix elements, and drop zero-amplitude entries before final padding. This is controlled by deduplicate_connected_components (default: True).

xp, mels = compiled.get_conn_padded(x_batch)

How to read printed IR

Every symbolic operator can be printed in a readable textual IR form:

op = (
    SymbolicDiscreteJaxOperator(hi, "heisenberg_sym", hermitian=True)
    .for_each(("i", "j"), over=edges)
    .emit(identity(), matrix_element=site("i").value * site("j").value)
    .for_each(("i", "j"), over=edges)
    .where(site("i").value * site("j").value < 0)
    .emit(swap("i", "j"), matrix_element=2.0)
    .build()
)

print(op.to_ir())

Example output:

symbolic.operator @"heisenberg_sym" [dtype=float64, hermitian=true, hilbert_size=8] {
  ; 2 term(s)

  term #0 "0" [kbody, n_iter=8, max_conn_size=8] {
    iterate: for (i, j) in [(0, 1), (1, 2), (2, 3), ... +5 more]
    where:   true
    emit #0:
      update:    identity
      amplitude: (x[i] * x[j])
  }

  term #1 "1" [kbody, n_iter=8, max_conn_size=8] {
    iterate: for (i, j) in [(0, 1), (1, 2), (2, 3), ... +5 more]
    where:   ((x[i] * x[j]) < 0)
    emit #0:
      update:    x'[i], x'[j] = x[j], x[i]
      amplitude: 2
  }

}

Interpretation guide:

  • symbolic.operator ...: global header with operator name, dtype, hermiticity, and Hilbert size.

  • term #k: one independent contribution to the operator action.

  • iterate: ...: static iteration domain (the tuples the term runs over).

  • where: ...: predicate gate for each iterator tuple.

  • emit #m: one emitted branch for that term.

  • update: ...: state rewrite rule from x to x'.

  • amplitude: ...: matrix element assigned to that emitted branch.