Skip to content

CLI: qvr

The qvr console script ships with the package as a thin wrapper around the parser, constraint solver, and compiler. Subcommands:

qvr check FILES...

Parse, run the constraint solver, and compile every supplied .qvr file. It emits structured diagnostics and exits 0 on full success, 1 on any error.

Flags:

  • --json: emit a single JSON document on stdout containing the full diagnostic list, as expected by CI and pre-commit hooks.
  • --target TARGET: compare every checked QIEC computation with the selected transpiler's advertised capabilities. Unsupported features are errors, so this mode is useful as an early target-specific release gate. It does not render the target program; run qvr transpile --to TARGET FILE before release to validate the complete lowering and rendering path.

Diagnostic codes:

  • parse: tree-sitter rejected the source.
  • compile: the compiler raised CompileError.
  • residuated_constraint: a TypeSlash pattern appears outside a residuated context.
  • effect_constraint: a TypeEffectApply references an effect whose name doesn't match the conventional pattern.
  • bundle_unknown_member: a bundle declaration references a member that isn't a declared rule, schema, bundle, or built-in schema.
  • family-arg-shape: a distribution call has the wrong number of arguments or incompatible argument shapes.
  • implicit-family-defaults: a distribution call relies on deprecated implicit defaults. This diagnostic is a warning.
  • qiec-route: the exact qvr-source/v0.20 to qiec-core/v1alpha1 Didactic route is unavailable or mismatched.
  • qiec-kind: a static argument or binder has the wrong kind.
  • qiec-index: an index expression has the wrong sort or constructor shape.
  • qiec-row: an effect row violates instance identity, unification, or a lacks constraint.
  • qiec-coverage: indexed case branches are incomplete, duplicated, or not justified by refinement.
  • qiec-skolem-escape: branch-local static evidence escapes its scope.
  • qiec-handler: a handler signature violates coverage, forwarding, or resumption requirements.
  • qiec-unhandled-effect: a computation performs an instance absent from its declared row.
  • qiec-backend: a requested backend boundary cannot preserve a QIEC form.
  • qiec:capability:FEATURE:COMPUTATION: --target selected a backend that cannot preserve FEATURE in the named computation.
  • io: a file-system error, such as a missing or unreadable file.

qvr migrate PATHS...

Lower .qvr source forward along the QVR grammar release chain. The composer chains the adjacent-pair migrators defined in quivers.cli.migrations so users do not have to know the intermediate versions; pinning the boundary with --from / --to selects a sub-chain when needed.

Flags:

  • --from VERSION: required source revision in the chain. QVR files do not encode an unambiguous grammar version.
  • --to VERSION: target revision (defaults to HEAD).
  • --dry-run: report which files would change without writing.
  • --output DIR: write migrated copies under DIR instead of overwriting the originals.
  • --check: compare each adjacent grammar pair with its migrator and fail if a removed rule has no converter. This mode does not migrate files.

Directory arguments are walked recursively; individual .qvr files may also be supplied. The migration tooling is built on the in-tree panproto VCS at grammars/qvr/vcs/. A new release adds a migration without changing the earlier migration steps.

qvr repl [FILE] [--plain] [--target TARGET]

Start the interactive REPL. Without a file, opens an empty session; with a file, loads and elaborates it before dropping to the prompt. --plain uses the single-line front end instead of the TUI, and --target TARGET makes every load report the capability diagnostics of that transpile target beside the module's own, as qvr check --target does. See REPL and Language Server.

qvr run FILE [ENTRY [ARGUMENTS...]]

Execute one entry point of a checked module: a define computation or a program. With no entry name, or with --list, the command prints every entry point with its signature: a computation's value parameters, statics, performed effects, and result type; a program's data parameters, result type, and the sample sites a run may condition on.

Each positional argument is a JSON literal and corresponds to one value parameter in declaration order. A computation runs under the configured runtime providers, validating the arguments and result against the specialized QIEC types with a fresh attachment table. A program runs forward on the reference machine: its random instance is handled by a conditioning replay of the sites given with --site inside a scoring draw of every other site, and its score instance by an accumulator, so the run reports the program's value together with the log joint of the drawn and given sites, its observations, and its scores. A program's parameters may also be given by name with --data; its extents are read off the data where the data fix them, so --static names only an extent no data fixes, the object parameter of a program template (qvr run pooling.qvr school_effects 0.6 --static K=8), and its handlers are fixed by the run, so --runtime applies to computations only.

Flags:

  • --list: print the file's entry points and exit.
  • --static NAME=TERM: specialize one static telescope binder with a closed type, index, or effect term. Repeat the flag once for every binder.
  • --data NAME=JSON: supply one program parameter by name (its data, an observation, a fibration, or a scalar). Repeatable.
  • --site NAME=JSON: condition one sample site of a program on a value; every other site is drawn. Repeatable. A site the run never reaches is an error.
  • --seed N: seed the reference generator a program run draws with, so two runs seeded alike draw alike.
  • --runtime FILE.json: select runtime providers and their options through a non-executable JSON configuration.
  • --fuel STEPS: bound the number of evaluation steps.
  • --trace: write stable execution events to stderr in human-readable mode.
  • --json: emit the result, runtime label, and complete trace as JSON, with the entry's kind and a program's log joint. A failure instead emits a JSON diagnostic and exits 1.

For instance, this command specializes A to Int and supplies the value argument 7:

qvr run identity.qvr identity 7 --static A=Int --json

and this one runs a program on four observations with its site a fixed:

qvr run calls.qvr prog '[0.1, 0.2, 0.3, 0.4]' --site a=0.5 --seed 0

The same invocation is available from Python through Program.run on a loaded module, from Compiler.entry and invoke_entry on its checked module, and from the REPL's :run, with the same validation, providers, trace, and diagnostic codes.

The default runtime configuration selects the built-in core provider. A configuration can select that provider with handler behavior or name an installed quivers.qiec_runtime entry point:

{
  "providers": [
    {
      "name": "core",
      "options": {
        "handlers": {
          "pass": {"kind": "passthrough"}
        }
      }
    }
  ]
}

The core provider accepts passthrough, scripted, and state handler configurations, and the prelude kinds draw, draw-scoring, enumerate, replay, collect, score, search, and param. These options supply process-local clause behavior; the source handler still fixes the checked effect, coverage, forwarding policy, clause set, and resumption grades. Runtime plugins are discoverable by name but are loaded only when the configuration explicitly selects them.

Execution failures use stable codes including qiec-run-computation, qiec-run-config, qiec-run-arity, qiec-run-static, qiec-run-provider, qiec-run-validator, qiec-run-argument, qiec-run-evaluation, qiec-run-fuel, and qiec-run-result.

qvr lsp

Run the Language Server over stdio, or pass --tcp PORT to bind a TCP port. --target TARGET enables live qiec:capability:* diagnostics for the selected transpiler. Editor extensions invoke this command; the protocol is LSP 3.17. The server accepts later target changes through workspace/didChangeConfiguration, using either {"qvr": {"transpileTarget": "TARGET"}} or {"transpileTarget": "TARGET"}; an empty target disables these diagnostics. See REPL and Language Server.

qvr transpile FILE

Transpile a .qvr file to a registered probabilistic-programming backend. Use --to BACKEND for one target, --to-all for every registered target, or --list-targets to inspect the registry. The --output and --out-dir flags select destinations for single- and multi-target runs, respectively.

qvr kernel

Install or run the Quivers Jupyter kernel. qvr kernel install registers the kernelspec; --user selects the user kernel directory, and --prefix PREFIX selects an explicit Jupyter prefix.

Module reference

The quivers.cli package exports the qvr entry point together with the tooling its subcommands share: per-file checking, the REPL session and its front ends, completion, and highlighting. quivers.cli.migrations exports the grammar migration chain, the language server is built by quivers.lsp, and the Jupyter kernel lives in quivers.kernel.

cli

Command-line entry points for quivers.

The main function is registered as the qvr console script in pyproject.toml.

Subcommands:

  • qvr check FILES...: parse and compile every supplied .qvr file, emitting structured diagnostics. Exits 0 on full success, non-zero when any file produces an error.
  • qvr migrate --from VER --to VER PATHS...: lower .qvr source files from one tagged grammar revision to another, via panproto migrations composed from the in-tree grammars/qvr/vcs chain.
  • qvr run FILE COMPUTATION [ARGS...]: execute one checked QIEC computation with JSON value arguments and an explicit runtime configuration.

Output format: human-readable by default, structured JSON when --json is supplied. Each diagnostic carries:

  • file: source path,
  • line, col: 1-indexed source location,
  • severity: "error", "warning", or "note",
  • code: stable diagnostic code (parse, compile, effect_constraint, residuated_constraint),
  • message: human-readable description.

The Python surface beside main is the tooling the subcommands share: check_file and CheckDiagnostic for checking, and ReplSession with its completion, highlighting, and front-end helpers for driving a REPL session from Python. run_tui binds lazily because its Textual front end is an optional dependency.

SEMANTIC_TOKEN_MODIFIERS module-attribute

SEMANTIC_TOKEN_MODIFIERS: tuple[str, ...] = ('declaration', 'definition', 'deprecated')

The semantic token modifiers the language server advertises in its legend.

SEMANTIC_TOKEN_TYPES module-attribute

SEMANTIC_TOKEN_TYPES: tuple[str, ...] = ('comment', 'keyword', 'operator', 'punctuation', 'string', 'number', 'variable', 'type', 'function', 'namespace', 'decorator', 'error')

The semantic token types every QVR token reduces to. The order is significant: a token's index here is its type in the LSP semantic-token legend.

STYLE_TABLE module-attribute

STYLE_TABLE: dict[str, str] = {'comment': 'italic #5c6370', 'keyword': 'bold #c678dd', 'operator': '#56b6c2', 'punctuation': '#7f848e', 'string': '#98c379', 'number': '#d19a66', 'variable': '#abb2bf', 'type': 'bold #61afef', 'function': 'bold #e5c07b', 'namespace': 'bold #e06c75', 'decorator': '#c678dd', 'error': 'bold #e06c75 underline'}

Rich style strings keyed by semantic token type, shared by the TUI, the prompt_toolkit lexer, and the Jupyter kernel so that every front end colours a token alike.

CheckSeverity

CheckSeverity = Literal['error', 'warning', 'note']

The severity of a CheckDiagnostic.

CheckDiagnostic didactic-model

CheckDiagnostic(*, file: str, line: int, col: int, severity: CheckSeverity, code: str, message: str)

Bases: Model

One structured diagnostic that qvr check reports for a file.

PARAMETER DESCRIPTION
file

The path of the checked file.

TYPE: str

line

The 1-indexed source line, or 0 when the diagnostic has no position.

TYPE: int

col

The 1-indexed source column, or 0 when the diagnostic has no position.

TYPE: int

severity

"error", "warning", or "note".

TYPE: CheckSeverity

code

The stable diagnostic code, such as "parse" or "compile".

TYPE: str

message

The human-readable description.

TYPE: str

Diagnostic didactic-model

Diagnostic(*, message: str, severity: Severity, line: int = 0, col: int = 0, end_line: int = 0, end_col: int = 0, code: str = '')

Bases: Model

One structured diagnostic surfaced by the REPL or LSP.

ReplResponse didactic-model

ReplResponse(*, body: str = '', diagnostics: tuple[Diagnostic, ...] = (), body_kind: Literal['text', 'qvr', 'json', 'markdown'] = 'text')

Bases: Model

A single command's result, ready for any frontend to render.

body is the primary payload, which a frontend renders by pairing it with body_kind (text / qvr / json / markdown); each frontend is responsible for choosing the concrete renderer (Rich syntax block, prompt_toolkit ANSI, etc.) from body_kind. diagnostics carry structured errors and warnings.

ReplSession

ReplSession()

Stateful evaluator for a single REPL session.

Source code in src/quivers/cli/repl_session.py
def __init__(self) -> None:
    self._loaded_path: Path | None = None
    self._loaded_source: str = ""
    self._module: Module = Module(statements=())
    self._compiler: Compiler | None = None
    self._qiec_module: QiecModule | None = None
    self._qiec_registry: KernelRegistry | None = None
    self._env: dict[str, Any] = {}
    self._last_diags: tuple[Diagnostic, ...] = ()
    self._runtime = RuntimeConfiguration()
    self._runtime_source: str = "built-in core"
    self._last_run: ExecutionResult | None = None
    self._last_entry: EntryRun | None = None
    self._last_failure: ExecutionDiagnostic | None = None
    self.options = SessionOptions()
    # Track when the loaded file was last read so :reload can be
    # auto-fired on a modified mtime.
    self._loaded_mtime: float | None = None
    # Pinned `:watch EXPR` re-evaluations. The TUI renders these in
    # a dedicated panel; each install + reload re-runs every entry.
    self._watches: list[str] = []
    self._watch_results: dict[str, str] = {}

env property

env: dict[str, Any]

Live environment dict (every declared atom: objects, spaces, morphisms, rules, programs, deductions, signatures, encoders, decoders, losses, bundles, contractions).

last_entry property

last_entry: EntryRun | None

The most recent entry-point run of this session, or None.

last_failure property

last_failure: ExecutionDiagnostic | None

The diagnostic of the most recent failed run, or None once a run succeeds after it.

watch_results

watch_results() -> dict[str, str]

Return the current expr -> rendered map for pinned watches.

Source code in src/quivers/cli/repl_session.py
def watch_results(self) -> dict[str, str]:
    """Return the current ``expr -> rendered`` map for pinned watches."""
    return dict(self._watch_results)

env_kinds

env_kinds() -> dict[str, str]

Return a name -> semantic-token-type map for the live env.

Frontends pass this to the highlighter so an identifier renders in its env-known colour everywhere it appears, regardless of whether the surrounding grammar context parses cleanly.

