Skip to content
code0.HDL

Types, widths and explicit conversion

See why a sized value never changes width on its own, how an unsized literal takes the width of its target, and which function to call for zero extension, sign extension, truncation and reinterpretation.

In this chapter

  • Predict whether an assignment type-checks from the widths on both sides, and read T0020, T0060 and T0051
  • Choose truncate, zeroExtend, signExtend or extend for an intended width change, with the width taken from the target type
  • Read one bit pattern as Bit, UInt and Int, and order unpack before signExtend when a signed value widens
  • Tell a type assertion from a conversion and an Integer from a hardware value
Concepts in this chapter
  • Type width and explicit conversion
Prerequisite concepts

1. The width is part of the type

Bit#(4) and Bit#(8) are two different types, not one type with two sizes. BSV never changes a sized value's width on its own, in either direction. Assigning a 4-bit value to an 8-bit name is a compile error, and so is assigning an 8-bit value to a 4-bit name. There is no silent zero fill and no silent dropping of high bits; the compiler stops with T0020 (expected one type, inferred another) or T0060 (bit-size mismatch), and no circuit is built.

This is the opposite of Verilog, where an expression is extended or truncated to fit its context. Keep the two rules apart: in BSV width changes must be explicit.

1.1 When type checking happens

Compiling BSV runs through several stages, and type checking comes before elaboration, the stage that builds the hardware structure.

StageWhat it does
ParsingGroups tokens into declarations, expressions and statements
Type checkingConfirms that every expression's type and width agree
ElaborationTurns modules and instances into a hardware structure
SimulationRuns that hardware cycle by cycle

If type checking fails, no circuit is generated. A type error therefore never describes a misbehaving circuit; it tells you that no circuit was built.

1.2 Four questions to ask of a type

QuestionWhat to look forExample
RepresentationHow many bits exist?Bit#(4) has four
InterpretationWhat number do the bits mean?UInt#(4) and Int#(4) read the same bits as different numbers
CapabilityWhich operations are allowed?With Eq#(t), you may use == on t
StageIs this a type or a value?The 8 in Bit#(8) is a numeric type, not a value

All four are settled at compile time and never change from cycle to cycle. The traces in this chapter therefore follow how types and values are worked out, not how registers change. The capability question, and the provisos that answer it for a type variable, belong to Typeclasses and provisos.

1.3 Parameterized types

A parameterized type writes its parameters after #.

Form

TypeConstructor#(param1, param2, ...)
Bit#(8)              // eight raw bits
Vector#(4, Bit#(8))  // four 8-bit elements
Bit#(n)              // width given by numeric type variable n
PartRule
Type constructorStarts uppercase, like Bit and Vector; the Verilog-compatible int and bit are the exceptions
A fixed numberThe 8 in Bit#(8) is a fixed numeric type
A type variableStarts lowercase, like the n in Bit#(n), and is settled by the context where it is used

The Verilog-compatible spelling bit[m:0] also means a vector of m + 1 bits; its second index must currently be 0.

1.4 Width changes must be explicit

Narrowing and widening assignments are both compile errors; only an unsized integer literal takes the width of its target, so Bit#(8) x = 5; is fine.

Bit#(4) nibble = 4'b1100;
Bit#(8) wide   = nibble;      // error T0020: expected Bit#(8), inferred Bit#(4)
Bit#(4) narrow = 5'b10101;    // error T0060: bit-size mismatch

To change a width, call the function that states your intent; section 5 lists them.

1.5 Example: implicit narrowing

This example fails to compile on purpose. narrow is declared with four bits, but the literal 5'b10101 has five.

Narrowing.bsv
package Narrowing;

(* synthesize *)
module mkTop(Empty);

  rule show;
    // Five bits into a four-bit variable: rejected before any circuit exists.
    Bit#(4) narrow = 5'b10101;
    $display("%b", narrow);
    $finish(0);
  endrule

endmodule

endpackage

Practice this concept

Use this concept in a short BSV problem as your next step.

