Skip to content

Interpreter Plugins

Lackpy was originally built around one execution model: restricted Python over a resolved kit of callable tools. Starting in v0.5.0, that model is one plugin among several. An interpreter is anything that takes a program string, validates it, and executes it against an ExecutionContext to produce a structured result.

Library-only in v0.5.x

The interpreter plugin system is currently a library API, not yet plumbed through LackpyService.delegate() or the lackpy delegate CLI. To use any non-default interpreter, import it and call run_interpreter() directly (see the examples below). CLI and service integration is planned for a future release — when it lands, the python interpreter will remain the default so existing callers are unaffected.

The protocol is intentionally small:

class Interpreter(Protocol):
    name: str
    description: str

    def validate(self, program: str, context: ExecutionContext) -> InterpreterValidationResult: ...

    async def execute(self, program: str, context: ExecutionContext) -> InterpreterExecutionResult: ...

Enforcement during execution is the interpreter's own responsibility — the Python interpreter uses AST restrictions, ast-select relies on pluckit's read-only AST queries, and so on.

Using an interpreter directly

All bundled interpreters are exported from lackpy.interpreters. The run_interpreter() helper validates then executes, returning a single InterpreterExecutionResult:

import asyncio
from lackpy.interpreters import (
    AstSelectInterpreter,
    ExecutionContext,
    run_interpreter,
)

async def main():
    interp = AstSelectInterpreter()
    ctx = ExecutionContext(config={"code": "src/greetings.py"})
    result = await run_interpreter(interp, ".fn#greet", ctx)
    print(result.output)        # markdown string
    print(result.output_format) # "markdown"
    print(result.metadata)      # {"selector": ".fn#greet", "match_count": 1, ...}

asyncio.run(main())

Bundled interpreters

python — restricted Python (the default execution path)

The original lackpy execution model, now exposed as a plugin. Accepts a restricted subset of Python (no import, def, class, no attribute access outside the kit) and runs it against the resolved kit's callables. Output is whatever the program's last expression evaluated to.

from lackpy.interpreters import PythonInterpreter, ExecutionContext, run_interpreter
from lackpy.kit.registry import resolve_kit

interp = PythonInterpreter()
kit = resolve_kit(["read_file"])
ctx = ExecutionContext(kit=kit)
result = await run_interpreter(
    interp,
    'content = read_file("README.md")\ncontent',
    ctx,
)

This is the same interpreter LackpyService.delegate() uses under the hood — you rarely need to drive it yourself, but it's useful for testing validation or running a program you already generated elsewhere.

ast-select — bare CSS selectors

Evaluates a single pluckit CSS-style selector against source code and renders the matches as markdown. The selector itself is the program — no chaining, no function calls, just one selector per invocation. The source file path is supplied via context.config["code"].

from lackpy.interpreters import AstSelectInterpreter, ExecutionContext, run_interpreter

interp = AstSelectInterpreter()
ctx = ExecutionContext(config={"code": "src/greetings.py"})
result = await run_interpreter(interp, ".fn#greet", ctx)

Output is markdown with the selector as an H1, one H2 per match with the qualified name and file:line-range, and a language-tagged code block for each match body. Set config={"mode": "brief"} for single-line signature-only output.

pss — pluckit selector sheets

Accepts a multi-rule selector sheet (selector + declaration blocks, like CSS) and renders all matches as markdown via pluckit's AstViewer plugin. Useful for building multi-section views of a codebase in one shot.

from lackpy.interpreters import PssInterpreter, ExecutionContext, run_interpreter

sheet = """
.fn#main { show: signature; }
.class#User { show: body; }
"""

interp = PssInterpreter()
ctx = ExecutionContext(config={"code": "src/app.py"})
result = await run_interpreter(interp, sheet, ctx)

plucker — fluent chain expressions

A thin wrapper over the python interpreter with a pluckit-specific kit. The program is a fluent chain over pluckit's Plucker and Selection classes, entered via source(code):

from lackpy.interpreters import PluckerInterpreter, ExecutionContext, run_interpreter

interp = PluckerInterpreter()
ctx = ExecutionContext(config={"code": "src/app.py"})
result = await run_interpreter(
    interp,
    'source().find(".fn#main").names()',
    ctx,
)

The chain's terminal operation determines the output type: .names() returns a list of strings, .count() returns an int, .view(...) returns a markdown string. Because plucker delegates to the python interpreter, the output_format on the execution result stays "python" — inspect the actual value type for the shape.

source() with no arguments uses the code key from ExecutionContext.config, letting callers set a default source and chain against it repeatedly. source(path) with an explicit argument overrides the default.

Registering a custom interpreter

Interpreters register themselves through the module-level registry. Bundled interpreters register at import time; custom interpreters register wherever their module is imported:

from lackpy.interpreters import register_interpreter, get_interpreter

class MyInterpreter:
    name = "my-interp"
    description = "My custom interpreter"

    def validate(self, program, context): ...
    async def execute(self, program, context): ...

register_interpreter(MyInterpreter)

# Later:
cls = get_interpreter("my-interp")  # → MyInterpreter
instance = cls()

Output formats

The output_format field on InterpreterExecutionResult identifies the shape of the result so consumers can dispatch accordingly. Known values:

Format Produced by Shape
python python, plucker Arbitrary Python value
markdown ast-select, pss Markdown string
text any Plain text string
json any JSON-serializable structure
none any Failed execution or explicit no-op

Interpreters may define their own formats; consumers should treat unknown values as opaque text.