The Loup Scripting Book — contents
  1. Chapter 1: Getting Started
  2. Chapter 2: Variables and Data Types
  3. Chapter 3: Functions and Closures
  4. Chapter 4: Control Flow
  5. Chapter 5: Collections
  6. Chapter 6: Structs
  7. Chapter 7: Enums and Pattern Matching
  8. Chapter 8: Classes and Objects
  9. Chapter 9: Interfaces and Generics
  10. Chapter 10: Error Handling
  11. Chapter 11: Modules and Packages
  12. Chapter 12: Strings, Interpolation, and Template Fences
  13. Chapter 13: Concurrency
  14. Chapter 14: Reflection and Dynamic Compilation
  15. Chapter 15: Reactive UI
  16. Chapter 16: Testing
  17. Chapter 17: JavaScript Host Interop
  18. Chapter 18: Building Script Tools
  19. Chapter 19: Memory Safety and Resource Management

Chapter 17: JavaScript Host Interop

⚠ Planned — not in Loup 1.25. This chapter describes the design in the Language Reference (§2-3.1 extern js, §2-3.2 @jsexport, §2-3.3 the lang.interop.js facades). The toolchain does not implement it yet, so its listings are design sketches and are not verified.

What you'll learn: how scripts actually run today and what that means for JavaScript interop; how to check which capabilities a script was granted, which works now; and the planned design for calling JavaScript from Loup (extern js), exposing Loup to JavaScript (@jsexport), and the typed lang.interop.js facades.

Scripts Do Not Run on a JavaScript Host

Earlier editions of this book said that scripts run on Node, Bun, Deno or in a browser. That is not what ships. In Loup 1.25, loup run type-checks a script and runs it on an interpreter built into the loup binary, as an ordinary process on Linux, macOS or Windows (Chapter 1). There is no JavaScript engine underneath, no --host option, and no JavaScript globals to call.

The interop features are accordingly absent:

  • extern js func parseInt(s: string, radix: int): int; is accepted by the parser, but a call to it fails to compile: type of 'parseInt' is not yet known and 'parseInt' is not callable.
  • @jsexport on a function is accepted and ignored; the function is an ordinary Loup function.
  • from lang.interop.js import (fetch); fails: 'lang.interop.js' is not a standard-library module.
  • A WebAssembly build for the browser (loup-bootstrap build --target browser) produces a bare .wasm file with no JavaScript loader around it; see The Loup Web Book.

For the things scripts usually wanted JavaScript for, use the standard library: files and processes through the prelude (readText, writeText, exec) and io.fs, io.path and io.proc; JSON through system.json; sockets through net.tcp; an HTTP server through net.http. (An HTTP client is planned; net.http is server-only in 1.25.)

What Works Today: Checking Capabilities

Every script runs with a set of capabilities granted by loup run (Chapter 1, "Permissions"). A script can ask which ones it has, which lets it fail early with a useful message instead of halfway through its work:

@mode(host.scripted)

let caps = runtime.capabilities();
println(i"granted: {$ caps $}");

for (needed in ["fs", "net"]) {
    if (runtime.has(needed)) {
        println(i"{$ needed $}: yes");
    } else {
        println(i"{$ needed $}: no (run with --allow-{$ needed $})");
    }
}

Listing 17-1: caps.lpscr reports its capabilities

With the default permissions:

loup run caps.lpscr
granted: ["entropy", "fs", "proc"]
fs: yes
net: no (run with --allow-net)

With loup run --allow-net caps.lpscr, the list becomes ["entropy", "fs", "net", "proc"]. (--allow-all also reports names such as interop, ui and eval that belong to features this chapter and Chapters 14 and 15 describe as planned; holding the capability does not make the feature exist.)

A tool that needs the network can refuse to start without it:

@mode(host.scripted)

class ConfigError : Exception {}

if (not runtime.has("net")) {
    throw ConfigError("this tool needs network access: run it with --allow-net");
}
println("starting");

Listing 17-2: needs_net.lpscr stops early without the net capability

Run without --allow-net, it stops before doing any work:

