Language reference

Data Types & Operators

Primitive types, fixed-size arrays, and operators — the vocabulary for representing and manipulating values in Livt.

This chapter introduces the fundamental data types and operators used in Livt. Understanding these concepts is essential for writing effective code and manipulating data efficiently.

The primitive types are intentionally similar to those in many other programming languages, because Livt aims to stay simple. This also makes it easier to migrate existing software code into the FPGA domain.

Data Types

Boolean (bool)

Boolean is a type provided by the base library and Livt provides the bool keyword to declare fields, parameters, and variables. A boolean represents exactly two values, and Livt provides the literals true and false.

Booleans are used for decisions in control flow (for example in if and while) and they are also the result of comparisons and equality checks.

It is important to distinguish bool from logic:

  • bool is strictly two-valued (true / false) and is the right choice for algorithmic conditions.
  • logic is multi-valued and can represent U, X, Z, … which is useful for modeling digital signals and buses.

In other words: use logic for signals, use bool for decisions.

livt
var value: Boolean  // Declaration using the Boolean type name
var value: bool     // Declaration using the bool keyword
true                // a literal representing true
false               // a literal representing false

If a bool variable is declared without an initializer, it defaults to false. Typical boolean operators are available:

livt
!true           // false
true && false   // false
true || false   // true
livt
var enabled: bool = true
if (enabled)
{
    // ...
}

var a: int = 3
var b: int = 5
var is_smaller: bool = a < b

Byte (byte)

Byte is a type provided by the base library and Livt provides the byte keyword to declare fields, parameters, or variables. Byte represents a signed 8-bit integer value ranging from -128 to 127.

Byte inherits from Int8, so both can be used interchangeably. Use byte when you mean "a byte" and want that intent to be obvious; use Int8 when you want to be explicit about size and signedness.

livt
var value: Byte   // base library type name
var value: Int8   // explicit signed 8-bit integer
var value: byte   // primitive keyword

0
13
127   // max
-128  // min

Unsigned Byte (ubyte)

UnsignedByte represents an unsigned 8-bit integer ranging from 0 to 255. Livt provides the ubyte keyword as the primitive spelling for this type. UnsignedByte inherits from UInt8, so both names can be used interchangeably.

livt
var value: UnsignedByte  // base library type name
var value: UInt8         // explicit unsigned 8-bit integer
var value: ubyte         // primitive keyword

0    // min
13
255  // max

Integer (int)

Integer represents a signed 32-bit integer ranging from -2,147,483,648 to 2,147,483,647. Livt provides the int keyword as the primitive spelling. Integer inherits from Int32. The base library also provides Int8, Int16, and Int64 for other widths.

livt
var value: Integer  // base library type name
var value: Int32    // explicit signed 32-bit integer
var value: int      // primitive keyword

0
13
2_147_483_647   // max
-2_147_483_648  // min

Unsigned Integer (uint)

UnsignedInteger represents an unsigned 32-bit integer ranging from 0 to 4,294,967,295. Livt provides the uint keyword as the primitive spelling. UnsignedInteger inherits from UInt32. The base library also provides UInt8, UInt16, and UInt64.

livt
var value: UnsignedInteger  // base library type name
var value: UInt32           // explicit unsigned 32-bit integer
var value: uint             // primitive keyword

0
13
4_294_967_295  // max

Logic (logic)

Logic is the fundamental signal type in Livt, used for parameters, fields, and variables. Unlike bool, logic is a multi-valued logic type, similar to logic types in HDL languages. This is useful to model uninitialized signals, unknown values, or tri-stated buses during simulation.

The following logic literals are supported:

livt
0b0  // logic 0
0b1  // logic 1
0bU  // uninitialized
0bX  // unknown
0bZ  // high impedance (tri-state)
0bL  // weak 0
0bH  // weak 1

Logic values can also be used in fixed-size arrays using the same binary literal style:

livt
var a: Logic
var b: logic
var c: logic[4] = 0b1010

Clock (clock)

Clock is a special type used to represent clock signals in hardware designs. Livt requires clocks to be declared explicitly so that the compiler can reason about clock domains and help prevent common errors such as metastability issues with asynchronous clock crossings.

