The built-in functions, defined once. module.f.js holds two tables and exports runtime, three functions an executor asks: two over the tables, and the body of the in operator. The member table holds, by name, the member functions whose body is written, startsWith: each a FunctionalScript function of the receiver alone, answering undefined for a receiver the name is no member of, or the function of the arguments as the call passed them, undefined past the end — the body dispatches on its receiver's type itself, and an executor asks it in two steps, the first before any argument exists. The statics table holds the static functions by id, the path ../static admits joined by . as the EDAG spells the node — Object.is — each a function of the arguments alone, written here and exported nowhere else: a module calls the built-in, Object.is or Number.isInteger, never the body, so the body is a VM's and the helper a built-in replaced moves in here when the built-in is admitted. An id every executor binds natively, Object.entries, holds the built-in itself, so every admitted id has a value and the table is complete. runtime.static answers the entry of statics for an id, or undefined; runtime.dot answers the callee of a member call, below; runtime.in is the body of "k" in o, the enumerable own entry of an object, an array or a function, or the length an array and a function own, and a throw on a primitive, which an executor answers the operator's node by, or substitutes natively — the key rule that keeps its answer JavaScript's is the parser's (2346-in-operator.md). The tables are exported too, for the type pins, the proof and the Rust printer; no executor reads them. The proof checks every static body against the host's own built-in at the same path. The design of the statics is ../todo/runtime-static.md. A name without a body is absent, and each VM answers it from its own native table. The proposal and the steps still to take are ../todo/runtime.md.
Which names a module may call stays ../prototype's: the prototype name lists and prohibitedCalls for the parser, allowedCalls for every executor. types.ts pins every key of member to that set, and every entry to a body of the receiver alone, so a body for a name the language does not admit fails tsc, and proof.f.mjs checks the same at run time. Which types a name is a member of is the body's own first step, typeof self === 'string', not a key of the table: one function per name, for every receiver. The keys are not the admitted set and need not be: a table keying every admitted name with a marker in place of a body would only duplicate the parser's list.
Two kinds, and both are each VM's to bind natively:
- the primitives, which no body can define in terms of the rest — for a string, the code-unit read
charCodeAtand the substringslice; among the static functions,String.fromCharCode, for constructing UTF-16 strings from code units, andObject.entries, since enumerating keys is what no body can do otherwise, which../staticlists asnatives;lengthis a property each value owns and is on no list, and an array element is read by a runtime number index, which the language has. A member primitive a body needs is an intrinsic (../intrinsic): a function the body calls,slice(self, start, end)infoundandcharCodeAt(self, at)incodePointAt, whose own body is the one member call and which the compiler recognizes whole as a node every executor answers natively, so no body of this module makes a member call; - the names whose body waits on further implementation —
toLowerCaseandtoUpperCaseon a string from a code unit, now admitted as the nativeString.fromCharCode, over which the Unicode case tables are written; the number formatters and a nondecimaltoStringradix onBigInt, over which the exact decimal expansion of a double is written. The casing bodies can now use the admitted native static.
Every other name is absent only until its body is written; the rest of the string and array names are the next batches.
A VM answers a call on a receiver in the order the runtime's dot writes once: the receiver's own property, its presence asked by hasEntry so that an own undefined is found and its value read by entry; else the runtime's body, asked for the receiver alone — the function of the arguments, which the VM then calls, or undefined; else the TypeError for calling undefined, with nothing built on the way. A native implementation stands in for a body a name does not have yet, asked where dot answers undefined: every VM asks dot first and its natives after, so a body answers its name wherever it is called, and the native table holds the names without one, which ../todo/runtime-members.md gives their bodies one at a time. The guard of an optional call, a.startsWith?.(x), asks the same steps before it evaluates x, the body's first step among them; a body is pure, so a guard and a call ask it twice at no cost but the call. A native beside a body is unreachable, since dot answers first; a VM that measures a gain in a native substitutes it for the body in its dispatch (DESIGN.md §13), never as an entry in the table. The corpus in ../../nanvm/member checks every body against the host engine, which stays the oracle for a name without one, as it was for every name before. The completeness table ../../nanvm/methods generates counts a body as an answer: every admitted pair is answered, natively or by a body, and no other pair is.
- The represented interpreter —
edag/value/method— implements most names natively through the host's own built-ins, and the rest from the runtime its executor is given: the evaluatedruntimeexport, in theruntimefield of anamnesiaormemocontext (edag/operations/types.ts), which reaches every function the invocation calls. The transpiler takes it asinterpret(load), aRuntimeLoadrun once the root is known to be a module;fjs compileloads it from the package it runs from,fjs/js/runtime/module.f.jsthere, when a data output interprets a module and not before;fjs/nanvm/harnessloads it the same way, from the repository root, for the fixture expectations it writes; and the compiler's demo, which has no file system, takes the generated EDAG. An executor given no runtime answers a body's name with theTypeErrorof a VM that binds nothing, never with a host built-in. - NaNVM —
npm run gencompiles this module tonanvm/lib/src/vm/gen.runtime.rsthrough the compiler's runtime printer, the compiler being the generator: theruntimeexport as a value,runtime::<A>(), printed by the ordinary backend as any module is, which whoever runs compiled modules builds once and hands each as its parameter, and through which a compiled module reads a static and a call step asks the body of a name ("How NaNVM finds it" below). No VM holds a runtime. The native tables invm/lambdakeep every name without a body.
The generator also writes the module's EDAG as gen.edag.data.js beside it, for the one consumer that needs the runtime as data without the compiler: the corpus proof ../../nanvm/proof.f.mjs, which runs every member-function case through amnesia with this runtime and against the host. Nothing the compiler imports reads a generated file: npm run gen deletes every gen.* before it regenerates, and a generator whose own imports needed one could not start.
One source, three forms, and each executor runs the one it can:
The host function is what import gives and runs on host values only; the runtime's own proof uses it and nothing else does. The EDAG is what the interpreter uses: a consumer evaluates it once to the module's export object and passes that in the executor's context, or, where it already has the compiler loaded, interprets the source on the spot, as the harness does. The Rust is what NaNVM binds. Whatever the form, a call step resolves the same way: the receiver's own property, the runtime's body, the VM's native implementation for a name without one, the TypeError.
A compiled module takes the runtime as its parameter, pub fn module<A: IVm>(runtime: Any<A>) -> Result<Any<A>, Any<A>>: the value runtime::<A>() in nanvm-lib answers, this module's runtime export printed by the ordinary backend as any module is, which whoever runs compiled modules — the harness, a program embedding a VM — builds once and hands to each; two VMs in one process build two. The module reaches every built-in through it. For [1].map(() => 5), the output is Any::dot(c0, string_any("map")).end_call(&runtime, c1), and the library takes it from there:
sequenceDiagram
participant M as compiled module
participant L as nanvm-lib call step
participant T as native table
participant R as the runtime value
M->>L: Any::dot([1], "map").end_call(&runtime, args)
L->>L: own property? none on an array
L->>R: runtime.dot(receiver, "map")
R-->>L: the function of the arguments, or undefined
L->>T: method(receiver, "map"), for undefined
T-->>L: the native, while map has no body
L->>L: f.call(args), or the TypeError
L-->>M: the answer, or Err(thrown)
Member::call in vm/lambda/member.rs resolves in that order — the receiver's own property, the runtime value's dot, asked for the receiver and the key and answering the function of the arguments or undefined, the native table in vm/lambda/method.rs for a name without a body, the TypeError for calling undefined — and the ?.() guard asks the same lookups, before any argument is evaluated. A call step on a member takes &runtime for that; a step on a plain value, and a property read, take none. The native table shrinks as the names it answers get bodies (../todo/runtime-members.md), down to the intrinsics' names, slice and charCodeAt, which no body defines; its step stays while the table holds a name.
A static function is a value with no receiver. The EDAG spells Object.is as the value ['Object.is'], and Number.isInteger as ['Number.isInteger'], and the Rust backend prints the read as static_read(&runtime, "Object.is")?, the value's static function called with the id: the one closure the runtime holds for it, so every read is the same function and === compares identities, as JavaScript's does, which nanvm/harness/fixtures/object-is.mjs checks on NaNVM against the interpreter, and number-predicates.mjs for the Number predicates over every kind of value. A path the value holds no function at is refused, never answered with undefined: the parser admits a static only from ../static, and the represented interpreter, asking its context's runtime's static the same way, fails naming the id where a runtime answers no body. Object.entries has no body: the statics table lists it as the built-in itself, the represented interpreter answers it with a function value of its own, edag/value/entries, and in NaNVM's value it is a closure over Any::entries (nanvm/harness/fixtures/object-entries.mjs). The completeness test in vm/lambda/method.rs checks every path ../static admits is a function of the value.
Where the runtime rides. A closure captures nothing from the Rust scope around it, so a function body that reaches a built-in — a member call, a static read, in it or in a function it makes — holds the runtime in its frame, one slot after its own, and reads it as A::frame(self_)[k] as it reads any slot; a module whose scope reaches none takes its parameter as _runtime, so every module has the one signature.
The runtime module itself is printed with no runtime in scope. The runtime printer, fjs/compiler/rust/runtime, hands this module's runtime export to the same backend, into pub fn runtime<A: IStaticFunction>() -> Result<Any<A>, Any<A>>, with two differences. Every static body the statics table names carries the NativeFunction text of its id, function is() { [native code] }, in place of its body's, since a static function is the built-in, not its body, and String(Object.is) is observable — the represented interpreter spells the same text from the static node, staticText in fjs/edag, and object-is.mjs holds the two to one text; a member body, a helper such as found, and dot and static themselves keep their bodies' texts, since no program reads any of them as a value. And a member call or a read of a static function this module defines, anywhere in it, is refused by name, since the scope has no runtime to dispatch through: the core may not use what it defines. A body reaches a native through an intrinsic, slice, whose call step asks no runtime, end_call_native, and another body by reading it as a value. gen.runtime.rs is included into the crate by #[path], which is why it says use nanvm_lib::… and the crate declares extern crate self as nanvm_lib.
Nothing is held by the library and no VM binds anything: a VM is a value representation, and the runtime is a value a module is given. A VM that wants a native implementation of a name that has a body substitutes it by a measured change to the call step's order (DESIGN.md §13); the table is asked after dot, so an entry beside a body is unreachable.
The shape is a bootstrap — the compiler writes the runtime, and a compiled compiler runs under it — and two invariants keep it from being a cycle:
- Compiling needs no runtime; only running does. Parsing and lowering run no body. The host engine runs the compiler as JavaScript, its own built-ins answering the compiler's
.mapand.slice, sonpm run gencan delete every generated file and still write the runtime's EDAG and Rust from source. A runtime is needed to run: an executor evaluating a module whose initialization calls a body's name, or the compiler itself once compiled and run on a VM, where the runtime the previous compiler wrote is the fixed point. An optimizing pass that evaluated at compile time would be the one place the compiler runs bodies; it takes the runtime as an input, as the executors do, and folds nothing a body answers when given none, so the runtime is always compiled with no runtime, whatever the pass does elsewhere. - Nothing in the runtime calls a member function or reads a static function: no initializer, and no body. The module builds closures and nothing else, and so does every module it imports, transitively — a helper shared with the rest of
fjs/must hold this too, or be inlined — so the runtime can be interpreted from nothing. Loading it from source relies on exactly that:runtimeAtinterprets it undernoRuntime, so a member call or a static read in any initializer of the closure fails the load rather than passing unnoticed. A body reaches a native through an intrinsic,sliceinfoundorcharCodeAtincodePointAt, called as a function (../intrinsic), and another body by reading it as a value, never by its name: the core may not use what it defines, and the whole module is the core, which the proof checks on the compiled EDAG — no chain step anywhere in it, and no static node but the natives the table lists as themselves,Object.entries, which every executor binds and no body reads. This is what lets NaNVM hold the runtime as a value its bodies never name: the runtime printer prints the module with no runtime in scope, so a member call or a static read of a body anywhere in it, body or initializer, is the backend's refusal, by name, and a native static is the built-in itself ("How NaNVM finds it").
- Write the body here — a function of the receiver,
undefinedfor a receiver the name is no member of and the function of the arguments otherwise — with its proof case by case, andnpm start compileaccepting the module. - Remove the interpreter's native case in
edag/value/method, so the body is what answers it there. - Remove NaNVM's entry in
vm/lambdaand the typed method under it:dotis asked first, so the entry is unreachable once the body exists, and the completeness test counts the body as the answer. npm run gen, then the corpus: the name's cases innanvm/memberrun the body throughamnesiaand on NaNVM.
startsWith is the first; codePointAt, over the intrinsic charCodeAt, the second.