Predict before you run: will bsc drop the extra high bit and start the simulation, or stop during compilation?

Check

What happens when you run Narrowing.bsv?

Your answer

Press Run and read the message.

ItemDetail
Faulty codeA 5-bit literal assigned to a Bit#(4) variable
Failing stageType checking in bsc; elaboration and simulation never start
MessageT0060 with bit-size mismatch
FixWiden the target to Bit#(5) to keep every bit; write 4'b0101 if the source should be four bits; use truncate if dropping the high bit is the intent

The type derivation is short:

  1. The sized literal 5'b10101 has type Bit#(5).
  2. The declaration requires Bit#(4).
  3. Those types match only if 5 equals 4, so type checking fails.

Whether the value would fit in four bits is irrelevant. 5'b00101 also has type Bit#(5) and fails the same way. A type error never describes a misbehaving circuit. It says no circuit exists.

Try it yourself. If the target were Bit#(3), making the literal's number smaller would not help. You would need a 3-bit literal such as 3'b101, or an explicit truncate.

2. An unsized literal takes the width of its target

A literal without a size, such as 12, has no width of its own. The type of the place it is used supplies one: in Bit#(4) a = 12; the literal becomes four bits. The value must fit, though. Bit#(4) a = 16; is rejected with T0051, Literal 16 is not a valid Bit#(4), because four bits hold 0 to 15.

LiteralWidthRule
5'b101015, fixed by the literalmust equal the target width exactly
12taken from the targetmust be representable in that width

So the two kinds of literal fail differently: a sized literal of the wrong width is a type mismatch, an unsized literal that is too large is a range error. Both are compile-time. The spelling rules for literals, including bases, underscores and the fill literals '0 and '1, are in Lexical elements.

Check

Bit#(4) x = 16; — what does bsc do?

Your answer

3. Widths add in a concatenation; the declaration only checks

BitWidth.bsv joins a 4-bit nibble and an 8-bit octet with { nibble, octet } and stores the result in a Bit#(12). In a concatenation, the first operand takes the high bits. The 12 is not chosen by the declaration; it is 4 + 8, the width the expression already has. The declaration agrees with it, which is why the program compiles.

BitWidth.bsv
package BitWidth;

(* synthesize *)
module mkTop(Empty);

  rule show;
    // The number in Bit#(n) is the width, and it is part of the type.
    Bit#(4) nibble = 4'b1100;
    Bit#(8) octet  = 8'h3f;

    // Concatenation adds the widths: 4 + 8 fixes this type, not the declaration.
    Bit#(12) joined = { nibble, octet };

    $display("nibble = %b", nibble);
    $display("octet  = %b", octet);
    $display("joined = %b", joined);
    $finish(0);
  endrule

endmodule

endpackage

Practice this concept

Use this concept in a short BSV problem as your next step.

Predict before you run: how wide is the concatenation, and what are the top four bits of joined?

Check

If nibble were declared Bit#(5), how wide would { nibble, octet } be?

Run it and read the three printed vectors.

Output lineWhat it shows
nibble1100, four bits
octet00111111, eight bits
joined110000111111, twelve bits: nibble on top, octet below

The width 12 comes from 4 + 8. The Bit#(12) declaration checks that result; it does not create it.

Try it yourself.

ChangeExpected result
{ nibble, octet } to { octet, nibble }Still twelve bits, so it compiles, and joined becomes 001111111100
Declare nibble as Bit#(5) nibble = 5'b11000;To keep the result at twelve bits, octet would have to be seven bits, so compilation fails with T0020, reporting that octet was expected to be Bit#(7)

Types check the total width; the order inside the braces decides the layout.

4. Four ways to hold a number

BSV separates how many bits a value has from how those bits are read. The width is always part of the type, so a width mistake is a type error rather than a silent change in the circuit.

Form