Source code in src/quivers/cli/repl_session.py
def env_kinds(self) -> dict[str, str]:
    """Return a name -> semantic-token-type map for the live env.

    Frontends pass this to the highlighter so an identifier renders
    in its env-known colour everywhere it appears, regardless of
    whether the surrounding grammar context parses cleanly.
    """
    kinds: dict[str, str] = {}
    if self._compiler is not None:
        for name in self._compiler.objects:
            kinds[name] = "type"
        for name in self._compiler.spaces:
            kinds[name] = "type"
        for name in self._compiler.morphisms:
            kinds[name] = "function"
        for name in self._compiler.rules:
            kinds[name] = "namespace"
        for name in self._compiler.programs:
            kinds[name] = "function"
        for name in self._compiler.deductions:
            kinds[name] = "namespace"
        for name in self._compiler.signatures:
            kinds[name] = "type"
        for name in self._compiler.encoders:
            kinds[name] = "function"
        for name in self._compiler.decoders:
            kinds[name] = "function"
        for name in self._compiler.losses:
            kinds[name] = "function"
        for name in self._compiler.bundles:
            kinds[name] = "namespace"
        for name in self._compiler.contractions:
            kinds[name] = "function"
    kinds.update(qiec_env_kinds(self._module))
    return kinds

dispatch

dispatch(line: str) -> ReplResponse

Route a raw input line to the right handler.

Source code in src/quivers/cli/repl_session.py
def dispatch(self, line: str) -> ReplResponse:
    """Route a raw input line to the right handler."""
    stripped = line.strip()
    if not stripped:
        return ReplResponse()
    if stripped.startswith(":"):
        cmd, _, rest = stripped[1:].partition(" ")
        return self._dispatch_meta(cmd, rest.strip())
    # Bare line: try statement first, then expression.
    return self._eval_source(stripped)

type_of

type_of(expr_source: str) -> ReplResponse

Print the type of a value-level expression.

Mirrors GHCi's :type: works on morphisms, programs, deductions, scoped sample / observe / let sites, and any expression that resolves to a morphism. For type-level names (objects, spaces, sorts), returns an error directing the user to :kind.

Source code in src/quivers/cli/repl_session.py
def type_of(self, expr_source: str) -> ReplResponse:
    """Print the type of a value-level expression.

    Mirrors GHCi's ``:type``: works on morphisms, programs,
    deductions, scoped sample / observe / let sites, and any
    expression that resolves to a morphism. For type-level names
    (objects, spaces, sorts), returns an error directing the user
    to ``:kind``.
    """
    if not self._compiler:
        return _err("no environment loaded; use :load <FILE> first")

    if SCOPE_SEPARATOR in expr_source:
        ref = resolve_scoped_path(self._compiler, expr_source.strip())
        if ref is None:
            return _err(f"unknown path: {expr_source}")
        if _ref_kind_class(ref.kind) == "type":
            return _err(
                f"{expr_source} is a type, not an expression; use :kind {expr_source}"
            )
        return _resp(self._type_signature_for_ref(ref), body_kind="qvr")

    bare = expr_source.strip()
    qiec = qiec_binding_map(self._module).get(bare)
    if qiec is not None:
        if qiec.kind in {"index", "family", "effect"}:
            return _err(f"{bare} is a type, not an expression; use :kind {bare}")
        return _resp(
            render_qiec_signature(self._module, bare) or f"{bare} :: {qiec.kind}",
            body_kind="qvr",
        )
    qiec_candidates = qiec_binding_candidates(self._module, bare)
    if len(qiec_candidates) > 1:
        choices = ", ".join(item.qualified_name for item in qiec_candidates)
        return _err(f"ambiguous name {bare!r}; use one of: {choices}")
    if prelude_effect(bare) is not None:
        return _err(f"{bare} is a type, not an expression; use :kind {bare}")
    if "." in bare and all(part.isidentifier() for part in bare.split(".")):
        operation = qualified_operation_signature(self._module, bare)
        if operation is not None:
            return _resp(operation, body_kind="qvr")
        owner, _, member = bare.partition(".")
        return _err(
            f"unknown operation {member!r}: {owner!r} is neither an effect "
            "interface nor an instance declaring it"
        )
    if bare.isidentifier():
        line = self._value_line_for_name(bare)
        if line is not None:
            return _resp(line, body_kind="qvr")
        if self._is_type_level_name(bare):
            return _err(f"{bare} is a type, not an expression; use :kind {bare}")

    # Try to resolve as an expression (morphism reference / composition).
    # We drive the compiler's own ``_compile_expr`` inference rather
    # than re-running the full module compile: ``_compile_expr``
    # reads ``_morphisms`` / ``_objects`` / ``_spaces`` but does
    # not mutate them, so the session's already-compiled state is
    # the right environment to ask "what is the type of e?" in.
    try:
        mod = parse(f"export {expr_source}", file_path="<expr>")
    except ParseError as e:
        # If the user wrote a type expression like ``FinSet 3``,
        # parsing as an output binding fails; surface a hint.
        if self._parses_as_type(expr_source):
            return _err(
                f"{expr_source} is a type expression; use :kind {expr_source}"
            )
        return _err(f"parse error: {e}")
    expr_ast = _extract_export_expr(mod)
    if expr_ast is None:
        return _err("expression did not resolve to a morphism")
    try:
        morph = self._compiler._compile_expr(expr_ast)
    except CompileError as e:
        return _err(f"type error: {e}")
    if morph is None:
        return _err("expression did not resolve to a morphism")
    return _resp(
        self._type_signature_for_morphism(expr_source, morph),
        body_kind="qvr",
    )

transpile_module

transpile_module(target: str) -> ReplResponse

Emit the loaded module as source for the named transpile backend.

Mirrors the qvr transpile CLI subcommand, dispatched in-session against the currently-loaded Module. Returns the transpiled bytes decoded as UTF-8; never mutates session state.

Errors:

  • No module loaded yet (caller must run :load FILE first).
  • Empty or whitespace-only target string.
  • Target is not in available_targets.
  • The walker raises UnsupportedConstruct (the message carries the offending construct kinds).
Source code in src/quivers/cli/repl_session.py
def transpile_module(self, target: str) -> ReplResponse:
    """Emit the loaded module as source for the named transpile
    backend.

    Mirrors the ``qvr transpile`` CLI
    subcommand, dispatched in-session against the currently-loaded
    [`Module`][quivers.dsl.ast_nodes.Module]. Returns the
    transpiled bytes decoded as UTF-8; never mutates session state.

    Errors:

    - No module loaded yet (caller must run ``:load FILE`` first).
    - Empty or whitespace-only target string.
    - Target is not in
      [`available_targets`][quivers.transpile.available_targets].
    - The walker raises
      [`UnsupportedConstruct`][quivers.transpile.UnsupportedConstruct]
      (the message carries the offending construct kinds).
    """
    if self._compiler is None:
        return _err("no environment loaded; use :load <FILE> first")
    target = target.strip()
    if not target:
        return _err("usage: :transpile <TARGET>")

    targets = available_targets()
    if target not in targets:
        return _err(f"unknown target {target!r}; available: {', '.join(targets)}")
    try:
        output = transpile(self._module, target=target)
    except UnsupportedConstruct as e:
        return _err(str(e))
    return _resp(output.decode("utf-8"))

runtime

runtime(specification: str = '') -> ReplResponse

Show or replace the explicit runtime-provider configuration.

Source code in src/quivers/cli/repl_session.py
def runtime(self, specification: str = "") -> ReplResponse:
    """Show or replace the explicit runtime-provider configuration."""

    specification = specification.strip()
    if not specification:
        payload = self._runtime.to_data()
        payload["source"] = self._runtime_source
        return _resp(json.dumps(payload, indent=2), body_kind="json")
    path = Path(specification).expanduser()
    try:
        if path.exists():
            self._runtime = load_runtime_configuration(path)
            self._runtime_source = str(path)
        elif path.suffix.lower() == ".json" or "/" in specification:
            return _err(
                f"runtime configuration not found: {path}", code="qiec-run-config"
            )
        else:
            self._runtime = RuntimeConfiguration((RuntimeSelection(specification),))
            # Instantiate now so a misspelled provider fails at :runtime,
            # not after the next computation has been prepared.
            self._runtime.instantiate()
            self._runtime_source = f"provider {specification}"
    except (ExecutionFailure, ValueError) as error:
        if isinstance(error, ExecutionFailure):
            return _execution_error(error)
        return _err(str(error), code="qiec-run-config")
    self._last_run = None
    return _resp(f"runtime attached: {self._runtime.label}")

detach_runtime

detach_runtime() -> ReplResponse

Remove every runtime provider from this session.

Source code in src/quivers/cli/repl_session.py
def detach_runtime(self) -> ReplResponse:
    """Remove every runtime provider from this session."""

    previous = self._runtime.label
    self._runtime = RuntimeConfiguration(selections=())
    self._runtime_source = "detached"
    self._last_run = None
    return _resp(f"runtime detached: {previous}")

run_computation

run_computation(invocation: str) -> ReplResponse

Execute NAME [JSON ...] [--data NAME=JSON] [--site NAME=JSON] [--static NAME=TERM] [--fuel STEPS] [--seed N].

The entry is a define computation or a program, invoked through the same invoke_entry as qvr run, so its validation, providers, trace, and error codes are those of the command line. :run alone lists the module's entry points.

PARAMETER DESCRIPTION
invocation

The entry name and its arguments, as typed after :run.

TYPE: str

RETURNS DESCRIPTION
ReplResponse

The value, its type, the runtime label, the trace length, and a program's log joint; or the diagnostic of a failure.

Source code in src/quivers/cli/repl_session.py
def run_computation(self, invocation: str) -> ReplResponse:
    """Execute ``NAME [JSON ...] [--data NAME=JSON] [--site NAME=JSON]
    [--static NAME=TERM] [--fuel STEPS] [--seed N]``.

    The entry is a ``define`` computation or a ``program``, invoked
    through the same [`invoke_entry`][quivers.qiec.entries.invoke_entry]
    as ``qvr run``, so its validation, providers, trace, and error
    codes are those of the command line. ``:run`` alone lists the
    module's entry points.

    Parameters
    ----------
    invocation : str
        The entry name and its arguments, as typed after ``:run``.

    Returns
    -------
    ReplResponse
        The value, its type, the runtime label, the trace length,
        and a program's log joint; or the diagnostic of a failure.
    """

    if self._qiec_module is None:
        return _err(
            "no checked module loaded; use :load <FILE> first",
            code="qiec-run-module",
        )
    try:
        parts = shlex.split(invocation)
    except ValueError as error:
        return _err(f"invalid :run invocation: {error}", code="qiec-run-config")
    if not parts:
        lines = [render_entry(point) for point in entry_points(self._qiec_module)]
        return _resp("\n".join(lines) if lines else "(no entry points)")
    name = parts.pop(0)
    statics: list[str] = []
    data: list[str] = []
    sites: list[str] = []
    values: list[HostValue] = []
    fuel: int | None = None
    seed: int | None = None
    index = 0
    try:
        while index < len(parts):
            option, separator, inline = parts[index].partition("=")
            if option in ("--static", "--data", "--site", "--fuel", "--seed"):
                if separator:
                    text = inline
                else:
                    index += 1
                    if index >= len(parts):
                        raise ValueError(f"{option} requires a value")
                    text = parts[index]
                if option == "--static":
                    statics.append(text)
                elif option == "--data":
                    data.append(text)
                elif option == "--site":
                    sites.append(text)
                elif option == "--fuel":
                    fuel = _fuel(text)
                else:
                    seed = int(text)
            else:
                values.append(json_value(json.loads(parts[index])))
            index += 1
        run = invoke_entry(
            self._qiec_module,
            name,
            tuple(values),
            data=parse_bindings(data),
            sites=parse_bindings(sites),
            static_arguments=tuple(statics),
            runtime=self._runtime,
            fuel=fuel,
            seed=seed,
        )
    except json.JSONDecodeError as error:
        return _err(
            f"value arguments must be JSON: {error.msg}",
            code="qiec-run-config",
        )
    except (ValueError, KeyError) as error:
        self._last_failure = ExecutionDiagnostic(
            "qiec-run-config", str(error), name
        )
        return _err(str(error), code="qiec-run-config")
    except ExecutionFailure as error:
        self._last_failure = error.diagnostic
        return _execution_error(error)
    self._last_run = run.result
    self._last_entry = run
    self._last_failure = None
    payload = run.to_data()
    summary: dict[str, object] = {
        "kind": payload["kind"],
        "value": payload["value"],
        "type": payload["result_type"],
        "runtime": payload["runtime"],
        "trace_events": len(run.result.trace),
    }
    if run.log_joint is not None:
        summary["log_joint"] = run.log_joint
    return _resp(json.dumps(summary, indent=2), body_kind="json")

kind_of

kind_of(expr_source: str) -> ReplResponse

Print the kind of a type-level name or type expression.

Mirrors GHCi's :kind: works on objects, spaces, sorts, atoms, constructors, and bare type expressions like FinSet 3 or A * B. For value-level names (morphisms, programs, sites), returns an error directing the user to :type.

