# Chapter 3: Integer Types and Wrapping Arithmetic

Kernel code is mostly integer code: register values, addresses, page-table bits, ring-buffer indexes, tick counters. This chapter covers the integer rules you meet in `@mode(freestanding)`:

- the fixed-width types and the pointer-sized `uint`;
- why mixing widths is a compile error and how to convert;
- the rule that fixed-width arithmetic **wraps** in freestanding;
- how to detect overflow yourself today, and what is planned;
- bit manipulation for addresses and flag words;
- what to do about `bigint`, `decimal` and floating point.

## The integer types

| Signed | Unsigned | Width |
|---|---|---|
| `int8` | `uint8` | 8 bits |
| `int16` | `uint16` | 16 bits |
| `int32` | `uint32` | 32 bits |
| `int64` | `uint64` | 64 bits |
| `int` | `uint` | pointer width of the target (64 bits on x86_64, aarch64 and riscv64; 32 bits on armv7, riscv32 and Cortex-M) |

`uint` is the type for addresses. `Volatile[T].at(addr)` takes one, `addressOf(global)` returns one, an `@allocator` returns `uint?`, and a `buffer` overlay is initialized from one. It is an integer, not a pointer type: you can add, mask and compare it, and nothing dereferences it unless you hand it to one of those APIs.

A plain integer literal has type `int`. A suffix gives it a fixed width: `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`. Underscores group digits, and `0x`, `0b` and `0o` select the base:

```loup
@mode(freestanding)

# 16550 UART register offsets and bits.
const UART_THR: uint16 = 0u16          # transmit holding register
const UART_LSR: uint16 = 5u16          # line status register
const LSR_THR_EMPTY: uint8 = 0x20u8    # bit 5: ready for the next byte

# Local APIC base on x86_64 and a 2 MiB large-page mask.
const LAPIC_BASE: uint64 = 0xFEE0_0000u64
const LARGE_PAGE_MASK: uint64 = 0x1F_FFFFu64

func lsrReady(lsr: uint8): bool {
    return (lsr & LSR_THR_EMPTY) != 0u8
}
```

*Listing 3-1: Fixed-width constants written with literal suffixes*

There is no bare `u` suffix: `4096u` is a syntax error. Write `4096u64`, or give the binding a type and let the literal take it (`let size: uint = 4096`).

A suffixed literal that does not fit its type is rejected:

```loup,does_not_compile
@mode(freestanding)

func lineControl(): uint8 {
    let dlab: uint8 = 0x80u8
    let eightN1: uint8 = 0x03u8
    let bogus: uint8 = 0x103u8
    return dlab | eightN1 | bogus
}
```

```text
error[E_LITERAL_OVERFLOW]: E_LITERAL_OVERFLOW: integer literal `0x103u8` does not fit in `uint8` (0..=255) (§1-3.6)
 --> src/main.lpfree:6:24
  |
6 |     let bogus: uint8 = 0x103u8
  |                        ^^^^^^^
  |
```

The check applies to suffixed literals. An unsuffixed literal assigned to a narrow binding (`let b: uint8 = 300`) is not caught by `loup check` today, which is one more reason to write the suffix on register constants.

## Widths never mix

Loup does not promote between integer widths. Adding a `uint32` to a `uint64` is an error, not a silent widening:

```loup,does_not_compile
@mode(freestanding)

# Bytes written so far, and the size of the chunk just sent.
func advance(offset: uint64, chunk: uint32): uint64 {
    return offset + chunk
}
```

```text
error[E_OPERATOR_MISSING]: E_NUMERIC_TYPE_MISMATCH: mixed 'uint64' and 'uint32' operands for operator '+' — integer widths do not promote; convert one operand explicitly (`as uint32`)
 --> src/main.lpfree:5:12
  |
5 |     return offset + chunk
  |            ^^^^^^^^^^^^^^
  |
```

