# Chapter 1: Getting Started

**What you'll learn:** how to install Loup, run your first script, read the two kinds of error Loup reports, give a script the permissions it needs, and turn a script into a program with a `main()` function when it grows up.

Loup is a statically typed language with six execution modes. This book is about the one you reach for first: **`host.scripted`**, the mode for scripts. A script is a file whose top-level statements run in order, like a Python or shell script. It is checked by the type checker before a single line runs, so a typo in the last line of a long script is reported before the first line does anything.

## What Is a Script?

A Loup script is a source file that starts with `@mode(host.scripted)`, has the extension `.lpscr`, and is run with `loup run`:

```sh
loup run hello.lpscr
```

There is no build step and no project manifest. `loup run` reads the file and everything it imports, type-checks the whole program, and then executes it on your machine as an ordinary operating-system process: it can read and write files, run other programs and read environment variables, subject to the permissions described later in this chapter.

> **How `loup run` works today.** The Language Reference (§1-2.1) describes script mode as a JIT pipeline that compiles each statement to WebAssembly and caches the result. The 1.25 toolchain does not do that: `loup run` type-checks the program and then runs it on an interpreter built into the `loup` binary. Scripts do not run on Node, Deno, Bun or in a browser. What you observe as a script author is the same either way: no build step, and type errors are reported before anything runs.

Five other modes exist for code that is compiled ahead of time — `host.dual` and `host.native` for programs, `web.scripted` and `web.compiled` for the browser, and `freestanding` for bare metal. You will meet `host.dual` at the end of this chapter, because it is where a script goes when it becomes a program.

## Installing Loup

