A Refingere program can call JavaScript and TypeScript, and JavaScript and TypeScript can call it. Either way it's done with ordinary functions marked @js - which way depends on whether the function has a body. The JavaScript itself lives in its own modules, written in JavaScript or TypeScript. Values are converted as they cross, so neither side sees the other's representation.
A @js declaration - no body - is a JavaScript function, implemented by the module it names: Refingere calls it. A @js definition - with a body - is a Refingere function JavaScript can call. See @js.
@js("./text.ts", "slugify") on a function without a body makes slug call the module's slugify export. The path is relative to the .rfn file; a package name such as node:path works too. Leaving out the second argument calls the export with the function's own name. The declared types say what crosses: the arguments are converted to JavaScript values, and the result back into the declared return type - so the lengths above come back as U64s, not as floats.
| Refingere | JavaScript |
|---|---|
F64, F32 | number |
U8 ... U256, S8 ... S256 | number, or bigint past 253 - either comes back as the declared type - an OverflowError when it doesn't fit |
String, Boolean | string, boolean |
undefined | undefined - and null comes back as undefined |
Array, Map, Set | an array, a Map, a Set, item by item |
Bytes | Uint8Array |
| a struct | an object of its fields - and an object comes back as the struct the function's return type names, field by field, or as a Map<String, Unknown> |
| an enum case | its name as a string; with a payload, { variant, values } |
JsValue | anything else - a function, a class instance, a DOM node - held as it is and passed back unchanged |
A JsValue can be kept, compared and handed to another JavaScript function, which gets the very same object - and Refingere can reach into it without writing any JavaScript: get and set its properties, call its methods, construct an instance of it, or await it with toPromise when it's a JavaScript promise (see The Browser). A Refingere function or lambda can't be passed to JavaScript as it is - JsValue.callback wraps one in a function JavaScript can call.
An exception thrown on the JavaScript side comes back as an Error with the exception's message, so it's handled like any other - with !!, ?? or match. Going the other way, a @js definition that ends with an error throws a JavaScript Error with its message.
A shared library written in C - or anything exporting C functions: Rust, Zig, Go - is called the same way, through a module that opens it with Bun's bun:ffi. The module says what C types each function takes and gives; the @js declaration says what Refingere sees.
Numbers and truth values cross as they are. A string goes to C as a pointer - ptr(Buffer.from(text + "\0")) in the module - and comes back with new CString(pointer); memory the library hands out stays the module's to free. It runs where Bun does - with --run and --test, or a program compiled with --js - but not in plain WebAssembly or a native executable, which have no JavaScript to call.
Compiled without --js, a module is plain WebAssembly (WASI): wasmtime runs it, so it can be a server or a command-line tool, and it will compile to a native binary the same way. There's no JavaScript there: a @js definition is an ordinary function, and a @js declaration panics if it's reached. --run and --test compile with --js whenever the program calls JavaScript, and run it on Bun.
A @js definition is callable from JavaScript, under its own name or the one given. --compile --js writes, next to the module, a loader (.mjs) with the JavaScript modules the program calls bundled in, and its TypeScript declarations (.d.mts). load() instantiates the module and returns an object with a method per @js definition - plus main() when the program has a @main, returning its exit status. It takes the same options a program run from the command line gets: args, env and the folders it may open. See @js and Command Line.
Each function's TypeScript type comes from its Refingere one, with the table above read the other way: a 64-bit or wider integer is number | bigint, a struct an object type of its fields, and an Error ! T result just T, since the error is thrown. A parameter with a default can be left out.
With --target=js, the whole program is compiled to JavaScript rather than WebAssembly: one ES module, with the JavaScript and TypeScript it calls imported - and, written with --compile, bundled in. Its @js definitions are the module's own exports, so there's nothing to load first, and run() runs its @main (or, built with --test, every @test), taking the same options as load() above and giving the exit status. Run as a script - node words.mjs - it runs @main by itself. Values cross the same way, by the table above, and the program behaves the same as it does compiled to WebAssembly. See Compiling to JavaScript.
--compile --js=browser writes a loader for a web page: its load() is async, fetching and compiling the module next to it, and the program's &stdout and &stderr go to the browser's console. With both directions, the page and the program work together: the script calls update on every keystroke, and update writes into the page through the @js declarations - a DOM element crosses as a JsValue, and comes back to JavaScript as the very same element. The repository's examples/js-website is a complete page built this way, with its build script.