Variables and Basic Types

Variables give names to values. Every variable in Sec has a type, a scope, and a defined mutability. Sec is statically typed, so the compiler determines the type of every variable during compilation. A variable cannot silently change from one type to another while the program is running.

Sec variables are immutable by default. This is a deliberate safety rule rather than a restriction added for style. A value can only be reassigned when the declaration explicitly includes mut.

Declaration and Type Inference

A variable may be declared with an initial value. When the type is obvious from that value, the compiler can infer the type.

Every variable declaration must begin with let. The declaration keyword is mandatory even when the compiler can infer the variable's type from its initial value. Sec does not permit a bare name followed by := to introduce a variable.

Rule: let declares an immutable variable. let mut declares a mutable variable. The operator := assigns the initial value as part of the declaration.

Valid Declarations

let b := 8          // immutable, inferred type
let b: int := 9     // immutable, explicit type
let mut b: int      // mutable, explicit type, not initialized here
let mut b := 10     // mutable, inferred type

Invalid Declarations

b := 8 // compile-time error: missing let

The compiler treats the final example as invalid because := is not itself a declaration keyword. Requiring let makes every declaration immediately visible and distinguishes declarations from later assignments.

let name := "Anna"
let age := 42
let enabled := true

In this example, the compiler infers that name is a string, age is an integer type, and enabled is a bool. Type inference does not make the language dynamically typed. The inferred type is fixed at compile time and remains the type of the variable throughout its lifetime.

A type may also be written explicitly when the programmer wants to document the intended representation or when inference would be ambiguous.

let retryCount: int32 := 3
let price: decimal := 19.95
let ratio: float64 := 0.75
Rule: Once declared, a variable has one static type. Assignment may replace its value, but it may not replace its type.
let value := 10
value = 20       // same type
value = "twenty" // compile-time type error

Immutable by Default

A normal variable declaration creates an immutable binding. After the variable has received its initial value, that binding cannot be assigned another value.

let port := 8080
port = 8081 // compile-time error

Immutability makes the flow of data easier to understand. When a reader encounters an immutable variable, they know that the name continues to refer to the same value for the remainder of its scope. The reader does not need to search the entire function for later assignments before understanding the current value.

Immutable-by-default code also helps the compiler. A value that cannot change is easier to analyze, easier to optimize, and safer to share with other parts of a program.

Binding Immutability

Immutability applies to the variable binding. It means that the variable cannot be assigned a different value. Whether data reachable through a reference or contained in a more complex type can change is governed by the rules of that type and Sec's reference and ownership model.

For basic value types, the result is straightforward: an immutable variable cannot be changed after initialization.

let count := 5
count += 1 // compile-time error
Rule: A variable is immutable unless its declaration explicitly contains mut.

Mutable Variables

Add mut when a variable must receive a new value after initialization. Mutability is therefore visible at the declaration site rather than discovered later by reading assignments elsewhere in the function.

let mut retryCount := 0
retryCount += 1
retryCount = 0

The mut modifier permits reassignment. It does not weaken static typing and does not permit the variable to contain arbitrary types.

let mut retryCount := 0
retryCount = 3       // valid
retryCount = "three" // compile-time type error

Mutability should describe a real requirement of the algorithm. It should not be added automatically to every declaration. Values such as configuration, identifiers, function inputs, and intermediate results often do not need to change.

Use the Smallest Mutable Scope

When mutation is necessary, keep the mutable variable in the smallest practical scope. This limits the amount of code that can change it and makes its possible states easier to reason about.

let total := calculateBaseTotal(order)

{
    let mut adjustedTotal := total
    adjustedTotal += calculateTax(order)
    adjustedTotal -= calculateDiscount(order)
    print(adjustedTotal)
}

Assignment

Assignment replaces the current value of a mutable variable. The assigned expression must be assignable to the variable's declared type.

let mut temperature: float64 := 18.5
temperature = 19.0

Compound assignment operators read the current value, apply an operation, and assign the result back to the same variable. Because they perform assignment, they also require a mutable variable.

let mut count := 10
count += 5
count -= 2
count *= 3
count /= 2
count %= 4

Sec does not use the increment and decrement operators ++ and --. Use explicit compound assignment instead.

