# Chapter 2: Variables and Data Types

**What you'll learn:** how Loup binds names to values with `let`, `var` and `const`; the primitive types you use every day; how to convert between numeric types; how `Option[T]` replaces `null`; and when to use `Any` and union types.

Loup is statically typed: every value has a type the compiler knows before the script runs. Most of the time you do not write the types — the compiler infers them from the values — but they are checked all the same.

## Bindings: `let`, `var` and `const`

A binding gives a name to a value. `let` makes a binding that cannot be reassigned, `var` makes one that can, and `const` makes one whose value the compiler works out before the script runs:

```loup
@mode(host.scripted)

const MAX_RETRIES = 3;           # known at compile time
let name = "Alice";              # inferred as string
let age: int = 30;               # with an explicit type
var attempts = 0;                # mutable

attempts = attempts + 1;
attempts = attempts + 1;

println(i"{$ name $} is {$ age $}");
println(i"attempt {$ attempts $} of {$ MAX_RETRIES $}");
```

*Listing 2-1: The three kinds of binding*

```text
Alice is 30
attempt 2 of 3
```

Start with `let`. When you read `let total = …`, you know `total` never changes after that line, which makes code easier to follow. Reach for `var` for counters, running totals and values built up in a loop.

### Mistake: reassigning a `let`

```loup,does_not_compile
@mode(host.scripted)

let total = 0;
for (price in [5, 10, 20]) {
    total = total + price;
}
println(total);
```

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

A running total changes, so it must be a `var`:

```loup
@mode(host.scripted)

var total = 0;
for (price in [5, 10, 20]) {
    total = total + price;
}
println(total);
```

```text
35
```

### Mistake: a run-time value in a `const`

`const` is not another spelling of `let`. Its initializer must be something the compiler can evaluate: a literal, another `const`, an operator applied to those, or a call to a `const func` (Chapter 3).

```loup,does_not_compile
@mode(host.scripted)

const ARG_COUNT = args().length();
println(ARG_COUNT);
```

```text
main.lpscr:3:19: error[E_CONST_NOT_EVALUABLE]: the initialiser of `const ARG_COUNT` is not a const-expression — use `let` for a runtime value (E_CONST_NOT_EVALUABLE)
```

The number of command-line arguments is only known when the script runs. Use `let`:

```loup
@mode(host.scripted)

let ARG_COUNT = args().length();
println(ARG_COUNT);
```

```text
0
```

### Type annotations

Annotations are optional when there is an initializer to infer from. You need one when there is no initializer, or when the inferred type is not the one you want:

```loup
#~ @mode(host.scripted)
let x = 42;                 # int
let ratio: float = 1.0;     # float
var label: string;          # no initializer: the type is required
label = "ready";
println(label);
```

A `var` declared without an initializer must be assigned before it is read. The Language Reference makes reading it earlier a compile-time error (`E_USE_BEFORE_ASSIGN`, §1-6.2); the 1.25 interpreter does not check this yet and reads an empty value instead, so always assign before you read.

### `let` fixes the name, not the value

A `let` binding cannot be pointed at a different value, but the value itself may still be changeable:

```loup
#~ @mode(host.scripted)
let queue = [1, 2, 3];
queue.push(4);              # fine: the list is changed, the binding is not
println(queue);
```

```text
[1, 2, 3, 4]
```

Writing `queue = [4, 5, 6];` would be the `cannot assign to immutable binding` error from above. Chapters 5 and 8 come back to this for collections and classes.

## Integers

The everyday integer type is `int`, a signed 64-bit integer. Literals can use underscores, and hexadecimal, binary and octal prefixes:

```loup
#~ @mode(host.scripted)
let population = 8_100_000_000;
let mask = 0xFF;            # 255
let flags = 0b1010;         # 10
let perms = 0o755;          # 493
println(population);
println(mask + flags + perms);
```

```text
8100000000
758
```

For binary formats and compact storage there are fixed-width types, chosen with a literal suffix:

| Signed | Unsigned | Suffixes |
|---|---|---|
| `int8`, `int16`, `int32`, `int64` (= `int`) | `uint8`, `uint16`, `uint32`, `uint64` | `127i8`, `0xFFFFu16`, `42i32`, `255u8`, … |