Bit#(width)   name = value;   // raw bits; arithmetic and comparison treat them as unsigned
UInt#(width)  name = value;   // unsigned number
Int#(width)   name = value;   // two's-complement signed number
Integer       name = value;   // unbounded, elaboration time only
Bit#(8)  raw    = 8'hA5;
UInt#(8) count  = 200;
Int#(8)  offset = -3;
Integer  depth  = 16;
TypeWidthMeaningBecomes hardware?
Bit#(n)exactly na bit pattern; layout is the pointyes
UInt#(n)exactly nunsigned arithmetic and comparisonyes
Int#(n)exactly nsigned arithmetic and comparisonyes
int32a synonym for Int#(32)yes
Integernonea mathematical integer used while the design is elaboratedno

Pick Bit#(n) when you care where each bit sits, such as fields in a frame, and UInt#(n) or Int#(n) when you care what the number means. How each type changes the result of >>, <, / and % is shown with a runnable example in Bool, Bit and pure functions.

Note. Integer values are used for loop bounds, depths and other parameters. They are gone by the time hardware is generated, so an Integer can never be stored in a register.

5. Changing a width is a function call

When a width really must change, call the function that says how. The destination type tells each one how wide the result is.

Form

narrower = truncate(wider);      // drop the high bits
wider    = zeroExtend(narrower); // fill the new high bits with 0
wider    = signExtend(narrower); // copy the old top bit into the new high bits
wider    = extend(narrower);     // zero for Bit/UInt, sign for Int
Bit#(8)  a = 8'hA5;               // 1010_0101
Bit#(4)  t = truncate(a);         // 4'h5
Bit#(12) z = zeroExtend(a);       // 12'h0a5
Bit#(12) s = signExtend(a);       // 12'hfa5
Bit#(12) e = extend(a);           // 12'h0a5, because a is a Bit
CallWhat happens to 8'hA5Result
truncate to 4 bitsbits 7..4 are dropped5
zeroExtend to 12 bitsfour 0s are added on top0a5
signExtend to 12 bitsthe top bit, 1, is copied four timesfa5
extend on a Bitbehaves like zeroExtend0a5
FunctionEffectExample
zeroExtend(x)widen; new high bits are 0Bit#(8) w = zeroExtend(4'hC); gives 8'h0C
signExtend(x)widen; new high bits copy the top bitInt#(8) w = signExtend(Int#(4)'(-3)); gives -3
truncate(x)narrow; keep the low bitsBit#(4) t = truncate(8'hA5); gives 4'h5
extend(x)widen the way the type sayson an Int#(8) holding -91, extend to Int#(12) keeps -91

None of these takes a bit count. The target type, usually the declaration on the left, tells the function how many bits to produce. The same truncate call yields three bits when the target is Bit#(3).

Note. Writing Bit#(4) b = a; with an 8-bit a is a compile error, T0020, with expected type Bit#(4) and inferred type Bit#(8). The fix is to say which bits you want with truncate, not to change a.

6. The same bits, read two ways

Bit#(n), UInt#(n) and Int#(n) all occupy n bits, and they are three types. Bit is a raw pattern, UInt reads it as an unsigned number, Int as two's complement.

TypeInterpretationRange at four bits
Bit#(n)raw bit vector0000 to 1111
UInt#(n)unsigned integer0 to 15
Int#(n)two's-complement signed integer-8 to 7

6.1 pack and unpack

pack turns a value into a Bit#(n) of the same width, and unpack turns a Bit#(n) into a value of the target type. Neither moves any bits; they change only how the compiler reads them.

Form

Bit#(n) bits = pack(value);
TargetType v = unpack(bits);
Bit#(4)  raw = 4'b1101;
UInt#(4) u   = unpack(raw);   // 13
Int#(4)  s   = unpack(raw);   // -3
Bit#(4)  b   = pack(s);       // 1101 again

6.2 Example: one pattern, two types

Signedness.bsv unpacks raw = 4'b1101 once as UInt#(4) and once as Int#(4) and displays both.

Signedness.bsv
package Signedness;

