# Chapter 3: Variables and Data Types

**What you'll learn.** How `let`, `var` and `const` differ, the primitive types, how integers and floats behave (including what happens on overflow today), characters and strings, `bytes`, `Option` instead of null, union types, and type aliases.

**Why it matters.** Most production bugs are not exotic: a number of the wrong kind arrives in the wrong place, or a missing value is treated as present. Loup refuses to convert between numeric kinds silently and puts "might be missing" into the type, so many of those bugs become compile errors.

Everything in this chapter is the same in `web.compiled` and `web.scripted`. The listings use `web.compiled`, so each program's statements live in `main()`, which `loup run` calls for you (Chapter 2 explains why).

## `let` and `var`

`let` declares a binding you cannot reassign; `var` declares one you can:

```loup
@mode(web.compiled)

func main(): void {
    let price = 19.99;      # immutable
    var quantity = 1;       # mutable

    quantity = quantity + 2;
    println(price);
    println(quantity);
}
```

*Listing 3-1: `let` for a value that stays put, `var` for one that changes*

```text
19.99
3
```

Reach for `let` first and switch to `var` only when the value really has to change. The point is not speed; it is that a reader can see at a glance which names move.

If you reassign a `let`, the compiler stops you:

```loup,does_not_compile
@mode(web.compiled)

func main(): void {
    let total = 10;
    total = 11;
    println(total);
}
```

```text
main.lpweb:5:5: error[E0001]: cannot assign to immutable binding 'total'
```

The message names the binding. The fix is to declare it with `var` if it is meant to change, or to introduce a new name (`let adjusted = total + 1;`) if it is not.

Redeclaring a name in the same scope is also an error, so you cannot "shadow" your way around a `let` the way you can in Rust:

```loup,does_not_compile
#~ @mode(web.compiled)
#~
func main(): void {
    let total = 10;
    let total = 11;
    println(total);
}
```

```text
main.lpweb:5:9: error[E0001]: duplicate declaration of `total` in the same scope (E_REDECLARED)
```

Pick a second name, such as `adjusted`.

## Type inference and annotations

Inside a function, Loup infers a binding's type from its initializer. You can always write the type yourself after a colon:

```loup
#~ @mode(web.compiled)
#~
func main(): void {
    let count = 10;                  # int
    let ratio = 0.75;                # float
    let label = "draft";             # string
    let ready: bool = false;         # annotated
    let tags: list[string] = [];     # annotated: an empty list has no element to infer from
    println(count);
    println(ratio);
    println(label);
    println(ready);
    println(tags);
}
```

*Listing 3-2: Inferred and annotated bindings*

```text
10
0.75
draft
false
[]
```

A binding needs either a type or an initializer. With neither, there is nothing to infer from:

```loup,does_not_compile
#~ @mode(web.compiled)
#~
func main(): void {
    var retries;
    retries = 3;
    println(retries);
}
```

```text
main.lpweb:4:5: error[E0001]: binding 'retries' has no type annotation and no initializer; cannot infer type
```

Write `var retries: int = 0;` or initialize it where you declare it. An annotation is also checked against the initializer: `var count: int = "3";` is rejected with `cannot assign 'string' to binding 'count' of type 'int'`.

Function signatures are different: parameter and return types are part of the function's contract and are written out (Chapter 4).

## Constants: `const`

`const` is for values the compiler can compute while it compiles: literals, arithmetic on other constants, and calls to `const func` functions (Language Reference §1-7.1). Constants usually live at module level:

```loup
@mode(web.compiled)

const TAX_RATE: float = 0.0725;
const KB: int = 1024;
const MB: int = KB * 1024;

const func square(n: int): int {
    return n * n;
}

const TILE_AREA: int = square(12);

func main(): void {
    println(TAX_RATE);
    println(MB);
    println(TILE_AREA);
}
```

*Listing 3-3: Compile-time constants*

```text
0.0725
1048576
144
```

A `const` whose initializer needs a running program is rejected:

```loup,does_not_compile
@mode(web.compiled)

func siteName(): string {
    return "Example Store";
}

const SITE: string = siteName();

func main(): void {
    println(SITE);
}
```

```text
main.lpweb:7:22: error[E_CONST_NOT_EVALUABLE]: the initialiser of `const SITE` is not a const-expression — use `let` for a runtime value (E_CONST_NOT_EVALUABLE)
```