let mut index := 0
index += 1

Scope

A variable exists only inside its scope. A block delimited by { and } creates a nested scope. A name declared in that block is not available after the block ends.

fn Example() void {
    let message := "outside"

    {
        let detail := "inside"
        print(message)
        print(detail)
    }

    print(message)
    print(detail) // compile-time error: detail is out of scope
}

Function parameters are local symbols in the function body. They are immutable by default, just like other variables.

fn PrintUser(name: string, age: int) void {
    print(name)
    print(age)

    age = 43 // compile-time error
}

Shadowing

Shadowing occurs when a nested scope declares a new variable with the same name as a variable in an outer scope. The inner declaration is a different variable; it does not mutate the outer variable.

let value := 10

{
    let value := 20
    print(value) // 20
}

print(value) // 10

Shadowing can be useful when a name represents the same conceptual role at a more specific stage of processing. It should not be used to hide unrelated values, because that makes code harder to read.

Initialization

A variable must contain a valid value before it is read. An initializer establishes the variable's first value and normally establishes its inferred type.

let name := "Anna"
let count := 0

Basic types have defined zero or empty values where applicable. Domain-specific and constrained types may only have a default value when that value satisfies the type's rules. A later chapter describes default values and constrained types in detail.

Rule: Sec does not permit reading an uninitialized variable.

Basic Types

Basic types are built directly into the language. They form the foundation used to construct domain types, structures, collections, interfaces, and other abstractions.

The decided basic types are:

bool
byte
char
decimal
float
float32
float64
int
int8
int16
int32
int64
uint
uint8
uint16
uint32
uint64
rune
string
void
any

bool

bool represents a logical truth value. It has exactly two values: true and false.

let enabled := true
let finished := false

Conditions in if, while, logical operators, and match guards require boolean expressions. Sec does not implicitly convert integers, strings, references, or other values into booleans.

let count := 3

if count {        // compile-time error
}

if count > 0 {    // valid
}

The logical operators are:

!value
left && right
left || right

Comparison operators such as ==, !=, <, <=, >, and >= produce bool results when they are supported by the operand types.

The default value of bool is false.

byte

byte represents one byte of raw data. It is intended for binary formats, buffers, network data, encoded files, cryptographic material, and other data where the individual unit is a byte rather than a number with domain meaning.

header: byte = 0x7F

Although a byte can be interpreted numerically, byte communicates that the value is part of a binary representation. Use uint8 when the value is conceptually an unsigned eight-bit integer and byte when it is conceptually raw data.

packetType: byte = 0x02
retryCount: uint8 = 2

Text should normally use string, char, or rune rather than byte. A UTF-8 character may occupy more than one byte.

The default value of byte is zero.

char

char represents a character value. It is used when code works with a single textual character rather than a complete string.

separator: char = ','
answer: char = 'Y'

A character literal uses single quotes. A string literal uses double quotes.

letter: char = 'A'
word: string = "A"

char and rune are distinct types. Use rune when the code explicitly works with Unicode code points. Use char when the value is treated as a character in normal text processing. Conversion between the types must remain explicit when required by an API.

A character is not interchangeable with a one-character string. The former is one value; the latter is a string containing one textual element.

decimal

decimal represents decimal numeric values. It is intended for calculations where decimal representation and predictable base-10 behavior are more important than the speed or range characteristics of binary floating-point arithmetic.

let price: decimal := 19.95
taxRate: decimal = 0.25
let total := price + price * taxRate

Typical uses include money, tax, accounting, measurements expressed and rounded in decimal, and business rules that require decimal precision.

Binary floating-point types cannot represent every decimal fraction exactly. For example, a value such as 0.1 generally has an approximate binary representation. Code that requires decimal semantics should therefore use decimal rather than float, float32, or float64.

unitPrice: decimal = 0.10
quantity: decimal = 3
let total := unitPrice * quantity

decimal is also the natural foundation for semantic types with units or currencies.

type SEK decimal<SEK>
type Meter decimal<m>

The default value of decimal is decimal zero.

float

float is the general binary floating-point type. It is suitable for scientific, graphical, statistical, simulation, and engineering calculations where fractional values, large ranges, and floating-point performance are required.

