Unions
In Neo, a union
(commonly known as a tagged union or sum type) enables a logic value to contain different data types. The union contains a tag, which functions like an enum where each variant is associated with a LogicType
representing one of the data types. The tag is checked via a match expression before accessing the data, which ensures the data on the wire is always interpreted as the correct type.
Validator
Unions have a single assignability status and timing. Unions can only be assigned from other instances of the same union. To access the value of a union, a match expression is used. The validator ensures each case matches a valid variant of the union and contains an identifier for the associated data value if and only if that variant has a data value. That value will have the type specified by the variant and the same timing as the union. It cannot be assigned.
To construct a variant of a union, the union's LogicType
value contains methods for each variant. For variants with data, the method requires an argument of the variant's specified LogicType
, and that value's timing will apply to the instance of the union. For variants without data, the method takes no arguments and the instantiated value will be a constant.
Generator
Unions are implemented as structs with two fields, tag
and data
. tag
's width is the minimum number of bits required to represent each variant (log2 rounded up). Tag values are assigned to variants by the order they are listed, starting at 0. data
's width is that of the widest data variant. If there is only one variant with data, the data wire from the struct is used directly. If there are multiple data variants, a wire named [union name]_data__variant_[variant name]
is created and assigned from the appropriantly wide slice of the struct's data wire. Match expressions are translated into Verilog case
statements, switching on the tag value.
Example
Source:
union MyUnion(t: LogicType) {
A,
B(Int(32)),
C(t)
}
@top
module Union {
input in(1): MyUnion(Bits(1)),
output out(1): Int(32)
} {
comb {
out = match in {
A => 1#32,
B(x) => x,
C(x) => if x {42#32} else {0#32}
}
}
}
Output:
module Union (
input logic __defaultClock,
// Since unions are implemented as structs, they too get condensed to a single wire in module IO
input logic [33:0] in,
output logic [31:0] out
);
// Outside of module IO, the struct is split up into the tag and data fields
logic [31:0] in_data;
logic [1:0] in_tag;
// Since the union has multiple data variants, each one gets its own wire
logic [31:0] in_data__variant_B;
logic in_data__variant_C;
assign in_data__variant_B = in_data;
assign in_data__variant_C = in_data[0];
always_comb begin
// The match expression becomes a case on the tag
case (in_tag)
2'h0 : begin
out = 32'h1;
end
2'h1 : begin
// Case bodies are able to access their respective data value
out = in_data__variant_B;
end
2'h2 : begin
if(in_data__variant_C) begin
out = 32'h2a;
end else begin
out = 32'h0;
end
end
endcase
end
assign in_data = in[33:2];
assign in_tag = in[1:0];
endmodule : Union