The Loup Native Book — contents
  1. Chapter 1: Getting started
  2. Chapter 2: Native mode overview
  3. Chapter 3: Variables and numerics
  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 ARC
  10. Chapter 10: Interfaces
  11. Chapter 11: Generics
  12. Chapter 12: Error handling
  13. Chapter 13: Modules and packages
  14. Chapter 14: Template fences
  15. Chapter 15: Ownership verbs
  16. Chapter 16: borrow, excl borrow and noescape
  17. Chapter 17: unique ownership
  18. Chapter 18: Lifetimes and regions
  19. Chapter 19: @unsafe and calling C
  20. Chapter 20: Concurrency with OS threads
  21. Chapter 21: Async on native
  22. Chapter 22: Desktop UI
  23. Chapter 23: SIMD and GPU
  24. Chapter 24: Building and distributing
  25. Chapter 25: Memory safety patterns

Chapter 2: Native mode overview

Chapter 1 got a program running. This chapter steps back and explains what the two native modes, host.native and host.dual, mean to the compiler: where they sit among Loup's six modes, what they share, how they differ, and which parts of the language a native binary cannot run yet. The rest of the book builds on this picture.

Loup's six modes

Every Loup file declares a mode on its first line. The mode decides where the code runs (a browser, an operating-system process, or bare metal), how it is delivered (interpreted, compiled ahead of time, or either), and which features it may use (Language Reference §1-2.4):

Mode Runs in Delivered as Entry point Extension
host.scripted an OS process interpreted by loup run top-level statements, in order .lpscr
host.dual an OS process interpreted, WebAssembly, or a native binary main() .loup
host.native an OS process a native binary main() .lphc
web.scripted, web.compiled a browser WebAssembly a mount(...) call .lpweb
freestanding bare metal a native image with no runtime an @entry function .lpfree

This book covers the two middle rows. Scripts have their own book (The Loup Scripting Book), and so do browser code (The Loup Web Book) and firmware (The Loup Freestanding Book).

The directive must be the first token in the file; comments may precede it. A later directive is E_MODE_POSITION, a second one is E_MODE_DUPLICATE. If a file has no directive, the project's [build] mode applies, and failing that the file is a host.scripted script, so always write the line.

What the two native modes share

host.native and host.dual are both ahead-of-time modes. That gives them the same structure:

  • main() is the entry point. The runtime calls it after the module is initialized. It takes no parameters and returns void or int (ruling MAIN-1).
  • No top-level statements. Only declarations and constant initializers may appear at module scope. Anything else is E_TOPLEVEL_CONTROL_AOT.
  • Static types are expected. Every parameter and return type is declared. Any still compiles, but it produces a warning, because the compiler has to fall back to dynamic dispatch:
@mode(host.native)

func show(x: Any): void {
    println(x);
}

func main(): void {
    show(3);
}

Listing 2-1: Any compiles in an AOT mode, with a warning

3
main.lphc:3:14: warning: W_ANY_IN_AOT_MODE: use of `Any` in host.native mode prevents AOT specialisation — consider a static type if performance is critical
main.lphc:3:14: warning: W_ANY_IN_AOT_MODE: use of `Any` in host.native mode prevents AOT specialisation — consider a static type if performance is critical

loup run prints warnings after the program's own output (Stage0 1.25.1 prints this one twice). The program still runs; a warning never stops a build.

  • ARC memory semantics. Class instances are reference-counted; structs, enums, tuples and collections are values that are copied (Language Reference §1-8.1.1). Chapter 9 covers this in detail. This is not a difference from the other modes: every mode presents the same reference-counting semantics, and none of them uses a tracing garbage collector.
  • The ownership system. Parameter verbs (reads, edits, takes, shares), modifier methods, safe { } blocks, borrow, and unique bindings are checked at compile time in every mode. Native programs lean on them most, because they decide when reference counts change and who may mutate what. Chapters 15 to 18 cover them.

Here is a small program that uses several of these pieces: a class with a modifier method, a safe block that permits calling it, a dictionary, and string methods.

@mode(host.native)

class WordCounter {
    var counts: dict[string, int] = {};

    pub modifier func add(word: string): void {
        let w = word.toLowerCase();
        let current = this.counts.get(w) ?? 0;
        this.counts[w] = current + 1;
    }

    pub func count(word: string): int {
        return this.counts.get(word) ?? 0;
    }

    pub func vocabularySize(): int {
        return this.counts.length();
    }
}

func main(): void {
    let text = "the cat and the hat and THE bat";
    let wc = WordCounter();
    safe {
        for (w in text.split(" ")) {
            wc.add(w);
        }
    }
    println(i"'the' appears {$ wc.count("the") $} times");
    println(i"{$ wc.vocabularySize() $} distinct words");
}

Listing 2-2: A word counter

'the' appears 3 times
5 distinct words

A few things to notice:

  • The field counts is a var, and only a method declared modifier may write to it.
  • wc is a reference-counted class instance, and calling a modifier method on one requires a safe { … } block (or a safe func). That is where the compiler checks that nothing else is mutating the same object at the same time. Chapter 15 explains the rule.
  • counts.get(w) returns an Option[int]; ?? supplies the value to use when it is None.
  • Fields without pub are private to the class.

Choosing between host.native and host.dual

The two modes accept almost the same language. The difference is how the code may be delivered, and that decides which features are allowed.

host.native is for programs whose deliverable is a native binary. It permits the features that only make sense when Loup compiles straight to machine code: unique class (single-owner class types, chapter 17), inline asm { } blocks (chapter 19), and volatile borrow parameters and Volatile[T] for memory-mapped device access.