`siteName` is an ordinary function, so the compiler cannot run it. Either mark it `const func` (it only returns a literal, so that works), or compute the value at run time with `let` inside `main`.

## Primitive types

These are the primitive types (Language Reference §1-4.1):

| Type | What it holds |
|---|---|
| `int` | 64-bit signed integer (alias for `int64`) |
| `int8`, `int16`, `int32`, `int64` | Signed integers of a fixed width |
| `uint8`, `uint16`, `uint32`, `uint64` | Unsigned integers of a fixed width |
| `float` | 64-bit IEEE 754 float (alias for `float64`) |
| `float32` | 32-bit IEEE 754 float |
| `bool` | `true` or `false` |
| `char` | One Unicode scalar value, written `c"A"` |
| `string` | Immutable UTF-8 text |
| `bytes` | An arbitrary byte sequence, written `b"..."` |

The literal forms:

```loup
#~ @mode(web.compiled)
#~
func main(): void {
    println(1_000_000);      # underscores for readability
    println(0xff);           # hex
    println(0b1010);         # binary
    println(0o755);          # octal
    println(3.14);
    println(2.5e3);          # exponent
    println(true);
    println(c"λ");           # a char
    println("\u{1F600}");    # escape by code point
}
```

*Listing 3-4: Literal syntax*

```text
1000000
255
10
493
3.14
2500
true
λ
😀
```

Notice that `println(2.5e3)` printed `2500`: printing renders a whole-number float without its `.0`. `repr(2500.0)` shows `2500.0` when you need to see the type.

### Arithmetic

`+`, `-`, `*`, `/`, `//`, `%` and `**` work on both integers and floats. Two details trip up newcomers:

- `/` on two `int` values gives an `int`, truncated toward zero (`7 / 2` is `3`). `//` is floor division (`-7 // 2` is `-4`).
- Loup never mixes integers and floats on its own. You convert one side with `as`.

Here is the second one in a shopping-cart total:

```loup,does_not_compile
@mode(web.compiled)

func main(): void {
    let quantity = 3;
    let unitPrice = 2.5;
    let total = quantity * unitPrice;
    println(total);
}
```

```text
main.lpweb:6:17: error[E_OPERATOR_MISSING]: E_NUMERIC_TYPE_MISMATCH: mixed 'int' and 'float' operands for operator '*' — convert one operand explicitly (`as float` or `as int`)
```

The message says what to do. In TypeScript or Python this would quietly produce a float; Loup makes you choose:

```loup
@mode(web.compiled)

func main(): void {
    let quantity = 3;
    let unitPrice = 2.5;
    let total = (quantity as float) * unitPrice;
    println(total);
    println(7 / 2);
    println(7 // 2);
    println(-7 / 2);
    println(-7 // 2);
    println(2 ** 10);
}
```

*Listing 3-5: Converting explicitly, and the two divisions*

```text
7.5
3
3
-3
-4
1024
```

### Integer overflow

The Language Reference says integer arithmetic in the web modes is checked: a result that does not fit its type throws `NumericOverflowError` (§1-4.1.1). Loup 1.25.1 does this for the sized types such as `int32` and `uint8`, but **not for `int`**: `int` arithmetic silently wraps around. This program computes factorials in `int`:

```loup,not_desired_behavior
@mode(web.compiled)

func factorial(n: int): int {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

func main(): void {
    println(factorial(20));
    println(factorial(21));
}
```

```text
2432902008176640000
-4249290049419214848
```

`21!` is larger than the biggest `int` (about 9.2 × 10¹⁸), and instead of an error you get a negative number. Until the toolchain implements the specified check, guard the range yourself when a value can grow without bound (here: refuse `n > 20`).

The sized types already behave as specified. The same mistake in `int32` stops the program:

```loup,throws
@mode(web.compiled)

func main(): void {
    let pixels: int32 = 2_000_000_000;
    let doubled = pixels + pixels;
    println(doubled);
}
```

```text
Uncaught NumericOverflowError: arithmetic overflow: 4000000000 does not fit int32
  at main (main.lpweb:5:19)
```

> ⚠ **Planned.** The explicit overflow functions in Language Reference §1-4.1.1 (`int32.checkedAdd(a, b)` returning an `Option`, `int32.wrappingAdd`, `int32.saturatingAdd` and the rest of the family) are not implemented in Loup 1.25.1; the compiler reports `type 'int32' has no method 'checkedAdd'`.