A literal that does not fit its suffix is a compile error:

```loup,does_not_compile
@mode(host.scripted)

let level = 300i8;
```

```text
main.lpscr:3:13: error[E_LITERAL_OVERFLOW]: E_LITERAL_OVERFLOW: integer literal `300i8` does not fit in `int8` (-128..=127) (§1-3.6)
```

Arithmetic on a fixed-width type that leaves its range throws `NumericOverflowError` in a script:

```loup,throws
@mode(host.scripted)

let brightness: uint8 = 250u8;
let brighter = brightness + 10u8;
println(brighter);
```

```text
Uncaught NumericOverflowError: arithmetic overflow: 260 does not fit uint8
  at <main> (main.lpscr:4:16)
```

*Listing 2-2: Fixed-width overflow is checked in scripts*

### Known gap: `int` overflow wraps

The Language Reference (§1-4.1.1) says `int` arithmetic is checked as well. In Loup 1.25 it is not: `int` overflow silently wraps around.

```loup,not_desired_behavior
@mode(host.scripted)

let largest = 9_223_372_036_854_775_807;
println(largest + 1);
```

```text
-9223372036854775808
```

Until this is fixed, do not rely on an exception to catch an `int` that grows too large; check the range yourself when it matters.

> ⚠ **Planned.** `bigint` (arbitrary-precision integers, `123n`) and `decimal` (exact base-10 numbers, `19.99d`, for money) are specified in §1-4.1.3 and §1-4.1.4. Their literals parse, but arithmetic and printing on them do not work in the 1.25 interpreter. Until they do, keep money in an `int` count of cents.

## Floating-Point Numbers

`float` is a 64-bit IEEE 754 number; `float32` is the 32-bit variant. A literal with a decimal point or an exponent is a `float`:

```loup
#~ @mode(host.scripted)
let pi = 3.14159;
let small = 1.5e-3;
let sum = 0.1 + 0.2;
println(pi);
println(small);
println(sum);
println(sum == 0.3);
```

```text
3.14159
0.0015
0.30000000000000004
false
```

`0.1 + 0.2 != 0.3` is how binary floating point works in every language, not a Loup quirk.

One printing quirk *is* Loup's: a `float` with no fractional part prints without one. `println(3.0)` prints `3`, and so does `i"{$ 3.0 $}"`. The value is still a `float`.

## Mixing Numeric Types

Loup never converts between the integer and floating-point families behind your back:

```loup,does_not_compile
@mode(host.scripted)

let items = 7;
let average = items / 2.0;
println(average);
```

```text
main.lpscr:4:15: 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 tells you the fix. `as` converts between numeric types:

```loup
@mode(host.scripted)

