# Chapter 2: The Web Modes

Loup has six execution modes. Two of them are for the web: `web.compiled`
and `web.scripted`. This chapter covers how a file declares its mode, where
each web mode starts running, what each mode allows, and which rules the
1.25.1 toolchain enforces today. Refer back to it when the compiler tells you
that something is not allowed in your mode.

## Six modes, two for the web

A mode combines an environment (browser, operating-system process, or bare
metal) with a delivery strategy (compiled just in time or ahead of time). The
Language Reference defines them in §1-2.4 and lists every feature per mode in
the matrix of §1-2.6.

| Mode | Environment | Delivery | Program starts at | Extension |
|---|---|---|---|---|
| `web.scripted` | browser | JIT, from source at load time | top-level statements | `.lpweb` |
| `web.compiled` | browser | ahead of time, to Wasm | `main()` today (see below) | `.lpweb` |
| `host.scripted` | OS process | JIT | top-level statements | `.lpscr` |
| `host.dual` | OS process | JIT or ahead of time | `main()` | `.loup` |
| `host.native` | OS process | ahead of time, native code | `main()` | `.lphc` |
| `freestanding` | bare metal | ahead of time, no runtime | an `@entry` function | `.lpfree` |

The two web modes accept the same language. The difference is the delivery:
`web.scripted` source is meant to be compiled in the browser when the page
loads, and `web.compiled` source is compiled when you build. Because
ahead-of-time code has no "run this file from the top" moment, a
`web.compiled` file holds only declarations.

The other modes have their own books: `host.scripted` and `host.dual` in
[The Loup Scripting Book](../loup-scripting/README.md), `host.native` in
[The Loup Native Book](../loup-native/README.md), and `freestanding` in
[The Loup Freestanding Book](../loup-freestanding/README.md).

## The `@mode` directive

A file declares its mode with `@mode(...)` as its first token. Comments may
come before it; nothing else may.

```loup
# Shopping cart totals for the checkout page.
@mode(web.compiled)
namespace shop.cart;

func main(): void {
    println("cart module loaded");
}
```

*Listing 2-1: Comments may precede `@mode`; everything else follows it*

```text
cart module loaded
```

### Mistake: a declaration before the directive

Putting the `namespace` line first is a natural slip:

```loup,does_not_compile
# Shopping cart totals for the checkout page.
namespace shop.cart;
@mode(web.compiled)

func main(): void {
    println("cart module loaded");
}
```

```text
main.lpweb:3:1: error[E_MODE_POSITION]: E_MODE_POSITION: `@mode` must be the first token of the file — only comments and `@compiled` may precede it (§1-2.5.1)
  note: move the `@mode(...)` line to the top of the file; a late `@mode` is otherwise not the module's mode
```

The message says what to do: move `@mode` up. A second `@mode` line in the
same file is `E_MODE_DUPLICATE`.

Three more rules about the directive:

- **Always write it.** Ruling MODE-2 says every file declares its mode in its
  own text. Without a directive the mode comes from fallbacks, and they do not
  agree: the Language Reference gives a `.lpweb` file `web.scripted`, while
  `loup run` in Loup 1.25.1 runs it as `host.scripted`.
- **Use the full names.** `@mode(compiled)` and `@mode(scripted)` were
  retired in v1.17. As Chapter 1 showed, the compiler does not report an
  unknown name; it silently runs the file as `host.scripted`.
- **Use `.lpweb` for both web modes** (ruling MODE-3). The extension does not
  decide the mode; the directive does.

## `web.scripted`: top-level statements run in order

In `web.scripted`, the file is the program. Its top-level statements run once,
from top to bottom, the way a Python script or a JavaScript `<script>` does:

```loup
@mode(web.scripted)

# Prices in cents.
let prices = [1250, 399, 2000];
var total = 0;
for (p in prices) {
    total = total + p;
}
println("items: " + str(prices.length()));
println("total: " + str(total) + " cents");
```

*Listing 2-2: A web.scripted program*

```text
items: 3
total: 3649 cents
```

There is no `main()`. If a `web.scripted` file declares one, nothing calls it
unless the file does. In the planned browser runtime, a `web.scripted` entry
file's top-level code is where the application mounts its user interface
(Chapter 16).

The `if (__name__ == "__main__")` guard of host scripts has no meaning in a
web mode, because the browser never runs a file "directly". The Language
Reference makes it an error in web files (`E_MAIN_GUARD_NOT_APPLICABLE`);
Loup 1.25.1 does not report it yet, so just leave it out.

## `web.compiled`: declarations, and `main()`

Copy Listing 2-2 into a `web.compiled` file and it no longer compiles:

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

# Prices in cents.
let prices = [1250, 399, 2000];
var total = 0;
for (p in prices) {
    total = total + p;
}
println("total: " + str(total) + " cents");
```

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

The `for` loop and the `println` call are statements, and a `web.compiled`
module has no top-to-bottom execution to run them in. At the top level it
may only declare things: `func`, `class`, `struct`, `enum`, `interface`,
`type`, imports, and `let`/`var` bindings with constant initializers. The
`let prices` and `var total` lines are fine; the loop and the call are not.

The fix is to put the work in functions and start from `main()`:

```loup
@mode(web.compiled)