Source code in src/quivers/cli/repl_session.py
def kind_of(self, expr_source: str) -> ReplResponse:
    """Print the kind of a type-level name or type expression.

    Mirrors GHCi's ``:kind``: works on objects, spaces, sorts,
    atoms, constructors, and bare type expressions like
    ``FinSet 3`` or ``A * B``. For value-level names (morphisms,
    programs, sites), returns an error directing the user to
    ``:type``.
    """
    if not self._compiler:
        return _err("no environment loaded; use :load <FILE> first")

    if SCOPE_SEPARATOR in expr_source:
        ref = resolve_scoped_path(self._compiler, expr_source.strip())
        if ref is None:
            return _err(f"unknown path: {expr_source}")
        if _ref_kind_class(ref.kind) == "value":
            return _err(
                f"{expr_source} is an expression, not a type; use :type {expr_source}"
            )
        return _resp(self._type_line_for_ref(ref), body_kind="qvr")

    bare = expr_source.strip()
    qiec = qiec_binding_map(self._module).get(bare)
    if qiec is not None:
        if qiec.kind in {"index", "family", "effect"}:
            return _resp(
                render_qiec_signature(self._module, bare)
                or f"{bare} :: {qiec.kind}",
                body_kind="qvr",
            )
        return _err(f"{bare} is an expression, not a type; use :type {bare}")
    qiec_candidates = qiec_binding_candidates(self._module, bare)
    if len(qiec_candidates) > 1:
        choices = ", ".join(item.qualified_name for item in qiec_candidates)
        return _err(f"ambiguous name {bare!r}; use one of: {choices}")
    prelude = prelude_effect(bare)
    if prelude is not None:
        return _resp(render_effect(prelude).splitlines()[0], body_kind="qvr")
    if bare.isidentifier():
        line = self._type_line_for_name(bare)
        if line is not None:
            return _resp(line, body_kind="qvr")
        if self._is_value_level_name(bare):
            return _err(f"{bare} is an expression, not a type; use :type {bare}")

    # Type-expression form: resolve through the compiler's space
    # resolver and pretty-print as a QVR object declaration.
    try:
        mod = parse(f"object __probe__ : {expr_source}", file_path="<kind>")
    except ParseError as e:
        return _err(f"parse error: {e}")
    if (
        not mod.statements
        or not isinstance(mod.statements[0], ObjectDecl)
        or not isinstance(mod.statements[0].init, TypeFromExpr)
    ):
        return _err("expected a type expression")
    texpr: ObjectExpr = mod.statements[0].init.expr
    probe = self._scratch_compiler()
    try:
        obj = probe._resolve_any_space(texpr)
    except CompileError as e:
        return _err(f"kind error: {e}")
    except Exception as e:
        return _err(f"kind error: {e}")
    return _resp(
        f"{expr_source} :: {_pretty_object(obj)}",
        body_kind="qvr",
    )

plate

plate(name: str, *, fmt: str = 'table') -> ReplResponse

Render the plate diagram for name (a program).

fmt selects the output format: "table" (default, in-TUI Rich table; also has a plain-text fallback), "mermaid", "dot", "tikz", "daft", or "open" (render via daft or graphviz, save to a temp PNG, and open with the system default opener).

Source code in src/quivers/cli/repl_session.py
def plate(self, name: str, *, fmt: str = "table") -> ReplResponse:
    """Render the plate diagram for ``name`` (a program).

    ``fmt`` selects the output format: ``"table"`` (default,
    in-TUI Rich table; also has a plain-text fallback),
    ``"mermaid"``, ``"dot"``, ``"tikz"``, ``"daft"``, or
    ``"open"`` (render via daft or graphviz, save to a temp
    PNG, and open with the system default opener).
    """
    if self._compiler is None:
        return _err("no environment loaded; use :load <FILE> first")
    graph = build_plate_graph(self._compiler, name)
    if graph is None:
        return _err(f"no program named {name!r} in the loaded module")
    if fmt == "mermaid":
        return _resp(render_mermaid(graph), body_kind="markdown")
    if fmt == "dot":
        return _resp(render_dot(graph))
    if fmt == "tikz":
        return _resp(render_tikz(graph))
    if fmt == "daft":
        return _resp(render_daft(graph))
    if fmt == "open":
        return self._plate_open(graph)
    # Default: plain-text table (TUI also accepts this and
    # wraps it through the Rich highlighter for colour).
    return _resp(render_table_plain(graph))

graph

graph(name: str, *, fmt: str = 'table') -> ReplResponse

Render a step-flow view of name (a program).

The step-flow view lists the program's body steps one per row with their dependency parents on the side, plus an indented sub-block for any marginalize body. Same fmt flags as :plate.

Source code in src/quivers/cli/repl_session.py
def graph(self, name: str, *, fmt: str = "table") -> ReplResponse:
    """Render a step-flow view of ``name`` (a program).

    The step-flow view lists the program's body steps one per
    row with their dependency parents on the side, plus an
    indented sub-block for any marginalize body. Same ``fmt``
    flags as ``:plate``.
    """
    if self._compiler is None:
        return _err("no environment loaded; use :load <FILE> first")
    graph = build_plate_graph(self._compiler, name)
    if graph is None:
        return _err(f"no program named {name!r} in the loaded module")
    # The graph and plate diagrams share a model; ``:graph``
    # uses a step-oriented rendering vs ``:plate``'s
    # variable-oriented one.
    if fmt == "mermaid":
        return _resp(render_mermaid(graph), body_kind="markdown")
    if fmt == "dot":
        return _resp(render_dot(graph))
    if fmt == "open":
        return self._plate_open(graph)
    return _resp(_render_step_flow(graph))

where

where(name: str) -> ReplResponse

List every scope path whose final segment is name.

Source code in src/quivers/cli/repl_session.py
def where(self, name: str) -> ReplResponse:
    """List every scope path whose final segment is ``name``."""
    if self._compiler is None:
        return _err("no environment loaded; use :load <FILE> first")
    refs = find_all_references(self._compiler, name)
    if not refs:
        return _err(f"no references to {name!r}")
    lines = [f"references to {name!r}:"]
    for ref in refs:
        lines.append(f"  {ref.kind:14} {ref.path}")
    return _resp("\n".join(lines))

effects

effects(name: str) -> ReplResponse

Compare declared and inferred effect sets for a program.

Source code in src/quivers/cli/repl_session.py
def effects(self, name: str) -> ReplResponse:
    """Compare declared and inferred effect sets for a program."""
    ambiguity = self._qiec_ambiguity(name)
    if ambiguity is not None:
        return _err(ambiguity)
    qiec = qiec_binding_map(self._module).get(name)
    if qiec is not None:
        declaration = qiec.declaration
        if isinstance(declaration, QiecEffectDecl):
            operations = ", ".join(op.name for op in declaration.operations)
            return _resp(
                f"effect {declaration.name}\n  operations: {{{operations}}}"
            )
        if isinstance(declaration, QiecComputationDecl):
            row = declaration.effects
            entries = ", ".join(entry.instance for entry in row.entries)
            tail = row.tail or "(closed)"
            lacks = ", ".join(row.lacks) or "(none)"
            lines = [
                f"computation {declaration.name}:",
                f"  instances : {{{entries}}}",
                f"  tail      : {tail}",
                f"  lacks     : {{{lacks}}}",
            ]
            inferred = self._inferred_row(declaration.name)
            if inferred is not None:
                lines.append(f"  inferred  : {inferred}")
            return _resp("\n".join(lines))
        if isinstance(declaration, QiecHandlerDecl):
            grades = ", ".join(
                f"{clause.operation}:{clause.grade}"
                for clause in declaration.clauses
            )
            return _resp(
                f"handler {declaration.name} for {declaration.effect.name}:\n"
                f"  coverage  : {declaration.coverage}\n"
                f"  forwards  : {declaration.forwards_unknown}\n"
                f"  resumptions: {{{grades}}}"
            )
        return _err(f"{name!r} does not declare an effect row")
    prelude = prelude_effect(name)
    if prelude is not None:
        operations = ", ".join(op.name for op in prelude.operations)
        return _resp(f"effect {name} (prelude)\n  operations: {{{operations}}}")

    if self._compiler is None:
        return _err("no environment loaded; use :load <FILE> first")
    graph = build_plate_graph(self._compiler, name)
    if graph is None:
        return _err(f"no program named {name!r} in the loaded module")
    declared = _declared_effects_for_program(self._compiler, name)
    inferred = _inferred_effects_from_graph(graph)
    lines = [
        f"program {name}:",
        f"  declared : {{{', '.join(sorted(declared)) or '(none)'}}}",
        f"  inferred : {{{', '.join(sorted(inferred))}}}",
    ]
    checked = self._inferred_row(name)
    if checked is not None:
        lines.append(f"  checked  : {checked}")
    leak = inferred - (declared or inferred)
    missing = declared - inferred if declared else set()
    if declared and leak:
        lines.append(
            f"  ! leak    : {{{', '.join(sorted(leak))}}}"
            " (body uses effects not declared)"
        )
    if declared and missing:
        lines.append(
            f"  ! unused  : {{{', '.join(sorted(missing))}}}"
            " (declared but not used)"
        )
    if not declared:
        lines.append("  (no [effects=[...]] declared)")
    return _resp("\n".join(lines))

shape

shape(name: str) -> ReplResponse

Render the ChainShape of the named program.

Source code in src/quivers/cli/repl_session.py
def shape(self, name: str) -> ReplResponse:
    """Render the ``ChainShape`` of the named program."""
    if self._compiler is None:
        return _err("no environment loaded; use :load <FILE> first")
    try:
        shape_obj = ChainShape.from_module(self._module)
    except Exception as exc:
        return _err(f"chain shape: {exc}")
    # ChainShape walks the module's first program decl
    # automatically; ``name`` is used only to disambiguate
    # output. Stay graceful when the module has multiple
    # programs and only one matches.
    steps = list(shape_obj.steps)
    if not steps:
        return _err(f"no program steps in module for {name!r}")
    lines = [
        f"chain shape ({shape_obj.algebra_name}):",
        "  #  depth  kind         name        size",
        "  -  -----  -----------  ----------  ----",
    ]
    for i, step in enumerate(steps, start=1):
        size = (
            str(step.intermediate_size)
            if step.intermediate_size is not None
            else "?"
        )
        lines.append(
            f"  {i:<3d} {step.depth:<5d}  {step.kind:<11s}  "
            f"{step.name:<10s}  {size:<4s}"
        )
    return _resp("\n".join(lines))

trace

trace(expr_source: str) -> ReplResponse

Step through elaboration of a morphism expression.

Source code in src/quivers/cli/repl_session.py
def trace(self, expr_source: str) -> ReplResponse:
    """Step through elaboration of a morphism expression."""
    if self._compiler is None:
        return _err("no environment loaded")
    try:
        mod = parse(f"export {expr_source}", file_path="<trace>")
    except ParseError as e:
        return _err(f"parse error: {e}")
    scratch = Compiler(_extend_module(self._module, mod.statements))
    # Re-run statement compilation so the env is fresh.
    try:
        scratch.compile()
    except (CompileError, QiecDiagnosticError) as e:
        return _err(f"compile error: {e}")
    # Walk the expression once and emit shape info as we go.
    lines: list[str] = []
    try:
        steps = _trace_expr(scratch, scratch._output_expr)
    except Exception as e:
        return _err(f"trace failed: {e}")
    for step in steps:
        lines.append(step)
    return _resp("\n".join(lines))

save

save(path: str = '') -> ReplResponse

Write the live module back to a .qvr file.

Source code in src/quivers/cli/repl_session.py
def save(self, path: str = "") -> ReplResponse:
    """Write the live module back to a ``.qvr`` file."""
    target = Path(path).expanduser() if path else self._loaded_path
    if target is None:
        return _err("usage: :save <FILE> (no file currently loaded)")
    try:
        source = module_to_source(self._module)
    except NotImplementedError as e:
        return _err(
            f"cannot emit: {e}. (the canonical emitter does not yet "
            "support every statement variant; edit the source file "
            "directly instead)"
        )
    try:
        target.write_text(source, encoding="utf-8")
    except OSError as e:
        return _err(f"write failed: {e}")
    return _resp(f"saved {target}")

watch

watch(expr: str) -> ReplResponse

Pin an expression for re-evaluation on every recompile.

Source code in src/quivers/cli/repl_session.py
def watch(self, expr: str) -> ReplResponse:
    """Pin an expression for re-evaluation on every recompile."""
    expr = expr.strip()
    if not expr:
        return _err("usage: :watch <EXPR>")
    if expr not in self._watches:
        self._watches.append(expr)
    self._refresh_watches()
    rendered = self._watch_results.get(expr, "(unresolved)")
    return _resp(f"watch {expr} => {rendered}", body_kind="qvr")

unwatch

unwatch(expr: str) -> ReplResponse

Remove a previously-pinned watch expression.

Source code in src/quivers/cli/repl_session.py
def unwatch(self, expr: str) -> ReplResponse:
    """Remove a previously-pinned watch expression."""
    expr = expr.strip()
    if not expr:
        # No argument: clear every watch.
        self._watches.clear()
        self._watch_results.clear()
        return _resp("cleared all watches")
    if expr not in self._watches:
        return _err(f"not watching: {expr}")
    self._watches.remove(expr)
    self._watch_results.pop(expr, None)
    return _resp(f"unwatched {expr}")

autoreload_if_stale

autoreload_if_stale() -> ReplResponse | None

Re-run :reload if the loaded file's mtime advanced.

Source code in src/quivers/cli/repl_session.py
def autoreload_if_stale(self) -> ReplResponse | None:
    """Re-run :reload if the loaded file's mtime advanced."""
    if not self.options.autoload_on_save:
        return None
    if self._loaded_path is None or self._loaded_mtime is None:
        return None
    try:
        current = self._loaded_path.stat().st_mtime
    except OSError:
        return None
    if current > self._loaded_mtime:
        return self.reload()
    return None

SessionOptions didactic-model

SessionOptions(*, highlight: bool = True, unicode: bool = True, show_axes: bool = True, paranoid: bool = False, autoload_on_save: bool = True, theme: str = 'ansi_dark', target: str = '')

Bases: Model

User-tunable knobs exposed via :set.

theme selects a Rich syntax theme for QVR bodies rendered via :info / :dump etc. Common values: ansi_dark, ansi_light, monokai, nord, solarized-dark, solarized-light, github-dark.

target class-attribute instance-attribute

target: str = ''

A transpile target whose capability diagnostics :load and :reload report beside the checked module's own, as qvr check --target does; empty for none.

Completion dataclass

Completion(text: str, kind: str, detail: str = '')

One completion candidate.

Span dataclass

Span(start: int, end: int, token: str, text: str, line: int = 0, col: int = 0, end_line: int = 0, end_col: int = 0)

One styled run of source.

PARAMETER DESCRIPTION
start

The run's start byte offset in the source.

TYPE: int

end

The run's end byte offset, exclusive.

TYPE: int

token

Its semantic token type, one of SEMANTIC_TOKEN_TYPES.

TYPE: str

text

The source text of the run.

TYPE: str

line

Its 0-indexed start line.

TYPE: int DEFAULT: 0

col

Its 0-indexed start column, in UTF-16 code units.

TYPE: int DEFAULT: 0

end_line

Its 0-indexed end line.

TYPE: int DEFAULT: 0

end_col

