Cross a network boundary into a combinational task
A combinational task inside a child network needs its inputs to be
valid in the same cycle the trigger fires. When the producer lives
in the parent network, the obvious choice - a push port - is the
wrong tool. The push pulse does not reliably reach a combinational
reader on the other side of a network boundary in the same cycle.
This guide shows the pattern that does work: a bare port for the
value, and a bare bool flag the producer writes every cycle.
The reader gates on the flag combinationally. The pattern is in
production use in the Intel-8088 reference CPU, where the execution
unit feeds an immediate operand into the register file's
combinational decode path.
The wrong shape (push across the boundary)
// Parent network: Execute is sequential, RegisterFile is a child
// network whose get_operands task is combinational.
task Execute {
out push u16 imm_oper2; // intent: pulse the value
// …
void loop() {
if (is_alu_imm) {
imm_oper2.commit(decoded_imm);
// expect get_operands.imm_oper2.available() == true this cycle
}
}
}The push pulse fires inside Execute's loop body. When the child
network's get_operands task tries to read imm_oper2.available()
in the same cycle, it sees false. The value itself does arrive on
bare ports - push valid is the part that does not propagate
combinationally across the boundary.
The right shape (bare value + always-written bare bool)
Split the signal into two bare ports. The value port holds its last value across cycles. The flag port is written every cycle by the producer - true on the active op, false otherwise.
task Execute {
out u16 imm_oper2_val; // bare value, holds across cycles
out bool imm_oper2_active; // bare flag, always written
void loop() {
if (is_alu_imm) {
imm_oper2_val = decoded_imm;
imm_oper2_active = true;
} else {
imm_oper2_active = false; // unconditional in inactive case
}
}
}The combinational reader in the child network gates on the flag:
task GetOperands {
in u16 imm_oper2_val;
in bool imm_oper2_active;
out u16 oper2;
out u16 src_val;
void loop() {
if (imm_oper2_active) {
oper2 = imm_oper2_val;
} else {
oper2 = src_val; // fall back to the register read
}
}
}Same-cycle handoff works because both ports are bare. The value port holds its last write so the reader always observes a defined value; the flag tells the reader whether that value is the one to use this cycle.
Why the producer must always write the flag
If the producer only writes imm_oper2_active = true in the active
branch and never touches it in the inactive branch, the flag holds
its last value into the next cycle. The reader then sees active == true after the operation has moved on, and forwards a stale
immediate into the register read path. The bug is silent - the value
port is still defined, just wrong.
The rule: every cycle the producer runs, every bare bool flag the
consumer gates on must be assigned. Use an if/else with explicit
false in the inactive arm, not a conditional if that leaves it
sticky.
Why push pulses do not cross
Push semantics are defined per-task scheduling. Inside a single
network the scheduler can observe the pulse and dispatch any
downstream task that gates on .available(). Across a network
boundary, the child network has its own scheduler. By the time the
child resolves its combinational chain for the cycle, the parent's
push valid has not crossed the boundary in the bytecode runtime;
the bare value has, because it is just a wire. The Verilog backend
behaves the same way - the pattern this guide describes is the one
that survives both backends.
The deeper diagnostic and the original observation are tracked in
the developer-side note
feedback_push_network_boundary.md.
If you suspect this is biting you on a new design, the symptom is
always the same: the value reaches the reader, the gate does not.
Verify in the bytecode simulator
A short monitor confirms the handoff. Put a print in the consumer
that fires only when the flag is true, and run the trace:
task GetOperands {
// …
void loop() {
if (imm_oper2_active) {
print("get_operands saw imm = ", imm_oper2_val);
oper2 = imm_oper2_val;
} else {
oper2 = src_val;
}
}
}Run Fast Sim. Every cycle the parent's
is_alu_imm branch fires you should see exactly one print from
the child, with the correct value, in the same cycle.
Where next
- Networks for the wiring syntax this guide builds on.
- Declarations - ports for the full push / stream / confirm / bare port semantics.
- Bytecode simulator for the trace tooling used to verify the pattern.