The message offers one conversion; the one you want here is the widening one, because the result is a `uint64`:

```loup
#~ @mode(freestanding)
#~
func advance(offset: uint64, chunk: uint32): uint64 {
    return offset + (chunk as uint64)
}
```

*Listing 3-2: Converting the narrower operand explicitly*

Narrowing is never implicit either. Returning a `uint16` where a `uint8` is expected fails:

```loup,does_not_compile
@mode(freestanding)

func lowByte(value: uint16): uint8 {
    return value
}
```

```text
error[E0001]: return type 'uint16' is not assignable to expected return type 'uint8'
 --> src/main.lpfree:4:5
  |
4 |     return value
  |     ^^^^^^^^^^^^
  |
```

Say what you mean with `as`. On the native targets (`host.native` and freestanding) a numeric `as` that cannot represent the value keeps the low bits — `300 as uint8` is `44`, `-1 as uint8` is `255` (Language Reference §1-4, ruling CAST-2). That is exactly the truncation a byte-splitting helper wants:

```loup
#~ @mode(freestanding)
#~
func lowByte(value: uint16): uint8 {
    return (value & 0xFFu16) as uint8
}

func highByte(value: uint16): uint8 {
    return (value >> 8) as uint8
}

func joinBytes(hi: uint8, lo: uint8): uint16 {
    return ((hi as uint16) << 8) | (lo as uint16)
}
```

*Listing 3-3: Splitting and joining bytes with explicit conversions*

Note the parentheses around each cast. In Loup `as` binds more loosely than the arithmetic, shift and comparison operators (Language Reference §1-3.7), so `hi as uint16 << 8` does not mean what a C programmer expects. Parenthesize every cast that sits inside a larger expression.

## Fixed-width arithmetic wraps

In `@mode(host.native)` and `@mode(freestanding)`, plain `+`, `-` and `*` on fixed-width integers **wrap** at the type boundary: two's complement for signed types, modular for unsigned ones (Language Reference §1-4.1). There is no exception to throw into and no runtime to catch one. `int` and `int64` wrap too in freestanding.

```loup
#~ @mode(freestanding)
#~
func wrapDemo(): uint8 {
    let a: uint8 = 250u8
    let b: uint8 = 10u8
    let sum = a + b          # 4   (260 mod 256)
    let product = a * b      # 196 (2500 mod 256)

    let up: int8 = 100i8
    let down = up + 50i8     # -106: signed overflow wraps; it is not undefined behaviour
    return sum + product
}
```

*Listing 3-4: Wrapping is the defined behaviour of fixed-width arithmetic*

Two things differ from C. Signed overflow is defined (it wraps), so the optimiser cannot assume it never happens. And the rule is the same for every width, so a `uint8` counter and a `uint64` counter behave alike.

Wrapping is often what kernel code wants. A free-running hardware counter wraps; a ring-buffer index computed modulo a power of two wraps; a hash or checksum is defined in terms of wrapping multiplication. Mark such functions with `@overflowWrapping` so the reader — and a future build configured to trap on overflow — knows the wrap is intended:

```loup
#~ @mode(freestanding)
#~
# FNV-1a over the first `n` bytes of a block. The multiply is meant to wrap.
@overflowWrapping
func fnv1a(data: buffer[uint8, 16], n: int): uint32 {
    var hash: uint32 = 0x811C9DC5u32
    var i = 0
    while (i < n) {
        hash = hash ^ (data[i] as uint32)
        hash = hash * 0x01000193u32
        i = i + 1
    }
    return hash
}
```

*Listing 3-5: Documenting intended wrapping with `@overflowWrapping`*

The Language Reference says `@overflowWrapping` keeps a function wrapping even when the package is built to trap on overflow. Today nothing traps (see the next note), so the annotation is documentation — which is still worth having.

