On 4/3/2025 3:35 PM, Lawrence D'Oliveiro wrote:
On Thu, 3 Apr 2025 01:20:48 -0500, BGB wrote:
So, extended features:
_UBitInt(5) cr, cg, cb;
_UBitInt(16) clr;
clr = (_UBitInt(16)) { 0b0u1, cr, cg, cb };
Composes an RGB555 value.
>
cg = clr[9:5]; //extract bits
clr[9:5] = cg; //assign bits
clr[15:10] = clr[9:5]; //copy bits from one place to another.
>
And:
(_UBitInt(16)) { 0b0u1, cr, cg, cb } = clr;
>
Decomposing it into components, any fixed-width constants being treated
as placeholders.
Next step: what if they’re variable-width bitfields, not fixed-width?
Not really addressed here...
I was basically implementing features matching those in Verilog, where except in limited scenarios (such as bit arrays), variable width or variable position bitfields don't really exist.
So:
bi[hi:lo]
Isn't valid unless both hi and lo are constant, and the bit-packing and decomposition also only work for fixed layouts, etc.
And, as it currently works in my compiler (in the type-system and code generation), it could not work with variably modified bit offsets (there is a bit more going on here than just syntax sugar over shifts and masks).
It is possible to use a variable index into a bitint, though in this case it internally behaves like a shift. So, say:
_UBitInt(128) bi;
_UBitInt(7) ix;
...
v=bi[ix]
Essentially does the equivalent of:
v=(bi>>ix)&1;
There are other things that would be useful to have, such as an efficient way to implement operations like (val[4]?vala:valb).
Say, for example, in Verilog is someone wants to implement shift logic, they might write, say:
tVal0 = { valExt[63:0], valIn[63:0] };
tVal1 = valShr[5] ? tVal0[127:32] : tVal0[95:0];
tVal2 = valShr[4] ? tVal1[ 95:16] : tVal1[79:0];
tVal3 = valShr[3] ? tVal2[ 79: 8] : tVal2[71:0];
tVal4 = valShr[2] ? tVal3[ 71: 4] : tVal3[67:0];
tVal5 = valShr[1] ? tVal4[ 67: 2] : tVal4[65:0];
tVal6 = valShr[0] ? tVal5[ 65: 1] : tVal5[64:0];
While, granted, this will be nowhere near as fast as using a native shift instruction, you don't want it to be too unreasonably slow (preferably branch-free).
This being for a language where, for the most part, you don't have generic high level operators and instead the practice tends to be to build them manually.
Where, it is a language where most of the logic is moving bits around, and driving logic based on clock edges and similar.
But, here one would usually target an FPGA, which while flexible, does not tend to dynamically rewire its logic.
But, I had designed the compiler extension along the assumption of similar constraints.
Though, in C, one does have the advantage that, if they want a movable bitfield, they still have shift-and-mask.
Well, and shift-and-mask is a "necessary evil" for dealing with this sort of variability. Even if my compiler did support variable bitfields, they likely couldn't be optimized in any ways more efficient than what can already be done with shift and mask (or, the ability to optimize this to anything useful depending solely on the assumption that the ranges don't move or change in size).
Due to reasons, it is also more efficient with unsigned types, as it is cheaper to zero extend a non-power-of-2 value than to sign-extend it (say, zero-extension using an AND operator, and sign extension needing a pair of shift operations).
While, in theory, one could infer these sorts of constant bitfield manipulations from shift-and-mask code, for things like bitfield insertion the logic would be too complex to deal with.
However, inferring a bitfield extraction from something like ((val>>SHR)&MASK) is at least doable (but, unlike the bit-range notation, will result in a normal integer type rather than a BitInt).
And intermediate form can be used, say:
(val>>idx)[3:0]
Which would produce a bitfield output.
But, can note that there are some restrictions on using this notation with normal integer types, so:
int val, idx, v;
...
v=(val>>idx)[0];
Is not allowed (mostly because this seemed like a scenario that had too high of a risk of someone stumbling onto by accident, this will work with _BitInt(32) or _UBitInt(32) but not with "int" or "unsigned int").
Note also that in the inspiration language, [hi:lo] is not the same thing as [lo:hi]. Though, Verilator doesn't support [lo:hi], in some other Verilog compilers, it is understood as reversing the bit-order, and something like:
tDst[31:0]=src[0:31];
Would reverse the order of the bits.
Though, I could add compiler support for this if needed.
Well, and in the language in question, the usual way to implement a constant value lookup to do something resembling a switch statement:
case(tIdx)
6'h00: tVal = 8'h3F;
6'h01: tVal = 8'h1E;
...
endcase
TODO is possibly to detect this case and turn it into a hidden array lookup:
static const uint8_t tIdx_LUT1[64] = { 0x3F, 0x1E, ... };
...
tVal = tIdx_LUT1[tIdx];
But, not done yet...
In theory, it could be done for C switch via similar pattern matching (say, detecting that every case label is followed by assigning a constant to the same variable followed immediately by a "break;"), but it is uncommon to use a switch this way in C.
Verilog does support a vaguely C like preprocessor, but:
Uses ` rather than #
Doesn't support "if" with a general expression.
Imagine, you only have ifdef and ifndef.
Macro-substitution requires a ` prefix.
So:
`define FOO_1234 16'h1234
...
val = `FOO_1234;
Though, in this language, it is often more preferable to use "parameter"
parameter[15:0] FOO_1234 = 16'h1234;
Which can be vaguely compared with "const int" or similar (though, using parameter for constants in this way is not supported in Verilog-95 or Verilog-2001, which are the usual default assumption in a lot of tools; but the tools I have used do support doing this).
...