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:
# 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
.lpwebfileweb.scripted, whileloup runin Loup 1.25.1 runs it ashost.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 ashost.scripted. - Use
.lpwebfor 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:
@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:
@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 aweb.compiledfile and evaluates it when the module loads. Do not rely on that; compute such values insidemain()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:
@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.lpwebfile, andloup runexecutes 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:
- Change the directive to
@mode(web.compiled). - Move top-level statements into functions, and start the program in
main()(E_TOPLEVEL_CONTROL_AOTpoints at each one). - Make every top-level
letandvara constant, or move it into a function. - Remove standard-library imports from this file, or keep the code that
needs them in a
web.scriptedmodule (E_COMPILED_IMPORT_SCRIPTED). - Replace
Anywith concrete types, enums or type parameters where you can (W_ANY_IN_AOT_MODE). - Run
loup-bootstrap checkuntil it is clean.
Summary
- A file's first token is
@mode(...); only comments may precede it. Web files use.lpweband declareweb.compiledorweb.scripted. Retired names such as@mode(compiled)are silently run ashost.scripted. web.scriptedruns its top-level statements in order.web.compiledallows only declarations at the top level (E_TOPLEVEL_CONTROL_AOT); the runner callsmain(), which takes no parameters and returnsvoidorint.Anyanddynamic classare allowed in both web modes; inweb.compiledthey produce theW_ANY_IN_AOT_MODEadvisory.web.compiledcannot importweb.scriptedmodules or, in 1.25.1, any standard-library module.web.scriptedcan import both.- Host capabilities are forbidden in web modes by the Language Reference but not yet checked; do not use them.