### Converting between numeric types

`x as T` converts between numeric types (Language Reference §1-4.1.2). A float converted to an integer is truncated toward zero; a value that does not fit the target type throws `NumericOverflowError` in the web modes:

```loup
#~ @mode(web.compiled)
#~
func main(): void {
    let measured = 3.99;
    println(measured as int);
    let small: int8 = 100 as int8;
    println(small);
    let wide: int = small;             # widening within a family is implicit
    println(wide + 1);
    let ratio: float32 = 0.5 as float32;
    println(ratio);
}
```

*Listing 3-6: Numeric conversions with `as`*

```text
3
100
101
0.5
```

To turn text into a number, strings have `to_int()` and `to_float()`. Be careful: in Loup 1.25.1 `to_int()` returns `0` for text that is not a number, so check the input yourself when `0` is a legal value:

```loup,not_desired_behavior
#~ @mode(web.compiled)
#~
func main(): void {
    println("42".to_int());
    println("3.5".to_float());
    println("12a".to_int());     # not a number, but no error
}
```

```text
42
3.5
0
```

### `bigint` and `decimal`

> ⚠ **Planned.** The Language Reference specifies two arbitrary-precision types: `bigint` (literals such as `99_999_999_999_999_999_999n`) and `decimal` (literals such as `0.1d`, exact base-10 arithmetic for money) (§1-4.1.3, §1-4.1.4). Loup 1.25.1 parses their literals but cannot compute with them: `0.1d + 0.2d` is rejected with `type 'decimal' does not implement 'Add'`, `println` refuses a `bigint`, and the `bigint(…)`/`decimal(…)` constructors fail at run time with `internal interpreter limitation: no builtin named 'bigint'`. Until they land, keep money in integer cents.

Binary floats cannot represent most decimal fractions exactly, which is why money should not live in `float`:

```loup
#~ @mode(web.compiled)
#~
func main(): void {
    println(0.1 + 0.2);
    println(0.1 + 0.2 == 0.3);
    let cents = 10 + 20;           # keep money in integer cents
    println(cents == 30);
}
```

```text
0.30000000000000004
false
true
```

## Characters and strings

A `string` is immutable UTF-8 text. Its methods count Unicode scalar values, not bytes:

```loup
@mode(web.compiled)

func main(): void {
    let city = "Zürich";
    println(city.length());          # 6 characters
    println(city.byte_length());     # 7 bytes: ü takes two
    println(city[1]);                # indexing gives a one-character string
    println(city.to_uppercase());
    println(city.contains("rich"));
    println("a,b,c".split(","));
    for (letter in "héllo") {
        print(letter);
        print("|");
    }
    println("");
}
```

*Listing 3-7: Strings are sequences of characters*

```text
6
7
ü
ZÜRICH
true
["a", "b", "c"]
h|é|l|l|o|
```

Indexing and iteration give you one-character **strings**, not `char` values (Language Reference §1-4.1.5). A `char` is a separate type for a single scalar value, written `c"A"`, and is used where the code point itself matters:

```loup
#~ @mode(web.compiled)
#~
func main(): void {
    let initial = c"a";
    println(initial.toInt());        # the code point
    println(initial.toUpper());
    println(initial.isAlpha());
    println(initial.toString() + "!");
    println("Ada".scalars());        # the chars of a string
}
```

*Listing 3-8: The `char` type*

```text
97
A
true
a!
[A, d, a]
```

Because iteration yields strings, comparing the loop variable with a `char` literal is a type error. This vowel counter makes that mistake:

```loup,does_not_compile
@mode(web.compiled)

func countVowels(text: string): int {
    var count = 0;
    for (letter in text) {
        if (letter == c"a" or letter == c"e" or letter == c"o") {
            count = count + 1;
        }
    }
    return count;
}

func main(): void {
    println(countVowels("hello world"));
}
```

```text
main.lpweb:6:13: error[E_EQUALITY_TYPE_MISMATCH]: cannot compare 'string' and 'char' for equality: `==` and `!=` compare values of one type or of related types (E_EQUALITY_TYPE_MISMATCH)
main.lpweb:6:31: error[E_EQUALITY_TYPE_MISMATCH]: cannot compare 'string' and 'char' for equality: `==` and `!=` compare values of one type or of related types (E_EQUALITY_TYPE_MISMATCH)
main.lpweb:6:49: error[E_EQUALITY_TYPE_MISMATCH]: cannot compare 'string' and 'char' for equality: `==` and `!=` compare values of one type or of related types (E_EQUALITY_TYPE_MISMATCH)
```