temperature: float = 21.5
ratio: float = 2.0 / 3.0

Floating-point values are approximate. Equality comparisons should therefore be used with care when results have passed through arithmetic operations.

let result := calculateValue()
let closeEnough := abs(result - expected) < tolerance

Use float32 or float64 when an exact storage width is part of the data format, external interface, memory layout, or numerical requirements. Use float when the exact width is not the important part of the program's meaning.

The default value of float is floating-point zero.

float32

float32 is a 32-bit binary floating-point type. It uses less storage than float64 but provides less precision and a smaller numeric range.

vertexX: float32 = 12.5
vertexY: float32 = -4.75

Common uses include graphics data, machine-learning tensors, large numeric arrays, external binary formats, and hardware interfaces that explicitly use 32-bit floating-point values.

Do not select float32 merely because the current values appear small. Precision requirements, accumulated error, external representation, and memory cost should determine the choice.

The default value of float32 is 0.0.

float64

float64 is a 64-bit binary floating-point type. It provides greater precision and range than float32 at the cost of additional storage.

latitude: float64 = 59.3293
longitude: float64 = 18.0686

It is appropriate when numerical precision is important, when calculations accumulate error, or when an external API or file format explicitly requires a 64-bit floating-point value.

Greater precision does not make floating-point arithmetic exact. Values are still represented in binary and should not be used as a substitute for decimal where decimal semantics are required.

The default value of float64 is 0.0.

int

int is the general signed integer type. It represents whole numbers that may be negative, zero, or positive.

let temperature := -5
let count := 42
let offset := 0

Use int for ordinary counting, indexing, differences, offsets, and calculations when the exact bit width is not part of the data's meaning or external representation.

Use one of the fixed-width signed integer types when the width is required by a protocol, file format, memory layout, hardware interface, or interoperability boundary.

itemCount: int = 120
protocolValue: int32 = -120

The default value of int is 0.

int8

int8 is an 8-bit signed integer. Its values range from -128 through 127.

adjustment: int8 = -12

Use int8 when an external representation or compact memory layout specifically requires an 8-bit signed value. For ordinary arithmetic, int is usually clearer.

The default value of int8 is 0.

int16

int16 is a 16-bit signed integer. Its values range from -32,768 through 32,767.

sample: int16 = -2048

It is commonly useful for binary formats, audio samples, device registers, compact arrays, and APIs that define values as signed 16-bit integers.

The default value of int16 is 0.

int32

int32 is a 32-bit signed integer. Its values range from -2,147,483,648 through 2,147,483,647.

databaseValue: int32 = 1500000

Use it when a stable 32-bit representation is required, such as in external data formats, database mappings, network protocols, foreign-function interfaces, or memory-sensitive collections.

The default value of int32 is 0.

int64

int64 is a 64-bit signed integer. It supports a substantially larger range than int32 and is suitable for large counters, timestamps, file positions, identifiers, and external formats that specify signed 64-bit values.

fileSize: int64 = 5000000000
timestamp: int64 = 1783670400

A large available range does not automatically make int64 the best type for every integer. Prefer int for normal internal arithmetic unless the width or range is part of the requirement.

The default value of int64 is 0.

uint

uint is the general unsigned integer type. It represents zero and positive whole numbers, but cannot represent negative values.

mask: uint = 15
capacity: uint = 1024

Unsigned types are appropriate when bit-level operations or an external representation specifically require unsigned values. They should not be selected only because a value is expected to remain non-negative. Differences and error states may naturally require negative values even when the original quantities do not.

available: uint = 5
required: uint = 8
// available - required needs careful handling because the mathematical result is negative.

The default value of uint is 0.

uint8

uint8 is an 8-bit unsigned integer. Its values range from 0 through 255.

channel: uint8 = 255

Use uint8 for compact unsigned numeric values and external representations that explicitly require eight bits. Use byte instead when the value represents raw binary data rather than a number.

The default value of uint8 is 0.

uint16

uint16 is a 16-bit unsigned integer. Its values range from 0 through 65,535.

registerValue: uint16 = 65535

It is useful for device registers, network fields, encoded dimensions, compact counters, and other representations defined as unsigned 16-bit values.

