Skip to content
Noodle
InstallLearnPlayground
GitHub

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.

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())
end

Put the host implementation in a companion file next to the primary source:

clock.nl
clock.extern.mjs

The 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.

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) -> Unit

The 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.

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) -> Int
end

The 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 an opaque nominal type whose representation is supplied by the host and whose constructor set is empty:

module JsonBridge
export extern datatype JsonHandle end
@link("parse_json")
export extern func parse(text : String) -> JsonHandle

Noodle cannot construct, pattern-match, or structurally decompose JsonHandle. Values enter or leave that type only through external functions whose declarations describe the operations the host supports. Define explicit debugging, equality, or conversion operations when the host-backed type needs them; structural behavior is not generated automatically.

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, return the standard Task shape and bridge the host promise according to the asynchronous code guide. 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: Advanced pattern matching.