`letter` is a `string`, so compare it with strings:

```loup
@mode(web.compiled)

func countVowels(text: string): int {
    var count = 0;
    for (letter in text) {
        if (letter == "a" or letter == "e" or letter == "o") {
            count = count + 1;
        }
    }
    return count;
}

func main(): void {
    println(countVowels("hello world"));
}
```

*Listing 3-9: Counting vowels by comparing one-character strings*

```text
3
```

`as` does not convert to or from `char`; the compiler points you to the conversion functions:

```loup,does_not_compile
#~ @mode(web.compiled)
#~
func main(): void {
    let letter = 65 as char;
    println(letter);
}
```

```text
main.lpweb:4:18: error[E0001]: E_CHAR_CAST: `as` does not convert to or from `char` — use `char.of(n)`, `c.toInt()`, `char.fromString(s)` or `c.toString()` (§1-4.1.5)
```

### Interpolation

A plain string literal is taken literally, braces and all. To insert values, prefix the string with `i` and wrap each expression in `{$ … $}` (Language Reference §1-15). Forgetting the prefix is not an error, which makes it easy to miss:

```loup,not_desired_behavior
#~ @mode(web.compiled)
#~
func main(): void {
    let name = "Ada";
    let items = 3;
    println("Hello, {name}! You have {items} items in your cart.");
}
```

```text
Hello, {name}! You have {items} items in your cart.
```

The interpolated form, with a filter applied to one value:

```loup
#~ @mode(web.compiled)
#~
func main(): void {
    let name = "Ada";
    let items = 3;
    println(i"Hello, {$ name $}! You have {$ items $} items in your cart.");
    println(i"Shipping to {$ name | upper $}");
}
```

*Listing 3-10: Interpolated strings with `i"…"`*

```text
Hello, Ada! You have 3 items in your cart.
Shipping to ADA
```

Template fences, the multi-line big sibling of interpolated strings, are the subject of Chapter 14.

### Multi-line strings

Triple quotes make a multi-line string. The whitespace in front of the opening `"""` line is the left margin: every content line must start with it, it is removed from each line, and the closing `"""` sits at that margin (Language Reference §1-14):

```loup
#~ @mode(web.compiled)
#~
func main(): void {
    let note = """
    Dear customer,
      thanks for your order.
    """;
    println(note);
}
```

```text
Dear customer,
  thanks for your order.
```

## `bytes`

`bytes` holds raw bytes that need not be valid UTF-8, such as a file upload or a hash. A `b"…"` literal makes one; indexing gives a `uint8`:

```loup
#~ @mode(web.compiled)
#~
func main(): void {
    let data = b"Hi!";
    println(data.length());
    println(data[0]);
    println(i"base64: {$ data | base64 $}");
    println(i"hex: {$ data | hex $}");
}
```

*Listing 3-11: `bytes` and the `base64`/`hex` filters*

```text
3
72
base64: SGkh
hex: 486921
```

The Language Reference makes inserting `bytes` into an interpolation without a filter a compile error (`E_TEMPLATE_BYTES`, §1-16.4), so binary data cannot leak into a page by accident. Loup 1.25.1 does not enforce that yet: `i"{$ data $}"` renders the placeholder `Bytes(3 bytes)`. Always choose a filter.

## No null: `Option[T]`

Loup has no `null`. A value that may be missing has the type `Option[T]`, which is either `Some(value)` or `None`, and a plain `int` can never be `None`:

```loup,does_not_compile
#~ @mode(web.compiled)
#~
func main(): void {
    let discount: int = None;
    println(discount);
}
```

```text
main.lpweb:4:5: error[E0001]: cannot assign 'None' to binding 'discount' of type 'int'
```

Say that the value is optional, and supply a fallback where you use it:

```loup
#~ @mode(web.compiled)
#~
func main(): void {
    let discount: Option[int] = None;
    let coupon: Option[string] = Some("SPRING");
    println(discount.unwrap_or(0));
    println(coupon.unwrap_or("none"));
    println(coupon.is_some());
}
```

*Listing 3-12: Optional values*

```text
0
SPRING
true
```