livt
var my_clock: Clock  // base library type name
var my_clock: clock  // primitive keyword (preferred)

Clocks are typically passed into a component via its constructor and bound to its context:

livt
component MyDesign
{
    new(clk: clock)
    {
        this.context.clk = clk
    }
}

Reset (reset)

Like clock, reset is a special type used to represent reset signals. Livt requires resets to be declared explicitly so that the compiler can reason about reset domains and help prevent synchronization and metastability errors.

livt
var my_reset: Reset  // base library type name
var my_reset: reset  // primitive keyword (preferred)
livt
component MyDesign
{
    new(rst: reset)
    {
        this.context.rst = rst
    }
}

String (string)

String is a type provided by the base library for handling text data. String literals are written using double quotes. Strings in Livt are primarily used during simulation, testbenches, and debugging.

livt
var greeting: String  // base library type name
var greeting: string  // primitive keyword (preferred)

var message = "Hello, World!"

The base library provides several useful string operations through the Livt.String namespace:

  • Concatenation, slicing, searching — basic string manipulation operations
  • Format / interpolation — formatted string generation, e.g. "Value: {x}"
  • Conversion functionsToString, Parse, TryParse for converting between strings and numeric types
  • Comparison operationsEquals, Compare, StartsWith, EndsWith

A common use case is converting strings to byte arrays for transmission over communication interfaces. The Encode() method converts a string to its ASCII byte representation, and Decode() reverses this:

livt
var message = "Hello, World!"
var bytes = message.Encode()   // Convert to byte array

var decoded_message = bytes.Decode()

Fixed-Size Arrays

Livt supports fixed-size arrays for all primitive types. Arrays provide a way to group multiple values of the same type together, which is essential for hardware designs that work with buses, vectors, or collections of signals.

Array types are declared using square brackets with the size specified at compile time:

livt
var data: logic[8]       // 8-element logic array (1-dimensional)
var matrix: int[4, 4]    // 4×4 integer array (2-dimensional)
var cube: bool[2, 3, 4]  // 2×3×4 boolean array (3-dimensional)

Arrays can be initialized using literals:

livt
var nibble: logic[4] = 0b1010                   // Binary literal
var bytes: ubyte[3] = [0x12, 0x34, 0x56]        // Explicit values
var flags: bool[4]  = [true, false, true, false]

Individual elements are accessed using zero-based indexing:

livt
var data: logic[8] = 0b11001010
var bit3 = data[3]  // Access the 4th element (zero-indexed)

data[0] = 0b1       // Modify the first element

Multi-dimensional arrays use comma-separated indices:

livt
var matrix: int[3, 3]
matrix[0, 0] = 42
matrix[1, 2] = matrix[0, 0] + 10

Important: Livt does not support dynamic arrays (resizable at runtime). All array sizes must be known at compile time. This constraint is essential for hardware synthesis, as FPGA resources are allocated statically.

Operators

Livt provides a comprehensive set of operators for working with data. The operator syntax is intentionally similar to C, Rust, and C# to make the language familiar to developers coming from software backgrounds.

Arithmetic Operators

Livt supports the standard arithmetic operators for numeric types:

livt
var a: int = 10
var b: int = 3

var sum       = a + b  // 13 (addition)
var diff      = a - b  // 7  (subtraction)
var product   = a * b  // 30 (multiplication)
var quotient  = a / b  // 3  (integer division)
var remainder = a % b  // 1  (modulo)

The unary minus operator negates a number. All arithmetic operators work exclusively on numeric types — attempting to apply them to non-numeric types results in a compile-time error.

livt
var x: int = 42
var y = -x  // -42

var invalid = "text" + 5  // Error: cannot add string and int

Comparison and Equality

Comparison operators evaluate two values and return a boolean result:

livt
var a: int = 10
var b: int = 20

a < b   // true  (less than)
a <= b  // true  (less than or equal)
a > b   // false (greater than)
a >= b  // false (greater than or equal)

Equality operators work on any type:

