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 1: Getting Started

This chapter installs Loup, runs a first .lpweb program, sets up a project, and compiles it to WebAssembly. It ends with a look at the browser pipeline the Language Reference specifies, which is not in this release yet.

What works today

Loup 1.25.1 runs web-mode code, type-checks it, and compiles it to a WebAssembly module. It does not ship the browser side yet:

  • Works: loup run app.lpweb runs a web.compiled or web.scripted file on the host interpreter. This is how every runnable listing in this book was checked.
  • Works: loup-bootstrap check type-checks a project.
  • Experimental, not audited: loup-bootstrap build --target browser writes a single bare build/<name>.wasm. It runs under loup-launcher --run-bare, outside the browser.
  • ⚠ Planned: the browser runtime (loup-rt.js), the generated HTML shell, loup serve, the DOM, reactive UI, Web Components, workers and JavaScript interop.

The book's README has the full table.

Installing Loup

Official builds come from the code-craft-ai-company/loup-releases GitHub releases. On Linux or macOS:

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

On Windows, in PowerShell:

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

The installer unpacks the toolchain into ~/.loup (Windows: %LOCALAPPDATA%\Programs\Loup), adds its bin directory to your PATH and needs no administrator rights. To install a specific version, run sh -s -- --version 1.25.1 instead of sh, or set LOUP_VERSION before the Windows command. The macOS and Windows binaries are not notarized or signed yet; the installers are unaffected, but if you download an archive in a browser on macOS, clear the quarantine flag with xattr -dr com.apple.quarantine <dir>.

Open a new terminal and check the installation:

$ loup --version
loup 1.25.1 (freestanding-capable)

Every archive contains three programs this book uses:

  • loup runs source files (loup run).
  • loup-bootstrap is the compiler that creates projects, type-checks them and builds WebAssembly (init, check, build). On macOS and Windows, loup is loup-bootstrap; on Linux x86-64, loup is the self-hosted driver, which does not build Wasm. This book writes loup-bootstrap for those three commands so they work everywhere.
  • loup-launcher runs a built .wasm module.

Your first program

Create a file called hello.lpweb:

@mode(web.compiled)

func greeting(name: string): string {
    return "Hello, " + name + "!";
}

func main(): void {
    println(greeting("web"));
    println(greeting("Loup"));
}

Listing 1-1: hello.lpweb, a first web.compiled program

Run it:

$ loup run hello.lpweb
Hello, web!
Hello, Loup!

Three things to notice:

  • The first line declares the mode. @mode(web.compiled) must be the first thing in the file; only comments may come before it. Every file should declare its mode (ruling MODE-2). Web files use the .lpweb extension (ruling MODE-3).
  • A web.compiled file holds declarations. Statements go inside functions. The runner calls main() after loading the module (ruling ENTRY-1), so that is where the program starts. Chapter 2 explains the rules, and how web.scripted differs.
  • Signatures are typed. greeting takes a string and returns a string; main returns void. Inside a function body, let and var infer their types.

Mistake: an old mode name

Older Loup material writes @mode(compiled). That name was retired in v1.17:

Fragment — not a complete program
@mode(compiled)

func main(): void {
    println("Hello, web!");
}
$ loup run old.lpweb
$