(* synthesize *)
module mkTop(Empty);

  rule show;
    Bit#(4) raw = 4'b1101;

    // The same four bits under two types. unpack reinterprets, it moves no wires.
    UInt#(4) asUnsigned = unpack(raw);
    Int#(4)  asSigned   = unpack(raw);

    $display("raw         = %b", raw);
    $display("as UInt#(4) = %0d", asUnsigned);
    $display("as Int#(4)  = %0d", asSigned);
    $finish(0);
  endrule

endmodule

endpackage

Practice this concept

Use this concept in a short BSV problem as your next step.

Predict before you run: work out 1101 as an unsigned number and as a two's-complement number.

Check

What does 1101 print as an Int#(4)?

Output lineWhat it shows
raw1101
as UInt#(4)8 + 4 + 1 = 13
as Int#(4)the top bit weighs -8, so -8 + 4 + 1 = -3

Both variables hold the same four bits; only their types differ. unpack alone adds no conversion hardware.

Try it yourself.

raw valueas UInt#(4)as Int#(4)
4'b10008-8, the smallest Int#(4)
4'b011177, the largest Int#(4)

6.3 Widening a signed value

To keep a signed 4-bit value's meaning in eight bits, order matters: interpret it as Int#(4) first, then widen with signExtend.

Bit#(4) raw = 4'b1101;
Int#(8) good = signExtend(unpack(raw));   // -3: the sign bit is copied
Int#(8) bad  = unpack(zeroExtend(raw));   // 13: the new top bit is 0

Caution. Zero-extending the raw bits first makes the new top bit 0, so the signed reading is positive. Both lines compile. The mistake shows up only for inputs whose top bit is 1; a positive input such as 0111 looks fine. Testing 1000 and 0111 together exposes it.

7. Conversions in one table, and the type assertion that is not one

7.1 Converting between types

Changing how bits are interpreted, or how many there are, needs an explicit function.

FunctionWhat it doesExampleResult
pack(x)turns a value into its Bit#(n) representationpack(True)1'b1
unpack(b)turns bits into a value of the expected typeUInt#(8) u = unpack(8'hff);255
zeroExtend(x)widens by adding zeros at the topBit#(8) w = zeroExtend(4'b1101);8'h0d
signExtend(x)widens by copying the sign bitInt#(8) w = signExtend(Int#(4)'(-3));-3
truncate(x)keeps the low bitsBit#(4) t = truncate(8'hab);4'hb

The target width comes from the expected type, usually the declaration on the left.

7.2 A static type assertion is not a conversion

Syntax that looks like a C cast is a static type assertion.

Form

Type'(expression)
UInt#(8) ok = UInt#(8)'(255);   // tells the literal its type
UInt#(8) no = UInt#(8)'(bits);  // error if bits is Bit#(8)

The assertion only tells type checking which type the expression must have. It works for an overloaded expression such as a literal, which is why Int#(4)'(-3) appeared in the conversion table. It does not change a Bit#(8) into a UInt#(8): the second line fails with T0020, expected type UInt#(8), inferred type Bit#(8). Use unpack for that.

8. A map of common types

The Prelude and Foundation libraries provide types that fall into a few families.

FamilyRepresentative typesPurpose
Packed valuesBit#(n), UInt#(n), Int#(n), Bool, Maybe#(t), Tuple2#(a, b)Values with a fixed bit representation through a Bits instance
Elaboration or simulation valuesInteger, Real, String, Char, FmtCompile-time calculation and display; not all are synthesizable
Hardware interfacesReg#(t), FIFO#(t), Clock, Reset, Inout#(t)State elements and hardware connections
Program structureAction, ActionValue#(t), Rules, Module#(ifc)State changes, rule sets and module construction

Action and ActionValue#(t) are ordinary types, not special method syntax. Their role in interfaces and scheduling is covered in Methods: value, Action, ActionValue.

9. Apply it: make every conversion explicit

Three graded problems follow, one per rule of this chapter.