The default value of uint16 is 0.

uint32

uint32 is a 32-bit unsigned integer. Its values range from 0 through 4,294,967,295.

flags: uint32 = 0x80000000

Typical uses include bit masks, protocol fields, checksums, binary file formats, and foreign interfaces that require a stable unsigned 32-bit representation.

The default value of uint32 is 0.

uint64

uint64 is a 64-bit unsigned integer. It provides a very large non-negative range and is used when an external specification, identifier space, counter, or bit representation explicitly requires 64 unsigned bits.

sequenceNumber: uint64 = 10000000000

Arithmetic involving signed and unsigned values must remain explicit. The compiler should not silently reinterpret a negative signed value as a very large unsigned value.

The default value of uint64 is 0.

rune

rune represents a Unicode code point. It is intended for Unicode-aware text processing where code must examine, compare, transform, or classify individual code points.

letter: rune = 'Å'
symbol: rune = '界'

UTF-8 is a variable-length encoding. A single rune may occupy more than one byte when encoded as UTF-8. Consequently, indexing or counting bytes is not the same as indexing or counting Unicode code points.

let text := "Ångström"
// The UTF-8 byte length and rune count are different concepts.

A rune is not a string. A string may contain zero, one, or many runes. Use string for complete text and rune when operating on an individual Unicode code point.

string

string represents UTF-8 text. String literals use double quotes.

let name := "Jonas"
let message := "Sec is statically typed."

Strings are text, not arbitrary byte buffers. Use binary containers or byte-oriented types for compressed data, encrypted data, file contents that are not known to be text, and network packets.

String interpolation uses a dollar sign before the string literal. Expressions inside braces are evaluated and inserted into the resulting string.

let name := "Anna"
let count := 3
let message := $"Hello {name}, you have {count} messages."

Because strings use UTF-8, byte length, rune count, and user-perceived character count are separate concepts. APIs should name these operations clearly rather than treating them as one generic length operation.

The default value of string is the empty string "".

void

void indicates that a function does not return a value. It is used as a function return type, not as a normal value that can be stored in a variable.

fn PrintGreeting(name: string) void {
    print($"Hello {name}")
}

A void function may finish at the end of its body or use return without an expression.

fn PrintIfPresent(value: string) void {
    if value == "" {
        return
    }

    print(value)
}

Returning an expression from a void function is a compile-time error. Likewise, a caller cannot use the call as though it produced a normal value.

any

any can hold a value of any Sec type. It is intended for situations where the value's concrete type is genuinely not known at compile time.

value: any = 42
text: any = "hello"

Using any removes information that the compiler would otherwise use to verify operations. Code must determine or constrain the contained value before performing type-specific operations.

Prefer a concrete type when all values share one representation. Prefer a generic type parameter when the concrete type varies but is known during compilation. Prefer an interface when code depends on a defined set of behaviors. Use any only when none of those accurately describe the problem.

// Prefer a generic function when the type is known at compile time.
fn First[T](values: []T) T {
    return values[0]
}

// Use any only when the value is genuinely dynamic.
fn ReadDynamicValue(source: ref Source) any {
    // ...
}

Common legitimate uses may include dynamic document formats, debugging tools, generic serialization boundaries, reflection-related facilities, and interoperability with systems whose values are dynamically typed.

Guideline: any is an escape from specific static type information, not a replacement for designing useful types.

Choosing a Basic Type

Select a type according to the meaning and requirements of the value, not merely according to the smallest representation that can contain the current example.

Requirement Typical choice
Logical truth value bool
Raw binary unit byte
Single textual character char
Unicode code point processing rune
UTF-8 text string
Ordinary signed whole number int
Ordinary unsigned or bit-oriented whole number uint
Externally defined integer width int8 through int64, or uint8 through uint64
General binary floating-point calculation float
Externally defined floating-point width float32 or float64
Money or decimal business arithmetic decimal
Function returns no value void
Genuinely unknown runtime value any

Semantic types should be introduced when two values share a representation but have different meanings. For example, customer identifiers and product identifiers should not remain plain integers merely because both are stored as numbers.

type CustomerID int64
type ProductID int64

The compiler can then prevent accidental mixing of concepts that happen to share the same underlying representation.