Official Loup builds are published on GitHub at [`code-craft-ai-company/loup-releases`](https://github.com/code-craft-ai-company/loup-releases/releases). The installer downloads the archive for your machine, checks it against the release's signed `SHA256SUMS`, unpacks it, and puts its `bin` directory on your `PATH`. It needs no administrator rights.

On Linux and macOS:

```sh
curl -fsSL https://github.com/code-craft-ai-company/loup-releases/releases/latest/download/loup-install.sh | sh
```

On Windows, in PowerShell:

```powershell
irm https://github.com/code-craft-ai-company/loup-releases/releases/latest/download/loup-install.ps1 | iex
```

The toolchain lands in `~/.loup` (Linux, macOS) or `%LOCALAPPDATA%\Programs\Loup` (Windows). To install a particular version, pass it to the script — `curl -fsSL … | sh -s -- --version 1.25.1` — or set `$env:LOUP_VERSION = "1.25.1"` before the Windows command.

Open a new terminal and check the installation:

```sh
loup --version
```

You should see a line that starts with `loup 1.25`.

The archive contains two compilers. `loup-bootstrap` is the full toolchain and is the same on every platform. `loup` is a second name for `loup-bootstrap` on macOS, Windows and Linux arm64; on Linux x86-64, `loup` is a newer self-hosted driver that runs scripts, builds native programs and runs tests, but does not yet provide `init`, `check` or `repl`. This book writes `loup run` for running scripts and `loup-bootstrap` for the project commands, so every command works on every platform.

> **Note for macOS.** The macOS binaries are not notarized yet. The installer is unaffected, but if you download an archive with a browser, clear the quarantine flag before running it: `xattr -dr com.apple.quarantine ~/.loup`.

## Hello, World!

Create a file called `hello.lpscr`:

```loup
@mode(host.scripted)

println("Hello, world!");
```

*Listing 1-1: hello.lpscr, the smallest complete script*

Run it:

```sh
loup run hello.lpscr
```

```text
Hello, world!
```

The first line, `@mode(host.scripted)`, declares the file's execution mode. It must be the first thing in the file; only comments may come before it. If you leave it out, `loup run` still treats a `.lpscr` file as a script, but the language rules say every file declares its mode in its own text (ruling MODE-2), and every listing in this book does.

`println` writes its argument and a newline. `print` writes without the newline, which is useful when you build up one line from several pieces.

> **Watch the mode name.** Older Loup material uses `@mode(scripted)`. That name was retired in Loup 1.17. Today the compiler does not reject an unknown or retired mode name — it silently treats the file as `host.scripted` — so a wrong name in a script goes unnoticed, while a wrong name in a program means its `main()` is never called. Always write one of the six current names.

## Statements Run in Order

A script's top-level statements run once each, from the top of the file to the bottom:

```loup
@mode(host.scripted)

let price = 40;
println(price);
let withTax = price * 2;
println(withTax);
```

```text
40
80
```

Semicolons are optional. A newline ends a statement, and an expression that ends with an operator or an open bracket continues on the next line:

```loup
#~ @mode(host.scripted)
let base = 10
let total = base +
    5
let names = [
    "Ada",
    "Grace",
]
println(total)
println(names)
```

```text
15
["Ada", "Grace"]
```

This book writes the semicolons, as most Loup code does.

Functions can be called before the line that declares them: declarations are visible throughout the file, while statements run in order.

## Anatomy of a Script

Here is a slightly larger script:

```loup
@mode(host.scripted)

# A comment runs from `#` to the end of the line.

## A doc comment starts with `##` and documents the next declaration.
func greet(name: string): string {
    return i"Hello, {$ name $}!";
}

let message = greet("Loup");
println(message);
```

*Listing 1-2: Comments, a function, an interpolated string and a binding*

```text
Hello, Loup!
```

- **Comments** start with `#`. A doc comment starts with `##` and attaches to the declaration that follows it; documentation tools read it.
- **`func greet(name: string): string { … }`** declares a function with one `string` parameter that returns a `string`. Parameter types and the return type are written after a colon. Chapter 3 covers functions.
- **`i"Hello, {$ name $}!"`** is an *interpolated string*. The `i` prefix turns it on, and `{$ … $}` inserts the value of an expression. A plain string such as `"Hello, {name}"` is printed exactly as written. Chapter 12 covers strings.
- **`let message = …;`** declares an immutable binding. Use `var` for a binding you will reassign. Chapter 2 covers variables.

### Mistake: a `//` comment

If you come from C, Java or JavaScript, your fingers will type `//` for a comment:

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

// Say hello
println("Hello!");
```

```text
main.lpscr:3:1: error[E0001]: expected expression
main.lpscr:3:4: error[E0003]: expected `;`
```

In Loup, `//` is not a comment at all: it is the floor-division operator (`7 // 2` is `3`), so the parser sees an operator with nothing on its left. Use `#`:

```loup
@mode(host.scripted)

# Say hello
println("Hello!");
```

## A Second Script: Two Small Functions

```loup
@mode(host.scripted)

## The larger of two integers.
func larger(a: int, b: int): int {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}

## The absolute value of an integer.
func absolute(n: int): int {
    return if (n >= 0) { n } else { -n };
}

println(i"The larger number is {$ larger(17, 42) $}");

let negative = -15;
println(i"The absolute value of {$ negative $} is {$ absolute(negative) $}");
```

*Listing 1-3: if as a statement and as an expression*

```text
The larger number is 42
The absolute value of -15 is 15
```

- `int` is a 64-bit signed integer. Chapter 2 covers the other numeric types.
- The condition of an `if` is always written in parentheses, and the branches are always blocks in braces.
- `if` is also an expression: `if (n >= 0) { n } else { -n }` evaluates to the value of the branch that runs.

### Mistake: the ternary operator

Most C-family languages write a one-line choice as `condition ? a : b`. Loup has no ternary operator:

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

func absolute(n: int): int {
    return n >= 0 ? n : -n;
}

println(absolute(-15));
```

```text
main.lpscr:4:21: error[E0003]: expected `;`
```

The parser read `return n >= 0` as a complete statement and then found a `?` where a statement should end. The message does not mention a ternary, so remember this one: when `expected ';'` points into the middle of a line, look for syntax borrowed from another language. The fix is the `if` expression from Listing 1-3: `return if (n >= 0) { n } else { -n };`.

## Reading Error Messages

Loup reports two kinds of error, and they look different.

**Compile errors** are found before the script runs. Nothing in the script executes, and `loup run` exits with status 2:

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

let attempts = 1;
attempts = attempts + 1;
println(attempts);
```

```text
main.lpscr:4:1: error[E0001]: cannot assign to immutable binding 'attempts'
```

Each diagnostic is one line: the file, line and column, the error code in brackets, and the message. Here `attempts` was declared with `let`, which cannot be reassigned. Declare it with `var`:

```loup
@mode(host.scripted)

var attempts = 1;
attempts = attempts + 1;
println(attempts);
```

```text
2
```

Some codes are generic (`E0001`, `E0003`) and some are specific (`E_TOPLEVEL_CONTROL_AOT`, which you will see later in this chapter). When the code is specific, it is repeated at the start of the message.

**Run-time errors** happen while the script runs. An exception that nothing catches stops the script, prints the exception and the call stack, and exits with status 1:

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

func firstWord(sentence: string): string {
    let words = sentence.split(" ");
    return words[0];
}

println(firstWord("hello there"));

let noWords: list[string] = [];
println(noWords[0]);
```

```text
hello
Uncaught IndexError: list index 0 out of range (len 0)
  at <main> (main.lpscr:11:9)
```

The output before the error is still printed; the trace says which exception escaped (`IndexError`), why, and where. Chapter 10 shows how to catch exceptions.

When a project grows, `loup-bootstrap check` (see "Projects" below) reports the same compile errors in a longer form with the source line and a caret under the problem:

```text
error[E0001]: cannot assign to immutable binding 'count'
 --> src/main.loup:5:5
  |
5 |     count = count + 1;
  |     ^^^^^^^^^^^^^^^^^
  |
```

## Making a File Both Runnable and Importable

A file can be a library for other scripts and a script of its own. The variable `__name__` is `"__main__"` in the file you pass to `loup run`, so a block guarded by it runs only then:

```loup
@mode(host.scripted)

## n! computed recursively.
pub func factorial(n: int): int {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

if (__name__ == "__main__") {
    for (n in [0, 1, 5, 10]) {
        println(i"{$ n $}! = {$ factorial(n) $}");
    }
}
```

*Listing 1-4: factorial.lpscr, a module with a main guard*

```text
0! = 1
1! = 1
5! = 120
10! = 3628800
```

`pub` makes `factorial` visible to other files. A second script in the same directory can import it:

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

from factorial import (factorial);

println(i"20! = {$ factorial(20) $}");
```

*Listing 1-5: report.lpscr imports factorial.lpscr (a two-file example)*

```text
20! = 2432902008176640000
```

The import names the file without its extension, and the imported names go in parentheses. The guarded loop in `factorial.lpscr` does not run when it is imported. Chapter 11 covers modules.

> **Imports today.** The Language Reference says an imported module's top-level statements run once, when it is first loaded, with `__name__` set to the module's dotted name. In Loup 1.25, `loup run` compiles the entry script and its imports as one program: an imported file's declarations and `let` initializers take effect, but its other top-level statements (such as a `println` or the guarded block) do not run, and `__name__` reads `"__main__"` everywhere. Keep a library's top level to declarations and you will not notice the difference.

Three read-only variables describe the running file:

```loup
@mode(host.scripted)

println(i"name: {$ __name__ $}");
println(i"file: {$ __file__ $}");
println(i"mode: {$ __execmode__ $}");
```

```text
name: __main__
file: main.lpscr
mode: interpreted
```

`__file__` is the path as you passed it to `loup run`. The Language Reference (§1-2.2) says `__file__` is absolute and `__execmode__` is the mode name (`"host.scripted"`); the 1.25 interpreter prints the path as given and `interpreted`, so do not compare `__execmode__` against a mode name yet.

## Permissions

A script can only touch the outside world through capabilities granted by `loup run`. The capabilities are `fs` (files), `proc` (running programs), `net` (sockets), `entropy` (secure random numbers) and `debug`.

`loup run` grants `fs`, `proc` and `entropy` by default, but **not** `net`. A script that opens a socket without `--allow-net` fails when it tries:

```text
Uncaught IoError: capability 'net' is not available: not in permission set
```

The flags adjust the set:

| Flag | Effect |
|---|---|
| `--allow-net`, `--allow-fs`, `--allow-proc`, `--allow-entropy`, `--allow-debug` | add one capability |
| `--allow-all` | grant every capability |
| `--deny-all` | start from nothing; combine with `--allow-…` to grant exactly what you list |

Flags go before the script name; arguments for the script go after it:

```sh
loup run --deny-all --allow-fs wordcount.lpscr notes.txt
```

A script that writes a file under `--deny-all` stops at the first write:

```text
Uncaught IoError: capability 'fs' is not available: not in permission set
  at <main> (main.lpscr:3:1)
```

> **Platform difference.** These defaults and flags belong to `loup-bootstrap`, which is `loup` on macOS, Windows and Linux arm64. The self-hosted `loup` on Linux x86-64 grants every capability and ignores the `--allow-*` and `--deny-all` flags; use `loup-bootstrap run` there when you want the sandbox.

## The REPL

`loup-bootstrap repl` starts an interactive session, which is handy for trying out a method or an expression:

```text
Loup REPL — type :help for commands, :quit to exit

>>> let x = 42
>>> println(x * 2)
84
>>> func greet(name: string): string { return i"Hello, {$ name $}!"; }
>>> println(greet("world"))
Hello, world!
>>> :quit
```

REPL commands start with a colon: `:help` and `:quit`. To continue a statement on the next line, end the line with `{` or `\`.

## From Script to Program: `host.dual`

When a script grows into a tool you want to ship, move it to **`host.dual`**. A `host.dual` file uses the extension `.loup`, keeps all its statements inside a `main()` function, and can be run by `loup run` *or* compiled ahead of time to a WebAssembly module or a native binary, with the same behaviour on both paths:

```loup
@mode(host.dual)

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

func main(): int {
    for (n in [0, 1, 5, 10]) {
        println(i"{$ n $}! = {$ factorial(n) $}");
    }
    return 0;
}
```

*Listing 1-6: factorial.loup, the same program in host.dual*

`loup run factorial.loup` calls `main()` for you. `main` takes no parameters and returns `void` or `int`; an `int` result becomes the process exit status. Command-line arguments come from `runtime.args()`.

Two rules catch people moving a script to `host.dual`. First, code outside a function is not allowed:

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

func main(): void {
    println("in main");
}

println("loaded");
```

```text
main.loup:7:1: error[E_TOPLEVEL_CONTROL_AOT]: E_TOPLEVEL_CONTROL_AOT: top-level control flow is not allowed in `host.dual` — only declarations and constant initializers are permitted at module scope; the runtime invokes `main()` after module initialization (§1-2.4)
```

A compiled program has no "run the file from the top" step; move the statement into `main()`.

Second, `main` does not receive the arguments as a parameter:

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

func main(args: list[string]): void {
    println(args);
}
```

```text
main.loup:3:11: error[E_MAIN_SIGNATURE]: E_MAIN_SIGNATURE: `main` takes no parameters — command-line arguments come from `runtime.args()` (§1-2.4.5, ruling MAIN-1)
```

The fix:

```loup
@mode(host.dual)

func main(): int {
    let argv = runtime.args();
    println(i"{$ argv.length() $} argument(s)");
    return 0;
}
```

Scripts can read their arguments the same way, or with the shorter `args()`. Chapter 18 builds a complete command-line tool.

## Projects

A single script needs no project. When you want several source files, tests and a manifest, create a project:

```sh
loup-bootstrap init demo
cd demo
```

This writes:

```text
demo/
├── demo.lppkg      # the package manifest
├── src/
│   └── main.loup   # the entry point, in host.dual
└── tests/          # empty; tests go here
```

The manifest is TOML. The generated one begins like this (it also contains several empty tables):

```toml
name = "demo"
version = "0.1.0"
loup = "1.20.0"
authors = []

[build]
entry = "src/main.loup"
delivery = "cli"
profile = "release"
```

And `src/main.loup` is a `host.dual` program:

```loup
@mode(host.dual)

func main(): void {
    println("hello, demo");
}
```

Inside the project, `loup run src/main.loup` runs it and `loup-bootstrap check` type-checks every file without running anything. Chapter 11 covers projects and packages; Chapter 16 covers tests.

### Commands you will use

| Command | What it does |
|---|---|
| `loup run file.lpscr [args…]` | Type-check and run a script (or a `.loup` program's `main()`) |
| `loup-bootstrap repl` | Start the REPL |
| `loup-bootstrap init name` | Create a project |
| `loup-bootstrap check` | Type-check the project in the current directory |
| `loup-bootstrap test` | Run the project's tests (Chapter 16) |
| `loup fmt file.lpscr` | Format a source file |
| `loup-bootstrap build` | Build a project into a WebAssembly module |

## The Six Modes at a Glance

| Mode | Extension | Entry point | How it runs today | Book |
|---|---|---|---|---|
| `host.scripted` | `.lpscr` | top-level statements, in order | `loup run` (interpreter) | **this book** |
| `host.dual` | `.loup` | `main()` | `loup run`, or compiled to Wasm or native | this book, Chapter 18; [The Loup Native Book](../loup-native/) |
| `host.native` | `.lphc` | `main()` | `loup build --target native` | [The Loup Native Book](../loup-native/) |
| `web.scripted` | `.lpweb` | browser bootstrap | no browser runtime ships yet | [The Loup Web Book](../loup-web/) |
| `web.compiled` | `.lpweb` | browser bootstrap | a bare `.wasm` module; no browser runtime yet | [The Loup Web Book](../loup-web/) |
| `freestanding` | `.lpfree` | an `@entry` function | `loup build --target freestanding` (Linux) | [The Loup Freestanding Book](../loup-freestanding/) |

The language is the same in every mode; the mode decides which features and library modules are available and how the file is delivered. Every mode uses the same memory model — reference counting for class instances, copying for structs and collections — so code you write here keeps its meaning if you move it to `host.dual` or `host.native`.

## A Roadmap of the Rest of the Book

| Chapters | Topic |
|---|---|
| 2–5 | Values and types, functions and closures, control flow, collections |
| 6–9 | Structs, enums and `match`, classes, interfaces and generics |
| 10–12 | Errors, modules and packages, strings and templates |
| 13 | Concurrency |
| 14, 15, 17 | Reflection, reactive UI and JavaScript interop (design previews: not in Loup 1.25) |
| 16, 18, 19 | Testing, building command-line tools, ownership and resources |

The first twelve chapters are the core; read the rest as you need them.

## Summary

- A Loup script is a `.lpscr` file that starts with `@mode(host.scripted)`; `loup run` type-checks it and then runs its top-level statements in order.
- In Loup 1.25, `loup run` is an interpreter on your operating system, not a JavaScript or browser runtime.
- Install with the official installers from `code-craft-ai-company/loup-releases`.
- Comments start with `#`; `//` is floor division. There is no ternary operator; `if` is an expression.
- Compile errors print one `file:line:col: error[CODE]: message` line each and exit 2; uncaught exceptions print `Uncaught …` with a stack trace and exit 1.
- `__name__ == "__main__"` guards code that should run only when the file is run directly.
- Scripts get `fs`, `proc` and `entropy` by default; `--allow-net` and the other flags change that.
- A script that becomes a program moves to `host.dual`: a `.loup` file whose code lives in `main()`.

## Try It Yourself

1. Write `fizzbuzz.lpscr` that prints FizzBuzz from 1 to 100. (`for (i in 1..101) { … }` loops over 1 to 100, and `%` is the remainder.)
2. Move the logic into `pub func fizzbuzz(n: int): string`, guard the loop with `if (__name__ == "__main__")`, and import the function from a second script.
3. Write `let x: int = "hello";` and read the diagnostic. Then write `x = 3;` after `let x = 1;` and compare the two messages.
4. Convert `fizzbuzz.lpscr` into `fizzbuzz.loup` in `host.dual`, and see which error you get if you forget to move the loop into `main()`.