ProblemWhat is wrongThe one edit
Narrow a byte explicitlylowNibble must return the low four bits of a Bit#(8) but returns the whole input, so it does not compile, for the reason section 1 showedReplace the return expression with the one conversion whose meaning is "drop the high bits"; the return type tells it how many to keep
Zero-extend a nibblezeroExtendNibble returns { upper, lower }, already eight bits wide, but both locals are ?Give each local its role: a four-bit zero for upper, the input for lower; the total width was never the problem, the field order is
Sign-extend a four-bit integerasSignedByte widens with zeroExtend first and reads the bits as Int#(8) second, so 1101 prints 13 instead of -3Reverse the order from section 6.3: unpack while the value is still four bits, then signExtend to the return width

For the third problem, test both sides of the 0111/1000 boundary before you decide the repair is complete; a zero extension looks correct on every positive input.

10. Common mistakes

10.1 Assigning across widths

Bit#(8) octet  = 8'hA5;
Bit#(4) nibble = 4'hC;

// Wrong
Bit#(4) low  = octet;     // 8 bits into 4: T0020, expected Bit#(4), inferred Bit#(8)
Bit#(8) wide = nibble;    // 4 bits into 8: T0020

// Right
Bit#(4) low  = truncate(octet);
Bit#(8) wide = zeroExtend(nibble);

BSV adjusts neither direction automatically and refuses to drop bits silently. Name the conversion, truncate, zeroExtend, signExtend or extend, so the reader of the code can see the width change too.

10.2 Zero-extending a signed value

// Wrong: 1101 (-3) becomes 13
Int#(8) v = unpack(zeroExtend(raw));

// Right: interpret as signed first, then sign-extend
Int#(8) v = signExtend(unpack(raw));

Both versions compile. Only an input whose top bit is 1 reveals the difference.

10.3 Using a type assertion as a conversion

// Wrong: Bit#(8) is not UInt#(8)
UInt#(8) x = UInt#(8)'(bits);

// Right: reinterpret the bits
UInt#(8) x = unpack(bits);

A type assertion never changes a value. Use unpack, pack, zeroExtend, signExtend or truncate when the type or width must really change.

10.4 Expecting a declaration to set an expression's width

// Wrong expectation: declaring Bit#(12) trims the concatenation to 12 bits
Bit#(12) joined = { nibble5, octet };   // 5 + 8 = 13 bits: T0020

// Right: the expression's width comes from its operands; the declaration checks it
Bit#(13) joined = { nibble5, octet };

11. Summary

RuleConsequence
type checking runs before elaborationany error means no circuit is generated
width is part of the typeBit#(4) and Bit#(8) never mix without a conversion
sized literalits width must equal the target's, else T0060 or T0020
unsized literaltakes the target width; out of range is T0051
concatenationwidth is the sum of the operands; the declaration only checks
zeroExtend, signExtend, truncate, extendstandard width-conversion functions; target width from context
pack, unpackchange interpretation, not bits; Bit, UInt and Int read the same bits differently
sign extensioninterpret as Int first, then signExtend
T'(e)asserts a type for an overloaded expression such as a literal; it converts nothing
Integerexists only during elaboration and never becomes hardware

12. Check your understanding

  1. Why does assigning a 5-bit value to Bit#(4) cause a compile error instead of an overflow during simulation?

    Show answer

    Width is part of the type, and type checking runs before elaboration. Bit#(5) and Bit#(4) are different types, so compilation stops with T0060 before any circuit exists.

  2. What changes and what stays the same when 1101 is unpacked as UInt#(4) and as Int#(4)?

    Show answer

    The four bits 1101 stay the same. Only the interpretation changes: 13 as UInt#(4), -3 as Int#(4).

  3. What is extend(x) when x is an Int#(8) holding -91, and the result is an Int#(12)?

    Show answer

    -91. On a signed type, extend performs sign extension, so the value is preserved. On a Bit or UInt it would add zeros instead.

Checkpoint

Submit the Practice problem for grading

Grading runs on the server against hidden tests; the result is recorded in this browser only.

Sources

Licences and notices for the sources this site follows