# Part III: Wiring System
**Version:** 1.0 Draft
**Last Updated:** 2025-12-20
---
## 1. Introduction
The **wiring system** is responsible for constructing the forward propagation graph from decorated Python functions. It operates at "compile time" (before runtime evaluation), performing:
- Signature extraction from decorators
- Type resolution and validation
- Graph structure assembly
- Builder creation for runtime instantiation
```mermaid
graph TB
subgraph "User Code"
F1["@compute_node
def add(a: TS[int], b: TS[int]) -> TS[int]"]
F2["@graph
def my_graph()"]
end
subgraph "Wiring Phase"
WC[Wiring Context]
SIG[Signature Extraction]
TR[Type Resolution]
WN[Wiring Node Creation]
WP[Wiring Port Connection]
GB[Graph Builder]
end
subgraph "Output"
NB[Node Builders]
RG[Runtime Graph]
end
F1 --> SIG
F2 --> WC
WC --> SIG
SIG --> TR
TR --> WN
WN --> WP
WP --> GB
GB --> NB
NB --> RG
```
---
## 2. Decorators
Decorators mark Python functions for inclusion in the graph and define their behavioral characteristics.
### 2.1 Decorator Hierarchy
```mermaid
graph TD
WD[WiringDecorator]
WD --> SN[SourceNodeDecorator]
WD --> CN[ComputeNodeDecorator]
WD --> SK[SinkNodeDecorator]
WD --> GD[GraphDecorator]
WD --> OP[OperatorDecorator]
WD --> SV[ServiceDecorator]
SN --> PSN[push_source_node]
SN --> PLN[pull_source_node]
SN --> GEN[generator]
CN --> CN1[compute_node]
SK --> SK1[sink_node]
GD --> GD1[graph]
OP --> OP1[operator]
SV --> SVC[service_impl]
SV --> SVCI[default_path]
```
### 2.2 Decorator Definitions
#### 2.2.1 compute_node
```python
@compute_node
def add(a: TS[int], b: TS[int]) -> TS[int]:
return a.value + b.value
```
**Properties:**
| Property | Description |
|----------|-------------|
| Inputs | One or more time-series or scalar inputs |
| Output | Required (returns time-series) |
| Evaluation | When active inputs are modified |
| Side Effects | Not permitted |
#### 2.2.2 sink_node
```python
@sink_node
def log_value(ts: TS[int]):
print(f"Value: {ts.value}")
```
**Properties:**
| Property | Description |
|----------|-------------|
| Inputs | One or more time-series or scalar inputs |
| Output | None (no return value) |
| Evaluation | When active inputs are modified |
| Side Effects | Permitted and expected |
#### 2.2.3 graph
```python
@graph
def my_graph(x: TS[int]) -> TS[int]:
doubled = multiply(x, const(2))
return doubled
```
**Properties:**
| Property | Description |
|----------|-------------|
| Inputs | Any types (forwarded to contained nodes) |
| Output | Optional (can return time-series) |
| Body | Wiring calls, not evaluated at runtime |
| Purpose | Composition of other nodes |
#### 2.2.4 push_source_node
```python
@push_source_node
def async_events() -> TS[str]:
"""Inject asynchronous events into the graph."""
pass # Sender object provided by runtime
```
**Properties:**
| Property | Description |
|----------|-------------|
| Output | Required time-series |
| Trigger | External event (async callback) |
| Mode | Real-time only |
| Sender | Runtime provides sender object |
#### 2.2.5 pull_source_node
```python
@pull_source_node
def scheduled_data() -> TS[float]:
"""Generate values on a schedule."""
return get_next_value()
```
**Properties:**
| Property | Description |
|----------|-------------|
| Output | Required time-series |
| Trigger | Scheduler-driven |
| Mode | Both simulation and real-time |
| Scheduling | Node schedules itself for future ticks |
#### 2.2.6 generator
```python
@generator
def counter(start: int = 0) -> TS[int]:
"""Generate a sequence of values."""
count = start
while True:
yield count
count += 1
```
**Properties:**
| Property | Description |
|----------|-------------|
| Output | Required time-series |
| Body | Python generator (yields values) |
| Scheduling | Yields control between evaluations |
#### 2.2.7 operator
```python
@operator
def add_(lhs: TS[SCALAR], rhs: TS[SCALAR]) -> TS[SCALAR]:
"""Overloaded addition operator."""
...
```
**Properties:**
| Property | Description |
|----------|-------------|
| Purpose | Overload resolution for operators |
| Multiple Implementations | Can have multiple with different signatures |
| Selection | Based on input types and generic rank |
---
## 3. Wiring Context
The **WiringContext** maintains state during graph construction.
### 3.1 Context Stack
```mermaid
graph TB
subgraph "Context Stack"
direction TB
CTX1[Root Context]
CTX2[Graph Context: my_graph]
CTX3[Nested Graph Context]
end
CTX1 --> CTX2
CTX2 --> CTX3
```
### 3.2 Context Architecture
The `WiringContext` class uses **dynamic property storage** via `**kwargs`:
```python
class WiringContext:
_stack: ClassVar[list[dict[str, Any]]] = []
def __init__(self, **kwargs):
self._stack.append(kwargs)
def __getattr__(self, item):
# Searches stack from top to bottom for property
for frame in reversed(self._stack):
if item in frame:
return frame[item]
raise AttributeError(item)
```
**Common Dynamic Properties** (set by callers):
| Property | Description |
|----------|-------------|
| `current_signature` | Active wiring signature |
| `current_arg` | Current argument being processed |
| `current_kwargs` | Input keyword arguments |
**Note:** Properties are not predefined. Any key-value pair can be stored and retrieved from the context stack.
### 3.3 WiringGraphContext
For graph-level wiring, `WiringGraphContext` provides additional structure:
| Property | Description |
|----------|-------------|
| `sink_nodes` | Collection of sink nodes in the graph |
| `services` | Service registrations |
### 3.4 Context Management
```python
# Entering a new context
with WiringContext(
current_signature=signature,
current_kwargs={"x": input_ts}
):
result = wiring_function()
```
---
## 4. Signatures
**Signatures** define the input/output contract for nodes.
### 4.1 Signature Extraction
```mermaid
sequenceDiagram
participant D as Decorator
participant E as Extractor
participant S as Signature
D->>E: Function with annotations
E->>E: Extract parameters
E->>E: Parse type annotations
E->>E: Determine defaults
E->>S: Create WiringSignature
```
### 4.2 WiringNodeSignature
**Reference:** `hgraph/_wiring/_wiring_node_signature.py`
The actual class is `WiringNodeSignature` with many fields:
```python
@dataclass(frozen=True)
class WiringNodeSignature:
# Core identity
node_type: WiringNodeType
name: str
args: tuple[str, ...]
# Types and defaults
defaults: frozendict[str, Any]
input_types: frozendict[str, HgTypeMetaData]
output_type: HgTimeSeriesTypeMetaData | None
# Input categorization
time_series_args: frozenset[str]
injectables: InjectableTypesEnum # Note: NOT 'injectable_inputs'
context_inputs: frozenset[str]
unresolved_args: frozenset[str]
# Signature modifiers
active_inputs: frozenset[str] | None
valid_inputs: frozenset[str] | None
all_valid_inputs: frozenset[str] | None
# Additional metadata
src_location: SourceCodeDetails
label: str | None
deprecated: str | bool
requires: Callable | None
# Variadic support
kw_only_args: frozenset[str]
var_arg: str | None
var_kwarg: str | None
```
**Query Properties:**
- `uses_scheduler`, `uses_clock`, `uses_engine`, `uses_state`
- `uses_recordable_state`, `uses_output_feedback`
- `scalar_inputs`, `time_series_inputs`, `positional_inputs`
### 4.3 Argument Categories
| Category | Description | Example |
|----------|-------------|---------|
| Scalar Args | Python values, resolved at wiring | `multiplier: int = 2` |
| Time-Series Args | TS inputs, connected at wiring | `x: TS[float]` |
| Injectable Args | Provided by runtime | `state: STATE[dict]` |
| Keyword-Only | After `*` in signature | `*, active: bool = True` |
### 4.4 Special Signature Elements
#### 4.4.1 `*args` and `**kwargs`
```python
@compute_node
def sum_all(*ts: TSL[TS[int], SIZE]) -> TS[int]:
return sum(t.value for t in ts if t.valid)
```
#### 4.4.2 Unresolved Types
```python
@compute_node
def generic_add(a: TS[SCALAR], b: TS[SCALAR]) -> TS[SCALAR]:
return a.value + b.value
```
---
## 5. Wiring Nodes and Ports
### 5.1 Wiring Node
A **WiringNodeInstance** represents a node during graph construction:
```mermaid
classDiagram
class WiringNodeInstance {
+node: WiringNodeClass
+resolved_signature: WiringNodeSignature
+inputs: frozendict[str, Any]
+label: str | None
+error_handler_registered: bool
}
class WiringPort {
+node_instance: WiringNodeInstance
+path: tuple[SCALAR, ...]
+output_type: HgTimeSeriesTypeMetaData
}
WiringNodeInstance "1" --> "*" WiringPort : creates ports
```
**Key Properties:**
| Property | Description |
|----------|-------------|
| `node` | The WiringNodeClass (factory) |
| `resolved_signature` | Fully resolved signature |
| `inputs` | Input values/ports dict |
| `is_stub` | True if this is a stub node |
| `is_source_node` | True if node is a source |
| `output_type` | Output type metadata |
**Note:** Unlike the simplified UML, `rank` and `node_id` are not direct properties. Ranking is handled via `ranking_alternatives`, and node IDs are assigned during graph building.
### 5.2 Wiring Port
A **WiringPort** represents a connectable output:
| Property | Description |
|----------|-------------|
| `node_instance` | Owning wiring node |
| `path` | Path tuple within the node (for accessing bundle fields) |
| `output_type` | Type metadata (derived from node + path) |
### 5.3 Port Connections
```mermaid
graph LR
subgraph "Node A"
OA[Output Port
TS[int]]
end
subgraph "Node B"
IB[Input Port
TS[int]]
end
OA --> IB
```
Connections are established when:
1. An output port is passed as an input to another node
2. Types are validated for compatibility
3. The connection is recorded in the graph builder
---
## 6. Type Resolution
### 6.1 Resolution Process
```mermaid
sequenceDiagram
participant W as Wiring Call
participant S as Signature
participant R as Resolver
participant C as Context
W->>S: Get base signature
W->>C: Collect input types
C->>R: Resolve type variables
R->>R: Match actual to formal
R->>R: Substitute SCALAR, TIME_SERIES_TYPE
R->>S: Create resolved signature
S->>W: Return resolved types
```
### 6.2 Type Variable Resolution
When a signature contains type variables (`SCALAR`, `TIME_SERIES_TYPE`), they are resolved from the actual inputs:
```python
# Signature: def add(a: TS[SCALAR], b: TS[SCALAR]) -> TS[SCALAR]
# Call: add(ts_int, ts_int) where ts_int: TS[int]
# Resolution:
# SCALAR -> int
# Result: TS[int]
```
### 6.3 Resolution Table
| Type Variable | Resolves To |
|---------------|-------------|
| `SCALAR` | Any atomic or compound scalar type |
| `TIME_SERIES_TYPE` | Any time-series type |
| `SIZE` | Literal integer (for TSL) |
| `TypeVar("T")` | Inferred from first occurrence |
### 6.4 Generic Rank
When multiple operator implementations match, **generic rank** determines selection:
```python
# Rank 0: Exact type match
def add_(lhs: TS[int], rhs: TS[int]) -> TS[int]: ...
# Rank 1: One type variable
def add_(lhs: TS[SCALAR], rhs: TS[SCALAR]) -> TS[SCALAR]: ...
# Rank 2: More general
def add_(lhs: TIME_SERIES_TYPE, rhs: TIME_SERIES_TYPE) -> TIME_SERIES_TYPE: ...
```
Lower rank is preferred.
---
## 7. Graph Building
### 7.1 Graph Builder
The **GraphBuilder** accumulates wiring nodes into a runtime graph structure:
```mermaid
graph TB
subgraph "Graph Builder"
NL[Node List]
EL[Edge List]
TI[Type Info]
MD[Metadata]
end
WN1[Wiring Node 1] --> NL
WN2[Wiring Node 2] --> NL
WN3[Wiring Node 3] --> NL
NL --> |"Topological Sort"| SN[Sorted Nodes]
EL --> |"Connections"| SN
SN --> RG[Runtime Graph]
```
### 7.2 Graph Construction Sequence
```mermaid
sequenceDiagram
participant U as User Code
participant G as @graph function
participant GB as GraphBuilder
participant WN as WiringNode
participant RG as Runtime Graph
U->>G: Call graph function
G->>GB: Create builder
loop For each node call
G->>WN: Create wiring node
WN->>WN: Resolve signature
WN->>GB: Register node
WN->>GB: Register connections
end
G->>GB: Finalize
GB->>GB: Topological sort
GB->>GB: Create node builders
GB->>RG: Instantiate runtime
```
### 7.3 Node ID Assignment
Nodes are assigned sequential IDs during wiring:
| Step | Action |
|------|--------|
| 1 | Create wiring node |
| 2 | Assign next available ID |
| 3 | Record in graph builder |
| 4 | Maintain ID → node mapping |
---
## 8. Nested Graphs and Expansion
### 8.1 Graph Expansion
When a `@graph` is called within another graph, it **expands** inline:
```python
@graph
def outer():
x = source_node()
y = inner_graph(x) # inner_graph expands here
sink_node(y)
```
```mermaid
graph TB
subgraph "outer (after expansion)"
S[source_node]
subgraph "inner_graph contents"
I1[Node from inner]
I2[Node from inner]
end
K[sink_node]
end
S --> I1
I1 --> I2
I2 --> K
```
### 8.2 Nested Graph Semantics
| Aspect | Behavior |
|--------|----------|
| Identity | Nodes from nested graphs get unique IDs |
| Scope | Local variables isolated per expansion |
| Types | Resolved at point of call |
| Recursion | Not directly supported (would be infinite) |
---
## 9. Error Handling at Wiring Time
### 9.1 Wiring Errors
```mermaid
graph TB
WE[Wiring Errors]
WE --> TE[Type Errors]
WE --> CE[Connection Errors]
WE --> SE[Signature Errors]
WE --> RE[Resolution Errors]
TE --> TE1["Incompatible types"]
TE --> TE2["Cannot resolve type variable"]
CE --> CE1["Missing required input"]
CE --> CE2["Output not bound"]
SE --> SE1["Invalid signature"]
SE --> SE2["Duplicate parameter"]
RE --> RE1["No matching overload"]
RE --> RE2["Ambiguous overload"]
```
### 9.2 Error Messages
Wiring errors include context about:
- Source location (file, line)
- Node signature
- Input values and their types
- Expected vs. actual types
---
## 10. Wiring-Time Evaluation
Some constructs evaluate at wiring time, not runtime:
### 10.1 Scalar Arguments
```python
@graph
def my_graph():
# '2' evaluated at wiring time
x = const(compute_something()) # compute_something() runs at wiring
doubled = multiply(x, const(2))
```
### 10.2 Wiring-Time Functions
| Function | Purpose |
|----------|---------|
| `if_then_else(cond, true_branch, false_branch)` | Wiring-time conditional |
| `match_()` | Wiring-time pattern matching |
| `nothing()` | Null time-series (no connection) |
---
## 11. Special Wiring Constructs
### 11.1 Feedback Loops
```python
@graph
def with_feedback() -> TS[int]:
fb = feedback(TS[int])
current = add(const(1), fb())
fb(current) # Feed output back
return current
```
```mermaid
graph LR
C[const 1]
FB[feedback]
ADD[add]
C --> ADD
FB --> ADD
ADD --> FB
ADD --> OUT[output]
```
### 11.2 Switch
```python
@graph
def switcher(selector: TS[bool], a: TS[int], b: TS[int]) -> TS[int]:
return switch_(
selector,
{True: a, False: b}
)
```
### 11.3 Map Over Collection
```python
@graph
def process_dict(d: TSD[str, TS[int]]) -> TSD[str, TS[int]]:
return map_(double, d)
```
---
## 12. Builder Creation
### 12.1 From Wiring to Builders
```mermaid
graph LR
WN[Wiring Node] --> NB[Node Builder]
WP[Wiring Port] --> TSB[Time-Series Builder]
NB --> |"build()"| RN[Runtime Node]
TSB --> |"build()"| RTS[Runtime Time-Series]
```
### 12.2 Builder Types
| Builder | Creates |
|---------|---------|
| `NodeBuilder` | Runtime node instances |
| `GraphBuilder` | Runtime graph structures |
| `TSInputBuilder` | Time-series input instances |
| `TSOutputBuilder` | Time-series output instances |
| `TSBundleBuilder` | Bundle instances with fields |
| `TSDictBuilder` | Dictionary instances |
| `TSListBuilder` | Fixed-size list instances |
---
## 13. Implementation Notes
### 13.1 Additional Decorators
**@const_fn Decorator:**
Wraps constant functions that accept scalar inputs and produce constant values:
```python
@const_fn
def calculate(a: int, b: int) -> TS[int]:
return a + b
# Can be called both inside and outside graphs
result_in_graph = calculate(1, 2) # Returns TS[int]
result_outside = calculate(1, 2) # Returns scalar int (3)
```
### 13.2 Complete WiringNodeType Enum
| Value | Name | Description |
|-------|------|-------------|
| 1 | `COMPUTE_NODE` | Standard computation node |
| 2 | `SINK_NODE` | Node with no output (side effects only) |
| 3 | `GRAPH` | Nested graph container |
| 4 | `PUSH_SOURCE_NODE` | Asynchronous push source |
| 5 | `PULL_SOURCE_NODE` | Scheduled pull source |
| 6 | `REF_SVC` | Reference service interface |
| 7 | `SUBS_SVC` | Subscription service interface |
| 8 | `REQ_REP_SVC` | Request-reply service interface |
| 9 | `SVC_IMPL` | Service implementation |
| 10 | `OPERATOR` | Operator signature (no implementation) |
| 11 | `ADAPTOR` | Single-client adaptor interface |
| 12 | `ADAPTOR_IMPL` | Single-client adaptor implementation |
| 13 | `SERVICE_ADAPTOR` | Multi-client service adaptor interface |
| 14 | `SERVICE_ADAPTOR_IMPL` | Multi-client service adaptor implementation |
| 15 | `COMPONENT` | Graph with record/replay constraints |
| 16 | `CONST_FN` | Constant function wrapper |
### 13.3 WiringNodeInstanceContext
The `WiringNodeInstanceContext` provides caching infrastructure for node instances:
- Maintains a stack-based context for caching `WiringNodeInstance` objects
- Uses an `InputsKey` wrapper for hashable caching
- Tracks graph nesting depth
- Prevents duplicate node creation
```python
with WiringNodeInstanceContext():
# Node instances are cached within this context
instance = create_wiring_node_instance(...)
```
---
## 14. Reference Locations
| Component | Location |
|-----------|----------|
| Decorators | `hgraph/_wiring/_decorators.py` |
| WiringContext | `hgraph/_wiring/_wiring_context.py` |
| WiringSignature | `hgraph/_wiring/_wiring_signature.py` |
| WiringNode | `hgraph/_wiring/_wiring_node.py` |
| WiringPort | `hgraph/_wiring/_wiring_port.py` |
| GraphBuilder | `hgraph/_builder/_graph_builder.py` |
| NodeBuilder | `hgraph/_builder/_node_builder.py` |
| Type Resolution | `hgraph/_wiring/_type_resolution.py` |
---
## 14. Next Steps
Continue to:
- [04_RUNTIME_SYSTEM.md](04_RUNTIME_SYSTEM.md) - Execution semantics
- [05_TIME_SERIES_TYPES.md](05_TIME_SERIES_TYPES.md) - Time-series type details
- [06_NODE_TYPES.md](06_NODE_TYPES.md) - Node specifications