flowrep.parsers.symbol_scope module

class flowrep.parsers.symbol_scope.SymbolConsumption(symbol: str, consumer_node: str, consumer_port: str, source: flowrep.edge_models.InputSource | flowrep.edge_models.SourceHandle)[source]

Bases: object

consumer_node: str
consumer_port: str
source: InputSource | SourceHandle
symbol: str
class flowrep.parsers.symbol_scope.SymbolProduction(output_port: str, source: flowrep.edge_models.SourceHandle | flowrep.edge_models.InputSource)[source]

Bases: object

output_port: str
source: SourceHandle | InputSource
class flowrep.parsers.symbol_scope.SymbolScope(sources: dict[str, InputSource | SourceHandle], available_accumulators: set[str] | None = None, reserved_accumulators: set[str] | None = None, shadowed_symbols: set[str] | None = None)[source]

Bases: Mapping[str, InputSource | SourceHandle]

Tracks which symbols are in scope and where their data comes from.

Immutable-ish: forking for child scopes (e.g. for-node bodies) returns a new instance with remapped symbols.

Accumulators follow a three-stage lifecycle: - declared_accumulators: locally declared via acc = []. Owned by this scope

and passed to child scopes as available_accumulators on fork.

  • available_accumulators: inherited from the parent scope’s declared_accumulators.

    These are the only accumulators a scope is allowed to .append() to. This guarantees that an accumulator is only consumable one nesting level below its declaration, preventing grandparent accumulator access.

  • consumed_accumulators: maps accumulator_name appended_symbol. Populated by

    use_accumulator() and read by the parent to finalise control-flow node outputs.

alias(new_symbol: str, existing_symbol: str) None[source]

Bind new_symbol to the same source as existing_symbol (y = x).

No node is created; the source is copied. Re-assigning a symbol that was declared as an accumulator de-registers it. Aliasing from an accumulator is rejected, since accumulators are name-tracked rather than sourced.

property all_accumulators: set[str]
property assigned_symbols: list[str]

Identify symbols that were assigned (registered to child nodes) locally.

In a forked scope every inherited symbol starts as an InputSource. Any key whose source is now a SourceHandle must have been assigned by a node inside the branch.

consume(symbol: str, consumer_node: str, consumer_port: str) None[source]

Record that consumer_node.consumer_port reads from symbol.

consume_input_source(source: InputSource, consumer_node: str, consumer_port: str) None[source]

Record a consumption whose source is a fixed InputSource (e.g. a synthetic flow-control input port fed by an injected constant peer), bypassing symbol lookup so no synthetic symbol enters _sources and risks colliding with a user symbol. The InputSource twin of consume_source().

consume_source(source: SourceHandle, consumer_node: str, consumer_port: str) None[source]

Record a consumption whose source is a fixed SourceHandle (e.g. an injected constant node), bypassing symbol lookup so no synthetic symbol enters _sources and risks colliding with a user symbol.

property edges: dict[TargetHandle, SourceHandle]
fork(symbol_remap: dict[str, str] | None = None, added_symbols: Iterable[str] | None = None, available_accumulators: set[str] | None = None) SymbolScope[source]

Create a child scope for a nested control-flow body.

Every symbol in the current _sources is carried over as a fresh InputSource in the child. symbol_remap allows renaming symbols in transit (e.g. a for-loop replacing the iterable symbol with the iteration variable).

added_symbols introduces symbols the child has but the parent does not: a for-loop variable whose collection was an attribute chain, and so had no parent symbol to remap. They shadow a parent symbol of the same name, exactly as the loop variable does in plain Python.

Accumulator propagation is controlled explicitly via available_accumulators. For-loop bodies pass the parent’s declared_accumulators so the body can .append(); while-loop and if/else bodies pass None (the default) to start with an empty set, since those control-flow models do not support cross-iteration accumulation.

The parent’s available_accumulators are always added to the child’s reserved_accumulators so that erroneous grandparent access is caught with a clear error rather than silently ignored.

property input_edges: dict[TargetHandle, InputSource]
property inputs: list[str]

Ordered unique input ports consumed from InputSources.

is_input_alias(symbol: str) bool[source]

True if symbol was aliased directly to a workflow input.

property output_edges: dict[OutputTarget, SourceHandle | InputSource]
property outputs: list[str]

Ordered unique output port names.

produce(output_port: str, symbol: str | None = None) None[source]

Record that output_port is sourced from symbol.

produce_source(output_port: str, source: SourceHandle) None[source]

Record that output_port is sourced from a fixed SourceHandle.

The SourceHandle twin of produce(), for an output port whose name was generated rather than taken from a symbol (an attribute chain appended to an accumulator), so no synthetic symbol enters _sources and risks colliding with a user symbol.

produce_symbols(symbols: list[str]) None[source]

Record that an output port of the same name is sources from each symbol.

register(new_symbols: list[str], child: LabeledRecipe) None[source]

Map new symbols 1:1 to child node outputs. Enforces uniqueness.

register_accumulator(new: str) None[source]
property unavailable_names: set[str]

Every name a generated port name must avoid.

The compiler inlines flow-control bodies into the enclosing def, so a body’s ports and its enclosing scope’s symbols share one Python namespace. A generated name must therefore dodge more than this scope’s own symbols: it must also dodge shadowed_symbols – names live in the enclosing emitted scope but absent here, chiefly the collection symbol that fork() remapped into the loop variable.

use_accumulator(accumulator_symbol: str, appended_symbol: str) None[source]