flowrep.parsers.chain_parser module

Parse data-access chains on workflow data into injected std nodes.

A chain rooted at a known symboldc.a, d[k], dc.sub["item"].val – becomes one node per link: std.get_attr for an attribute link, std.getitem for an item link. The symbol map deliberately shadows the object scope, so a workflow input named os makes os.path an attribute access on that input, exactly as it would be at runtime.

chain_parser owns the walk; a LinkHandler owns only what differs between the two access syntaxes Python has. That set is closed, so the dispatch is total.

Data access is allowed anywhere a value is, with one exception: the workflow’s return. The rule is

a port name may be generated when the port is internal to the recipe; it must come from a symbol only when the port is public IO.

Ports get their names from four places:

Port

Name comes from

Workflow output port

the returned symbol

Flow-control input port

the enclosing symbol feeding it, or – for a constant or an access chain, which have no symbol – a generated name (see generate_port_name())

For-body output port

the appended symbol, or a generated name

Atomic/workflow node input port

the callee’s own parameter

Only the first is public IO. A workflow’s output ports are its interface, and a consumer wiring into them should be able to read their names straight off the source, so return dc.a is refused (see reject_unbound_access()). Everywhere else the port is internal wiring, and a generated name costs the reader nothing: the compiler emits val_0 = x.val and re-parsing regenerates exactly that port, so the sugared form and the bound form are not merely equivalent recipes – they are the same recipe.

(@atomic stays laxer about its own returns. That asymmetry is deliberate: atomic parsing must swallow arbitrary Python functions, including ones the caller did not write, whereas a @workflow author controls their own source.)

Two things a generated name cannot rescue, both refused elsewhere: a method call on workflow data (dc.method(x)), which needs a callable reference rather than a port name, and a chain in a while condition that depends on a symbol the loop body reassigns (see while_parser._reject_looped_chain_symbols()), where hoisting the access out of the loop would silently diverge from Python’s per-iteration re-read.

class flowrep.parsers.chain_parser.LinkHandler(*args, **kwargs)[source]

Bases: Protocol

How one kind of chain link (.attr or [key]) becomes a node.

feed_key(link: expr, symbol_map: SymbolScope, nodes: dict[Annotated[str, BeforeValidator(func=_validate_label, json_schema_input_type=PydanticUndefined)], Annotated[AtomicRecipe | ConstantRecipe | ForEachRecipe | IfRecipe | TryRecipe | WhileRecipe | WorkflowRecipe, FieldInfo(annotation=NoneType, required=True, discriminator='type')]], label: str, port: str) None[source]

Wire this link’s second input: the attribute name, or the item key.

port_base(link: expr) str[source]

The generated port name base, before label_helpers.unique_suffix().

property recipe: AtomicRecipe

The std recipe injected per link. Two inputs (obj, key), one output.

flowrep.parsers.chain_parser.chain_root(node: expr) Name | None[source]

The innermost ast.Name of a pure access chain, else None.

flowrep.parsers.chain_parser.dependency_symbols(node: expr) set[str][source]

Every symbol an access chain reads: its root, plus any symbol used as an item key.

An attribute name is a constant, but an item key can be a symbol, so d[k] depends on both d and k. The while guard needs the distinction: a chain that depends on a symbol the loop body reassigns cannot be hoisted faithfully.

flowrep.parsers.chain_parser.generate_port_name(node: expr, taken: Iterable[str]) str[source]

A deterministic port name for an access chain that has no symbol.

Takes the outermost link’s base – for an attribute, the attribute name; for an item, the std.getitem output port – and unique-suffixes it against taken. x.a.val gives val_0; x["a"] gives item_0.

taken must include every symbol in the enclosing scope, not merely the ports allocated so far: a flow-control node’s inputs are its condition’s inputs plus its body’s, and a body input port is always an enclosing symbol.

flowrep.parsers.chain_parser.hoist_call_arguments(call: Call, symbol_map: SymbolScope, nodes: dict[Annotated[str, BeforeValidator(func=_validate_label, json_schema_input_type=PydanticUndefined)], Annotated[AtomicRecipe | ConstantRecipe | ForEachRecipe | IfRecipe | TryRecipe | WhileRecipe | WorkflowRecipe, FieldInfo(annotation=NoneType, required=True, discriminator='type')]]) dict[expr, SourceHandle][source]

Inject every data-access argument of call before the call’s own node.

Hoisting is what makes f(x0, comp.val) and v = comp.val; f(x0, v) parse to identical recipes: the injected nodes are created (and their sources consumed) ahead of the call in both forms.

flowrep.parsers.chain_parser.inject_chain(node: expr, symbol_map: SymbolScope, nodes: dict[Annotated[str, BeforeValidator(func=_validate_label, json_schema_input_type=PydanticUndefined)], Annotated[AtomicRecipe | ConstantRecipe | ForEachRecipe | IfRecipe | TryRecipe | WhileRecipe | WorkflowRecipe, FieldInfo(annotation=NoneType, required=True, discriminator='type')]]) SourceHandle[source]

Add one node (plus any constant peer) per link of node.

Walks the chain root-outward so that node insertion order – and therefore labels and the enclosing scope’s input ordering – are a deterministic function of the source. Returns the handle of the outermost link’s output.

node must be a data access chain (see is_data_access()); callers are expected to gate on that before calling.

flowrep.parsers.chain_parser.is_data_access(node: expr, symbol_map: SymbolScope) bool[source]

True if node is attribute or item access rooted at a symbol known to the graph.

The symbol map deliberately shadows the object scope: a workflow input named os makes os.path a data attribute access on that input, not a module lookup, exactly as it would be at runtime.

flowrep.parsers.chain_parser.reject_method_call(call: Call, symbol_map: SymbolScope) None[source]

Raise if call invokes an attribute or item of a known symbol.

flowrep.parsers.chain_parser.reject_unbound_access(node: expr, symbol_map: SymbolScope, context: str) None[source]

Raise if node is an access chain used where the port name is public IO.

A workflow’s output ports are its interface. Flowrep names them after the returned symbol, and an access chain has no symbol – so the port would carry a generated name that a consumer cannot read off the source. Everywhere else in a workflow a generated name is fine (the port is internal wiring); here it is not, so we ask for the binding.