# Prices in cents.
let PRICES = [1250, 399, 2000];

func cart_total(prices: list[int]): int {
    var total = 0;
    for (p in prices) {
        total = total + p;
    }
    return total;
}

func main(): void {
    println("items: " + str(PRICES.length()));
    println("total: " + str(cart_total(PRICES)) + " cents");
}
```

*Listing 2-3: The same program as web.compiled*

```text
items: 3
total: 3649 cents
```

### Who calls `main()`

The runner calls `main()` after the module is initialized: `loup run`,
`loup-launcher`, and any other runner of an ahead-of-time entry file
(rulings ENTRY-1 and MAIN-1, Language Reference §1-2.4.5). A `web.compiled`
file without a `main()` loads, initializes its constants, and exits without
doing anything.

The Language Reference also describes a browser entry, in which the browser
bootstrap calls the application's registered `mount(...)` instead
(§1-2.4.2). That runtime is not in Loup 1.25.1, so a `web.compiled` program
starts at `main()`, and that is how every `web.compiled` listing in this book
runs.

`main` takes no parameters and returns `void` or `int`. An `int` result is the
process's exit status: a `main` that returns 3 makes `loup run` exit with
status 3. Anything else is an error:

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

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

```text
main.lpweb: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)
```

Nothing passes arguments to `main`. A host program reads its command line
with `runtime.args()`; a browser page has no command line, so web code
rarely needs it. Drop the parameter:

```loup
@mode(web.compiled)

func main(): int {
    println("checkout ready");
    return 0;
}
```

```text
checkout ready
```

### Top-level bindings must be constants

The Language Reference requires every top-level `let` and `var` in
`web.compiled` to have a compile-time constant initializer: literals,
arithmetic on literals, and enum variants (§3-1.3). They become static data
in the module.

> ⚠ **Not enforced yet.** Loup 1.25.1 accepts a non-constant top-level
> initializer such as `let started = load_settings();` in a `web.compiled`
> file and evaluates it when the module loads. Do not rely on that; compute
> such values inside `main()` or the function that needs them.

## `Any` and `dynamic class`: allowed, with an advisory

Both web modes accept the dynamic parts of Loup: the `Any` type and
`dynamic class`. In `web.scripted` they are ordinary. In `web.compiled` each
use produces the `W_ANY_IN_AOT_MODE` advisory, because ahead-of-time code has
to carry type tags and dispatch at run time for them:

```loup
@mode(web.compiled)

func describe(value: Any): string {
    return "setting = " + str(value);
}

func main(): void {
    println(describe(42));
    println(describe("blue"));
}
```

```text
setting = 42
setting = blue
main.lpweb:3:22: warning: W_ANY_IN_AOT_MODE: use of `Any` in web.compiled mode prevents AOT specialisation — consider a static type if performance is critical
main.lpweb:3:22: warning: W_ANY_IN_AOT_MODE: use of `Any` in web.compiled mode prevents AOT specialisation — consider a static type if performance is critical
```

The program runs; the warnings print after its output. It is advice, not an
error. When the set of possible values is known, an enum says so and gets
checked:

```loup
@mode(web.compiled)

enum Setting {
    Number(value: int),
    Text(value: string),
    Flag(on: bool),
}

func describe(s: Setting): string {
    return match s {
        Setting.Number(value) => "number " + str(value),
        Setting.Text(value) => "text \"" + value + "\"",
        Setting.Flag(on) => if (on) { "flag on" } else { "flag off" },
    };
}

func main(): void {
    println(describe(Setting.Number(value = 42)));
    println(describe(Setting.Text(value = "blue")));
    println(describe(Setting.Flag(on = true)));
}
```

*Listing 2-4: An enum instead of `Any`*

```text
number 42
text "blue"
flag on
```

Now `describe` cannot be handed a value it does not know how to describe,
and `match` must cover every variant. Chapter 8 covers enums; Chapter 11
covers generics, the other common replacement for `Any`.

## The standard library from web code

The standard library's modules (`core.math`, `system.json`, `data.csv`, and
the rest) are `host.scripted` modules. A `web.compiled` file may import only
other `web.compiled` modules, so in Loup 1.25.1 it cannot import any of them:

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

from core.math import (sqrt);

func hypotenuse(a: float, b: float): float {
    return sqrt(a * a + b * b);
}

func main(): void {
    println(hypotenuse(3.0, 4.0));
}
```

```text
main.lpweb:3:1: error[E0001]: `@mode(web.compiled)` module cannot import `@mode(host.scripted)` module
  note: `@mode(web.compiled)` modules can only import other `@mode(web.compiled)` modules
  note: error code: E_COMPILED_IMPORT_SCRIPTED
```

The prelude is not affected: `println`, `str`, `repr`, the numeric
conversions and the built-in types and exceptions are available in every
mode without an import. For a standard-library module, use a `web.scripted`
file, which may import it:

