flowrep.parsers.chain_parser module
Parse data-access chains on workflow data into injected std nodes.
A chain rooted at a known symbol – dc.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
|
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:
ProtocolHow one kind of chain link (
.attror[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.Nameof a pure access chain, elseNone.
- 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 bothdandk. Thewhileguard 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.getitemoutput port – and unique-suffixes it against taken.x.a.valgivesval_0;x["a"]givesitem_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)andv = 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
osmakesos.patha 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.