Veryl Unions: Fixing Non-Synthesizable SystemVerilog Code
Ever been in a situation where your carefully crafted code suddenly throws a fit, leaving you scratching your head?
That's exactly what can happen when you're working with Veryl and its inline union constructors, leading to the generation of non-synthesizable SystemVerilog code. It's a common hiccup that can halt your design process in its tracks. But don't worry, we're here to dive deep into this issue, understand why it happens, and more importantly, how to fix it. We'll walk through an example, explore the generated code, and see how a small modification can make a world of difference, ensuring your Veryl code translates seamlessly into synthesizable SystemVerilog.
Understanding the Problem: Inline Union Constructors and SystemVerilog
Let's start by getting a clear picture of the problem at hand. The core issue arises when Veryl generates SystemVerilog code for inline union constructors. While Veryl aims to simplify hardware description, sometimes the translation process can lead to code that isn't quite compliant with SystemVerilog synthesis rules. Specifically, when you use inline constructors for unions, the generated SystemVerilog can become illegal, particularly when it comes to assignments within these unions. This means that while your Veryl code might look perfectly fine and logical, the SystemVerilog it produces could be rejected by synthesis tools, or worse, lead to unexpected behavior in your hardware.
Consider the example provided. We have a struct S with a logic<4> member a. Then, we define a union U which can hold either a logic<4> named ww or an instance of our struct S named aa. Following this, we declare a constant uu of type U, initialized using an inline constructor for S'{ a : 1 }. Finally, we have a struct T that contains a tag and a union u of type U. The constant t of type T is then initialized, and within its initialization, the union u is assigned using S'{ a : 5 }. This setup, while intuitive in Veryl, presents a challenge when translated to SystemVerilog.
The resulting SystemVerilog code snippet is where the trouble begins. You'll notice the definitions for S, U, and T, which are standard. However, the instantiation of the constants uu and t reveals the problematic areas. The line localparam U uu = '{a : 1}; and the nested assignment u : '{ a : 5 }, within the t initialization are the culprits. The SystemVerilog compiler, particularly tools like Verilator, flags these as errors because the assignment pattern key 'a' is not found as a direct member of the union U. This is a critical distinction: when assigning to a union, you need to explicitly specify which member of the union you are initializing, especially when that member is itself a structure. The generic assignment pattern {...} doesn't provide this clarity for unions, leading to the non-synthesizable output.
The Root Cause: Type Inference and Union Ambiguity
The crux of the problem lies in how Veryl handles type inference, especially when dealing with inline constructors for unions. In SystemVerilog, when you assign a value to a union, it's crucial to be explicit about which member of the union you are targeting. This is because a union, by definition, can hold different types of data in the same memory location, and the compiler needs to know which specific member is intended at any given time.
In the original generated SystemVerilog code, the inline constructor '{a : 1} used for uu and the nested '{ a : 5 } used for u within t are too generic. They don't explicitly state that the assignment is intended for the S member (aa) of the union U. The SystemVerilog syntax requires a more specific way to indicate this. For instance, if you are assigning a struct to a union member that is a struct, you need to preface the assignment with the type of the struct member. This is where the SystemVerilog language enforces clarity to avoid ambiguity during synthesis.
The error message, Assignment pattern key 'a' not found as member, directly points to this ambiguity. The compiler sees {a : 1} and looks for a member named a directly within the U union. It finds ww (a logic<4>) and aa (a struct S), but no direct member named a. The value 1 is intended for the a member within the S struct, which is itself a member (aa) of the union U. The original generation process from Veryl was omitting this necessary type specifier, leading to the illegal syntax.
This issue highlights a subtle but important aspect of hardware description languages: the gap between high-level abstraction and low-level synthesis rules. Veryl's goal is to provide a more modern and expressive syntax, but ensuring that the generated SystemVerilog is always synthesizable requires careful handling of such language nuances. The complexity arises because the Veryl emitter needs to infer the specific type being assigned to the union member, rather than just assuming a generic assignment pattern will suffice. This requires a more sophisticated type checking and code generation mechanism within the Veryl compiler itself.
The Solution: Explicit Type Specifiers in Inline Constructors
Fortunately, the solution to this problem is relatively straightforward and involves making the generated SystemVerilog code more explicit. As identified, the key is to include the type identifier when generating the inline constructor for union members, especially when those members are structs. This ensures that the SystemVerilog compiler understands exactly which member of the union is being initialized and what type of data is being assigned to it.
Let's revisit the example code and see how the modification works. In the original generated code, the problematic lines were:
localparam U uu = '{a : 1};
// ... and within t:
u : '{ a : 5 },
The modification involves prepending the specific type of the union member to the inline constructor. So, for uu, which is of type U and is intended to hold the S struct, the assignment becomes S'{a : 1}. This clearly tells the SystemVerilog compiler that we are initializing the S type, and the value 1 is for the a member within that S struct. Similarly, for the union member u within T, the assignment is changed from '{ a : 5 } to S'{ a : 5 }. This ensures that the value 5 is correctly assigned to the a member of the S struct, which is then placed into the aa member of the U union u.
Here's how the modified SystemVerilog code looks:
localparam U uu = S'{a : 1};
localparam T t = T'{
tag: 1,
u : S'{ // Explicitly specify S struct for the union member
a : 5
}
};
This change, while making the generated code slightly more verbose, is crucial for its synthesizability. By adding S' before the assignment, we resolve the ambiguity that troubled the SystemVerilog compiler. The compiler now knows that the assignment pattern {a : 1} applies to the S struct type, which is then correctly interpreted as an assignment to the aa member of the U union.
This fix has been verified to work with major SystemVerilog tools like Vivado and Verilator, confirming its effectiveness. The modification essentially bridges the gap between Veryl's expressive syntax and SystemVerilog's strict synthesis requirements by ensuring that type information is not lost during code generation, particularly for complex types like unions containing structs.
Behind the Scenes: Emitter Modification in Veryl
The successful implementation of the fix in the SystemVerilog output points to a necessary adjustment within the Veryl compiler's emitter. The emitter is the component responsible for translating the abstract syntax tree (AST) of the Veryl code into actual SystemVerilog code. In this specific case, the emitter was incorrectly omitting the type identifier for inline constructors used within unions.
As noted in the provided information, the relevant part of the emitter code is located in the Veryl repository. The modification suggested involves ensuring that the emitter does not omit the type identifier, especially when dealing with unions. The original behavior might have been an attempt to generate more concise SystemVerilog, but it came at the cost of correctness and synthesizability.
The reasoning behind this is tied to the aforementioned type inference challenges. Without explicit type information, the emitter couldn't reliably determine the correct SystemVerilog syntax for initializing a union member that is itself a complex type like a struct. The emitter needs to perform a more thorough analysis of the Veryl code's types to reconstruct the correct SystemVerilog assignment pattern. This involves checking if the left-hand side of the assignment is a union and if the right-hand side is an inline constructor whose target type is a member of that union.
While this modification does lead to slightly more verbose SystemVerilog code, the gain in correctness and synthesizability is invaluable. The trade-off is well worth it, as the primary goal of generating hardware designs is to have them successfully synthesized and implemented in hardware. The emitter's role is critical in this process, acting as the bridge between the high-level Veryl language and the low-level SystemVerilog description understood by synthesis tools.
The alternative of omitting struct identifiers (except for unions) would indeed be a