Livt types describe values that will eventually become hardware: single signals, vectors, counters, byte streams, fixed tables, state variables, and test data. The type system is intentionally familiar, but every width and array dimension has hardware consequences.
This chapter introduces the core primitive types, fixed-size arrays, literals, casts, and operators. The goal is not to memorize every rule at once. The goal is to learn which type communicates the intent of a value most clearly.
Primitive Types
Livt provides keyword spellings for the primitive types used most often:
| Keyword | Meaning | Typical use |
|---|---|---|
bool |
Two-valued condition: true or false |
Decisions, function results, assertions |
logic |
Hardware logic value | Signals, ports, bit vectors |
byte |
Unsigned 8-bit value | Protocol data, buffers, encoded text |
int |
Signed 32-bit integer | Counters, loop variables, arithmetic |
uint |
Unsigned 32-bit integer | Non-negative counts and sizes |
string |
Text value | Simulation, constants, encoded byte data |
clock |
Clock signal | Sequential process context |
reset |
Reset signal | Sequential process context |
Prefer the keyword spelling in examples and application code. It is shorter and keeps the source focused on intent.
bool
Use bool for decisions:
var enabled: bool = true
var done: bool = false
if (enabled)
{
done = true
}
Comparisons produce bool:
var value: int = 7
var small: bool = value < 10
Keep bool separate from logic. A bool answers a language-level question: should this branch run, did this function succeed, did this assertion pass? A logic value represents a hardware signal and can have HDL-style values.
logic
Use logic for hardware signals:
var bit: logic = 0b1
var unknown: logic = 0bX
Common logic literals include:
0b0
0b1
0bU
0bX
0bZ
0bW
0bL
0bH
0b-
0b1010_1100
Binary logic literals use the same signal states as VHDL std_logic: U uninitialized, X unknown, 0, 1, Z high impedance, W weak unknown, L weak zero, H weak one, and - don't care. Underscores are allowed as separators in longer literals.
logic[N] is an N-bit vector:
var nibble: logic[4] = 0b1010
var word: logic[32] = 0x0000002A
logic and logic[1] are intentionally different. logic is one scalar hardware bit. logic[1] is a one-bit vector. Use the scalar form for single ports and flags, and use the vector form when a value should remain part of a uniform vector API. logic[0] is invalid because it would describe a zero-bit hardware value.
Use logic when a value is part of the hardware signal model. Use bool when a value is a condition in Livt control flow.
byte
byte is an unsigned 8-bit value with range 0 through 255:
var zero: byte = 0x00
var letterA: byte = 0x41
var max: byte = 0xFF
Bytes are natural for packet data, UART payloads, memory contents, encoded text, and protocol fields. Hex literals are common because they make byte boundaries obvious.
When you need signed arithmetic, use int. A byte should usually mean raw unsigned data.
int and uint
Use int for signed integer arithmetic and loop counters:
var offset: int = -4
var index: int = 0
Use uint when negative values do not make sense:
var length: uint = 1500
Both types are fixed-width Livt hardware types. int is signed 32-bit and uint is unsigned 32-bit. Their behavior does not depend on whether a VHDL simulator happens to implement integer internally as 32 or 64 bits.
In hardware-oriented code, prefer named constants for important limits and widths instead of repeating numeric literals.
clock and reset
clock and reset identify timing signals used by sequential processes:
component TimedCounter
{
public count: int
new(clk: clock, rst: reset)
{
this.context.clk = clk
this.context.rst = rst
}
process Count()
{
this.count = this.count + 1
}
}
Combinational functions and clockless processes do not need a clock or reset. Sequential processes do.
string
Strings are text values. They are especially useful in tests, simulation reports, and fixed text that should become byte data:
Simulation.Report("starting test")
A string can be encoded into bytes:
component TextConstants
{
const GREETING: byte[] = "Hello".Encode()
fn GetGreetingLength() int
{
return GREETING.Length()
}
}
Treat strings carefully in synthesizable code. Text is most often used at compile time, in constants, or in simulation-only APIs. When hardware needs to transmit text, encode it into byte data.
Fixed-Size Arrays
Hardware resources are statically allocated, so arrays usually have fixed sizes. The size is part of the type:
var payload: byte[64]
var table: int[16]
var matrix: byte[2, 3]
Array literals initialize fixed-size arrays:
var header: byte[4] = [0xDE, 0xAD, 0xBE, 0xEF]
var offsets: int[3] = [0, 14, 34]
The initializer may also supply the size when the declaration uses an unconstrained array type:
var offsets: int[] = [0, 14, 34] // inferred as int[3]
Fields in classes, components, and interfaces may also use an unconstrained declaration. An inline initializer or a statically known assignment supplies the concrete extent before hardware is generated:
component Lookup
{
values: int[]
new()
{
this.values = [0, 14, 34] // values is realized as int[3]
}
}
This is especially useful for interface fields, where each implementing component supplies the actual extent. An unconstrained declaration is not a zero-length array. A concrete component must supply a statically resolvable positive extent before hardware is generated.
Every array value must resolve to a positive hardware extent. Zero-length arrays and empty array literals are not supported:
var missingSize: int[] // invalid: storage has no extent
var zeroSize: int[0] // invalid: array dimensions must be greater than zero
var empty: int[] = [] // invalid: empty array values are not supported
Use at least one physical element when an algorithm needs to represent an empty logical collection, and track its logical length separately.
Multi-line literals are also supported and are useful for longer tables:
var mac: byte[6] = [
0x02, 0xAA, 0xBB,
0xCC, 0xDD, 0xEE
]
Multi-dimensional arrays use nested literals:
var matrix: byte[2, 3] = [
[0x01, 0x02, 0x03],
[0x04, 0x05, 0x06]
]
Indexing is zero-based:
var first = header[0]
header[1] = 0xAA
matrix[1, 2] = 0xFF
Array Parameters
Functions can receive fixed-size or unconstrained arrays. A direction may be omitted or written explicitly to describe how the function uses the array:
| Direction | Initial contents | Function may | Caller observes updates |
|---|---|---|---|
| omitted | supplied | read + write | yes |
in |
supplied | read only | no |
out |
not required | write only | yes |
inout |
supplied | read + write | yes |
A directionless array parameter is mutable. Indexed writes are visible to the caller after the function completes:
public static inline fn SetByte(values: byte[], index: int, value: byte)
{
values[index] = value
}
Use in for a read-only array and out when a function produces values without depending on the previous contents. Use inout when the function transforms an existing array. Sorting is a typical example:
public fn BubbleSort(data: inout int[])
{
for (var pass: int = 0; pass < data.Length() - 1; pass++)
{
for (var i: int = 0; i < data.Length() - pass - 1; i++)
{
if (data[i] > data[i + 1])
{
var temp: int = data[i]
data[i] = data[i + 1]
data[i + 1] = temp
}
}
}
}
The directionless form data: int[] is also mutable. Explicit inout is often the clearer choice for a public API because it states that the function reads and updates the supplied array.
An unconstrained parameter such as int[] can receive arrays of different lengths. Length() returns the actual length supplied for the current call. Unconstrained array types are also valid as function return types. In both positions, the concrete extent comes from the value passed or returned rather than declaring new unsized storage.
For a multidimensional array, partially index the array before querying the next dimension:
var rows = values.Length()
var columns = values[0].Length()
var depth = values[0, 0].Length()
Logic Vectors and Array Dimensions
logic[N] is a vector of N bits treated as a single value. Initialize it with a binary or hex literal:
var flags: logic[4] = 0b1010
Use bracket-list literals when the type is an array of elements:
var bytes: byte[3] = [0x10, 0x20, 0x30]
var rows: logic[4, 2] = [0b00, 0b01, 0b10, 0b11]
The distinction matters for how values are laid out in hardware. If you mean one bit vector, use logic[N]. If you mean several separate elements, use an array type such as byte[N] or logic[A, B].
Concat combines one-dimensional logic vectors. When both operand widths are known, the result width is the sum of both widths:
var a: logic[8] = 0xAA
var b: logic[8] = 0x55
var c = a.Concat(b) // logic[16]
Unconstrained logic[] is also convenient when the initializer gives the compiler a concrete width:
var a: logic[] = 0xAA // inferred as logic[8]
var b: logic[] = 0x55 // inferred as logic[8]
var c = a.Concat(b) // inferred as logic[16]
If the compiler cannot prove both operand widths, add explicit widths.
Slicing Logic Vectors and Byte Arrays
You can extract a sub-range of a logic[N] or byte[N] array using Python-style slice syntax:
var nibble: logic[4] = word[4:8] // bits 7..4 — upper nibble of an 8-bit word
var hi: logic[16] = frame[16:32] // bits 31..16
var tail: byte[3] = payload[3:6] // bytes at index 3, 4, 5
The form is a[start:stop] where start is inclusive and stop is exclusive, identical to Python list slicing. The result width is stop - start.
When both bounds are integer literals, the result type is inferred automatically:
var version: logic[4] = ipHeader[4:8] // logic[4] inferred — no annotation needed
When the position is a variable but the width is a constant, supply an explicit type annotation:
var chunk: logic[4] = word[offset:offset+4] // logic[4] explicit
If the width itself cannot be determined statically, an explicit annotation is always required:
var part: logic[4] = word[lo:hi] // explicit annotation required
Slicing also works on the left-hand side of an assignment to update a sub-range of a field in place:
this.state[16:32] = newHi // replace bits 31..16
this.buf[1:4] = threeBytes // replace byte elements 1, 2, 3
Slicing is allowed on any array type (logic[N], byte[N]) but not on scalars (logic, byte, int, bool). The compiler reports an error if you attempt to slice a scalar. Inverted bounds (stop <= start) are also a compile-time error.
Omitted Bounds
Either bound can be omitted. The compiler fills in 0 for a missing start and N for a missing stop:
var low: logic[4] = word[:4] // same as word[0:4] — bits 3..0
var high: logic[4] = word[4:] // same as word[4:8] — bits 7..4
var all: logic[8] = word[:] // same as word[0:8] — full copy
Omitted bounds are resolved at compile time — no runtime cost.
Negative Indices
Negative indices count from the end of the array. -k resolves to N - k at compile time:
var last: logic = word[-1] // same as word[N-1] — last bit
var tail: logic[4] = word[-4:] // same as word[N-4:N] — last 4 bits
var head: logic[6] = word[:-2] // same as word[0:N-2] — all but last 2
var mid: logic[4] = word[-6:-2] // same as word[N-6:N-2] — middle 4
var mix: logic[4] = word[2:-2] // same as word[2:N-2] — skip first/last 2
Negative indices work on both sides of a slice and with single-element access. They can be combined with omitted bounds and positive bounds freely.
Unsupported Forms
The following forms are not currently supported:
- Step or reverse:
word[::2],word[::-1] - Negative variable bounds:
word[-offset:](only negative integer literals are resolved)
Type Inference
Livt can infer many local variable types from their initializer:
var value = 42
var ok = true
var marker = 0xFF
Use an explicit type when the width, signedness, or hardware shape matters:
var marker: byte = 0xFF
var flags: logic[8] = 0b00001111
For public fields, function parameters, constants, and interfaces, prefer explicit types. They are part of the component contract.
Literal Convertibility
Literals are target-typed. A literal is accepted when it has a supported conversion path and can be represented by the target type:
var b: byte = 255
var bit: logic = 0b1
var vector: logic[4] = 0xF
var i: int = -2147483648
var u: uint = 0xffffffff
The compiler rejects literals that do not fit or do not make sense for the target:
var b: byte = 256 // invalid: byte is 8-bit
var bit: logic = 0b10 // invalid: scalar logic is one bit
var vector: logic[4] = 0x10 // invalid: needs 5 bits
var u: uint = -1 // invalid: uint cannot be negative
State-bearing logic literals such as 0bU, 0bX, 0bZ, 0bW, 0bL, 0bH, and 0b- are valid for logic and logic[N] targets. They are not numeric values and cannot be assigned to int, uint, or byte.
Implicit Conversions
Some conversions happen automatically when the target type is wider than the source. These are called widening conversions:
- A
bytevalue is accepted whereintoruintis expected. - A
bytevalue is accepted wherelogic[8]is expected. - A hex or binary literal like
0x0Ais accepted wherebyteis expected.
var n: int = 0xFF // byte literal accepted as int
var b: byte = 0x41 // hex literal accepted as byte without as
Conversions that may lose information — narrowing a int to byte, or reinterpreting a logic[N] value — always require an explicit as cast. The compiler will report an error if you try a lossy conversion without one.
Casts
The as operator converts a value from one type to another. Use it when the conversion is intentional and the type change is part of the design:
var b: byte = 0xC8
var n: int = b as int // 200
What Can Be Cast
The core cast matrix covers bool, int, uint, byte, logic, and logic[N]. Identity casts are valid but usually unnecessary.
| From | Supported targets | Result |
|---|---|---|
bool |
int, uint, byte, logic, logic[N] |
true becomes one; false becomes zero |
int |
bool, uint, byte, logic, logic[N] |
Nonzero is true; numeric and logic targets preserve or retain the low bits |
uint |
bool, int, byte, logic, logic[N] |
Nonzero is true; numeric and logic targets preserve or retain the low bits |
byte |
bool, int, uint, logic[N] |
Nonzero is true; numeric targets widen; vectors resize to N bits |
logic |
bool, int, uint, byte, logic[N] |
Zero or one is represented in the target type |
logic[N] |
bool, int, uint, byte, logic, logic[M] |
Nonzero is true; numeric interpretation is explicit; vectors resize to M bits |
byte as logic is intentionally unsupported because silently selecting one bit from a byte is easy to overlook. Select a bit explicitly, such as value[0], when that is the intended operation.
Fixed-width vector casts resize the value. A wider target is zero-extended; a narrower target retains the low bits:
var lowNibble: logic[4] = data as logic[4]
var extended: logic[12] = lowNibble as logic[12]
Use Livt.Bits.SignExtend when widening a vector should copy its sign bit instead of adding zeros.
String encoding and formatting use the string and conversion helpers described in the base-library chapter rather than the numeric matrix above.
Value Range and Data Loss
When the source value does not fit in the target type, high bits are silently dropped. There is no overflow check:
var n: int = 300
var b: byte = n as byte // 44 — 300 = 0x12C, low 8 bits = 0x2C = 44
To avoid unexpected truncation, check range before casting or use the Livt.Convert helpers, which offer explicit clamped, wrapping, and truncating variants.
logic[N] to int
logic[N] as int uses signed interpretation. The highest bit of the vector is the sign bit:
var v: logic[8] = 0xFF
var n: int = v as int // -1
Use as uint when the same vector should be interpreted as an unsigned value:
var u: uint = v as uint // 255
Boolean Conversions
Conversions between bool and numeric or logic values are semantic conversions, so they require as:
var enabled: bool = count as bool // false for zero, true otherwise
var bit: logic = enabled as logic // 0b0 or 0b1
var value: int = enabled as int // 0 or 1
Without the explicit cast, Livt reports a type error. This keeps a change in meaning visible at the call site.
Mixed-Type Arithmetic
Arithmetic operators (+, -, *, /, %) require both operands to be the same type. There is no implicit promotion. Writing sum + item where sum is int and item is byte is a type error:
// WRONG — type mismatch: int + byte
sum = sum + item
// CORRECT — explicit cast makes the intent clear
sum = sum + (item as int)
This is intentional. Livt targets hardware, where every bit matters and widening has a concrete cost. An implicit promotion rule would have to silently pick a signedness — unsigned for byte, but what for logic[N]? Making the cast explicit keeps the semantics visible and keeps the rule uniform:
- Use
as intto widen abyteorlogic[N]for arithmetic with anint. - Use
as uintto widen for arithmetic with auint. - Use
as bytewhen narrowing back to store a low-8-bit result.
Note that the rule applies at the arithmetic operator, not just at assignment. Even if the result will eventually be stored in an int field, each sub-expression must already have a defined arithmetic domain.
byte does not define a standalone arithmetic domain for +, -, *, /, or %. Cast before arithmetic even when both operands are bytes:
// WRONG - the result domain is not selected by the return type
return left + right
// Signed arithmetic
return (left as int) + (right as int)
// Unsigned arithmetic
return (left as uint) + (right as uint)
Byte comparison, masking, and shifts remain byte operations. Compound updates such as value += amount are also valid: the byte target supplies the result width, and the stored value retains the low eight bits.
Choosing Between byte and logic[N]
byte and logic[8] represent the same 8-bit quantity in hardware, but they communicate different intent:
- Use
bytewhen the value is data: a character, a protocol field, a checksum, a memory word. It reads as unsigned 0–255 when cast toint. - Use
logic[N]when the value is a signal or a register: a flag vector, a control word, a port value with a specific hardware width. Preferuintorbyteas the intermediate when converting toint.
Parentheses Around Casts
Parentheses make cast expressions unambiguous, especially when combined with slicing or arithmetic:
var word: logic[32] = this.GetWord()
var high: byte = (word[24:32]) as byte
var value: int = high as int
Without parentheses, operator precedence can produce surprising results. When in doubt, add them.
Arithmetic Operators
Arithmetic operators work on numeric values:
var a: int = 10
var b: int = 3
var sum = a + b
var difference = a - b
var product = a * b
var quotient = a / b
var remainder = a % b
In hardware, arithmetic is not free. Multiplication, division, and wide additions can affect resource use and timing. Use them when they express the design, but remember that the compiler must lower them into hardware.
Arithmetic is for numeric scalar values. Arrays and logic vectors should be indexed, sliced, concatenated, masked, or explicitly cast before arithmetic is applied.
Comparison and Equality
Comparison operators return bool:
var isDigit = value >= 0x30 && value <= 0x39
var isEmpty = count == 0
var changed = previous != current
Use comparisons to turn signal or numeric values into control-flow conditions. When checking logic, compare it explicitly:
if (this.valid == 0b1)
{
this.Accept()
}
Logical Operators
Logical operators work on bool values:
var validLength: bool = length > 0
var validChecksum: bool = checksum == 0
var accept: bool = validLength && validChecksum
var reject: bool = !accept
! inverts a single bool or logic (single-bit) value. && and || short-circuit. The right-hand side is evaluated only when needed.
if (index < length && payload[index] == 0x00)
{
return true
}
Bitwise Operators
Bitwise operators work on integer, byte, and logic-style values:
var flags: byte = 0xF0
var lowNibble = flags & 0x0F
var highNibble = flags & 0xF0
Common bitwise operators are:
| Operator | Meaning | |
|---|---|---|
& |
bitwise AND | |
| ` | ` | bitwise OR |
^ |
bitwise XOR | |
~ |
bitwise NOT (flips all bits) | |
<< |
shift left | |
>> |
shift right |
~ inverts every bit of a byte or logic[N] value:
var mask: byte = 0xF0
var inverted: byte = ~mask // result: 0x0F
Note that ~ operates on byte and logic[N], not on bool. To invert a single boolean use ! instead.
Shifts are valid for logic[N] values and preserve the vector width. Vacated bits are filled with zero, and bits shifted beyond the vector width are discarded:
var shifted: logic[8] = flags << 2
Cast to int or uint first when signed or unsigned numeric interpretation is part of the operation.
Use bitwise operators for masks, flags, protocol fields, and compact status values.
Assignment Operators
Assignment updates a variable, field, or output parameter:
count = count + 1
this.acceptedCount = this.acceptedCount + 1
Compound assignments are shorthand:
count += 1
flags &= 0x0F
For a byte target, arithmetic compound assignments (+=, -=, *=, /=, and %=) store the result back at eight-bit width. Use an explicit as int or as uint cast when the full arithmetic result must be retained instead.
Increment and decrement are useful for counters and loops:
index++
remaining--
Precedence and Parentheses
Operators have precedence rules, but readable Livt code should not require the reader to remember all of them. Use parentheses when expressions combine several kinds of operators:
var inRange = (value >= 0x30) && (value <= 0x39)
var masked = (flags & 0x0F) == 0x05
Parentheses are especially helpful around casts, slices, bit masks, and combined conditions.
Hardware Meaning
Type choices affect generated hardware. Wider values generally need more storage and wider arithmetic. Fixed-size arrays describe statically sized collections, not dynamically growing containers. Casts can extend, reinterpret, or truncate values, so they are also design decisions about width and range.
Common Mistakes
- Mixing
byte,int, andlogic[N]without making the result width explicit. - Assuming a cast preserves every value when the destination is narrower.
- Forgetting that negative indices resolve against a fixed array boundary.
- Using a literal that is not convertible to the destination type.
Summary
Choose types for intent:
boolfor decisions.logicfor hardware signals and vectors.bytefor unsigned 8-bit data.intanduintfor arithmetic and counts.stringfor simulation text and encoded byte constants.clockandresetfor sequential process context.- Fixed-size arrays for statically allocated collections.
Operators let you calculate, compare, mask, shift, and assign values. The syntax is familiar, but every expression still becomes hardware or simulation behavior. When in doubt, make width, signedness, and intent explicit.