```loup
@mode(web.scripted)

from core.math import (sqrt);

func hypotenuse(a: float, b: float): float {
    return sqrt(a * a + b * b);
}

println(hypotenuse(3.0, 4.0));
```

*Listing 2-5: Standard-library imports work in web.scripted*

```text
5
```

(`println` prints a whole `float` such as `5.0` as `5`; `repr(5.0)` shows
`5.0`.)

The import rules between your own modules follow the same direction
(Language Reference §3-1.2). Chapter 13 shows them in a project.

| Importing file | may import `web.compiled` | may import `web.scripted` | may import the stdlib |
|---|---|---|---|
| `web.compiled` | yes | no (`E_COMPILED_IMPORT_SCRIPTED`) | no, in 1.25.1 |
| `web.scripted` | yes | yes | yes |

So a typical layout keeps logic that needs no library in `web.compiled`
modules and lets a `web.scripted` file import both those modules and the
standard library. The listings in this book follow that rule: they use
`web.compiled` unless they import a standard-library module.

## Host capabilities

A browser page cannot read your disk or start processes. The Language
Reference therefore forbids the host capabilities in web modes: file system,
processes, host networking and `ThreadLocal[T]` are `E_CAPABILITY_NOT_WEB`
(§1-2.4.1).

> ⚠ **Not enforced yet.** Loup 1.25.1 does not check this. The prelude's
> host functions (`readText`, `writeText`, `exists`, `exec`, `env`) compile in
> an `.lpweb` file, and `loup run` executes them on your machine. Do not use
> them in web code: they will not exist in a browser.

## What else each web mode allows

Most of the language is the same in every mode. The table lists the features
where the web modes differ from what you might expect, with what the
Language Reference specifies and what Loup 1.25.1 does.

| Feature | Language Reference | Loup 1.25.1, both web modes |
|---|---|---|
| Ownership verbs `reads`, `edits`, `takes`, `shares`; `modifier`, `safe {}`, `synchronized` | available | work (Chapters 9 and 20) |
| `unique let` / `unique var` | available | work: a moved value cannot be used again (`E_USE_AFTER_MOVE`) |
| `unique class` | available | rejected: `E_UNIQUE_CLASS_ARC_BINDING`, accepted only in `host.native` and `freestanding` |
| `weak` fields | matrix (§1-2.6): available; §3-1.2: not on the Wasm target | accepted |
| `arc let` / `arc var` | matrix: available; §3-1.2: `E_ARC_LET_NOT_NATIVE` on the Wasm target | ⚠ Planned: does not parse |
| `consume` | not on the Wasm target | rejected: `E_CONSUME_NOT_AVAILABLE` |
| `atomic {}` | available | works |
| `Thread.spawn`, `ThreadPool`, `Mutex`, `ThreadLocal[T]` | not in web modes | `Thread.spawn` is accepted and runs inline; `ThreadLocal` is undefined. Do not use them. |
| `async` / `await` | available | works; runs eagerly on the interpreter (Chapter 15) |
| Template fences | available | work; `html` fences do not auto-escape yet (Chapter 14) |
| `asm { }` | `web.compiled` only, with `@unsafe` | `web.scripted` rejects it (`E_ASM_NOT_COMPILED`); `loup run` cannot execute it in `web.compiled` |
| `ui.web`, `@webcomponent`, DOM, `extern js` | available | ⚠ Planned (Chapters 16 to 18) |

`ui.desktop`, `extern native`, and the host capability surface belong to the
host modes and are not available in either web mode.

## Moving code from `web.scripted` to `web.compiled`

Every `web.scripted` module is meant to be a valid `web.compiled` module,
apart from its top-level statements. To move one:

1. Change the directive to `@mode(web.compiled)`.
2. Move top-level statements into functions, and start the program in
   `main()` (`E_TOPLEVEL_CONTROL_AOT` points at each one).
3. Make every top-level `let` and `var` a constant, or move it into a
   function.
4. Remove standard-library imports from this file, or keep the code that
   needs them in a `web.scripted` module (`E_COMPILED_IMPORT_SCRIPTED`).
5. Replace `Any` with concrete types, enums or type parameters where you can
   (`W_ANY_IN_AOT_MODE`).
6. Run `loup-bootstrap check` until it is clean.

## Summary

- A file's first token is `@mode(...)`; only comments may precede it. Web
  files use `.lpweb` and declare `web.compiled` or `web.scripted`. Retired
  names such as `@mode(compiled)` are silently run as `host.scripted`.
- `web.scripted` runs its top-level statements in order.
- `web.compiled` allows only declarations at the top level
  (`E_TOPLEVEL_CONTROL_AOT`); the runner calls `main()`, which takes no
  parameters and returns `void` or `int`.
- `Any` and `dynamic class` are allowed in both web modes; in `web.compiled`
  they produce the `W_ANY_IN_AOT_MODE` advisory.
- `web.compiled` cannot import `web.scripted` modules or, in 1.25.1, any
  standard-library module. `web.scripted` can import both.
- Host capabilities are forbidden in web modes by the Language Reference but
  not yet checked; do not use them.
