Extern bindings
An extern declaration is a typed boundary between Noodle and the host
runtime. The Noodle source declares the name, parameters, and result type; a
companion ES module supplies the implementation. The compiler type-checks
calls against the Noodle declaration, but it does not inspect the JavaScript
function’s body.
Declare an external function
Section titled “Declare an external function”An external function has no Noodle body. Use @link when the host export has a
different name from the Noodle declaration:
module Clock
@link("now_ms")export extern func now_milliseconds() -> Int
func main() -> Unit do Debug.trace(now_milliseconds())endPut the host implementation in a companion file next to the primary source:
clock.nlclock.extern.mjsThe companion’s named export must match the @link key:
export const now_ms = () => Date.now();For a source file named clock.nl, the JavaScript backend looks for
clock.extern.mjs. A missing companion is a build error. A missing or
non-callable named export, an exception thrown by the companion, or a value
that violates the declared boundary follows the host runtime’s normal failure
behavior; Noodle does not insert an implicit conversion layer.
The binding name
Section titled “The binding name”With @link("key"), key is the exact named export selected from the
companion module. @link accepts no argument or one string argument:
module Environment
export extern func read_mode() -> String
@link("set_mode")export extern func write_mode(value : String) -> UnitThe first declaration binds to read_mode because no @link attribute was
provided. The second binds to set_mode. The binding key is implementation
metadata; it is not part of the exported function type or the module’s public
signature. Consumers only see the typed Noodle declaration.
extern is a contextual word. It introduces an external declaration only in
the expected declaration forms; it remains available as an ordinary lower-case
identifier elsewhere. External functions may be generic, but their parameters
are positional. They use the same visibility rules as ordinary functions:
without export, the declaration is private to its module.
External methods and datatypes
Section titled “External methods and datatypes”An extension can provide an external method when its implementation needs a host operation but should be called with dot syntax:
module NativeText
extension TextMethods for String @link("text_length") extern method length(value : String) -> IntendThe host companion exports text_length with the same positional calling
shape:
export const text_length = (value) => value.length;An extern datatype is different from an external function. It declares a
matchable nominal type whose representation is supplied by the host. A
non-empty extern datatype names its companion classifier with
@js_recognizer and tags every constructor with @js_tag — either an
integer or a non-empty string tag, one shape per datatype (all-int or
all-string, never a mix):
module BodyInitBridge
@js_recognizer("recognize_body")export extern datatype BodyInit @js_tag(1) Blob @js_tag(2) Streamendexport const recognize_body = (value) => { if (value instanceof Blob) return 1; if (value instanceof Stream) return 2; throw new Error("unrecognized BodyInit value");};The recognizer contract: a synchronous, pure function from host value to
Int | String tag, total over the datatype’s inclusive constructor set
(direct constructors plus inherited bases). The declared tag kind selects
the comparison primitive. Lowering calls the expected type’s
recognizer once per scrutinee and switches on the result; an unknown tag or
a throwing recognizer is a loud runtime error. @link survives only on
payload fields, where it renames the read host property. The same trust
posture as extern func returns applies: recognizer purity, totality, and
base/derived agreement are conventions, not checked rules.
Host-visible field names
Section titled “Host-visible field names”Field names are an implementation detail, not an ABI. Whole-program builds rename record and datatype-payload fields to short generated names, so a host companion must not rely on the source spelling of a field it was not promised.
Mark a field the host reads or constructs with a bare @link attribute. The
attribute pins the runtime property name to the source name:
module CursorBridge
export opaque datatype Cursor Cursor{ @link offset: Int, }endThe same spelling works for record types, which is also how a companion-built value keeps its shape:
module ChildBridge
export type ChildResult = { @link exit_code: Int, @link output: String,}export const spawn_child = (command, on_exit) => { const child = start_child(command); child.on("exit", code => on_exit({exit_code: code, output: ""})); return child;};The companion reads cursor.offset in one direction and constructs
{exit_code, output} in the other; both spellings hold because the fields
are pinned. @link on a field takes no arguments; @link("other") is an
error. Fields without the attribute may still cross an extern boundary, but
they arrive opaque: the host sees whatever name the build assigned, with no
guarantee. Publish a field to the host only by annotating it.
Tuple positions (_0, _1, …) never need the attribute: they are positional,
not names, so there is nothing to rename.
Package and artifact boundaries
Section titled “Package and artifact boundaries”The companion belongs to the package containing the primary .nl source. It
is not a separately importable Noodle module and it is not listed in
nlpkg.json as a source dependency. An exported extern function is published
to other Noodle modules through its ordinary function signature; the absolute
path of the .extern.mjs file is kept out of .nli signatures.
Parts do not create extra host boundaries. If a primary source includes
functions.part, an extern declaration in that part still uses the primary
module’s one .extern.mjs companion. A .part file never gets its own
companion.
Keep host code narrow: use an extern binding for platform capabilities that Noodle cannot perform directly, such as filesystem access, process control, JavaScript APIs, or a promise bridge. Put ordinary language logic in Noodle so it remains statically checked, testable, and portable across host setups.
For an asynchronous host operation, declare the promise it produces and settle
it only after the operation really stopped; the
asynchronous code guide describes the ownership, completion,
and runtime-failure contract that a host operation participates in. For
compiler-provided operations that require compiler knowledge, use the
standard library’s native or runtime-provider mechanisms instead of inventing
an extern binding; see Structural capabilities
for an example of the distinction.
Next: Prelude and modules.