A node's navigation slots — the state the fluent operators keep on each node.
Descending a node one step ([.navigation][]) lands on a slot, and the slot is
what carries authoring state: the child node built for it (memoized, so the same
step always names the same node) and the value cached there. The invariant this
module enforces is that a slot is a value or a container, never both.
Every import is lazy: the node and dispatch layers import Procedure back.
absorb_cache_rebind(node, memo, value, slot)
Swallow the |= / &= rebind at node's slot memo; reject the rest.
path.x |= v desugars to setattr(path, 'x', path.x.__ior__(v)) (and
path[k] |= v to the __setitem__ twin); __ior__ already recorded the
cache and returns it, so the rebind is a no-op — but only when the value is
this slot's own cache. Every other write, including a plain =, raises.
Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/slots.py
| def absorb_cache_rebind(
node: Procedure[CGType],
memo: tuple[object, ...] | None,
value: object,
slot: str,
) -> None:
"""Swallow the ``|=`` / ``&=`` rebind at ``node``'s slot ``memo``; reject the rest.
``path.x |= v`` desugars to ``setattr(path, 'x', path.x.__ior__(v))`` (and
``path[k] |= v`` to the ``__setitem__`` twin); ``__ior__`` already recorded the
cache and returns it, so the rebind is a no-op — but only when the value is
*this* slot's own cache. Every other write, including a plain ``=``, raises.
"""
from capturegraph.procedures.nodes.destination.cache import (
CacheProcedure,
)
child = node._children.get(memo) if memo is not None else None
if child is not None and isinstance(value, CacheProcedure) and value.destination is child:
return
raise TypeError(
f"Procedures are immutable: cache at a path with '|=' (skip if exists) or "
f"'&=' (overwrite), and use set_label() for labels (tried to set {slot})"
)
|
catch_superseded_key(key)
Mark a key node consumed when the slot it names was already built.
map[cg.const("k")] a second time returns the memoized entry, leaving that
call's freshly built key node with no consumer; the navigation is its consumer.
Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/slots.py
| def catch_superseded_key(key: object) -> None:
"""Mark a key node consumed when the slot it names was already built.
``map[cg.const("k")]`` a second time returns the memoized entry, leaving that
call's freshly built key node with no consumer; the navigation is its consumer.
"""
from capturegraph.procedures.procedure import (
Procedure,
)
from capturegraph.procedures.procedure.authoring.contexts.base import (
ProcedureContext,
)
context = ProcedureContext.current_or_none()
if isinstance(key, Procedure) and context is not None:
context._catch(key)
|
memoized_child(parent, key, factory)
The parent's navigation child for key, built once.
Dedup is node-local: each node remembers its own children, so repeated
navigation to the same slot returns the same node — which is what carries
_assigned_value — with no central bookkeeping. A non-empty _children
also marks a path slot a container, so saving a value there is rejected.
Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/slots.py
| def memoized_child(
parent: Procedure[CGType],
key: tuple[object, ...],
factory: Callable[[], Procedure[CGType]],
) -> Procedure[CGType]:
"""The ``parent``'s navigation child for ``key``, built once.
Dedup is node-local: each node remembers its own children, so repeated
navigation to the same slot returns the same node — which is what carries
``_assigned_value`` — with no central bookkeeping. A non-empty ``_children``
also marks a *path* slot a container, so saving a value there is rejected.
"""
existing = parent._children.get(key)
if existing is not None:
return existing
child = factory()
parent._children[key] = child
return child
|
record_cache(position, value, *, skip_if_exists)
Record a cache at position and return it, typed as its value T.
Builds the CacheProcedure, wraps it in a RequiredProcedure (so the
stored value is a Void step the context keeps, with the cache as its
caught child), and records the assignment on the slot for read-back. A slot
that has been navigated into (non-empty _children) is a container, and one
already assigned cannot be assigned again.
Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/slots.py
| def record_cache[T: CGType](
position: Procedure[Path[T]],
value: Procedure[T],
*,
skip_if_exists: bool,
) -> CacheProcedure[T]:
"""Record a cache at ``position`` and return it, typed as its value ``T``.
Builds the ``CacheProcedure``, wraps it in a ``RequiredProcedure`` (so the
stored value is a ``Void`` step the context keeps, with the cache as its
caught child), and records the assignment on the slot for read-back. A slot
that has been navigated into (non-empty ``_children``) is a container, and one
already assigned cannot be assigned again.
"""
from capturegraph.procedures.nodes.destination.cache import (
CacheProcedure,
)
from capturegraph.procedures.nodes.process.structural import (
RequiredProcedure,
)
from capturegraph.procedures.procedure.authoring.bridge import (
coerce_input,
)
from capturegraph.procedures.procedure.authoring.errors import (
AlreadyAssignedError,
AuthoringError,
)
from capturegraph.procedures.procedure.authoring.paths import (
render_path,
)
if position._children:
raise AuthoringError(
f"cannot save to navigated directory {render_path(position)} "
"(a slot is a value or a container, not both)"
)
if position._assigned_value is not None:
raise AlreadyAssignedError(f"{render_path(position)} is assigned more than once")
cache = CacheProcedure(
destination=position,
value=coerce_input(value),
skip_if_exists=skip_if_exists,
)
RequiredProcedure(procedure=cache) # = cg.do(cache): the kept Void step; catches the cache
object.__setattr__(position, "_assigned_value", cache)
return cache
|
require_map_position(node, op)
Guard .keys() / .values(): both enumerate a Path[Map] position.
A captured map value supports [key] indexing only.
Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/slots.py
| def require_map_position(node: Procedure[Path], op: str) -> None:
"""Guard ``.keys()`` / ``.values()``: both enumerate a ``Path[Map]`` position.
A captured map *value* supports ``[key]`` indexing only.
"""
from capturegraph.procedures.procedure.authoring.dispatch import (
navigable,
)
nav = navigable(node)
if nav is None or not nav.is_map:
raise TypeError(f"{node} has no {op} (it is not a map)")
if not nav.is_path:
raise TypeError(
f"{node} returns a map value; .{op}() enumerates a Path[Map] position — "
"index a captured map by key instead (map[key])"
)
require_no_saved_value(node)
|
require_no_saved_value(node)
Guard descending into a path position.
A slot is a value or a container, not both, so a slot with a saved value has no
children.
Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/slots.py
| def require_no_saved_value(node: Procedure[Path]) -> None:
"""Guard descending into a *path* position.
A slot is a value or a container, not both, so a slot with a saved value has no
children.
"""
if node._assigned_value is not None:
from capturegraph.procedures.procedure.authoring.errors import (
AuthoringError,
)
from capturegraph.procedures.procedure.authoring.paths import (
render_path,
)
raise AuthoringError(
f"cannot navigate into {render_path(node)}: a value was saved there "
"(a slot is a value or a container, not both)"
)
|
string_literal(key)
A constant string key as a plain str, or None if it can't be one.
Accepts a raw str or a String value; returns None when the key is
computed and so can't be checked statically.
Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/slots.py
| def string_literal(key: object) -> str | None:
"""A constant string key as a plain ``str``, or ``None`` if it can't be one.
Accepts a raw ``str`` or a ``String`` value; returns ``None`` when the key is
computed and so can't be checked statically.
"""
from capturegraph.types.scalars.primitives.string import (
String,
)
if isinstance(key, str):
return key
if isinstance(key, String):
return key.value
return None
|
subscript_child(node, nav, key)
Build (once) node[key] — an array element (int) or a map entry (key value).
Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/slots.py
| def subscript_child(
node: Procedure[Path],
nav: Navigable,
key: int | Procedure[CGType] | JSONScalar | bool | float | str,
) -> Procedure[CGType]:
"""Build (once) ``node[key]`` — an array element (int) or a map entry (key value)."""
memo = subscript_memo(nav, key)
if memo is None:
raise TypeError(f"array index must be an int literal, got {key!r}")
existing = node._children.get(memo)
if existing is not None:
catch_superseded_key(key)
return existing
if nav.is_array:
index = cast("int", key)
return memoized_child(node, memo, lambda: nav.nodes.index(source=node, index=index))
from capturegraph.procedures.procedure.authoring.bridge import (
coerce_input,
)
if not nav.is_path:
warn_unknown_map_key(node, key)
return memoized_child(
node,
memo,
lambda: nav.nodes.key(source=node, key=coerce_input(key)),
)
|
subscript_memo(nav, key)
[key]'s slot identity on nav, or None if key cannot index it.
An array slot is its integer index. A map slot is a literal key's value —
raw or already lifted, so m["a"], m[cg.String(value="a")] and
m[cg.const("a")] all name one slot — and a computed key's node otherwise.
Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/slots.py
| def subscript_memo(
nav: Navigable,
key: int | Procedure[CGType] | JSONScalar | bool | float | str,
) -> tuple[object, ...] | None:
"""``[key]``'s slot identity on ``nav``, or ``None`` if ``key`` cannot index it.
An array slot is its integer index. A map slot is a *literal* key's value —
raw or already lifted, so ``m["a"]``, ``m[cg.String(value="a")]`` and
``m[cg.const("a")]`` all name one slot — and a computed key's node otherwise.
"""
if nav.is_array:
return ("index", key) if isinstance(key, int) else None
from capturegraph.procedures.nodes.process.constant import (
Constant,
as_scalar,
)
from capturegraph.procedures.procedure import (
Procedure,
)
if isinstance(key, Procedure):
if isinstance(key, Constant):
return ("key", key.return_type.__name__, repr(key.value))
return ("key", key.uuid)
scalar = as_scalar(key)
return ("key", type(scalar).__name__, repr(type(scalar).encode(scalar)))
|
warn_unknown_map_key(source, key)
Warn when a value-level map[key] names a key the source map provably lacks.
Only a node that advertises its keys (static_map_keys) with a literal key is
checked; a dynamic source or computed key is left alone. The access still
builds — at runtime it yields no value, and the client reports the unknown key.
Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/slots.py
| def warn_unknown_map_key(source: Procedure[CGType], key: object) -> None:
"""Warn when a value-level ``map[key]`` names a key the source map provably lacks.
Only a node that advertises its keys (``static_map_keys``) with a literal key is
checked; a dynamic source or computed key is left alone. The access still
builds — at runtime it yields no value, and the client reports the unknown key.
"""
import warnings
known = source.static_map_keys()
literal = string_literal(key)
if known is None or literal is None or literal in known:
return
warnings.warn(
f"{source} has no key {literal!r}; its keys are {sorted(known)}. "
f"This access produces no value at runtime.",
stacklevel=4, # warn ← here ← subscript_child ← __getitem__ ← user code
)
|