The method names are snake_case (`unwrap_or`, `is_some`, `is_none`). Chapter 8 covers `Option` and its partner `Result` in depth.

## Union types

A union type `A | B` holds a value of either type, without declaring an enum (Language Reference §1-4.5). To use the value you narrow it, either with a `match` on type patterns or with `is`:

```loup
@mode(web.compiled)

func describe(value: string | int | bool): string {
    return match value {
        string(s) => i"text: {$ s $}",
        int(n) => i"number: {$ n $}",
        bool(b) => if (b) { "yes" } else { "no" },
    };
}

func nextLabel(value: string | int): string {
    if (value is string) {
        return value.to_uppercase();     # narrowed to string
    } else {
        return str(value + 1);           # narrowed to int
    }
}

func main(): void {
    println(describe("hi"));
    println(describe(42));
    println(describe(true));
    println(nextLabel("draft"));
    println(nextLabel(41));
}
```

*Listing 3-13: Narrowing a union with `match` and `is`*

```text
text: hi
number: 42
yes
DRAFT
42
```

Narrow before you print: Loup 1.25.1 does not accept an un-narrowed union as a `println` argument (`expected 'Any', found 'int | string'`).

Use a union for an ad-hoc "one of these types"; use an enum (Chapter 8) when the alternatives have names and meaning of their own.

## Type aliases

`type Name = …` gives a type a shorter or more meaningful name. An alias is the same type as what it names, and aliases can take type parameters:

```loup
@mode(web.compiled)

type UserId = int;
type Directory = dict[UserId, string];
type Parsed[T] = Result[T, string];

func lookup(users: Directory, id: UserId): string {
    return users.get(id).unwrap_or("unknown");
}

func parsePort(raw: string): Parsed[int] {
    let port = raw.to_int();
    if (port <= 0 or port > 65535) {
        return Err(i"bad port: {$ raw $}");
    }
    return Ok(port);
}

func main(): void {
    let users: Directory = {1: "ann", 2: "bob"};
    println(lookup(users, 2));
    println(lookup(users, 9));
    let id: UserId = 7;
    let plain: int = id + 1;      # an alias is interchangeable with its type
    println(plain);
    println(repr(parsePort("8080")));
    println(repr(parsePort("http")));
}
```

*Listing 3-14: Type aliases, including a generic one*

```text
bob
unknown
8
Ok(8080)
Err("bad port: http")
```

Because `UserId` *is* `int`, nothing stops you from passing an order number where a user ID was expected. When you want the compiler to keep them apart, wrap the value in a one-field struct (Chapter 7).

> ⚠ **Planned.** The `typeof` operator (`typeof x` returning a `Type` value) is not implemented in Loup 1.25.1 (`expected expression`), and `x instanceof T` crashes the 1.25.1 parser. Use `x is T`, shown in Listing 3-13; the Language Reference defines `is` as a synonym of `instanceof` (§1-4.7).

## Names you cannot use

Keywords are reserved and cannot name a binding (Language Reference §1-3.4). Several are words you might well want for a variable:

```loup,does_not_compile
#~ @mode(web.compiled)
#~
func main(): void {
    let type = "admin";
    println(type);
}
```

```text
main.lpweb:4:9: error[E0001]: `type` is a reserved word (§1-3.4) and cannot name a binding
  note: rename the binding — e.g. `type_value`
main.lpweb:5:13: error[E0001]: expected expression
```

The first message is the real one; the second follows from it. Rename the binding (`let role = "admin";`). `class`, `match`, `from`, `this` and `template` are other common collisions.

## Summary

- `let` bindings cannot be reassigned, `var` bindings can, and `const` values are computed at compile time.
- Types are inferred from initializers; annotate when there is nothing to infer from.
- Integers and floats never mix implicitly: convert with `as`. `/` on integers truncates; `//` floors.
- In Loup 1.25.1, `int` overflow wraps silently while sized types like `int32` throw `NumericOverflowError`; `bigint`, `decimal` and the `checkedAdd` family are planned.
- Strings are UTF-8; indexing and iteration give one-character strings, and `char` (`c"A"`) is a separate type.
- Use `i"…{$ x $}…"` to interpolate; a plain string keeps its braces.
- `bytes` holds raw data; insert it into text through a filter such as `base64`.
- There is no null: optional values are `Option[T]`.
- Union types are narrowed with `match` or `is`; type aliases rename a type without creating a new one.