let items = 7;
let average = (items as float) / 2.0;
println(average);
println(items / 2);
println(items // 2);
println(-7 / 2);
println(-7 // 2);
```

*Listing 2-3: Converting with as, and the two integer divisions*

```text
3.5
3
3
-3
-4
```

For integers, `/` truncates toward zero and `//` rounds toward negative infinity; they differ only for negative results. Integer division by zero throws `IntegerDivisionError`.

`as` between numeric types truncates a `float` toward zero (`3.9 as int` is `3`) and throws `NumericOverflowError` when the target cannot hold the value (`300 as uint8`). Widening between integer types is automatic: an `int8` value can be assigned to an `int16` or `int` binding without `as`.

> **Note.** The Language Reference also lists conversion functions — `int8(x)`, `int32(x)`, `float32(x)`, `float(x)` and so on (§1-4.1.2). They are not available in the 1.25 interpreter; use `as`.

## Booleans

`bool` has two values, `true` and `false`. The logical operators are words: `and`, `or`, `not`. `and` and `or` stop as soon as the answer is known.

```loup
#~ @mode(host.scripted)
let loggedIn = true;
let isAdmin = false;
println(loggedIn and isAdmin);
println(loggedIn or isAdmin);
println(not isAdmin);
```

```text
false
true
true
```

### Mistake: using a number as a condition

Loup has no "truthy" values. A condition must be a `bool`:

```loup,does_not_compile
@mode(host.scripted)

let unread = 3;
if (unread) {
    println("you have mail");
}
```

```text
main.lpscr:4:1: error[E0001]: if condition must be 'bool', found 'int'
```

Say what you mean:

```loup
@mode(host.scripted)

let unread = 3;
if (unread > 0) {
    println("you have mail");
}
```

## Characters

`char` is one Unicode scalar value, written with a `c` prefix:

```loup
#~ @mode(host.scripted)
let letter: char = c"A";
let smile: char = c"\u{1F600}";
println(letter);
println(smile);
println(letter.toInt());
println(c"7".isDigit());
println(c"q".toUpper());
println(char.of(66));
```

```text
A
😀
65
true
Q
B
```

`char.of(n)` returns an `Option[char]`, because not every number is a valid code point. A `char` literal must contain exactly one character: `c"ab"` is `E_CHAR_LITERAL_INVALID`. And a `char` is not a `string`: `let letter: char = "A";` is rejected with `cannot assign 'string' to binding 'letter' of type 'char'`.

## Strings

Strings are immutable UTF-8 text in double (or single) quotes. Indexing and slicing work on characters, and indexing gives back a one-character `string`:

```loup
#~ @mode(host.scripted)
let word = "héllo";
println(word.length());
println(word[1]);
println(word[1:4]);
println(word.toUpperCase());
println(word + ", world");
println(i"{$ word $} has {$ word.length() $} characters");
println("tab:\tquote:\" newline:\nend");
```

```text
5
é
éll
HÉLLO
héllo, world
héllo has 5 characters
tab:	quote:" newline:
end
```

Other forms: `"""…"""` for text spanning several lines, and `r"…"` for a raw string in which backslashes are literal (`r"C:\temp\new"`). Chapter 12 covers strings, interpolation and template fences in depth.

## Bytes

`bytes` holds raw binary data — file contents, hashes, network packets. Indexing gives each byte as an `int`:

```loup
#~ @mode(host.scripted)
let header = b"PNG\x0d\x0a";
println(header.length());
println(header[0]);
println(header.toHex());
println(b"hello".toString());
```

```text
5
80
504e470d0a
hello
```

## `Option[T]`: No Null

Loup has no `null`. A value that may be absent has type `Option[T]`, written `T?` for short. It is either `Some(value)` or `None`:

```loup
@mode(host.scripted)

func findUser(id: int): string? {
    if (id == 1) {
        return Some("alice");
    }
    return None;
}

let found = findUser(1);
let missing = findUser(2);

println(found?.toUpperCase() ?? "anonymous");
println(missing?.toUpperCase() ?? "anonymous");

match found {
    Some(name) => println(i"found {$ name $}"),
    None => println("no such user"),
}
```

*Listing 2-4: Producing and consuming an Option*

```text
ALICE
anonymous
found alice
```

- `?.` calls the method only when the value is `Some`, and gives `None` otherwise.
- `??` supplies a default for `None`.
- `match` (Chapter 7) handles both cases explicitly.

### Mistake: using an `Option` as if it were the value

```loup,does_not_compile
@mode(host.scripted)

func findUser(id: int): string? {
    if (id == 1) {
        return Some("alice");
    }
    return None;
}

println(findUser(1).toUpperCase());
```

```text
main.lpscr:10:9: error[E0001]: type 'Option[string]' has no method 'toUpperCase'
```

The compiler does not let you forget the `None` case. Decide what should happen when the user is missing — `findUser(1)?.toUpperCase() ?? "anonymous"`, or a `match` — and the error goes away. Chapter 10 shows how `Option` and `Result` work together for error handling.

## `Any`, `as?` and Union Types

`Any` holds a value of any type. You get back to a concrete type with `as?`, which returns an `Option`:

```loup
@mode(host.scripted)

let settings: list[Any] = [30, "verbose", true];

for (item in settings) {
    let n = item as? int;
    match n {
        Some(seconds) => println(i"timeout: {$ seconds $}s"),
        None => println(i"not a number: {$ item $}"),
    }
}
```

```text
timeout: 30s
not a number: verbose
not a number: true
```

`Any` is useful for data whose shape you only know at run time, such as parsed JSON (Chapter 18). Prefer real types everywhere else: with `Any` the compiler can no longer check what you do with the value.

> **Known gap.** `value as int` (without the `?`) is specified to throw `TypeError` when the value is not an `int` (§1-4.3). In Loup 1.25 it does not check: the value passes through unchanged and fails later, somewhere less obvious. Use `as?` and handle `None`.

A **union type** says a value is one of a few specific types, and `is` tests which one it is. Inside each branch of the `if`, the compiler knows the narrower type:

```loup
@mode(host.scripted)

func describe(value: string | int): string {
    if (value is int) {
        return i"number {$ value + 1 $}";
    } else {
        return i"text {$ value.toUpperCase() $}";
    }
}

println(describe(41));
println(describe("ok"));
```

```text
number 42
text OK
```

Without the `is` test, a method that only one member type has is rejected:

```loup,does_not_compile
@mode(host.scripted)

func shout(value: string | int): string {
    return value.toUpperCase();
}

println(shout("ok"));
```

```text
main.lpscr:4:12: error[E0001]: method 'toUpperCase' does not exist on all types in union 'string | int'; missing on: int. Use 'is' narrowing to call it
```

The message names the member that lacks the method. Narrow with `is`, as `describe` does, or use `match` (Chapter 7). (The Language Reference names this error `E_UNION_METHOD_PARTIAL`; the 1.25 compiler reports it under the generic code `E0001`.)

## Type Aliases

`type` gives an existing type a second name. It documents intent; the alias and the original are interchangeable:

```loup
@mode(host.scripted)

from core.math import (round);

type Celsius = float;
type Fahrenheit = float;
type Kelvin = float;

func toFahrenheit(c: Celsius): Fahrenheit {
    return c * 9.0 / 5.0 + 32.0;
}

func toKelvin(c: Celsius): Kelvin {
    return c + 273.15;
}

## Round to one decimal place.
func round1(x: float): float {
    return round(x * 10.0) / 10.0;
}

let readings: list[Celsius] = [-40.0, 0.0, 21.5, 37.0, 100.0];
for (c in readings) {
    println(i"{$ c $} C = {$ round1(toFahrenheit(c)) $} F = {$ round1(toKelvin(c)) $} K");
}
```

*Listing 2-5: A temperature table using type aliases*

```text
-40 C = -40 F = 233.2 K
0 C = 32 F = 273.2 K
21.5 C = 70.7 F = 294.7 K
37 C = 98.6 F = 310.2 K
100 C = 212 F = 373.2 K
```

Notice the float printing quirk again: `0.0` prints as `0` and `212.0` as `212`.

## Summary

- `let` is immutable, `var` is mutable, and `const` is a compile-time constant (`E_CONST_NOT_EVALUABLE` otherwise). Prefer `let`.
- `int` is 64-bit; `int8`…`uint64` are fixed-width, chosen with a suffix, and overflow on them throws `NumericOverflowError`. `int` overflow currently wraps.
- `float` is IEEE 754 double precision; a whole-number `float` prints without `.0`.
- `int` and `float` never mix implicitly: convert with `as`. `/` truncates and `//` floors.
- Conditions must be `bool`; there are no truthy values.
- `char`, `string` and `bytes` hold characters, text and binary data.
- `Option[T]` (`T?`) replaces `null`; use `?.`, `??` or `match` to get at the value.
- `Any` with `as?`, union types with `is`, and `type` aliases cover the less common cases. `bigint` and `decimal` are planned.

## Try It Yourself

1. Write `findPort(name: string): int?` that returns `Some(80)` for `"http"`, `Some(443)` for `"https"` and `None` otherwise, and print `findPort("ftp") ?? 21`.
2. Compute the average of `[3, 4, 4]` as a `float`. Which conversions do you need, and what does `println` show for an average of exactly `4`?
3. Add `10u8` to `250u8` in a `try` block (Chapter 10) and print the exception message.