Its 0-indexed end column, in UTF-16 code units.

TYPE: int DEFAULT: 0

check_file

check_file(path: Path, *, target: str | None = None) -> list[CheckDiagnostic]

Parse, constraint-check, and compile one .qvr file.

This is the per-file pipeline behind qvr check. It reports failures as diagnostics rather than raising, so an unreadable file yields an io diagnostic and a syntax error a parse one.

PARAMETER DESCRIPTION
path

The .qvr file to check.

TYPE: Path

target

A transpile target whose missing capabilities are also reported, as qvr check --target does; None for none.

TYPE: str or None DEFAULT: None

RETURNS DESCRIPTION
list[CheckDiagnostic]

Every diagnostic the file produces, empty when it checks cleanly.

Source code in src/quivers/cli/check.py
def check_file(path: Path, *, target: str | None = None) -> list[CheckDiagnostic]:
    """Parse, constraint-check, and compile one ``.qvr`` file.

    This is the per-file pipeline behind ``qvr check``. It reports
    failures as diagnostics rather than raising, so an unreadable file
    yields an ``io`` diagnostic and a syntax error a ``parse`` one.

    Parameters
    ----------
    path : Path
        The ``.qvr`` file to check.
    target : str or None
        A transpile target whose missing capabilities are also
        reported, as ``qvr check --target`` does; ``None`` for none.

    Returns
    -------
    list[CheckDiagnostic]
        Every diagnostic the file produces, empty when it checks
        cleanly.
    """
    try:
        source = path.read_bytes()
    except OSError as e:
        return [
            CheckDiagnostic(
                file=str(path),
                line=0,
                col=0,
                severity="error",
                code="io",
                message=f"could not read file: {e}",
            )
        ]

    diags: list[CheckDiagnostic] = []

    try:
        module = parse(source, file_path=str(path))
    except ParseError as e:
        match = _PARSE_POSITION.search(str(e))
        line = int(match.group(1)) if match is not None else 0
        col = int(match.group(2)) if match is not None else 0
        diags.append(
            CheckDiagnostic(
                file=str(path),
                line=line,
                col=col,
                severity="error",
                code="parse",
                message=str(e),
            )
        )
        return diags

    compiler_module = non_qiec_projection(module)
    try:
        compiler = Compiler(
            module,
            module_name=qiec_module_name(path),
            file_path=str(path),
        )
        qiec_module = compiler.qiec_module
        if qiec_module is not None and qiec_module.gap:
            diags.append(
                CheckDiagnostic(
                    file=str(path),
                    line=0,
                    col=0,
                    severity="error",
                    code="qiec-program-gap",
                    message=qiec_module.gap,
                )
            )
    except Exception as error:
        if not has_qiec_surface(module):
            raise
        diagnostic = qiec_diagnostic(error)
        diags.append(
            CheckDiagnostic(
                file=str(path),
                line=diagnostic.line,
                col=diagnostic.col,
                severity="error",
                code=diagnostic.code,
                message=diagnostic.message,
            )
        )
        compiler = Compiler(compiler_module, lower_qiec=False)

    # Constraint solver runs before compile so users see structural
    # diagnostics even when compilation would also fail.
    diags.extend(
        CheckDiagnostic(
            file=str(path),
            line=v.line,
            col=v.col,
            severity=_normalize_severity(v.severity),
            code=v.code,
            message=v.message,
        )
        for v in check_constraints(compiler_module)
    )

    # Family argument-shape pass: arity and elementwise compatibility
    # against the distribution's `arg_constraints`. Surfaces the
    # ``implicit-family-defaults`` warning and ``family-arg-shape``
    # errors / warnings.
    family_diags = [
        CheckDiagnostic(
            file=str(path),
            line=v.line,
            col=v.col,
            severity=_normalize_severity(v.severity),
            code=v.code,
            message=v.message,
        )
        for v in validate_family_arg_shapes(compiler_module)
    ]
    diags.extend(family_diags)
    has_family_shape_error = any(
        d.code == "family-arg-shape" and d.severity == "error" for d in family_diags
    )
    if has_family_shape_error:
        # Skip compile: the wider compiler would crash on shape-mismatched
        # args before we get a useful diagnostic.
        return diags

    try:
        compiler.compile()
    except CompileError as error:
        diags.append(
            CheckDiagnostic(
                file=str(path),
                line=getattr(error, "line", 0),
                col=getattr(error, "col", 0),
                severity="error",
                code="compile",
                message=str(error),
            )
        )

    if target is not None and compiler.qiec_module is not None:
        from quivers.transpile import UnsupportedConstruct
        from quivers.transpile.plan import checked_program_plan
        from quivers.transpile.qiec_ir import analyze_qiec_capabilities

        capabilities = analyze_qiec_capabilities(compiler.qiec_module, target)
        for capability in capabilities:
            origin = capability.origin
            diags.append(
                CheckDiagnostic(
                    file=str(path),
                    line=(origin.line or 0) if origin is not None else 0,
                    col=(origin.column or 0) if origin is not None else 0,
                    severity="error",
                    code=capability.kind,
                    message=capability.message,
                )
            )
        if not capabilities:
            try:
                checked_program_plan(compiler.qiec_module, module, target)
            except UnsupportedConstruct as error:
                diags.extend(
                    CheckDiagnostic(
                        file=str(path),
                        line=0,
                        col=0,
                        severity="error",
                        code=kind,
                        message=str(error),
                    )
                    for kind in error.kinds
                )

    return diags

render_qiec_signature

render_qiec_signature(module: Module, name: str) -> str | None

Return a compact source signature for one QIEC binding.

This works from the authored AST rather than a backend compiler object, so it remains available when a document contains only indexed families or effect interfaces.

PARAMETER DESCRIPTION
module

The parsed module holding the binding.

TYPE: Module

name

The QIEC binding to describe.

TYPE: str

RETURNS DESCRIPTION
str or None

The compact signature, or None when name is not a QIEC binding of module.

Source code in src/quivers/cli/repl_session.py
def render_qiec_signature(module: Module, name: str) -> str | None:
    """Return a compact source signature for one QIEC binding.

    This works from the authored AST rather than a backend compiler object, so
    it remains available when a document contains only indexed families or
    effect interfaces.

    Parameters
    ----------
    module : Module
        The parsed module holding the binding.
    name : str
        The QIEC binding to describe.

    Returns
    -------
    str or None
        The compact signature, or ``None`` when ``name`` is not a QIEC
        binding of ``module``.
    """

    binding = qiec_binding_map(module).get(name)
    if binding is None:
        return None
    if binding.kind == "index":
        return f"index {name} :: Index"

    rendered = _render_decl(binding.owner)
    lines = [
        line.strip()
        for line in rendered.splitlines()
        if line.strip() and not line.lstrip().startswith("#!")
    ]
    if binding.declaration is binding.owner:
        return lines[0] if lines else f"{name} :: {binding.kind}"

    pattern = re.compile(rf"\b{re.escape(binding.name)}\b")
    for line in lines[1:]:
        if pattern.search(line):
            if line.startswith("constructor "):
                line = line.removeprefix("constructor ")
            if name != binding.name:
                line = pattern.sub(name, line, count=1)
            return line
    return f"{name} :: {binding.kind}"

render_signature

render_signature(compiler: Compiler | None, name: str) -> str | None

Return the GHCi-style name :: type (or object NAME : ... for type-level bindings) for a top-level name, or None when the compiler is absent or the name is unknown.

This is the shared entry point used by both the REPL (:type / :kind / bare-expression fallback) and the LSP server's hover panel: by routing every surface that wants a one-line signature through the same function, a binding looks identical regardless of whether the user is in the TUI or hovering in an editor.

PARAMETER DESCRIPTION
compiler

The compiler holding the elaborated environment.

TYPE: Compiler or None

name

The top-level name to describe.

TYPE: str

RETURNS DESCRIPTION
str or None

The one-line signature, or None when there is no compiler or the name is unbound.

Source code in src/quivers/cli/repl_session.py
def render_signature(compiler: Compiler | None, name: str) -> str | None:
    """Return the GHCi-style ``name :: type`` (or ``object NAME : ...``
    for type-level bindings) for a top-level name, or ``None`` when
    the compiler is absent or the name is unknown.

    This is the shared entry point used by both the REPL (``:type``
    / ``:kind`` / bare-expression fallback) and the LSP server's
    hover panel: by routing every surface that wants a one-line
    signature through the same function, a binding looks identical
    regardless of whether the user is in the TUI or hovering in an
    editor.

    Parameters
    ----------
    compiler : Compiler or None
        The compiler holding the elaborated environment.
    name : str
        The top-level name to describe.

    Returns
    -------
    str or None
        The one-line signature, or ``None`` when there is no compiler
        or the name is unbound.
    """
    if compiler is None:
        return None
    s = ReplSession()
    s._compiler = compiler
    s._module = getattr(compiler, "_module", Module(statements=()))
    line = s._value_line_for_name(name)
    if line is not None:
        return line
    return s._type_line_for_name(name)

all_completions

all_completions(session: ReplSession, prefix: str) -> list[Completion]

Return every candidate whose text starts with prefix.

The caller (prompt_toolkit Completer, LSP completion handler) decides which slice to show.

PARAMETER DESCRIPTION
session

The session whose environment supplies name completions.

TYPE: ReplSession

prefix

The text typed so far.

TYPE: str

RETURNS DESCRIPTION
list[Completion]

Meta-command, transpile-target, environment, keyword, and path candidates, in that order.

Source code in src/quivers/cli/repl_complete.py
def all_completions(session: ReplSession, prefix: str) -> list[Completion]:
    """Return every candidate whose text starts with ``prefix``.

    The caller (prompt_toolkit Completer, LSP completion handler)
    decides which slice to show.

    Parameters
    ----------
    session : ReplSession
        The session whose environment supplies name completions.
    prefix : str
        The text typed so far.

    Returns
    -------
    list[Completion]
        Meta-command, transpile-target, environment, keyword, and path
        candidates, in that order.
    """
    out: list[Completion] = []
    out.extend(_meta_completions(prefix))
    out.extend(_transpile_target_completions(prefix))
    out.extend(_env_completions(session, prefix))
    out.extend(_keyword_completions(prefix))
    out.extend(_path_completions(prefix))
    return out

public_meta_commands

public_meta_commands() -> tuple[str, ...]

Return one public spelling for every registered meta-command.

The dispatch table registers each long spelling before its short aliases. Keeping the first name for each handler thus makes completion and the TUI palette follow the executable command surface without advertising aliases.

RETURNS DESCRIPTION
tuple[str, ...]

The long spelling of each meta-command, in registration order.

Source code in src/quivers/cli/repl_complete.py
def public_meta_commands() -> tuple[str, ...]:
    """Return one public spelling for every registered meta-command.

    The dispatch table registers each long spelling before its short aliases.
    Keeping the first name for each handler thus makes completion and the TUI
    palette follow the executable command surface without advertising aliases.

    Returns
    -------
    tuple[str, ...]
        The long spelling of each meta-command, in registration order.
    """
    seen: set[object] = set()
    commands: list[str] = []
    for name, handler in repl_session._META_COMMANDS.items():  # noqa: SLF001
        if handler in seen:
            continue
        seen.add(handler)
        commands.append(name)
    return tuple(commands)

to_pygments_pairs

to_pygments_pairs(source: str) -> list[tuple[Any, str]]

Build (Pygments token, text) pairs suitable for prompt_toolkit.

prompt_toolkit's PygmentsLexer wraps a Pygments Lexer that yields (index, token, text); here we deliver the simpler (token, text) form directly used by prompt_toolkit.formatted_text.

PARAMETER DESCRIPTION
source

The QVR source to highlight.

TYPE: str

RETURNS DESCRIPTION
list[tuple[_TokenType, str]]

One pair per span, in source order.

Source code in src/quivers/cli/repl_highlight.py
def to_pygments_pairs(source: str) -> list[tuple[Any, str]]:
    """Build ``(Pygments token, text)`` pairs suitable for prompt_toolkit.

    prompt_toolkit's PygmentsLexer wraps a Pygments Lexer that yields
    ``(index, token, text)``; here we deliver the simpler ``(token,
    text)`` form directly used by ``prompt_toolkit.formatted_text``.

    Parameters
    ----------
    source : str
        The QVR source to highlight.

    Returns
    -------
    list[tuple[pygments.token._TokenType, str]]
        One pair per span, in source order.
    """
    from pygments.token import (
        Comment,
        Keyword,
        Name,
        Number,
        Operator,
        Punctuation,
        String,
        Text,
    )

    mapping = {
        "comment": Comment,
        "keyword": Keyword,
        "operator": Operator,
        "punctuation": Punctuation,
        "string": String,
        "number": Number,
        "variable": Name.Variable,
        "type": Name.Class,
        "function": Name.Builtin,
        "namespace": String.Symbol,
        "decorator": Name.Decorator,
        "error": Text,
    }
    out: list[tuple[Any, str]] = []
    for span in tokenize(source):
        out.append((mapping.get(span.token, Text), span.text))
    return out

to_rich_text

to_rich_text(source: str, *, env_kinds: dict[str, str] | None = None, link_action: str | None = None) -> Any

Build a rich.text.Text from the highlighted source.

Rich is imported lazily so that importing this module does not pull rich in for callers that need only raw spans.

If link_action is set (e.g. "info"), identifiers that the grammar or env classified as something meaningful (type / function / namespace / variable when the name is in env_kinds) are wrapped as Rich console links pointing to "<link_action>:<name>". The TUI's RichLog on_click translator then runs :link_action <name> when the user clicks.

PARAMETER DESCRIPTION
source

The QVR source to highlight.

TYPE: str

env_kinds

Semantic kinds of names bound in the environment, as for tokenize.

TYPE: dict[str, str] or None DEFAULT: None

link_action

The meta-command clickable identifiers run, or None for no links.

TYPE: str or None DEFAULT: None

RETURNS DESCRIPTION
Text

The styled text.

