Hola Charli,
Según parece, el error es de Yosys y más acusado con las Xilinx 7 series.
Se llama AST and Width interference
Y aquí la IA me lo explica detalladamente
The "AST width-inference crash" is not a problem with your logic; it is a C++ exception thrown by the Yosys compiler itself when its internal math gets confused by certain Verilog syntax patterns.
Here is a detailed breakdown of what is happening, why it crashes, and why our workarounds fix it.
1. What is the AST and Width Inference?When Yosys reads your Verilog code, it doesn't immediately turn it into gates. First, it parses the text into an Abstract Syntax Tree (AST)—a massive, interconnected web of C++ objects where every variable, operator, and literal is a "node."
Verilog is a "loosely typed" language regarding bit widths. If you write a + b, and a is 8 bits and b is 4 bits, Yosys has to figure out how many bits the result should be. This process is called Width Inference. Yosys traverses the AST, passing "width context" up and down the tree to ensure every node knows exactly how many bits it is supposed to be.
2. The Root Cause of the CrashIn certain versions of Yosys (particularly those bundled with open-source flows like F4PGA/SymbiFlow for Xilinx 7-series), there is a bug in the C++ code that handles width inference for specific comparison operators (especially >= and >) when they are mixed with complex boolean logic or inline assignments.
When Yosys encounters a triggering pattern, the width propagator gets confused. It fails to assign a bit-width to a temporary AST node, leaving it at 0 bits.
Later in the compilation flow (usually during the opt optimization pass or the techmap technology mapping pass), the C++ backend tries to read or write to bit index 5 of this 0-bit vector. Because the vector's size is 0, C++ throws a fatal exception:
terminate called after throwing an instance of 'std::out_of_range'
what(): vector::_M_range_check: __n (which is 5) >= this->size() (which is 0)
The compiler literally crashes to your desktop, aborting the scons build.
3. The Specific Triggers (Why your code crashed)
Trigger A: The >= Operator and Parameter Widths
localparam H_SYNC_START = 816;
// ...
hsync <= (h_count >= H_SYNC_START);
By default, Verilog parameters are parsed as 32-bit signed integers. Your h_count register is 11-bit unsigned.
When Yosys evaluates >=, it has to extend the 11-bit register to 32 bits to do the comparison. The AST node for $ge (greater-than-or-equal) in these specific Yosys versions has a bug where it miscalculates the resulting width during this signed/unsigned extension, resulting in the 0-bit vector crash.
Trigger B: Inline Wire Initialization with Comparisons
wire display_en = (h_count < 800) && (v_count < 600);
When you initialize a wire on the exact same line you declare it, Yosys builds the AST slightly differently than if you used a separate assign statement. If the right side of the = contains a comparison operator, the AST builder sometimes fails to propagate the 1-bit boolean context correctly, again resulting in a 0-bit width inference.
Trigger C: Deeply Nested Inline Boolean Logic
hsync <= ~((h_count < 816) ^ (h_count < 912));
When you put complex logic (~, ^, &&) directly inside a sequential always block, Yosys has to create "temporary" AST nodes to hold the intermediate results. The width propagator struggles to pass the 1-bit context through multiple layers of inline temporary nodes, leading to the same 0-bit crash.
4. How the Workarounds Fix the AST
The "bulletproof" Verilog style we used isn't just about avoiding bugs; it's about forcing Yosys to use its most stable, heavily tested C++ code paths.
Fix 1: Replacing >= with < and ~
// BAD (Triggers the $ge bug):
wire x = (h_count >= 816);
// GOOD (Uses the highly stable $lt node):
wire h_lt_816 = (h_count < 816);
wire x = ~h_lt_816;
The < (less than) operator is the oldest, most battle-tested comparison operator in Yosys. By replacing >= with < and a logical NOT (~), we force Yosys to use the $lt AST node, which has perfect width inference.
Fix 2: Separating Wire Declarations from Assigns
// BAD (Triggers inline init bug):
wire display_en = (h_count < 800) && (v_count < 600);
// GOOD (Forces standard continuous assignment parsing):
wire display_en;
assign display_en = (h_count < 800) && (v_count < 600);
By separating the declaration from the assignment, we force Yosys to parse the wire as a standard node, and the assign as a standard continuous assignment. This bypasses the buggy inline-initialization AST builder entirely.
Fix 3: Atomic Decomposition (Flattening the AST)
// BAD (Deeply nested temporary nodes):
hsync <= ~((h_count < 816) ^ (h_count < 912));
// GOOD (Flat, 1-bit atomic wires):
wire h_lt_816; assign h_lt_816 = (h_count < 816);
wire h_lt_912; assign h_lt_912 = (h_count < 912);
wire h_sync_region; assign h_sync_region = (~h_lt_816) && h_lt_912;
always @(posedge clk) begin
hsync <= ~h_sync_region; // Trivial 1-bit register update
end
By breaking the logic into individual wire assignments, we flatten the AST. Instead of Yosys having to calculate the width of a massive, nested expression tree all at once, it calculates the width of tiny, 1-bit atomic gates. The always block then becomes a trivial, flat register update, which is guaranteed not to crash.
Summary
The "Yosys AST width-inference crash" is essentially the compiler's C++ backend tripping over its own shoelaces when trying to figure out how many bits a complex, inline, mixed-signedness comparison should be. By writing flat, atomic, Verilog-1995 style code using only the < operator, we bypass the buggy C++ code paths entirely and force the compiler to synthesize the exact same hardware safely.
Saludos
Fernando Mosquera