The Need for Bits
Low-level code often has to manipulate data that is encoded in specific patterns of bits. For example, often a hardware register for controlling a device will have several subfields, where some bits select the mode, another bit enables the device, etc. Languages such as C and Java force programmers to express such bit level operations with masks and shifts on integer quantities, often mixed with hexadecimal constants. Such code is tedious to write and obscure to read.
local x: 32 // x is of type 32 local y: 17 // y is of type 17
Virgil instead defines a family of types that correspond to bit-level quantities.
The types 1
, 2
, 3
, to 64
define
fixed-width bit quantities called raw types. Raw types are values
like integers and booleans; they are not passed by reference. The assignment and
promotion rules for raw types are simple:
- a smaller raw type can be assigned to (or used in the place of) a larger raw type.
- upper bits are always zero-filled when converting.
- assigning a larger raw type to a smaller raw type requires an explicit
conversion (using the
::
conversion operator) which discards the upper bits.
Literals
0xfc3 // hex, 12 bits 0b10110 // binary, 5 bits 0752 // octal, 9 bits 997 // int
Virgil supports writing hexadecimal, octal, and
binary literals as expressions in programs. Hexadecimal constants
must begin with 0x
, binary constants must begin with 0b
, and
octal constants begin with a leading zero. The length of the literal determines the
number of bits, and thus the resulting raw type. (Note that decimal constants are
always interpreted as integers, never as raw literals).
Operators
The raw types support a family of familiar bit-level operators:
&
- the bitwise and operation on two operands|
- the bitwise or operation on two operands^
- the bitwise exclusive-or operation on two operands~
- the one's complement operation on a single operand<<
- the shift left within a window operation>>
- the shift right within a window operation#
- the concatenate operation on two operands
0xffc[3] // access bit 3 expr[n] // access bit n v[2] = 0b1 // update bit 2
[]
with an index expression that must be of type int
.
This operator, when used as a suffix to an expression
of a raw type, retrieves the specified bit as a raw value of type 1
(i.e. 1 bit).
Similarly, this operator can be used to update an individual bit of a variable with raw type
by using it as the target of an assignment. Bit number 0
is always the
least-significant bit. Out of bounds accesses do not generate exceptions: a read of a bit
number outside of the valid range for the type returns the value 0b0
, and a write
out of the valid range is ignored.
Conversions
local a: 32 = -377 // OK local b: int = 0xffff // ERROR local c: 8 = 'c' // OK local d: char = 0xfc // ERROR local e: int = 0xffff :: int // OK local g: char = 0xfc :: char // OK local h: boolean = 0b1 :: boolean // OK
Virgil supports implicit and explicit conversions of primitive types to and from raw values:
- values of type
int
can be implicitly converted to raw type32
(the resulting bit pattern is the standard two's complement encoding of the integer). - values of type
char
can be implicitly converted to raw type8
(the resulting bit pattern is the standard ASCII encoding of the character). - raw values of type
32
(or less) can be explicitly converted to typeint
. - raw values of type
8
(or less) can be explicitly converted to typechar
. - raw values of type
1
can be explicitly converted to typeboolean
.
For more detail on each of these operations see the operator reference.
Go back to the tutorial.