host.dual is for code that must also run without the native toolchain: on the interpreter with loup run, as WebAssembly with loup-bootstrap build and loup-launcher, or as a packaged app with loup-bootstrap build --target desktop. Because a host.dual file must behave the same on every path, the features that need direct machine-code generation are refused there. Today a host.dual program is also the way to use the two features that native binaries cannot run yet, template fences and async, on the interpreter or in a WebAssembly build.

Choose host.native when you ship a binary and want the native-only features. Choose host.dual for libraries and tools that should work everywhere, including Windows and Linux arm64, where native builds are not available yet. Every host.dual file is also a valid host.native file, so moving from dual to native is a one-line change; the reverse may not be.

Mistake: a native-only feature in a dual file

A unique class lives in exactly one place at a time, and the compiler moves it rather than counting references. That cannot round-trip through the other delivery paths, so host.dual rejects it:

This code does not compile
@mode(host.dual)

unique class FileLock {
    pub let path: string;
    new(path: string) { this.path = path; }
}

func main(): void {
    let lock = FileLock("/tmp/app.lock");
    println(lock.path);
}
main.loup:3:1: error[E_UNIQUE_CLASS_ARC_BINDING]: E_UNIQUE_CLASS_ARC_BINDING: `unique class 'FileLock'` is only valid in `@mode(native)` or `@mode(freestanding)`

The message still uses the retired name native for host.native. There are two fixes. If the program will only ever be a native binary, change the first line to @mode(host.native) and rename the file to .lphc:

@mode(host.native)

unique class FileLock {
    pub let path: string;
    new(path: string) { this.path = path; }
}

func main(): void {
    let lock = FileLock("/tmp/app.lock");
    println(lock.path);
}

Listing 2-3: unique class is a host.native feature

If the code must stay portable, keep host.dual, declare an ordinary class, and give the one binding that owns it single-owner semantics with unique let lock = FileLock(…). unique let works in every mode; chapter 17 shows how.

Inline assembly is the other common case:

This code does not compile
@mode(host.dual)

func cpuPause(): void {
    asm { "pause" }
}

func main(): void {
    cpuPause();
}
main.loup:4:5: error[E_ASM_NOT_COMPILED]: E_ASM_NOT_COMPILED: `asm { }` is only valid in a compiled mode (`web.compiled`, `host.native`, `freestanding`); the module mode is `host.dual` (§3-3.7)

Here there is no portable alternative: assembly is machine code for one architecture, so the function belongs in a host.native file.

What native binaries cannot do yet

Loup 1.25.1 builds native binaries for a large part of the language: value types, strings, collections, classes with reference counting, interfaces, closures, enums and match, and exceptions. Some features that the specification lists for host.native are not implemented in the native code generator, or not shipped in the standard library, yet:

Feature Status in 1.25.1 Chapter
async / await Runs on the interpreter (eagerly) and in WebAssembly; a native build refuses it 21
Template fences (html, css, …) Run on the interpreter and in WebAssembly; a native build refuses them 14
for concurrent Refused by native builds 20
weak / unowned fields Accepted, but a native binary keeps them as strong references, so reference cycles leak 9, 18
int overflow Wraps silently; the specification says it should throw 3
Printing an enum whose payload is a tuple or a Result (and deeper nestings such as Option[list[…]]) Refused by native builds 8
--lib shared / --lib static Write a placeholder file, not a loadable native library 24
native.ffi, native.mem, native.simd, native.gpu, ui.desktop, sync.* Not in the standard library 19–23
arc let / arc var Not parsed 9

When a native build meets one of these, it stops with an E-BUILD-NATIVE error that names the construct rather than producing a binary that behaves differently. For example, building a program with an async func:

error[E-BUILD-NATIVE]: unsupported in managed native (libc foundation): async function `half` (native builds have no task scheduler: `async`/`await` run on the interpreters only); …

The chapters that cover these topics say exactly what works today and mark the rest ⚠ Planned.

Coming from another mode

From scripts (host.scripted). The language is the same; the structure changes. Code moves into main() and functions, every parameter gets a type, and Any becomes a warning. You will start writing ownership verbs, because a native program's correctness depends on who may mutate what. The __name__ == "__main__" guard has no purpose in an AOT mode, since main() is always the entry point.

From the browser (web.*). Host capabilities such as the file system (readText, writeText), processes (exec) and the environment (runtime.args(), env) are available. There is no DOM.

From freestanding. You gain the runtime: reference-counted classes, exceptions, growable collections and the standard library. You lose @entry, @interrupt, @naked and the manifest's panic policy, which exist only in freestanding.

Summary

  • Loup has six modes. This book covers host.native (.lphc), compiled to a native binary, and host.dual (.loup), which can also run on the interpreter or as WebAssembly.
  • Both start at main(), forbid top-level statements, expect static types (Any is W_ANY_IN_AOT_MODE), present ARC semantics, and check the ownership system at compile time.
  • host.native adds unique class, asm { } and Volatile[T]; using them in host.dual is an error (E_UNIQUE_CLASS_ARC_BINDING, E_ASM_NOT_COMPILED).
  • Choose host.native for binaries that need those features; choose host.dual for code that must run everywhere. Every host.dual file is a valid host.native file.
  • Native builds in 1.25.1 refuse async, template fences and for concurrent, keep weak references strong, and let int arithmetic wrap. The relevant chapters mark these ⚠ Planned.
  • Chapter 3 starts on the language itself: bindings, numeric types, and what happens on overflow.