Skip to content

Names, values and build state

Need Mechanism Example
Choose a Python identifier ident / identifier demand Parameter x
Choose an attribute path astichi_ref(external=slot) and external path string Direct self.x
Insert literal data external / literal wrapper Slot tuple ("x", "y")
Read a containing lexical binding astichi_pass(..., outer_bind=True) Store reads constructor parameter
Prevent deliberate name hygiene keep, astichi_keep, suffix keep Public self / selected method name
Retain a live Python object Fresh execution namespace or authored factory capture Callback, opaque default, live type

A spelling ending __astichi_arg__ is an identifier demand. Putting that spelling in an ordinary attribute does not make it an attribute-path demand. keep keeps a spelling; it does not supply the value or fix an unresolved binding.

flowchart TB
  N["Consumer namespace:<br/>private capture<br/>identifier → object"] --> C["Generated function<br/>globals or closure"]
  F["Frozen fact:<br/>identifier/path string"] --> B["Build-time<br/>binding"]
  B --> C
  C --> I["Instance storage<br/>after construction"]

For an arbitrary object, install it under a deterministic private name in a fresh namespace. Bind that identifier string into a template. Never turn its repr into executable source and never call a factory during generation to guess a literal. Two independent builds need independent capture namespaces, even when they reuse one unbound template.

Frozen data, cached templates, mutable builder state, materialized AST and completed classes have separate lifetimes. Do not mutate a returned cached unbound composable expecting all clients to be isolated automatically. Use the owning SDK's composition/binding operations and qualify copy/materialization behavior for your backend. Native failure isolation, foreign handles and repeated materialization are not broadly promised by this documentation.

Execute the capture-isolation recipe

Run example/documentation/captures.py with the installed interpreter and Python engine. It executes one generated AST in two fresh namespaces, checks hostile-repr objects retain identity, and proves callbacks are called only at runtime.

Source authority: example/documentation/captures.py.

"""Live captures stay in independent namespaces; they never become literal reprs."""

from __future__ import annotations

import ast
from dataclasses import dataclass
import json

from yidl.concept_parser import compile_yidl_files
from yidl.generation.data_def_sys import emit_concept_runtime_source, from_literal

SOURCE = """
concept Captures {
    resource Root = code $[
        class class_name__astichi_arg__:
            def value(self):
                return default_name__astichi_arg__
            def call(self):
                return callback_name__astichi_arg__()
    ]$ { keep value, call }
    production RootProduction -> composable {
        root Root = Root {
            ident class_name = "Box"
            ident default_name = "_default"
            ident callback_name = "_callback"
        }
    }
    assembly Module = RootProduction
}
"""


@dataclass(frozen=True)
class Opaque:
    label: str

    def __repr__(self) -> str:
        raise AssertionError("opaque capture must never be rendered")


def run() -> str:
    concept = compile_yidl_files({"captures.yidl": SOURCE}, "captures.yidl").concepts[
        "Captures"
    ]
    runtime = emit_concept_runtime_source(
        concept.plan.build_data_definition(),
        resources=concept.resources,
        assembly_plan=concept,
    )
    schema: dict[str, object] = {}
    exec(runtime, schema)
    tree = schema["build_Module"](schema["new_builder"]().freeze()).to_executable_ast()
    code = compile(tree, "capture_model.py", "exec")
    calls: list[str] = []
    first, second = Opaque("first"), Opaque("second")

    def callback_one() -> object:
        calls.append("one")
        return first

    def callback_two() -> object:
        calls.append("two")
        return second

    namespaces = [
        {"_default": first, "_callback": callback_one},
        {"_default": second, "_callback": callback_two},
    ]
    for namespace in namespaces:
        exec(code, namespace)
    assert calls == []
    assert namespaces[0]["Box"]().value() is first
    assert namespaces[1]["Box"]().value() is second
    assert namespaces[0]["Box"]().call() is first
    assert namespaces[1]["Box"]().call() is second
    assert calls == ["one", "two"]
    assert namespaces[0]["Box"] is not namespaces[1]["Box"]
    wrapper = from_literal(("one", 2))
    assert wrapper.to_generator() is wrapper.to_generator()
    try:
        from_literal(first)
    except TypeError as error:
        assert "unsupported matcher literal type" in str(error)
    else:
        raise AssertionError("opaque literal should reject")
    return ast.unparse(tree) + "\n"


if __name__ == "__main__":
    print(run(), end="")
    print(
        json.dumps(
            {
                "capture_isolation": True,
                "generation_calls": 0,
                "runtime_calls": ["one", "two"],
            },
            sort_keys=True,
        )
    )