Source code in src/quivers/cli/repl_highlight.py
def to_rich_text(
    source: str,
    *,
    env_kinds: dict[str, str] | None = None,
    link_action: str | None = None,
) -> Any:
    """Build a `rich.text.Text` from the highlighted source.

    Rich is imported lazily so that importing this module does
    not pull rich in for callers that need only raw spans.

    If ``link_action`` is set (e.g. ``"info"``), identifiers that the
    grammar or env classified as something meaningful (type / function
    / namespace / variable when the name is in env_kinds) are wrapped
    as Rich console links pointing to ``"<link_action>:<name>"``. The
    TUI's RichLog ``on_click`` translator then runs ``:link_action
    <name>`` when the user clicks.

    Parameters
    ----------
    source : str
        The QVR source to highlight.
    env_kinds : dict[str, str] or None
        Semantic kinds of names bound in the environment, as for
        `tokenize`.
    link_action : str or None
        The meta-command clickable identifiers run, or ``None`` for no
        links.

    Returns
    -------
    rich.text.Text
        The styled text.
    """
    from rich.style import Style
    from rich.text import Text

    out = Text()
    interesting = {"type", "function", "namespace"}
    for span in tokenize(source, env_kinds=env_kinds):
        style_str = STYLE_TABLE.get(span.token, "")
        if (
            link_action
            and span.token in interesting
            and span.text.replace("_", "").isalnum()
            and not span.text[0].isdigit()
        ):
            style = Style.parse(style_str) + Style(
                meta={"@click": f"{link_action}('{span.text}')"}
            )
            out.append(span.text, style=style)
        else:
            out.append(span.text, style=style_str)
    return out

to_semantic_token_data

to_semantic_token_data(source: str, *, env_kinds: dict[str, str] | None = None) -> list[int]

Encode source as the LSP SemanticTokens 5-tuple stream.

The stream is delta-encoded as [deltaLine, deltaStart, length, tokenType, tokenModifiers] per LSP 3.17. See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens for the encoding rules.

PARAMETER DESCRIPTION
source

The QVR source to encode.

TYPE: str

env_kinds

Semantic kinds of names bound in the environment, as for tokenize.

TYPE: dict[str, str] or None DEFAULT: None

RETURNS DESCRIPTION
list[int]

Five integers per token.

Source code in src/quivers/cli/repl_highlight.py
def to_semantic_token_data(
    source: str, *, env_kinds: dict[str, str] | None = None
) -> list[int]:
    """Encode ``source`` as the LSP SemanticTokens 5-tuple stream.

    The stream is delta-encoded as ``[deltaLine, deltaStart, length,
    tokenType, tokenModifiers]`` per LSP 3.17. See
    https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens
    for the encoding rules.

    Parameters
    ----------
    source : str
        The QVR source to encode.
    env_kinds : dict[str, str] or None
        Semantic kinds of names bound in the environment, as for
        `tokenize`.

    Returns
    -------
    list[int]
        Five integers per token.
    """
    type_index = {name: i for i, name in enumerate(SEMANTIC_TOKEN_TYPES)}
    out: list[int] = []
    prev_line = 0
    prev_col = 0
    for span in tokenize(source, env_kinds=env_kinds):
        if span.token in ("variable",) and not span.text.strip():
            # Pure whitespace at the start of the buffer; skip.
            continue
        if span.line != prev_line:
            prev_col = 0
        delta_line = span.line - prev_line
        delta_col = span.col - prev_col
        # Tokens that cross a newline aren't supported by the protocol;
        # split conservatively at the next newline.
        text = span.text
        length = _utf16_length(text)
        if "\n" in text:
            first_segment = text.split("\n", 1)[0]
            length = _utf16_length(first_segment)
        out.extend(
            [
                delta_line,
                delta_col,
                length,
                type_index.get(span.token, type_index["variable"]),
                0,
            ]
        )
        prev_line = span.line
        prev_col = span.col
    return out

to_semantic_token_legend

to_semantic_token_legend() -> tuple[tuple[str, ...], tuple[str, ...]]

Return the semantic-token legend the language server advertises.

RETURNS DESCRIPTION
tuple[tuple[str, ...], tuple[str, ...]]

SEMANTIC_TOKEN_TYPES and SEMANTIC_TOKEN_MODIFIERS.

Source code in src/quivers/cli/repl_highlight.py
def to_semantic_token_legend() -> tuple[tuple[str, ...], tuple[str, ...]]:
    """Return the semantic-token legend the language server advertises.

    Returns
    -------
    tuple[tuple[str, ...], tuple[str, ...]]
        `SEMANTIC_TOKEN_TYPES` and `SEMANTIC_TOKEN_MODIFIERS`.
    """
    return SEMANTIC_TOKEN_TYPES, SEMANTIC_TOKEN_MODIFIERS

tokenize

tokenize(source: str | bytes, *, env_kinds: dict[str, str] | None = None) -> list[Span]

Return the styled span list for source.

PARAMETER DESCRIPTION
source

The QVR source to highlight.

TYPE: str or bytes

env_kinds

Semantic kinds of names bound in the environment, as described below; None highlights from the grammar alone.

TYPE: dict[str, str] or None DEFAULT: None

RETURNS DESCRIPTION
list[Span]

The source's spans in order, each tagged with a semantic token type.

Drives directly on the tree-sitter parse so that anonymous tokens
(keywords, punctuation) are preserved; panproto's Schema view
strips those, so we go to tree-sitter for highlighting and keep
panproto for parsing into the didactic AST.
``env_kinds`` enables semantic highlighting: a mapping from name
to one of ``"type"``, ``"function"``, ``"namespace"`` (or any
SEMANTIC_TOKEN_TYPES value). Identifiers the grammar leaves as
``"variable"`` are upgraded to their env-known kind, so a name
looks the same across the input pane, ``:type``, ``:info``, and
any other surface, regardless of whether the surrounding context
parses as a valid declaration.
Failure path: if the parser raises, the entire source is returned
as a single ``error`` span. Callers can downgrade to a no-highlight
render but the buffer still displays.
Source code in src/quivers/cli/repl_highlight.py
def tokenize(
    source: str | bytes,
    *,
    env_kinds: dict[str, str] | None = None,
) -> list[Span]:
    """Return the styled span list for ``source``.

    Parameters
    ----------
    source : str or bytes
        The QVR source to highlight.
    env_kinds : dict[str, str] or None
        Semantic kinds of names bound in the environment, as described
        below; ``None`` highlights from the grammar alone.

    Returns
    -------
    list[Span]
        The source's spans in order, each tagged with a semantic token
        type.

    Drives directly on the tree-sitter parse so that anonymous tokens
    (keywords, punctuation) are preserved; panproto's Schema view
    strips those, so we go to tree-sitter for highlighting and keep
    panproto for parsing into the didactic AST.

    ``env_kinds`` enables semantic highlighting: a mapping from name
    to one of ``"type"``, ``"function"``, ``"namespace"`` (or any
    SEMANTIC_TOKEN_TYPES value). Identifiers the grammar leaves as
    ``"variable"`` are upgraded to their env-known kind, so a name
    looks the same across the input pane, ``:type``, ``:info``, and
    any other surface, regardless of whether the surrounding context
    parses as a valid declaration.

    Failure path: if the parser raises, the entire source is returned
    as a single ``error`` span. Callers can downgrade to a no-highlight
    render but the buffer still displays.
    """
    if isinstance(source, str):
        src_bytes = source.encode("utf-8")
    else:
        src_bytes = source
    if not src_bytes:
        return []
    try:
        parser, _, _ = _load_parser()
        tree = parser.parse(src_bytes)
    except Exception:
        return [
            Span(
                start=0,
                end=len(src_bytes),
                token="error",
                text=src_bytes.decode("utf-8", errors="replace"),
                line=0,
                col=0,
                end_line=src_bytes.count(b"\n"),
                end_col=0,
            )
        ]

    leaves: list[tuple[Any, str | None, str | None]] = []

    def walk(
        node: Any,
        parent_kind: str | None,
        field_name: str | None,
    ) -> None:
        if not node.children:
            leaves.append((node, parent_kind, field_name))
            return
        for index, child in enumerate(node.children):
            walk(child, node.type, node.field_name_for_child(index))

    walk(tree.root_node, None, None)

    spans: list[Span] = []
    cursor = 0
    for leaf, parent_kind, field_name in leaves:
        sb = leaf.start_byte
        eb = leaf.end_byte
        if sb > cursor:
            gap = src_bytes[cursor:sb].decode("utf-8", errors="replace")
            spans.append(
                _position(
                    Span(start=cursor, end=sb, token="variable", text=gap),
                    src_bytes,
                )
            )
        text = src_bytes[sb:eb].decode("utf-8", errors="replace")
        token = _classify(leaf.type, text, parent_kind, field_name)
        # Semantic upgrade: if the grammar produced a generic
        # "variable" classification but the env knows this name as a
        # type/function/namespace, paint it the env colour. This is
        # the seam that gives consistent highlighting across all REPL
        # output surfaces.
        if env_kinds and token == "variable":
            env_kind = env_kinds.get(text)
            if env_kind:
                token = env_kind
        spans.append(
            _position(Span(start=sb, end=eb, token=token, text=text), src_bytes)
        )
        cursor = eb
    if cursor < len(src_bytes):
        tail = src_bytes[cursor:].decode("utf-8", errors="replace")
        spans.append(
            _position(
                Span(
                    start=cursor,
                    end=len(src_bytes),
                    token="variable",
                    text=tail,
                ),
                src_bytes,
            )
        )
    return _merge_split_operators(spans, src_bytes)

run_plain

run_plain(session: ReplSession) -> int

Run the single-line REPL front end on session.

The front end reads with prompt_toolkit when it is installed and with bare standard input otherwise.

PARAMETER DESCRIPTION
session

The session that evaluates each line.

TYPE: ReplSession

RETURNS DESCRIPTION
int

The process exit status, 0 when the user quits.

Source code in src/quivers/cli/repl_prompt.py
def run_plain(session: ReplSession) -> int:
    """Run the single-line REPL front end on ``session``.

    The front end reads with prompt_toolkit when it is installed and
    with bare standard input otherwise.

    Parameters
    ----------
    session : ReplSession
        The session that evaluates each line.

    Returns
    -------
    int
        The process exit status, ``0`` when the user quits.
    """
    try:
        from prompt_toolkit import PromptSession
        from prompt_toolkit.completion import Completer, Completion
        from prompt_toolkit.history import FileHistory
        from prompt_toolkit.lexers import PygmentsLexer

        from quivers.cli.repl_complete import all_completions
        from quivers.dsl.pygments_lexer import QvrLexer

        history_path = _history_path()
        history_path.parent.mkdir(parents=True, exist_ok=True)

        class _Completer(Completer):
            def get_completions(self, document, complete_event):  # noqa: D401
                del complete_event
                word = document.get_word_before_cursor(WORD=True)
                for c in all_completions(session, word):
                    yield Completion(
                        text=c.text,
                        start_position=-len(word),
                        display_meta=c.detail,
                    )

        ps: PromptSession = PromptSession(
            message="qvr> ",
            history=FileHistory(str(history_path)),
            completer=_Completer(),
            lexer=PygmentsLexer(QvrLexer),
            multiline=False,
        )

        def getline() -> str:
            return ps.prompt()
    except ImportError:
        getline = _bare_input  # type: ignore[assignment]

    while True:
        try:
            line = getline()
        except EOFError, KeyboardInterrupt:
            sys.stdout.write("\n")
            return 0
        if line is None:
            return 0
        # Allow `:reload` to auto-fire if file changed on disk.
        auto = session.autoreload_if_stale()
        if auto is not None:
            _render(auto)
        response = session.dispatch(line)
        if response.body == "__quit__":
            return 0
        _render(response)

run_tui

run_tui(session: ReplSession) -> int

Run the Textual REPL front end on session.

PARAMETER DESCRIPTION
session

The session that evaluates each input.

TYPE: ReplSession

RETURNS DESCRIPTION
int

The process exit status, 0 once the app exits.

Source code in src/quivers/cli/repl_tui.py
def run_tui(session: ReplSession) -> int:
    """Run the Textual REPL front end on ``session``.

    Parameters
    ----------
    session : ReplSession
        The session that evaluates each input.

    Returns
    -------
    int
        The process exit status, ``0`` once the app exits.
    """
    app = _build_tui_app(session)
    app.run()  # type: ignore[attr-defined]
    return 0

__getattr__

__getattr__(name: str) -> Callable[[ReplSession], int]

Bind the re-exports whose modules need optional dependencies.

Importing run_tui loads Textual, which only the repl extra installs, so the package binds it on first access rather than at import time.

PARAMETER DESCRIPTION
name

The attribute being looked up.

TYPE: str

RETURNS DESCRIPTION
Callable[[ReplSession], int]

The lazily bound front end.

RAISES DESCRIPTION
AttributeError

When name is not a lazily bound re-export.

Source code in src/quivers/cli/__init__.py
def __getattr__(name: str) -> Callable[[ReplSession], int]:
    """Bind the re-exports whose modules need optional dependencies.

    Importing `run_tui` loads Textual, which only the ``repl`` extra
    installs, so the package binds it on first access rather than at
    import time.

    Parameters
    ----------
    name : str
        The attribute being looked up.

    Returns
    -------
    Callable[[ReplSession], int]
        The lazily bound front end.

    Raises
    ------
    AttributeError
        When ``name`` is not a lazily bound re-export.
    """
    if name == "run_tui":
        repl_tui = importlib.import_module("quivers.cli.repl_tui")
        return repl_tui.run_tui
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

main

main() -> int

Run the qvr command line on sys.argv.

RETURNS DESCRIPTION
int

The process exit status of the selected subcommand.