livt
1 == 2              // false
1 != 2              // true
0b1010 == 0b1010    // true
"hello" == "hello"  // true
"hello" != "world"  // true
314 == "pi"         // false

Livt supports implicit conversions in limited, well-defined cases. For instance, comparing an int with a byte will promote the byte to int before comparison.

Logical Operators

Logical operators combine boolean values and are essential for control flow:

livt
var a: bool = true
var b: bool = false

a && b  // false (logical AND)
a || b  // true  (logical OR)
!a      // false (logical NOT)

These operators short-circuit: && stops evaluating if the left side is false, and || stops if the left side is true. This is useful for safe conditional checks:

livt
var index: int = 5
var data: logic[8]

if (index < 8 && data[index] == 0b1)
{
    // Safe: data[index] is only accessed if index is within bounds
}

Bit Operators

Bit operators perform bitwise operations on integer and logic types:

livt
var a: logic[8] = 0b11001010
var b: logic[8] = 0b10101100

a | b   // 0b11101110 (bitwise OR)
a & b   // 0b10001000 (bitwise AND)
a ^ b   // 0b01100110 (bitwise XOR)
~a      // 0b00110101 (bitwise NOT/complement)

Shift operators move bits left or right:

livt
var x: int = 8

x << 2  // 32 (shift left by 2 bits)
x >> 1  // 4  (shift right by 1 bit)

Compound Assignment

Compound assignment operators combine an arithmetic or bitwise operation with assignment:

livt
var x: int = 10

x += 5   // x = x + 5  (x is now 15)
x -= 3   // x = x - 3  (x is now 12)
x *= 2   // x = x * 2  (x is now 24)
x /= 4   // x = x / 4  (x is now 6)
x %= 4   // x = x % 4  (x is now 2)
livt
var flags: logic[8] = 0b00001111

flags |= 0b11110000   // flags = flags | 0b11110000
flags &= 0b10101010   // flags = flags & 0b10101010
flags ^= 0b01010101   // flags = flags ^ 0b01010101

Unary Operators

In addition to negation (-) and logical NOT (!), Livt supports increment and decrement operators, which are particularly useful in loops and state machines:

livt
var counter: int = 0

counter++  // Increment by 1 (counter is now 1)
counter--  // Decrement by 1 (counter is now 0)
livt
for (var i: int = 0; i < 10; i++)
{
    // Loop body
}

Precedence and Grouping

Operators have a defined precedence that determines the order of evaluation, from highest to lowest:

  1. Unary: !, ~, -, ++, --
  2. Multiplicative: *, /, %
  3. Additive: +, -
  4. Shift: <<, >>
  5. Relational: <, <=, >, >=
  6. Equality: ==, !=
  7. Bitwise AND: &
  8. Bitwise XOR: ^
  9. Bitwise OR: |
  10. Logical AND: &&
  11. Logical OR: ||
  12. Assignment: =, +=, -=, etc.

Use parentheses to explicitly control evaluation order or improve readability:

livt
var result = (a + b) * c          // Add first, then multiply
var flag   = (x > 0) && (y < 10)  // Clear precedence

When in doubt, use parentheses. They make the code more explicit and easier to understand, especially for complex expressions.

Summary

This chapter introduced the fundamental data types and operators that form the foundation of Livt programming:

  • bool — two-valued boolean for algorithmic conditions and control flow.
  • byte / ubyte — signed and unsigned 8-bit integers.
  • int / uint — signed and unsigned 32-bit integers; the base library provides other widths (Int8, Int16, Int64, etc.).
  • logic — multi-valued signal type for hardware design, supporting U, X, Z, and other values beyond binary.
  • clock / reset — explicit timing control types that allow the compiler to reason about clock and reset domains.
  • string — text handling, primarily for simulation and debugging.

Fixed-size arrays group values together, with all dimensions known at compile time to support hardware synthesis.

Livt's operator set — arithmetic, comparison, equality, logical, bitwise, compound assignment, and unary — follows familiar conventions from C-family languages, making it easy to write expressive code while maintaining the precision required for hardware design.

With these building blocks in place, you're ready to explore control flow structures in the next chapter, where you'll learn how to make decisions and create loops in your Livt programs.