> ⚠ **Planned.** The manifest key `[freestanding] overflow = "trap"` (and the `--overflow=trap` flag) is specified to make plain fixed-width `+`, `-`, `*` and `/` trap through your panic policy (Language Reference §3-3.19, §3-3.21). The 1.25 toolchain accepts the key but does not act on it, and there is no `--overflow` flag. Do not rely on a trap to catch overflow.

## Detecting overflow yourself

When overflow would be a bug — a length field from a device, an address computed from user input, a counter that must not roll over — check for it. The pattern that works today is to widen, compute, and compare:

```loup
#~ @mode(freestanding)
#~
# Add two byte counts; None when the sum does not fit in a uint8.
func checkedAddU8(a: uint8, b: uint8): Option[uint8] {
    let wide = (a as uint16) + (b as uint16)
    if (wide > 255u16) {
        return None
    }
    return Some(wide as uint8)
}

# Add milliseconds to an uptime counter, sticking at the maximum instead of
# rolling over to 0 after about 49 days.
func saturatingAddU32(a: uint32, b: uint32): uint32 {
    let wide = (a as uint64) + (b as uint64)
    if (wide > 0xFFFF_FFFFu64) {
        return 0xFFFF_FFFFu32
    }
    return wide as uint32
}

func frameLength(header: uint8, payload: uint8): uint8 {
    match checkedAddU8(header, payload) {
        Some(total) => { return total }
        None => { return 0u8 }      # caller treats 0 as "malformed frame"
    }
}
```

*Listing 3-6: Checked and saturating addition written with a wider type*

For `uint64` there is no wider type to widen into. Compare against the headroom instead: `a + b` overflows exactly when `b > 0xFFFF_FFFF_FFFF_FFFFu64 - a`.

The Language Reference also specifies a family of static methods for this — `uint8.checkedAdd`, `uint8.wrappingMul`, `uint32.saturatingAdd` and so on (§3-3.19). A reader who has seen them in the reference will naturally reach for them:

```loup,does_not_compile
@mode(freestanding)

func addLength(total: uint8, extra: uint8): Option[uint8] {
    return uint8.checkedAdd(total, extra)
}
```

```text
error[E0001]: type 'uint8' has no method 'checkedAdd'
 --> src/main.lpfree:4:12
  |
4 |     return uint8.checkedAdd(total, extra)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
```

> ⚠ **Planned.** The `checked*`, `wrapping*` and `saturating*` method families on the fixed-width integer types (Language Reference §3-3.19) are not implemented in Loup 1.25. Write a helper like Listing 3-6 until they land. The `core.numeric` module cannot help here: it is a hosted module, and a freestanding file cannot import it.

Likewise, `uint8.max` and `int64.min` are not available as members today ("type 'uint8' does not support member access"); write the bound as a literal (`255u8`, `0xFFFF_FFFFu32`).

## Bits and addresses

Address arithmetic is masking and shifting. Loup spells bitwise NOT `bnot`, not `~` (the tilde is the lifetime-label sigil, chapter 11). A C habit produces this:

```loup,does_not_compile
@mode(freestanding)

const PAGE_SIZE: uint = 4096

func alignDown(addr: uint): uint {
    return addr & ~(PAGE_SIZE - 1)
}
```

```text
error[E0001]: `~` is not an operator — bitwise NOT is spelled `bnot` (§1-3.7)
 --> src/main.lpfree:6:19
  |
6 |     return addr & ~(PAGE_SIZE - 1)
  |                   ^
  |
```

The corrected helpers:

```loup
#~ @mode(freestanding)
#~
const PAGE_SIZE: uint = 4096

func alignDown(addr: uint): uint {
    return addr & bnot (PAGE_SIZE - 1)
}

func alignUp(addr: uint): uint {
    return (addr + (PAGE_SIZE - 1)) & bnot (PAGE_SIZE - 1)
}

func isAligned(addr: uint): bool {
    return (addr & (PAGE_SIZE - 1)) == 0
}
```