Source code in src/quivers/cli/__init__.py
def main() -> int:
    """Run the ``qvr`` command line on ``sys.argv``.

    Returns
    -------
    int
        The process exit status of the selected subcommand.
    """
    import argparse
    import sys

    parser = argparse.ArgumentParser(prog="qvr", description=__doc__)
    sub = parser.add_subparsers(dest="cmd", required=True)

    check = sub.add_parser(
        "check",
        help="Parse + compile .qvr files and report diagnostics.",
    )
    check.add_argument("files", nargs="+", help="Paths to .qvr files.")
    check.add_argument(
        "--json",
        action="store_true",
        help="Emit structured JSON diagnostics on stdout.",
    )
    check.add_argument(
        "--target",
        default=None,
        help="Also diagnose the features this transpile target lacks.",
    )

    repl = sub.add_parser(
        "repl",
        help="Start an interactive type-exploration REPL.",
    )
    repl.add_argument(
        "file",
        nargs="?",
        default=None,
        help="Optional .qvr file to :load on startup.",
    )
    repl.add_argument(
        "--plain",
        action="store_true",
        help="Use the prompt_toolkit single-line front end instead of the TUI.",
    )
    repl.add_argument(
        "--target",
        default=None,
        metavar="TARGET",
        help=(
            "Report this transpile target's capability diagnostics on :load "
            "and :reload, as `qvr check --target` does."
        ),
    )

    run = sub.add_parser(
        "run",
        help="Execute one entry point: a `define` computation or a `program`.",
    )
    run.add_argument("file", help="Path to the .qvr source file.")
    run.add_argument(
        "computation",
        nargs="?",
        default=None,
        help=(
            "Entry point to execute: a `define` computation or a `program`. "
            "Omit it, or pass --list, to list the file's entry points."
        ),
    )
    run.add_argument(
        "arguments",
        nargs="*",
        help="Value arguments as JSON literals, in declaration order.",
    )
    run.add_argument(
        "--list",
        action="store_true",
        help="List the file's entry points with their signatures and exit.",
    )
    run.add_argument(
        "--static",
        action="append",
        default=[],
        metavar="NAME=TERM",
        help="Closed static specialization; repeat once per telescope binder.",
    )
    run.add_argument(
        "--data",
        action="append",
        default=[],
        metavar="NAME=JSON",
        help=(
            "A program parameter by name (its data, an observation, a "
            "fibration, or a scalar); repeatable."
        ),
    )
    run.add_argument(
        "--site",
        action="append",
        default=[],
        metavar="NAME=JSON",
        help=(
            "Condition a program's sample site on a value; every other site "
            "is drawn. Repeatable."
        ),
    )
    run.add_argument(
        "--seed",
        type=int,
        default=None,
        metavar="N",
        help="Seed the reference generator a program run draws with.",
    )
    run.add_argument(
        "--runtime",
        default=None,
        metavar="FILE.json",
        help="Explicit JSON runtime-provider configuration.",
    )
    run.add_argument(
        "--fuel",
        type=int,
        default=None,
        metavar="STEPS",
        help=(
            "Stop after this many evaluation steps with a qiec-run-fuel "
            "diagnostic; recursion may otherwise run forever."
        ),
    )
    run.add_argument(
        "--trace",
        action="store_true",
        help="Write stable execution trace events to stderr.",
    )
    run.add_argument(
        "--json",
        action="store_true",
        help="Emit the result, diagnostics, and trace as JSON.",
    )

    lsp = sub.add_parser(
        "lsp",
        help="Run the QVR Language Server (LSP 3.17 over stdio).",
    )
    lsp.add_argument(
        "--tcp",
        type=int,
        default=None,
        metavar="PORT",
        help="Bind to TCP port instead of stdio.",
    )
    lsp.add_argument(
        "--target",
        default=None,
        help="Diagnose the features this transpile target lacks.",
    )

    migrate = sub.add_parser(
        "migrate",
        help="Migrate .qvr source files between tagged grammar revisions.",
    )
    migrate.add_argument(
        "--from",
        dest="from_ref",
        default=None,
        help="Source grammar revision (git tag or commit id). Required when "
        "migrating because QVR source does not yet carry a version marker.",
    )
    migrate.add_argument(
        "--to",
        dest="to_ref",
        default=None,
        help="Target grammar revision. Defaults to the upcoming "
        "release (last entry on the chain).",
    )
    migrate.add_argument(
        "--dry-run",
        action="store_true",
        help="Report what would change without writing output.",
    )
    migrate.add_argument(
        "--output",
        default=None,
        help="Write migrated files under this directory rather than "
        "overwriting in place.",
    )
    migrate.add_argument(
        "--check",
        action="store_true",
        help="Validate the migration chain against the panproto VCS schema "
        "diff, converter coverage, and asserted identity hops. Non-zero "
        "exit on uncovered or unexpected drift. Does not migrate files and "
        "does not require --from.",
    )
    migrate.add_argument(
        "paths",
        nargs="*",
        help=".qvr files or directories to migrate.",
    )

    kernel = sub.add_parser(
        "kernel",
        help="Jupyter kernel: install kernelspec or run a kernel.",
    )
    kernel_sub = kernel.add_subparsers(dest="kernel_cmd", required=True)
    kernel_install = kernel_sub.add_parser(
        "install", help="Register the `quivers` Jupyter kernelspec."
    )
    kernel_install.add_argument(
        "--user",
        action="store_true",
        help="Install to the user kernel directory instead of system.",
    )
    kernel_install.add_argument(
        "--prefix",
        default=None,
        help="Install kernelspec under PREFIX/share/jupyter/kernels.",
    )
    kernel_sub.add_parser(
        "run", help="Run as a Jupyter kernel (invoked by Jupyter itself)."
    )
    kernel_run_f = kernel_sub.add_parser(
        "_run_f",
        help=argparse.SUPPRESS,
    )
    kernel_run_f.add_argument("connection_file")

    transpile_sub = sub.add_parser(
        "transpile",
        help="Transpile a .qvr file to other probabilistic-programming "
        "languages (Stan, NumPyro, Pyro, PyMC, ...).",
    )
    transpile_sub.add_argument(
        "file",
        nargs="?",
        default=None,
        help="Path to the .qvr source file.",
    )
    transpile_sub.add_argument(
        "--to",
        default=None,
        help="Target backend name (e.g. 'stan', 'numpyro').",
    )
    transpile_sub.add_argument(
        "--to-all",
        action="store_true",
        help="Transpile to every registered backend.",
    )
    transpile_sub.add_argument(
        "--list-targets",
        action="store_true",
        help="List every registered backend and exit.",
    )
    transpile_sub.add_argument(
        "-o",
        "--output",
        default=None,
        help="Write the single-target output to this path instead of stdout.",
    )
    transpile_sub.add_argument(
        "-d",
        "--out-dir",
        default=None,
        help="With --to-all, write one file per backend under this "
        "directory (named <stem>.<extension>).",
    )

    args = parser.parse_args()
    if args.cmd == "check":
        return check_main(args.files, json_output=args.json, target=args.target)
    if args.cmd == "repl":
        from quivers.cli.repl import main as repl_main

        return repl_main(args)
    if args.cmd == "run":
        from quivers.cli.run import main as run_main

        return run_main(args)
    if args.cmd == "lsp":
        from quivers.cli.lsp import main as lsp_main

        return lsp_main(args)
    if args.cmd == "migrate":
        from quivers.cli.migrate import main as migrate_main

        return migrate_main(args)
    if args.cmd == "kernel":
        from quivers.kernel.install import main as kernel_main

        return kernel_main(args)
    if args.cmd == "transpile":
        from quivers.cli.transpile import main as transpile_main

        return transpile_main(args)
    parser.print_help(sys.stderr)
    return 2

migrations

Registry of structural one-hop .qvr source migrations.

Each adjacent-pair migrator is a callable bytes -> bytes built on the panproto pipeline in quivers.cli.migrations._common: parse with the source revision's tree-sitter parser, build the target schema by walking the source parse tree and emitting target vertices/edges/constraints, then emit canonical target bytes via the target revision's emit_pretty. The third stage is grammar- bound by panproto: a target schema that does not satisfy the target grammar's rules cannot emit.

The CHAIN tuple lists releases in chronological order with "HEAD" always last. Each adjacent pair (CHAIN[i], CHAIN[i+1]) must have a registered migrator in MIGRATORS. compose_migration walks CHAIN between any two listed releases and composes the intermediate hops into a single bytes -> bytes callable.

To add the next release:

  1. Tag the release in git.
  2. Run python grammars/qvr/vcs/build_schemas.py and python grammars/qvr/vcs/build_parsers.py.
  3. Replace the current v<latest> -> HEAD edge with the pinned v<latest> -> v<next> hop, then add a new v<next> -> HEAD hop covering the next batch of grammar changes. Use the explicit identity migrator when the authored grammar is unchanged.
  4. Insert the new release immediately before "HEAD" in CHAIN and register both affected edges in MIGRATORS.

The qvr migrate CLI consumes compose_migration and is agnostic to which pairs are registered.

CHAIN module-attribute

CHAIN: tuple[str, ...] = ('v0.2.0', 'v0.3.0', 'v0.4.0', 'v0.5.0', 'v0.6.0', 'v0.7.0', 'v0.9.0', 'v0.10.0', 'v0.11.0', 'v0.14.0', 'v0.15.0', 'v0.16.0', 'v0.17.0', 'v0.18.0', 'v0.19.0', 'HEAD')

The QVR grammar releases a migration can start or end at, oldest first, with "HEAD" (the current grammar) always last.

MIGRATORS module-attribute

MIGRATORS: dict[tuple[str, str], _Migrator] = {('v0.2.0', 'v0.3.0'): _hop_2_3.migrate, ('v0.3.0', 'v0.4.0'): _hop_3_4.migrate, ('v0.4.0', 'v0.5.0'): _hop_4_5.migrate, ('v0.5.0', 'v0.6.0'): _hop_5_6.migrate, ('v0.6.0', 'v0.7.0'): _hop_6_7.migrate, ('v0.7.0', 'v0.9.0'): _hop_7_9.migrate, ('v0.9.0', 'v0.10.0'): _identity.migrator('v0.9.0', 'v0.10.0'), ('v0.10.0', 'v0.11.0'): _hop_10_11.migrate, ('v0.11.0', 'v0.14.0'): _hop_11_14.migrate, ('v0.14.0', 'v0.15.0'): _hop_14_15.migrate, ('v0.15.0', 'v0.16.0'): _identity.migrator('v0.15.0', 'v0.16.0'), ('v0.16.0', 'v0.17.0'): _identity.migrator('v0.16.0', 'v0.17.0'), ('v0.17.0', 'v0.18.0'): _identity.migrator('v0.17.0', 'v0.18.0'), ('v0.18.0', 'v0.19.0'): _hop_18_19.migrate, ('v0.19.0', 'HEAD'): _hop_19_head.migrate}

The one-hop migrator registered for each adjacent pair of CHAIN, a bytes -> bytes rewrite of .qvr source.

COVERAGE module-attribute

COVERAGE: dict[tuple[str, str], frozenset[str]] = {('v0.2.0', 'v0.3.0'): _hop_2_3.SOURCE_RULE_COVERAGE, ('v0.3.0', 'v0.4.0'): _hop_3_4.SOURCE_RULE_COVERAGE, ('v0.4.0', 'v0.5.0'): _hop_4_5.SOURCE_RULE_COVERAGE, ('v0.5.0', 'v0.6.0'): _hop_5_6.SOURCE_RULE_COVERAGE, ('v0.6.0', 'v0.7.0'): _hop_6_7.SOURCE_RULE_COVERAGE, ('v0.7.0', 'v0.9.0'): _hop_7_9.SOURCE_RULE_COVERAGE, ('v0.9.0', 'v0.10.0'): _identity.SOURCE_RULE_COVERAGE, ('v0.10.0', 'v0.11.0'): _hop_10_11.SOURCE_RULE_COVERAGE, ('v0.11.0', 'v0.14.0'): _hop_11_14.SOURCE_RULE_COVERAGE, ('v0.14.0', 'v0.15.0'): _hop_14_15.SOURCE_RULE_COVERAGE, ('v0.15.0', 'v0.16.0'): _identity.SOURCE_RULE_COVERAGE, ('v0.16.0', 'v0.17.0'): _identity.SOURCE_RULE_COVERAGE, ('v0.17.0', 'v0.18.0'): _identity.SOURCE_RULE_COVERAGE, ('v0.18.0', 'v0.19.0'): _hop_18_19.SOURCE_RULE_COVERAGE, ('v0.19.0', 'HEAD'): _hop_19_head.SOURCE_RULE_COVERAGE}

The source-side grammar rules each hop's converters cover, keyed by adjacent pair of CHAIN. vcs_coverage_report checks these against the schema diff between the two revisions.

IDENTITY_PAIRS module-attribute

IDENTITY_PAIRS: frozenset[tuple[str, str]] = _declared_identity_pairs()

The adjacent pairs of CHAIN whose grammars are identical, so that their registered migrator returns its source unchanged.

MigrationError

Bases: Exception

Raised when a requested migration cannot be composed, or when a source construct admits no rewrite under the target grammar.

BlameReport dataclass

BlameReport(rule: str, introduced_at_commit: str | None, introduced_at_tag: str | None, last_present_at_commit: str | None, last_present_at_tag: str | None)

Where in the grammar's history a rule first appeared or was last seen.

PARAMETER DESCRIPTION
rule

The tree-sitter rule name.

TYPE: str

introduced_at_commit

The commit that introduced the rule, when the VCS records one.

TYPE: str or None

introduced_at_tag

The release tag of that commit, when it has one.

TYPE: str or None

last_present_at_commit

The newest commit whose schema contains the rule.

TYPE: str or None

last_present_at_tag

The release tag of that commit, when it has one.

TYPE: str or None

DiffCoverageReport dataclass

DiffCoverageReport(from_ref: str, to_ref: str, added_rules: tuple[str, ...], removed_rules: tuple[str, ...], uncovered_removed: tuple[str, ...])

The schema diff between two revisions, classified against a hop's declared converters.

PARAMETER DESCRIPTION
from_ref

The source release.

TYPE: str

to_ref

The target release.

TYPE: str

added_rules

Grammar rules present at to_ref but not at from_ref.

TYPE: tuple[str, ...]

removed_rules

Grammar rules present at from_ref but not at to_ref.

TYPE: tuple[str, ...]

uncovered_removed

The removed rules that the hop declares no converter for.

TYPE: tuple[str, ...]

uncovered_removed instance-attribute

uncovered_removed: tuple[str, ...]

