Skip to content

Conversation

@m2kar
Copy link
Contributor

@m2kar m2kar commented Feb 3, 2026

[Arc] Complete inout support for arcilator path (Fixes #9574)

Summary

This PR completes inout support for the circt-verilog --ir-hw | arcilator flow.

This specifically fixes issue #9574, where circt-verilog --ir-hw <inout-module>.sv | arcilator could crash while handling inout reads.

Previously, llhd.prb/llhd.drv on inout block arguments could fail during ConvertToArcs legalization, or crash in LowerState when probe reads were requested in non-New phases.

Now both SV-style and LLHD-style inout accesses are handled through Arc state lowering, and the end-to-end flow runs successfully.

Changes

ConvertToArcs updates

  • Treat llhd.probe / llhd.drive on block-argument signals as arc breakers, so they are not incorrectly absorbed into extracted arcs.
  • Mark llhd::ProbeOp / llhd::DriveOp dynamically legal in conversion when their signal is a block argument.

LowerState Pass Updates

  • Add lowering support for llhd::SignalOp, llhd::ProbeOp, and llhd::DriveOp on local LLHD signals used by inout flows.
  • Local llhd.sig values are allocated as Arc state and lowered through arc.state_read / arc.state_write, preventing Arc legalization failures.
  • llhd::ProbeOp lowering keeps phase-correct reads (module.getBuilder(phase)) so non-New phase uses still work.

Test updates

  • test/circt-verilog/arcilator-inout.sv: keeps the read-path regression for inout in the arcilator import flow.
  • test/circt-verilog/inout-write-ir.sv: new regression that checks inout write semantics are materialized as llhd.drv in emitted HW/LLHD IR.
  • test/Dialect/Arc/lower-state-errors.mlir: outdated "inout not supported" error case removed since this PR adds inout lowering support.

Example

Input:

hw.module @InoutReadWrite(inout %port: i32, in %delta: i32, out result: i32) {
  %0 = sv.read_inout %port : !hw.inout<i32>
  %1 = comb.add %0, %delta : i32
  sv.assign %port, %1 : i32
  hw.output %0 : i32
}

Output after --arc-lower-state:

arc.model @InoutReadWrite io !hw.modty<inout port : i32, input delta : i32, output result : i32> {
^bb0(%arg0: !arc.storage):
  %inout_port = arc.root_inout "port", %arg0 : (!arc.storage) -> !arc.state<i32>
  %in_delta = arc.root_input "delta", %arg0 : (!arc.storage) -> !arc.state<i32>
  %out_result = arc.root_output "result", %arg0 : (!arc.storage) -> !arc.state<i32>
  %0 = arc.state_read %inout_port : <i32>
  %1 = arc.state_read %in_delta : <i32>
  %2 = comb.add %0, %1 : i32
  arc.state_write %inout_port = %2 : <i32>
  arc.state_write %out_result = %0 : <i32>
}

Test Plan and Results

Targeted regression tests

ninja -C build bin/arcilator bin/circt-verilog bin/circt-opt
build/bin/llvm-lit test/circt-verilog/arcilator-inout.sv -v
build/bin/llvm-lit test/circt-verilog/inout-write-ir.sv -v
build/bin/llvm-lit test/Dialect/Arc/lower-state-errors.mlir -v

Result:

  • PASS: CIRCT :: circt-verilog/arcilator-inout.sv
  • PASS: CIRCT :: circt-verilog/inout-write-ir.sv
  • PASS: CIRCT :: Dialect/Arc/lower-state-errors.mlir

End-to-end run (SV -> circt-verilog -> arcilator -> C++ testbench)

E2E test module (circt-b6/e2e/inout_e2e.sv):

module InoutE2E(
  inout wire c,
  input logic clk,
  output logic q_comb,
  output logic q_ff,
  output logic c_drv
);
  logic c_next;

  assign c = c_drv;
  always_comb c_next = ~c;
  always_comb q_comb = c;

  always_ff @(posedge clk) begin
    q_ff <= c;
    c_drv <= c_next;
  end
endmodule

E2E testbench (circt-b6/e2e/tb_inout_e2e.cpp):

#include "InoutE2E.h"

#include <cstdint>
#include <iostream>

int main() {
  InoutE2E dut;
  const uint8_t pattern[] = {0, 1, 1, 0, 1, 0, 0, 1};
  bool allPass = true;

  dut.view.clk = 0;
  dut.view.c = 0;
  dut.view.q_comb = 0;
  dut.view.q_ff = 0;
  dut.eval();

  for (int cycle = 0; cycle < 8; ++cycle) {
    const uint8_t drive = pattern[cycle];
    dut.view.c = drive;

    dut.view.clk = 0;
    dut.eval();
    const int combRead = static_cast<int>(dut.view.q_comb);

    dut.view.clk = 1;
    dut.eval();
    const int ffRead = static_cast<int>(dut.view.q_ff);
    const bool pass = (combRead == static_cast<int>(drive)) &&
                      (ffRead == static_cast<int>(drive));
    allPass &= pass;

    std::cout << "cycle=" << cycle
              << " drive=" << static_cast<int>(drive)
              << " c_raw=" << static_cast<int>(dut.view.c)
              << " q_comb=" << combRead
              << " q_ff=" << ffRead
              << " check=" << (pass ? "PASS" : "FAIL") << "\n";
  }

  std::cout << (allPass ? "E2E_CHECK=PASS" : "E2E_CHECK=FAIL") << "\n";
  return allPass ? 0 : 1;
}

Coverage in this E2E:

  • Inout write path: DUT drives c via assign c = c_drv, and IR includes llhd.drv %c, ....
  • Inout read path (combinational): DUT reads c into q_comb at clk=0 eval.
  • Inout read path (sequential): DUT samples c into q_ff at posedge clk eval.
build/bin/circt-verilog --ir-hw circt-b6/e2e/inout_e2e.sv -o circt-b6/e2e/inout_e2e.hw.mlir
build/bin/arcilator circt-b6/e2e/inout_e2e.hw.mlir --state-file circt-b6/e2e/inout_e2e.json -o circt-b6/e2e/inout_e2e.ll
export PATH=/opt/llvm-22/bin:$PATH
python3 tools/arcilator/arcilator-header-cpp.py circt-b6/e2e/inout_e2e.json --view-depth 1 > circt-b6/e2e/InoutE2E.h
build/bin/llc -O3 --filetype=obj circt-b6/e2e/inout_e2e.ll -o circt-b6/e2e/inout_e2e.o
c++ -O2 -std=c++17 -Itools/arcilator -Icirct-b6/e2e circt-b6/e2e/tb_inout_e2e.cpp circt-b6/e2e/inout_e2e.o -o circt-b6/e2e/tb_inout_e2e
circt-b6/e2e/tb_inout_e2e

Output:

cycle=0 drive=0 c_raw=0 q_comb=0 q_ff=0 check=PASS
cycle=1 drive=1 c_raw=1 q_comb=1 q_ff=1 check=PASS
cycle=2 drive=1 c_raw=1 q_comb=1 q_ff=1 check=PASS
cycle=3 drive=0 c_raw=0 q_comb=0 q_ff=0 check=PASS
cycle=4 drive=1 c_raw=1 q_comb=1 q_ff=1 check=PASS
cycle=5 drive=0 c_raw=0 q_comb=0 q_ff=0 check=PASS
cycle=6 drive=0 c_raw=0 q_comb=0 q_ff=0 check=PASS
cycle=7 drive=1 c_raw=1 q_comb=1 q_ff=1 check=PASS
E2E_CHECK=PASS

Limitations

  • Tri-state (Z) resolution is still out of scope.
  • Multi-driver electrical semantics are not modeled in this patch.

Fixes #9574

Copy link
Contributor

@fabianschuiki fabianschuiki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks for adding an explicit error here 👍

@m2kar m2kar changed the title [Arc] Fix crash on inout ports in LowerState pass (Fixes #9574) [Arc] Complete inout support for arcilator path (Fixes #9574) Feb 11, 2026
@m2kar m2kar marked this pull request as ready for review February 11, 2026 03:26
@m2kar m2kar requested a review from maerhart as a code owner February 11, 2026 03:26
@m2kar
Copy link
Contributor Author

m2kar commented Feb 11, 2026

Hi @fabianschuiki @maerhart , I rewrite this PR. Now, inout port have been supported. Please review.☺️

Copy link
Contributor

@fabianschuiki fabianschuiki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @m2kar! I'm wondering how we should deal with inout ports in general. I don't think we can just lower them to llhd.sig and drive them. At the Verilog level, inout ports are nets that perform resolution among multiple drivers. We don't have such a concept yet in LLHD. The signals are more like variables that support delayed assignment and observation. We probably need something like an !llhd.net type that represents a reference to a multi-driver network. And we probably need ops that read the resolved value off of that net (maybe %0 = llhd.probe_net %n), and also to attach a driver, with potential drive condition, to that net (maybe llhd.drive_net %n, %0, %enable). That would separate the whole event queue and delayed assignment mechanism of llhd.sig from the multi-driver network and drive conflict resolution. WDYT?

@fabianschuiki fabianschuiki self-requested a review February 11, 2026 17:54
Copy link
Contributor

@fabianschuiki fabianschuiki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(We need a multi-driver net representation in LLHD.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Arc] Assertion failure when lowering inout ports in sequential logic

2 participants