*Listing 3-7: Page alignment with `bnot`*

`alignUp` wraps if `addr` is within one page of the top of the address space. Kernel addresses rarely are, but an address that came from user space can be anything; check it against your range limits before aligning it (chapter 23 shows the pattern).

Flag words are the other everyday use. Here are x86_64 page-table entry bits as constants and three helpers:

```loup
#~ @mode(freestanding)
#~
const PTE_PRESENT: uint64 = 1u64 << 0
const PTE_WRITABLE: uint64 = 1u64 << 1
const PTE_USER: uint64 = 1u64 << 2
const PTE_NO_EXECUTE: uint64 = 1u64 << 63
const PTE_ADDR_MASK: uint64 = 0x000F_FFFF_FFFF_F000u64

func makePte(frame: uint64, writable: bool): uint64 {
    var pte = (frame & PTE_ADDR_MASK) | PTE_PRESENT | PTE_NO_EXECUTE
    if (writable) {
        pte = pte | PTE_WRITABLE
    }
    return pte
}

func isPresent(pte: uint64): bool {
    return (pte & PTE_PRESENT) != 0u64
}

func frameOf(pte: uint64): uint64 {
    return pte & PTE_ADDR_MASK
}
```

*Listing 3-8: Page-table entry flags as `uint64` constants*

One precedence difference from C works in your favour: `&`, `^` and `|` bind more tightly than `==` and `!=` in Loup, so `pte & PTE_PRESENT != 0u64` groups as `(pte & PTE_PRESENT) != 0u64`. The parentheses in the listing are there for C readers.

## No `bigint`, no `decimal`

`bigint` needs a multi-precision arithmetic library and `decimal` a decimal128 library; a freestanding image links neither. The Language Reference makes both `E_FREESTANDING_RUNTIME` errors (§3-3.3). The 1.25 checker does not report a `bigint` declaration yet, so treat this as a rule you keep rather than one the compiler enforces: use fixed-width integers with explicit carry handling, or do the arbitrary-precision work in a hosted program.

## Floating point

Floating-point types exist in freestanding, but kernels usually avoid them:

1. On x86_64 the freestanding lanes compile with SSE and x87 disabled, so floating-point code does not belong in an x86_64 kernel at all (Language Reference §3-3.13).
2. On the Cortex-M targets, `float` arithmetic is software floating point by default; it works, but it is slow and pulls in a runtime routine for each operation (§3-3.2).
3. FPU state is large and must be saved on every context switch if any task uses it.

Most kernel math — frequencies, timeouts, scaling factors — is better written in fixed point on integers.

> ⚠ **Planned.** `[freestanding] no_float = true` is specified to reject every floating-point type with `E_FREESTANDING_FLOAT` (Language Reference §3-3.21). The 1.25 toolchain accepts the key without enforcing it. You can already read it from source with `if const (freestanding.no_float)` to select an integer code path.

## Summary

- The fixed-width types are `int8`…`int64` and `uint8`…`uint64`; `int` and `uint` are pointer-sized, and `uint` holds addresses.
- Literal suffixes (`u8` … `u64`, `i8` … `i64`) fix a literal's width; a suffixed literal that does not fit is `E_LITERAL_OVERFLOW`.
- Widths never promote: mixing them is `E_NUMERIC_TYPE_MISMATCH`, and narrowing needs `as`, which keeps the low bits on native targets. Parenthesize casts.
- Fixed-width arithmetic wraps in freestanding, signed included. Mark intended wrapping with `@overflowWrapping`.
- Check for overflow by widening and comparing. The `checkedAdd`/`wrappingAdd`/`saturatingAdd` methods and `overflow = "trap"` are planned, not implemented.
- Bitwise NOT is `bnot`; `&`, `^`, `|` bind more tightly than `==`.
- Keep `bigint`, `decimal` and floating point out of kernel code.

Next: functions and control flow.