Rules removed at the target revision that are also absent from declared_converters. Each one is a source-side vertex kind that the migrator's dispatch passes through as a structural clone, which emit_pretty then misrenders or drops. These are the actionable misses.

is_complete property

is_complete: bool

Whether every removed rule has a converter.

format

format() -> str

Render the report for CLI display.

RETURNS DESCRIPTION
str

A multi-line summary of the added, removed, and uncovered rules.

Source code in src/quivers/cli/migrations/_vcs.py
def format(self) -> str:
    """Render the report for CLI display.

    Returns
    -------
    str
        A multi-line summary of the added, removed, and uncovered
        rules.
    """
    lines = [f"{self.from_ref} -> {self.to_ref}:"]
    if not self.added_rules and not self.removed_rules:
        lines.append("    (grammar identical; no diff)")
        return "\n".join(lines)
    if self.removed_rules:
        lines.append(f"    removed: {', '.join(self.removed_rules)}")
    if self.added_rules:
        lines.append(f"    added:   {', '.join(self.added_rules)}")
    if self.uncovered_removed:
        lines.append(
            f"    UNCOVERED removed rules (no converter): "
            f"{', '.join(self.uncovered_removed)}",
        )
    elif self.removed_rules:
        lines.append("    all removed rules have converters [OK]")
    return "\n".join(lines)

blame_kind

blame_kind(rule: str) -> BlameReport

Report when a tree-sitter rule was introduced or removed.

The migrators' failure path uses this to point at the release that needs a new converter.

PARAMETER DESCRIPTION
rule

The tree-sitter rule name.

TYPE: str

RETURNS DESCRIPTION
BlameReport

The rule's introduction and last appearance in the grammar's VCS history.

Source code in src/quivers/cli/migrations/_vcs.py
def blame_kind(rule: str) -> BlameReport:
    """Report when a tree-sitter rule was introduced or removed.

    The migrators' failure path uses this to point at the release that
    needs a new converter.

    Parameters
    ----------
    rule : str
        The tree-sitter rule name.

    Returns
    -------
    BlameReport
        The rule's introduction and last appearance in the grammar's
        VCS history.
    """
    repo = _open_repo()
    head = repo.head() or ""

    introduced_commit: str | None = None
    try:
        info = repo.blame_vertex(head, rule)
        introduced_commit = str(info.get("commit"))  # type: ignore[union-attr]
    except Exception:
        introduced_commit = None

    introduced_tag = (
        _tag_for_commit(repo, introduced_commit) if introduced_commit else None
    )

    # Walk log oldest-first, find the last commit whose schema
    # contains the rule. If the current HEAD schema contains it,
    # introduced_commit is the answer to both "introduced" and
    # "last present." Otherwise scan history.
    last_commit: str | None = None
    for entry in repo.log():
        cid = str(entry["id"])
        try:
            schema = repo.schema_at(cid)
        except Exception:
            continue
        if schema.has_vertex(rule):
            last_commit = cid
            break  # log() is newest-first; the first hit is the last presence.
    last_tag = _tag_for_commit(repo, last_commit) if last_commit else None

    return BlameReport(
        rule=rule,
        introduced_at_commit=introduced_commit,
        introduced_at_tag=introduced_tag,
        last_present_at_commit=last_commit,
        last_present_at_tag=last_tag,
    )

check_chain_coverage

check_chain_coverage(chain: tuple[str, ...], converters_by_pair: dict[tuple[str, str], frozenset[str]]) -> list[DiffCoverageReport]

Run diff_coverage on every adjacent pair of a chain.

PARAMETER DESCRIPTION
chain

Releases in chronological order.

TYPE: tuple[str, ...]

converters_by_pair

The source-side rule names each (from, to) hop declares it converts. A pair absent from the map has no converters, so every rule it removes surfaces as uncovered.

TYPE: dict[tuple[str, str], frozenset[str]]

RETURNS DESCRIPTION
list[DiffCoverageReport]

One report per adjacent pair, in chain order.

Source code in src/quivers/cli/migrations/_vcs.py
def check_chain_coverage(
    chain: tuple[str, ...],
    converters_by_pair: dict[tuple[str, str], frozenset[str]],
) -> list[DiffCoverageReport]:
    """Run `diff_coverage` on every adjacent pair of a chain.

    Parameters
    ----------
    chain : tuple[str, ...]
        Releases in chronological order.
    converters_by_pair : dict[tuple[str, str], frozenset[str]]
        The source-side rule names each ``(from, to)`` hop declares it
        converts. A pair absent from the map has no converters, so
        every rule it removes surfaces as uncovered.

    Returns
    -------
    list[DiffCoverageReport]
        One report per adjacent pair, in chain order.
    """
    reports: list[DiffCoverageReport] = []
    for i in range(len(chain) - 1):
        from_ref = chain[i]
        to_ref = chain[i + 1]
        decl = converters_by_pair.get((from_ref, to_ref), frozenset())
        reports.append(diff_coverage(from_ref, to_ref, decl))
    return reports

diff_coverage

diff_coverage(from_ref: str, to_ref: str, declared_converters: frozenset[str]) -> DiffCoverageReport

Diff two revisions' grammar schemas against a hop's converters.

A rule appearing in from_ref's schema but not in to_ref's is a removed rule. A removed rule that declared_converters does not list would pass through the migrator unconverted, likely producing incorrect output; such rules surface in uncovered_removed. Identity hops, whose revisions share a commit, report no diff.

PARAMETER DESCRIPTION
from_ref

The source release.

TYPE: str

to_ref

The target release.

TYPE: str

declared_converters

The source-side rule names the hop declares it converts.

TYPE: frozenset[str]

RETURNS DESCRIPTION
DiffCoverageReport

The classified diff.

Source code in src/quivers/cli/migrations/_vcs.py
def diff_coverage(
    from_ref: str,
    to_ref: str,
    declared_converters: frozenset[str],
) -> DiffCoverageReport:
    """Diff two revisions' grammar schemas against a hop's converters.

    A rule appearing in ``from_ref``'s schema but not in ``to_ref``'s
    is a removed rule. A removed rule that ``declared_converters``
    does not list would pass through the migrator unconverted, likely
    producing incorrect output; such rules surface in
    ``uncovered_removed``. Identity hops, whose revisions share a
    commit, report no diff.

    Parameters
    ----------
    from_ref : str
        The source release.
    to_ref : str
        The target release.
    declared_converters : frozenset[str]
        The source-side rule names the hop declares it converts.

    Returns
    -------
    DiffCoverageReport
        The classified diff.
    """
    repo = _open_repo()
    from_id = commit_id_for(from_ref) or _resolve_via_chain(repo, from_ref)
    to_id = commit_id_for(to_ref) or _resolve_via_chain(repo, to_ref)
    if from_id == to_id:
        return DiffCoverageReport(
            from_ref=from_ref,
            to_ref=to_ref,
            added_rules=(),
            removed_rules=(),
            uncovered_removed=(),
        )
    src_schema = repo.schema_at(from_id)
    tgt_schema = repo.schema_at(to_id)
    diff = panproto.diff_schemas(src_schema, tgt_schema)
    diff_dict = diff.to_dict()
    added = tuple(sorted(diff_dict.get("added_vertices", [])))
    removed = tuple(sorted(diff_dict.get("removed_vertices", [])))
    uncovered = tuple(r for r in removed if r not in declared_converters)
    return DiffCoverageReport(
        from_ref=from_ref,
        to_ref=to_ref,
        added_rules=added,
        removed_rules=removed,
        uncovered_removed=uncovered,
    )

commit_id

commit_id(ref: str) -> str

Resolve a release name to its pinned panproto VCS commit id.

The commit ids come from the schema-commit manifest and are computed once, on first call.

PARAMETER DESCRIPTION
ref

A release name in CHAIN, such as "v0.19.0" or "HEAD".

TYPE: str

RETURNS DESCRIPTION
str

The commit id, or "" when ref is not in CHAIN.

RAISES DESCRIPTION
MigrationError

When the manifest disagrees with CHAIN or with the VCS tags.

Source code in src/quivers/cli/migrations/__init__.py
def commit_id(ref: str) -> str:
    """Resolve a release name to its pinned panproto VCS commit id.

    The commit ids come from the schema-commit manifest and are
    computed once, on first call.

    Parameters
    ----------
    ref : str
        A release name in `CHAIN`, such as ``"v0.19.0"`` or ``"HEAD"``.

    Returns
    -------
    str
        The commit id, or ``""`` when ``ref`` is not in `CHAIN`.

    Raises
    ------
    MigrationError
        When the manifest disagrees with `CHAIN` or with the VCS tags.
    """
    if not _COMMIT_IDS:
        _COMMIT_IDS.update(_build_commit_index())
    return _COMMIT_IDS.get(ref, "")

vcs_coverage_report

vcs_coverage_report() -> list[DiffCoverageReport]

Check every registered hop against the grammar's schema diff.

For each adjacent pair of CHAIN, the report carries the schema diff between the two revisions and the removed source rules that the hop's COVERAGE entry does not cover. qvr migrate --check runs this, and a CI test can run it to catch a migrator that has fallen behind a grammar change.

RETURNS DESCRIPTION
list[DiffCoverageReport]

One report per adjacent pair of CHAIN, in chain order.

RAISES DESCRIPTION
MigrationError

When MIGRATORS, COVERAGE, or IDENTITY_PAIRS disagree with CHAIN or with the grammar manifest.

Source code in src/quivers/cli/migrations/__init__.py
def vcs_coverage_report() -> list[DiffCoverageReport]:
    """Check every registered hop against the grammar's schema diff.

    For each adjacent pair of `CHAIN`, the report carries the schema
    diff between the two revisions and the removed source rules that
    the hop's `COVERAGE` entry does not cover. ``qvr migrate --check``
    runs this, and a CI test can run it to catch a migrator that has
    fallen behind a grammar change.

    Returns
    -------
    list[DiffCoverageReport]
        One report per adjacent pair of `CHAIN`, in chain order.

    Raises
    ------
    MigrationError
        When `MIGRATORS`, `COVERAGE`, or `IDENTITY_PAIRS` disagree with
        `CHAIN` or with the grammar manifest.
    """
    identity_pairs = _validate_registry()
    reports: list[DiffCoverageReport] = []
    for i in range(len(CHAIN) - 1):
        from_ref = CHAIN[i]
        to_ref = CHAIN[i + 1]
        if (from_ref, to_ref) in identity_pairs:
            _identity.validate_identity_pair(from_ref, to_ref)
        decl = COVERAGE.get((from_ref, to_ref), frozenset())
        from_id = commit_id(from_ref)
        to_id = commit_id(to_ref)
        if from_id == to_id:
            reports.append(
                DiffCoverageReport(
                    from_ref=from_ref,
                    to_ref=to_ref,
                    added_rules=(),
                    removed_rules=(),
                    uncovered_removed=(),
                )
            )
            continue
        # Re-implement diff_coverage's body using the resolved
        # commit ids directly to avoid double-resolution.
        from quivers.cli.migrations._vcs import _open_repo
        import panproto

        repo = _open_repo()
        src_schema = repo.schema_at(from_id)
        tgt_schema = repo.schema_at(to_id)
        diff_dict = panproto.diff_schemas(src_schema, tgt_schema).to_dict()
        added = tuple(sorted(diff_dict.get("added_vertices", [])))
        removed = tuple(sorted(diff_dict.get("removed_vertices", [])))
        uncovered = tuple(r for r in removed if r not in decl)
        reports.append(
            DiffCoverageReport(
                from_ref=from_ref,
                to_ref=to_ref,
                added_rules=added,
                removed_rules=removed,
                uncovered_removed=uncovered,
            )
        )
    return reports

compose_migration

compose_migration(from_ref: str, to_ref: str) -> _Migrator

Compose the hops between two releases into one migrator.

PARAMETER DESCRIPTION
from_ref

The source release, a member of CHAIN.

TYPE: str

to_ref

The target release, a member of CHAIN no earlier than from_ref.

TYPE: str

RETURNS DESCRIPTION
Callable[[bytes], bytes]

A rewrite of .qvr source from from_ref's grammar to to_ref's, applying every registered hop in between.

RAISES DESCRIPTION
MigrationError

When either release is not in CHAIN, when to_ref precedes from_ref, or when a hop between them has no migrator.

Source code in src/quivers/cli/migrations/__init__.py
def compose_migration(from_ref: str, to_ref: str) -> _Migrator:
    """Compose the hops between two releases into one migrator.

    Parameters
    ----------
    from_ref : str
        The source release, a member of `CHAIN`.
    to_ref : str
        The target release, a member of `CHAIN` no earlier than
        ``from_ref``.

    Returns
    -------
    Callable[[bytes], bytes]
        A rewrite of ``.qvr`` source from ``from_ref``'s grammar to
        ``to_ref``'s, applying every registered hop in between.

    Raises
    ------
    MigrationError
        When either release is not in `CHAIN`, when ``to_ref`` precedes
        ``from_ref``, or when a hop between them has no migrator.
    """
    pairs = _chain_slice(from_ref, to_ref)
    if not pairs:
        return _identity.migrator(from_ref, to_ref)
    missing = [pair for pair in pairs if pair not in MIGRATORS]
    if missing:
        raise MigrationError(
            "missing migrator(s) for pair(s): "
            + ", ".join(f"{a} -> {b}" for a, b in missing),
        )
    migrators = [MIGRATORS[pair] for pair in pairs]

    def _composite(source: bytes) -> bytes:
        for step in migrators:
            source = step(source)
        return source

    return _composite

available_targets

available_targets(from_ref: str) -> tuple[str, ...]

List the releases a migration from from_ref can target.

PARAMETER DESCRIPTION
from_ref

The source release.

TYPE: str

RETURNS DESCRIPTION
tuple[str, ...]

from_ref and every later release of CHAIN, or the empty tuple when from_ref is not in CHAIN.

