The Loup Web Book — contents
  1. Chapter 1: Getting Started
  2. Chapter 2: The Web Modes
  3. Chapter 3: Variables and Data Types
  4. Chapter 4: Functions
  5. Chapter 5: Control Flow
  6. Chapter 6: Collections
  7. Chapter 7: Structs
  8. Chapter 8: Enums and Pattern Matching
  9. Chapter 9: Classes and Objects
  10. Chapter 10: Interfaces
  11. Chapter 11: Generics
  12. Chapter 12: Error Handling
  13. Chapter 13: Modules and Packages
  14. Chapter 14: Template Fences — HTML and CSS
  15. Chapter 15: Async and Concurrency
  16. Chapter 16: Reactive UI
  17. Chapter 17: Web Components
  18. Chapter 18: DOM, Storage, Workers, and JavaScript Interop
  19. Chapter 19: SSR, Hydration, and Deployment
  20. Chapter 20: Memory Safety in Web Apps

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, host.native in The Loup Native Book, and freestanding in The Loup Freestanding Book.

The @mode directive

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

# 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

cart module loaded

Mistake: a declaration before the directive

Putting the namespace line first is a natural slip:

This code does not compile
# Shopping cart totals for the checkout page.
namespace shop.cart;
@mode(web.compiled)

func main(): void {
    println("cart module loaded");
}
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:

@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

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:

This code 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");
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():

@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

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:

This code does not compile
@mode(web.compiled)

func main(args: list[string]): void {
    println("checkout ready");
}
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:

@mode(web.compiled)

func main(): int {
    println("checkout ready");
    return 0;
}
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:

@mode(web.compiled)

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

func main(): void {
    println(describe(42));
    println(describe("blue"));
}
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:

@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

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:

This code 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));
}
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:

@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

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.