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

Cross-check against HDL

The bytecode simulator is the canonical correctness check for C⏚ designs. The inline monitor pattern with print() and assert() runs only on the bytecode side, so a naive "run the same testbench through both" workflow doesn't exist - those constructs have no synthesizable equivalent.

This guide covers when you actually need an HDL-level cross-check and how to build one when you do.

When the bytecode sim is enough

For most designs, don't bother with an HDL cross-check. The bytecode simulator runs the same three-phase cycle schedule as the language semantics and is the source of truth for cycle-by-cycle behaviour. Bugs you catch there are real bugs; tests you pass there will pass in silicon, modulo the synthesis-side limitations below.

The cg-ip-cores Intel-8088 reference IP passes its full Tier 0 → Tier 4 ladder on the bytecode side alone. The Verilog backend ships only the synthesizable RTL - everything that asserts correctness lives in the bytecode tests.

When to add an HDL cross-check

Reach for a Verilog-side check only when one of these is true:

  • You suspect a compiler bug in the Verilog generator specifically - the bytecode and HDL backends share most of the pipeline but emit through different printers. The Intel-8088 Fibonacci gate (run_fibonacci_tb.sh) exists precisely to catch Verilog-side regressions that the bytecode tests wouldn't.
  • Your design has external Verilog modules that are stubbed out in the bytecode sim (an external task with a Verilog implementation, a vendor IP, a black-box memory). The bytecode sim sees only the empty signature; the HDL sim runs the real thing.
  • You want synthesis-toolchain confidence before taping out - running the generated RTL through your target simulator catches toolchain-specific surprises (timing, blocking-vs-non-blocking assignment patterns, simulator-side X-propagation) that neither backend models.

If none of those apply, the bytecode sim is the right place to spend your time.

How to wire an iverilog cross-check

The honest pattern uses a hand-written Verilog testbench that drives the DUT directly - not the inline-monitor task from the bytecode side. The DUT's generated .v exposes its ports; your testbench drives them, captures outputs, and asserts in Verilog.

Step 1 - Generate the DUT

java -jar cg-language-server.jar generate path/to/Counter.cg --target verilog

This produces verilog-gen/com/example/Counter.v plus any stdlib modules the DUT pulls in (std/mem/*.v, std/fifo/*.v, std/lib/*.v). The generated .v declares the DUT's clock, reset, and data ports - that's the only entity surface you can drive from a Verilog testbench.

Step 2 - Write a Verilog testbench

Put it next to the generated DUT, name it tb_top.v or <dut>.tb.v so the VS Code "Simulate Verilog (iverilog)" command finds it automatically:

// verilog-gen/com/example/tb_top.v
`timescale 1ns/1ps
 
module tb_top;
  reg clock = 0;
  reg reset_n = 0;
  wire [3:0] count;
  wire count_valid;
 
  Counter dut (.clock(clock), .reset_n(reset_n),
               .count(count), .count_valid(count_valid));
 
  always #5 clock = ~clock;  // 100 MHz
 
  integer cycle = 0;
  always @(posedge clock) begin
    if (reset_n) begin
      $display("cycle=%0d count=%0d valid=%b", cycle, count, count_valid);
      cycle = cycle + 1;
      if (cycle == 8) begin
        $display("OK"); $finish;
      end
    end
  end
 
  initial begin
    $dumpfile("Counter_hdl.vcd"); $dumpvars(0, tb_top);
    #20 reset_n = 1;
  end
endmodule

The testbench is yours to maintain - it's not generated from .cg. Treat it like any other Verilog source.

Step 3 - Run it

cd verilog-gen
iverilog -g2012 -o counter.vvp tb_top.v com/example/Counter.v
vvp counter.vvp

Or, from VS Code, run Neosyn: Simulate Verilog (iverilog); the command picks up tb_top.v or any *.tb.v / *_test.v it finds under verilog-gen/.

Step 4 - Compare against the bytecode trace

Run the bytecode sim on the matching Counter_test.cg:

java -jar cg-language-server.jar simulate path/to/Counter_test.cg

The two traces won't match line-for-line (the bytecode monitor's print text is its own; the Verilog testbench's $display is yours), but the value sequence on the DUT's output ports should match cycle by cycle. Read both, eyeball the values, and treat any divergence as a real investigation target.

For waveform comparison, both runs drop VCDs (Counter_test.vcd at the project root from bytecode, Counter_hdl.vcd in verilog-gen/ from iverilog). Open them in GTKWave or Surfer side by side.

What to do if the traces disagree

Three possibilities, in rough order of likelihood:

  1. The Verilog testbench is wrong - clock or reset timing, port wiring, assumption about combinational vs registered outputs. Triple-check the testbench before suspecting the compiler.
  2. A compiler bug in the Verilog generator - open one at github.com/Neosyn-Logic/neosyn-studio/issues with the minimal .cg reproducer and both VCDs.
  3. A design ambiguity in the .cg source - most commonly, a timing-dependent read on a port that should be registered. Re-read Bytecode simulator → Common timing pitfalls.

Where next

  • FPGA integration - once the generated RTL passes whatever checks you need, hand it to the toolchain that targets your device.
  • Bytecode simulator - full reference for the canonical correctness loop.
  • Tutorial - the four-step bytecode-only loop for new users.