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 thelang.interop.jsfacades). 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 knownand'parseInt' is not callable.@jsexporton 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.wasmfile 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:
@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]:
@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:
@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:
@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 jscalls do not compile,@jsexportis ignored, andlang.interop.jsdoes not exist. Use the standard library for files, processes, JSON and sockets.runtime.capabilities()andruntime.has(name)work today and let a script check its permissions up front.- Write
readText(path)(orruntime.fs().readText(path)); the specified property formruntime.fs.readText(path)fails at run time. - Planned:
extern jsdeclarations with trusted types andTask[T]for promises,@jsexportforbrowserandservicetargets, and the typedlang.interop.jsfacades.