loup run needs_net.lpscr
Uncaught ConfigError: this tool needs network access: run it with --allow-net
  at <main> (needs_net.lpscr:6:11)

With loup run --allow-net needs_net.lpscr it prints starting.

Mistake: runtime.fs as a property

The Language Reference (§2-5.1) writes capability namespaces as properties — runtime.fs.readText(path). In Loup 1.25 that compiles and then fails when it runs:

This code throws at run time
@mode(host.scripted)

writeText("greeting.txt", "hello from a file");
let text = runtime.fs.readText("greeting.txt");
println(text);
Uncaught RuntimeError: internal interpreter limitation: no field `fs on class system.Runtime`

The prelude functions are the simplest way to work with files in a script, and they are what the rest of this book uses:

@mode(host.scripted)

writeText("greeting.txt", "hello from a file");
let text = readText("greeting.txt");
println(text);
hello from a file

(The method form runtime.fs().readText(path) also works today, but it is not the specified spelling.)

The Planned Design

The Language Reference places JavaScript interop on targets that have a JavaScript runtime: extern js on browser, node, bun and deno; @jsexport on browser and service. It is mostly web material, and The Loup Web Book covers the browser side. The essentials:

Calling JavaScript: extern js

An extern js declaration gives a JavaScript function or value a Loup type (§2-3.1). Declarations end with ;. The declared types are trusted, not checked, so a mismatch is a run-time error. A function that returns a JavaScript Promise is declared to return Task[T]:

Fragment — not a complete program
@mode(web.scripted)

extern js func parseInt(s: string, radix: int): int;
extern js func fetch(url: string, opts: Any): Task[Any];
extern js val document: Any;

let port = parseInt("8080", 10);
println(port + 1);

Listing 17-3: Declaring and calling JavaScript functions (design sketch)

extern js in a host.native or freestanding file is E_EXTERN_JS_NOT_NATIVE; native code talks to C through extern native instead (The Loup Native Book). An extern js declaration that passes a typed array backed by a SharedArrayBuffer must carry @unsafe("sharedMemory: …") explaining who shares it.

Exposing Loup: @jsexport

@jsexport makes a module-level function callable from JavaScript as a named export of the compiled module (§2-3.2). An async func becomes a function returning a Promise:

Fragment — not a complete program
@mode(web.compiled)

from system.json import (parse);

@jsexport
func countKeys(json: string): int {
    return match parse(json) {
        Ok(v) => (v as dict[string, Any]).length(),
        Err(_) => -1,
    };
}

@jsexport
async func summarize(text: string): Task[string] {
    return i"{$ text.split(" ").length() $} words";
}

Listing 17-4: Two functions exported to JavaScript (design sketch)

Arguments are converted from JavaScript to the declared Loup types; a value that cannot be converted raises FfiTypeError. await in a non-async export is E_JSEXPORT_AWAIT, and @jsexport on a target other than browser or service is E_JSEXPORT_NOT_BROWSER.

Typed facades: lang.interop.js

Rather than hand-writing extern js declarations, the design provides typed wrappers for the common Web and Node APIs (§2-3.3): fetch returning a Response with status, ok, text() and json(); setTimeout, setInterval and their clear… functions; and, in the browser only, querySelector, Element and Event:

Fragment — not a complete program
@mode(web.scripted)

from lang.interop.js import (fetch);

async func status(url: string): Task[int] {
    let response = await fetch(url);
    return response.status;
}

Listing 17-5: The typed fetch facade (design sketch)

The specification recommends these facades over raw extern js declarations wherever they cover what you need.

Summary

  • In Loup 1.25, scripts run on an interpreter on the host operating system, not on Node, Bun, Deno or a browser, so there is no JavaScript to call.
  • extern js calls do not compile, @jsexport is ignored, and lang.interop.js does not exist. Use the standard library for files, processes, JSON and sockets.
  • runtime.capabilities() and runtime.has(name) work today and let a script check its permissions up front.
  • Write readText(path) (or runtime.fs().readText(path)); the specified property form runtime.fs.readText(path) fails at run time.
  • Planned: extern js declarations with trusted types and Task[T] for promises, @jsexport for browser and service targets, and the typed lang.interop.js facades.