Source code in src/quivers/cli/migrations/__init__.py
def available_targets(from_ref: str) -> tuple[str, ...]:
    """List the releases a migration from ``from_ref`` can target.

    Parameters
    ----------
    from_ref : str
        The source release.

    Returns
    -------
    tuple[str, ...]
        ``from_ref`` and every later release of `CHAIN`, or the empty
        tuple when ``from_ref`` is not in `CHAIN`.
    """
    if from_ref not in CHAIN:
        return ()
    i = CHAIN.index(from_ref)
    return CHAIN[i:]

lsp

QVR Language Server.

Public entry: build_server returns a configured pygls LanguageServer that speaks LSP 3.17 over stdio. The qvr lsp subcommand wraps it with stdio and TCP plumbing.

The server reuses every analytic component the REPL uses: per-document state in the style of ReplSession, tokenize for semantic tokens, and all_completions for completion. There is no duplicate parser, type-checker, or token vocabulary.

SERVER_NAME module-attribute

SERVER_NAME = 'qvr-lsp'

The server name reported to clients and attached to diagnostics.

SERVER_VERSION module-attribute

SERVER_VERSION = '0.3.0'

The server version reported to clients in the initialize response.

build_server

build_server(*, target: str | None = None) -> LanguageServer

Build a QVR language server with every feature registered.

PARAMETER DESCRIPTION
target

A transpile target whose missing capabilities are published as qiec:capability:* diagnostics; None disables them. A client may change it later through workspace/didChangeConfiguration.

TYPE: str or None DEFAULT: None

RETURNS DESCRIPTION
LanguageServer

The configured pygls server, ready for start_io or start_tcp.

Source code in src/quivers/lsp/server.py
def build_server(*, target: str | None = None) -> LanguageServer:
    """Build a QVR language server with every feature registered.

    Parameters
    ----------
    target : str or None
        A transpile target whose missing capabilities are published as
        ``qiec:capability:*`` diagnostics; ``None`` disables them. A
        client may change it later through
        ``workspace/didChangeConfiguration``.

    Returns
    -------
    LanguageServer
        The configured pygls server, ready for ``start_io`` or
        ``start_tcp``.
    """
    server = LanguageServer(name=SERVER_NAME, version=SERVER_VERSION)
    docs: dict[str, DocumentState] = {}
    selected_target = target

    # ----- lifecycle ----------------------------------------------------

    @server.feature(lsp.TEXT_DOCUMENT_DID_OPEN)
    def _did_open(ls: LanguageServer, params: lsp.DidOpenTextDocumentParams) -> None:
        doc = DocumentState(uri=params.text_document.uri, target=selected_target)
        doc.update(
            source=params.text_document.text,
            version=params.text_document.version,
        )
        docs[doc.uri] = doc
        _publish(ls, doc)

    @server.feature(lsp.TEXT_DOCUMENT_DID_CHANGE)
    def _did_change(
        ls: LanguageServer, params: lsp.DidChangeTextDocumentParams
    ) -> None:
        uri = params.text_document.uri
        doc = docs.get(uri)
        if doc is None:
            return
        # Apply incremental changes if present; full-replace otherwise.
        for change in params.content_changes:
            if isinstance(change, lsp.TextDocumentContentChangePartial):
                doc.source = _apply_partial(doc.source, change)
            else:
                doc.source = change.text
        doc.update(source=doc.source, version=params.text_document.version)
        _publish(ls, doc)

    @server.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
    def _did_save(ls: LanguageServer, params: lsp.DidSaveTextDocumentParams) -> None:
        doc = docs.get(params.text_document.uri)
        if doc is None:
            return
        if params.text is not None:
            doc.update(source=params.text, version=doc.version)
        _publish(ls, doc)

    @server.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
    def _did_close(_ls: LanguageServer, params: lsp.DidCloseTextDocumentParams) -> None:
        docs.pop(params.text_document.uri, None)

    @server.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION)
    def _did_change_configuration(
        ls: LanguageServer, params: lsp.DidChangeConfigurationParams
    ) -> None:
        """Refresh target-capability diagnostics for every open document.

        Clients may send either the VS Code-shaped
        ``{qvr: {transpileTarget: ...}}`` object or the section value
        ``{transpileTarget: ...}``. An empty string disables target-specific
        diagnostics. Settings unrelated to the target leave it unchanged.
        """

        nonlocal selected_target
        configured = _target_from_settings(params.settings)
        if configured is _TARGET_UNCHANGED:
            return
        selected_target = cast(str | None, configured)
        for doc in docs.values():
            doc.retarget(selected_target)
            _publish(ls, doc)

    # ----- semantic tokens ---------------------------------------------

    @server.feature(
        lsp.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL,
        lsp.SemanticTokensLegend(
            token_types=list(SEMANTIC_TOKEN_TYPES),
            token_modifiers=list(SEMANTIC_TOKEN_MODIFIERS),
        ),
    )
    def _semantic_tokens(
        _ls: LanguageServer, params: lsp.SemanticTokensParams
    ) -> lsp.SemanticTokens:
        doc = docs.get(params.text_document.uri)
        if doc is None:
            return lsp.SemanticTokens(data=[])
        env_kinds = _env_kinds_for(doc)
        data = to_semantic_token_data(doc.source, env_kinds=env_kinds)
        return lsp.SemanticTokens(data=data)

    # ----- hover --------------------------------------------------------

    @server.feature(lsp.TEXT_DOCUMENT_HOVER)
    def _hover(_ls: LanguageServer, params: lsp.HoverParams) -> lsp.Hover | None:
        doc = docs.get(params.text_document.uri)
        if doc is None:
            return None
        name = doc.name_at_position(params.position.line, params.position.character)
        if name is None:
            return None
        body = _render_hover(
            doc,
            name,
            line=params.position.line,
            col=params.position.character,
        )
        if body is None:
            return None
        return lsp.Hover(
            contents=lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=body)
        )

    # ----- definition / references / documentSymbol --------------------

    @server.feature(lsp.TEXT_DOCUMENT_DEFINITION)
    def _definition(
        _ls: LanguageServer, params: lsp.DefinitionParams
    ) -> list[lsp.Location] | None:
        doc = docs.get(params.text_document.uri)
        if doc is None:
            return None
        name = doc.name_at_position(params.position.line, params.position.character)
        if name is None:
            return None
        decl = doc.find_decl(
            name,
            line=params.position.line,
            col=params.position.character,
        )
        if decl is None:
            return None
        line, col = _name_position(doc, decl, name)
        return [
            lsp.Location(
                uri=doc.uri,
                range=lsp.Range(
                    start=lsp.Position(line=line, character=col),
                    end=lsp.Position(line=line, character=col + len(name)),
                ),
            )
        ]

    @server.feature(lsp.TEXT_DOCUMENT_REFERENCES)
    def _references(
        _ls: LanguageServer, params: lsp.ReferenceParams
    ) -> list[lsp.Location] | None:
        doc = docs.get(params.text_document.uri)
        if doc is None:
            return None
        name = doc.name_at_position(params.position.line, params.position.character)
        if name is None:
            return None
        return list(
            _find_references(
                doc,
                name,
                line=params.position.line,
                col=params.position.character,
            )
        )

    @server.feature(lsp.TEXT_DOCUMENT_PREPARE_RENAME)
    def _prepare_rename(
        _ls: LanguageServer, params: lsp.PrepareRenameParams
    ) -> lsp.PrepareRenameResult_Type1 | None:
        doc = docs.get(params.text_document.uri)
        if doc is None:
            return None
        name = doc.name_at_position(params.position.line, params.position.character)
        if name is None:
            return None
        if _renameable_declaration(doc, name, params.position) is None:
            return None
        line, col = params.position.line, params.position.character
        text = doc.source.splitlines()[line]
        start = text.rfind(name, 0, col + len(name))
        return lsp.PrepareRenameResult_Type1(
            range=lsp.Range(
                start=lsp.Position(line=line, character=start),
                end=lsp.Position(line=line, character=start + len(name)),
            ),
            placeholder=name,
        )

    @server.feature(lsp.TEXT_DOCUMENT_RENAME)
    def _rename(
        _ls: LanguageServer, params: lsp.RenameParams
    ) -> lsp.WorkspaceEdit | None:
        doc = docs.get(params.text_document.uri)
        if doc is None:
            return None
        name = doc.name_at_position(params.position.line, params.position.character)
        if name is None:
            return None
        if _renameable_declaration(doc, name, params.position) is None:
            return None
        if not params.new_name.isidentifier():
            return None
        edits = [
            lsp.TextEdit(range=location.range, new_text=params.new_name)
            for location in _find_references(
                doc,
                name,
                line=params.position.line,
                col=params.position.character,
            )
        ]
        if not edits:
            return None
        return lsp.WorkspaceEdit(changes={doc.uri: edits})

    @server.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
    def _symbols(
        _ls: LanguageServer, params: lsp.DocumentSymbolParams
    ) -> list[lsp.DocumentSymbol]:
        doc = docs.get(params.text_document.uri)
        if doc is None:
            return []
        return _statement_symbols(doc, doc.module.statements)

    # ----- completion ---------------------------------------------------

    @server.feature(
        lsp.TEXT_DOCUMENT_COMPLETION,
        lsp.CompletionOptions(trigger_characters=[":", " ", "."]),
    )
    def _completion(
        _ls: LanguageServer, params: lsp.CompletionParams
    ) -> lsp.CompletionList:
        doc = docs.get(params.text_document.uri)
        if doc is None:
            return lsp.CompletionList(is_incomplete=False, items=[])
        session = ReplSession()
        session._module = doc.module  # noqa: SLF001
        session._compiler = doc.compiler  # noqa: SLF001
        session._qiec_module = doc.qiec_module  # noqa: SLF001
        session._env = doc.env  # noqa: SLF001
        prefix = _prefix_at(doc.source, params.position.line, params.position.character)
        items = _qiec_local_completion_items(
            doc,
            prefix,
            line=params.position.line,
            col=params.position.character,
        )
        local_names = {item.label for item in items}
        items.extend(
            lsp.CompletionItem(
                label=completion.text,
                kind=_completion_kind(completion.kind),
                detail=completion.detail,
            )
            for completion in all_completions(session, prefix)
            if completion.text not in local_names
        )
        return lsp.CompletionList(is_incomplete=False, items=items)

    # ----- formatting ---------------------------------------------------

    @server.feature(lsp.TEXT_DOCUMENT_FORMATTING)
    def _formatting(
        _ls: LanguageServer, params: lsp.DocumentFormattingParams
    ) -> list[lsp.TextEdit] | None:
        doc = docs.get(params.text_document.uri)
        return _format_document(doc) if doc is not None else None

    # The pygls feature decorator registers each handler with the
    # server, but the resulting name is never referenced from this
    # function's local scope, so the linter flags it as unused. The
    # tuple below names every handler explicitly to make the lifetime
    # contract legible to a reader and to the type checker.
    _registered_handlers = (
        _did_open,
        _did_change,
        _did_save,
        _did_close,
        _did_change_configuration,
        _semantic_tokens,
        _hover,
        _definition,
        _references,
        _prepare_rename,
        _rename,
        _symbols,
        _completion,
        _formatting,
    )
    del _registered_handlers

    return server

kernel

Jupyter kernel for QVR.

Two console-script entry points:

  • qvr kernel install registers the kernelspec.
  • qvr kernel run is invoked by Jupyter once a notebook starts.

The kernel reuses ReplSession so notebook cells behave exactly like the REPL: leading : runs a meta command; bare cells parse first as statements (appended to the session's module) and fall back to type-printing.

KERNELSPEC module-attribute

KERNELSPEC = {'argv': [sys.executable, '-m', 'quivers.kernel', '_run_f', '{connection_file}'], 'display_name': 'quivers', 'language': 'qvr', 'interrupt_mode': 'signal', 'metadata': {'debugger': False}}

The kernel.json document install_kernelspec registers: it launches the kernel with the running interpreter and names the language qvr.

QuiversKernel

QuiversKernel(**kwargs: Any)

Bases: Kernel

A Jupyter kernel that evaluates QVR cells in a ReplSession.

Each line of a cell is dispatched to the session as the REPL would dispatch it, so a leading : runs a meta-command and other input is elaborated against the session's module. Jupyter constructs the kernel; install_kernelspec registers it.

PARAMETER DESCRIPTION
**kwargs

Keyword arguments forwarded to ipykernel.kernelbase.Kernel.

TYPE: Any DEFAULT: {}

Source code in src/quivers/kernel/quivers_kernel.py
def __init__(self, **kwargs: Any) -> None:
    super().__init__(**kwargs)
    self.repl = ReplSession()

install_kernelspec

install_kernelspec(*, user: bool = True, prefix: str | None = None) -> Path

Register the QVR kernelspec with Jupyter.

PARAMETER DESCRIPTION
user

Install into the per-user kernel directory rather than the system one.

TYPE: bool DEFAULT: True

prefix

An explicit Jupyter prefix to install under, as PREFIX/share/jupyter/kernels; None uses Jupyter's default location.

TYPE: str or None DEFAULT: None

RETURNS DESCRIPTION
Path

The directory the kernelspec was installed into.

RAISES DESCRIPTION
ImportError

When jupyter_client is not installed.

Source code in src/quivers/kernel/install.py
def install_kernelspec(*, user: bool = True, prefix: str | None = None) -> Path:
    """Register the QVR kernelspec with Jupyter.

    Parameters
    ----------
    user : bool
        Install into the per-user kernel directory rather than the
        system one.
    prefix : str or None
        An explicit Jupyter prefix to install under, as
        ``PREFIX/share/jupyter/kernels``; ``None`` uses Jupyter's
        default location.

    Returns
    -------
    Path
        The directory the kernelspec was installed into.

    Raises
    ------
    ImportError
        When ``jupyter_client`` is not installed.
    """
    from jupyter_client.kernelspec import KernelSpecManager

    with tempfile.TemporaryDirectory() as tmp:
        tmp_path = Path(tmp)
        (tmp_path / "kernel.json").write_text(
            json.dumps(KERNELSPEC, indent=2), encoding="utf-8"
        )
        mgr = KernelSpecManager()
        dest = mgr.install_kernel_spec(
            str(tmp_path),
            kernel_name="quivers",
            user=user,
            prefix=prefix,
        )
    return Path(dest)