Nothing is printed, and the exit status is 0. The compiler does not report the unknown name; it runs the file as a host.scripted script, whose top-level statements are only declarations, so main() is never called. (This book's tooling refuses retired names, which is why the listing above is not run.) The fix is the full name, @mode(web.compiled). If a program prints nothing at all, check the mode line first.

Mistake: passing the wrong type

Change the second call in Listing 1-1 to pass a number:

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

func greeting(name: string): string {
    return "Hello, " + name + "!";
}

func main(): void {
    println(greeting("web"));
    println(greeting(2026));
}
main.lpweb:9:22: error[E0001]: argument 1 of 'greeting': expected 'string', found 'int'

loup run prints one line per error: the file, line and column, the error code, and the message. Nothing runs, not even the first println; the whole file is checked before any of it executes, and the exit status is 2. Loup never converts an int to a string for you. Convert it explicitly with str:

@mode(web.compiled)

func greeting(name: string): string {
    return "Hello, " + name + "!";
}

func main(): void {
    println(greeting("web"));
    println(greeting(str(2026)));
}
Hello, web!
Hello, 2026!

Starting a project

A single file is enough for learning. For an application you want a project: a directory with a manifest, so the compiler knows where the entry file is and what to build.

$ loup-bootstrap init hello-web
created project at …/hello-web

This writes:

hello-web/
  hello-web.lppkg
  src/main.loup
  tests/

The scaffold is a host.dual program in src/main.loup, because .loup is the host.dual extension. For a web project, rename it to src/main.lpweb, replace its contents with Listing 1-1, and point the manifest at it. The top of hello-web.lppkg then reads:

name = "hello-web"
version = "0.1.0"
loup = "1.20.0"
authors = []

[build]
entry = "src/main.lpweb"
delivery = "browser"
profile = "release"

entry names the file that holds main(). delivery = "browser" makes loup-bootstrap build produce a browser module by default (the key is also accepted under its older name, target). The rest of the scaffold, a set of empty tables, can stay as it is.

Type-check the project from its directory:

$ loup-bootstrap check
$

No output means no errors. check works on a project, not on a lone file, and prints its diagnostics in a longer form than loup run. With the greeting(2026) mistake from above it prints:

error[E0001]: argument 1 of 'greeting': expected 'string', found 'int'
 --> src/main.lpweb:9:22
  |
9 |     println(greeting(2026));
  |                      ^^^^
  |

Building WebAssembly

$ loup-bootstrap build
Resolving dependencies...
Compiling 1 module(s)...
Generating code...
Build complete: …/hello-web/build/hello-web.wasm (19991 bytes)
…/hello-web/build/hello-web.wasm

Because the manifest says delivery = "browser", this is the same as loup-bootstrap build --target browser (--delivery is the newer spelling of the same flag). web is not a target name: --target web fails with unknown delivery shape: web.

The build writes one file, build/hello-web.wasm. There is no dist/ directory, no HTML page and no JavaScript loader. To run the module, use the launcher:

$ loup-launcher --run-bare build/hello-web.wasm
Hello, web!
Hello, Loup!

The launcher is a WebAssembly runtime for your terminal. --run-bare runs a module that has no package manifest beside it; it initializes the module and then calls main().

Compiled WebAssembly is experimental and not audited in Loup 1.25.1, and some programs that run under loup run do not run under the launcher. For example, the interpolated strings of Chapter 3 (i"Hello, {$ name $}!") work on the interpreter but stop a Wasm build at run time:

loup: main trap: error while executing at wasm backtrace:
    0:   0x24ff - <module>!main

Caused by:
    call_dynamic: no exported function `RenderContext` for the function value

Floor division (a // b) and for await loops also stop a Wasm build at run time today. That is why Listing 1-1 joins strings with +. The rest of this book runs its listings with loup run.

The planned browser pipeline

⚠ Planned. Nothing in this section is in Loup 1.25.1. It describes the design in the Language Reference (§1-20.6 browser bootstrap, §2-4 reactive UI), and its listings are design sketches that are not checked.

In the specified design, loup build --target browser writes a dist/ directory with an HTML shell, a versioned JavaScript bootstrap (loup-rt.js), and app.wasm, plus compressed copies and, with --debug, a source map. When the page loads, loup-rt.js runs these steps in order:

  1. instantiate app.wasm, streaming it as it downloads;
  2. initialize the module (run its top-level declarations);
  3. register every @webcomponent class as a custom element;
  4. wire event handlers (elements marked with data-loup-ev);
  5. hydrate server-rendered state, if there is any;
  6. mount the application.

loup serve would serve dist/, rebuild on every change and reload the page. A counter app would be two files: an entry that mounts the root component, and the component itself.

Fragment — not a complete program
# src/main.lpweb
@mode(web.scripted)

from ui import (mount);
from app import (App);

mount(App(), "#root");
Fragment — not a complete program
# src/app.lpweb
@mode(web.compiled)

from ui import (signal, View);

pub func App(): View {
    let count = signal(0);
    return ```(html):(none)
    <main>
      <h1>Hello from Loup</h1>
      <p>Count: {$ count.get() $}</p>
      <button onclick={$ func() { count.set(count.get() + 1); } $}>+1</button>
    </main>
    ```;
}

Listing 1-2: Design sketch of a planned counter app

signal(0) creates a reactive value. Reading it inside the fence makes the paragraph depend on it, so set re-renders only that text. The onclick value is a Loup closure, not a JavaScript string; the compiler gives it an event slot, and the bootstrap attaches it with addEventListener. Chapter 16 covers the reactive model and Chapter 19 the build output and deployment.

Summary

  • Install Loup from the official loup-releases installers.
  • A web file has the .lpweb extension and starts with @mode(web.compiled) or @mode(web.scripted). A retired name such as @mode(compiled) is not reported; the program silently does nothing.
  • In web.compiled, the runner calls main(). loup run file.lpweb runs a file on the host interpreter, and errors print as file:line:col: error[CODE]: message.
  • loup-bootstrap init creates a project; set [build] entry to your .lpweb file. loup-bootstrap check type-checks it.
  • loup-bootstrap build --target browser writes a bare build/<name>.wasm, which loup-launcher --run-bare runs. Wasm output is experimental.
  • The browser runtime, loup serve, and the reactive UI are planned.