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 verilogThis 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
endmoduleThe 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.vvpOr, 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.cgThe 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:
- 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.
- A compiler bug in the Verilog generator - open one at
github.com/Neosyn-Logic/neosyn-studio/issues
with the minimal
.cgreproducer and both VCDs. - A design ambiguity in the
.cgsource - 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.