C⏚ v2.0.0Updated 2026-05-13·Guides

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