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.lpwebruns aweb.compiledorweb.scriptedfile on the host interpreter. This is how every runnable listing in this book was checked. - Works:
loup-bootstrap checktype-checks a project. - Experimental, not audited:
loup-bootstrap build --target browserwrites a single barebuild/<name>.wasm. It runs underloup-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:
loupruns source files (loup run).loup-bootstrapis the compiler that creates projects, type-checks them and builds WebAssembly (init,check,build). On macOS and Windows,loupisloup-bootstrap; on Linux x86-64,loupis the self-hosted driver, which does not build Wasm. This book writesloup-bootstrapfor those three commands so they work everywhere.loup-launcherruns a built.wasmmodule.
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.lpwebextension (ruling MODE-3). - A
web.compiledfile holds declarations. Statements go inside functions. The runner callsmain()after loading the module (ruling ENTRY-1), so that is where the program starts. Chapter 2 explains the rules, and howweb.scripteddiffers. - Signatures are typed.
greetingtakes astringand returns astring;mainreturnsvoid. Inside a function body,letandvarinfer their types.
Mistake: an old mode name
Older Loup material writes @mode(compiled). That name was retired in v1.17:
@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:
@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:
- instantiate
app.wasm, streaming it as it downloads; - initialize the module (run its top-level declarations);
- register every
@webcomponentclass as a custom element; - wire event handlers (elements marked with
data-loup-ev); - hydrate server-rendered state, if there is any;
- 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.
# src/main.lpweb
@mode(web.scripted)
from ui import (mount);
from app import (App);
mount(App(), "#root");
# 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-releasesinstallers. - A web file has the
.lpwebextension 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 callsmain().loup run file.lpwebruns a file on the host interpreter, and errors print asfile:line:col: error[CODE]: message. loup-bootstrap initcreates a project; set[build] entryto your.lpwebfile.loup-bootstrap checktype-checks it.loup-bootstrap build --target browserwrites a barebuild/<name>.wasm, whichloup-launcher --run-bareruns. Wasm output is experimental.- The browser runtime,
loup serve, and the reactive UI are planned.