node-js 0.1.13

JavaScript as a fusevm frontend: a lexer/parser and compiler to fusevm::Chunk on a JsHost object heap, with no bespoke VM or JIT
Documentation
//! Regenerate `src/arity.rs` — the `name`/`length` of every ECMAScript
//! intrinsic function, keyed the way this frontend names its builtins.
//!
//! ```text
//! cargo run --bin gen-arity > src/arity.rs
//! ```
//!
//! Every ECMAScript intrinsic's `length` is NORMATIVE (ECMA-262 clause 20-28
//! gives one for each), and its `name` is the property key except where two keys
//! share one function object (`String.prototype.trimLeft` IS `trimStart`). That
//! is why this table is generated by READING a reference engine rather than
//! hand-written: the values are the specification's, and a hand-copy of ~480
//! numbers would rot the first time one of them was mistyped.
//!
//! The core-module surface (`fs.readFileSync`, `util.format`, …) is deliberately
//! NOT included: those arities come from the signatures of Node's own JavaScript
//! implementations, are not specified anywhere, and change between releases.
//! Reading `.length` off one of those still answers `undefined` here.
//!
//! Dev tool — needs a reference `node`, so nothing in CI runs it; the generated
//! table is checked in.

use std::io::Write;
use std::process::Command;

/// Walk the intrinsics in the reference engine and print one `key\tname\tlength`
/// row per function. Keys are spelled the way `JsObj::Builtin` spells them: a
/// bare global (`parseInt`), a constructor (`Array`), a static
/// (`Array.from`), or a prototype-method thunk (`@proto:Array:slice`).
const ENUMERATE: &str = r#"
const ctors = ['Object','Function','Array','String','Number','Boolean','Symbol','BigInt','Math','JSON','Reflect','Promise','Map','Set','WeakMap','WeakSet','Date','RegExp','Error','TypeError','RangeError','SyntaxError','ReferenceError','EvalError','URIError','AggregateError','ArrayBuffer','DataView','Uint8Array','Int8Array','Uint8ClampedArray','Int16Array','Uint16Array','Int32Array','Uint32Array','Float32Array','Float64Array','BigInt64Array','BigUint64Array','WeakRef','FinalizationRegistry','Proxy','TextEncoder','TextDecoder','URL','URLSearchParams','Iterator'];
const globals = ['parseInt','parseFloat','isNaN','isFinite','encodeURIComponent','decodeURIComponent','encodeURI','decodeURI','structuredClone','queueMicrotask','btoa','atob'];
const rows = [];
const add = (key, f) => { if (typeof f === 'function') rows.push(key + '\t' + f.name + '\t' + f.length); };
// The well-known symbols this frontend represents, spelled the way it spells
// them as property keys: `Symbol.iterator` is the internal key `@@iterator`.
// A symbol-keyed method is a real intrinsic with a real `name` and `length`
// (`Array.prototype[Symbol.iterator].name` is `'values'`, NOT `'@@iterator'`),
// so leaving it out of the table did more than lose two numbers: it left the
// caller with no way to ask whether a symbol-keyed method EXISTS, and a
// prototype read of any absent one synthesized a function.
const wellKnown = new Map([[Symbol.iterator,'@@iterator'],[Symbol.asyncIterator,'@@asyncIterator'],[Symbol.toPrimitive,'@@toPrimitive'],[Symbol.toStringTag,'@@toStringTag'],[Symbol.hasInstance,'@@hasInstance']]);
const members = (holder, pre) => {
  for (const k of Object.getOwnPropertyNames(holder)) {
    if (k === 'prototype' || k === 'constructor' || k === 'caller' || k === 'arguments') continue;
    // `Error.prepareStackTrace` is Node's own hook, not an intrinsic.
    if (pre === 'Error.' && k === 'prepareStackTrace') continue;
    let d; try { d = Object.getOwnPropertyDescriptor(holder, k); } catch { continue; }
    if (d && typeof d.value === 'function') add(pre + k, d.value);
  }
  for (const sym of Object.getOwnPropertySymbols(holder)) {
    const key = wellKnown.get(sym);
    if (!key) continue;
    let d; try { d = Object.getOwnPropertyDescriptor(holder, sym); } catch { continue; }
    if (d && typeof d.value === 'function') add(pre + key, d.value);
  }
};
for (const g of globals) add(g, globalThis[g]);
for (const c of ctors) {
  const C = globalThis[c];
  if (!C) continue;
  add(c, C);
  members(C, c + '.');
  if (C.prototype) members(C.prototype, '@proto:' + c + ':');
}
// The shared %TypedArray% intrinsic, which this frontend names "TypedArray".
members(Object.getPrototypeOf(Uint8Array), 'TypedArray.');
members(Object.getPrototypeOf(Uint8Array.prototype), '@proto:TypedArray:');
// The own property NAMES of each prototype, in the engine's own order and with
// each one's enumerability. This is a different question from the arity table
// above, which only knows about FUNCTIONS: `Map.prototype.size`,
// `RegExp.prototype.source` and the twelve `URL.prototype` components are
// accessors, so `Object.getOwnPropertyNames(Map.prototype)` cannot be derived
// from the function list. Order is V8's insertion order, not alphabetical, and
// is preserved here rather than sorted because that is what a script observing
// it sees.
const protos = [];
// Which of those members are ACCESSORS, and so answer a `get` descriptor and
// run a brand check rather than reading a slot. Neither existing table records
// it: `BUILTIN_ARITY` holds functions, and a name in `PROTO_MEMBERS` says
// nothing about its descriptor kind.
const accessors = [];
const accessorRow = (label, holder) => {
  const names = Object.getOwnPropertyNames(holder).filter((k) => {
    let d; try { d = Object.getOwnPropertyDescriptor(holder, k); } catch { return false; }
    return !!(d && d.get);
  });
  if (names.length) accessors.push(label + '\t' + names.join(','));
};
// The members that are NON-WRITABLE data properties. An inherited one refuses
// an assignment on any object below it (10.1.9.2), so `o[Symbol.toStringTag] =
// 'x'` on an object inheriting from `Map.prototype` is silently dropped. There
// are few, and all but one are symbol-keyed.
const readonly = [];
const readonlyRow = (label, holder) => {
  const names = [];
  for (const k of Reflect.ownKeys(holder)) {
    let d; try { d = Object.getOwnPropertyDescriptor(holder, k); } catch { continue; }
    if (!d || d.get || d.writable) continue;
    if (typeof k === 'symbol') {
      const desc = String(k).slice('Symbol(Symbol.'.length, -1);
      if (desc && String(k) === 'Symbol(Symbol.' + desc + ')') names.push('@@' + desc);
    } else {
      names.push(k);
    }
  }
  if (names.length) readonly.push(label + '\t' + names.join(','));
};
const protoRow = (label, holder) => {
  const mark = (holder, k, spelling) => {
    let d; try { d = Object.getOwnPropertyDescriptor(holder, k); } catch { return spelling; }
    return d && d.enumerable ? '+' + spelling : spelling;
  };
  const names = Object.getOwnPropertyNames(holder).map((k) => mark(holder, k, k));
  // SYMBOL-keyed members, under this frontend's internal spelling: a
  // well-known symbol is `@@` plus its description minus the `Symbol.` prefix,
  // so `Symbol(Symbol.iterator)` is `@@iterator`. They are members like any
  // other — `Symbol.iterator in []` is true — and a table built from
  // `getOwnPropertyNames` alone cannot say so. A symbol that is not well-known
  // has no such spelling and is skipped rather than guessed at.
  for (const s of Object.getOwnPropertySymbols(holder)) {
    const d = String(s).slice('Symbol(Symbol.'.length, -1);
    if (!d || String(s) !== 'Symbol(Symbol.' + d + ')') continue;
    names.push(mark(holder, s, '@@' + d));
  }
  protos.push(label + '\t' + names.join(','));
};
for (const c of ctors) {
  const C = globalThis[c];
  if (C && C.prototype) protoRow(c, C.prototype);
}
protoRow('TypedArray', Object.getPrototypeOf(Uint8Array.prototype));
for (const c of ctors) {
  const C = globalThis[c];
  if (C && C.prototype) accessorRow(c, C.prototype);
}
accessorRow('TypedArray', Object.getPrototypeOf(Uint8Array.prototype));
for (const c of ctors) {
  const C = globalThis[c];
  if (C && C.prototype) readonlyRow(c, C.prototype);
}
readonlyRow('TypedArray', Object.getPrototypeOf(Uint8Array.prototype));
console.log(rows.join('\n'));
console.log('===PROTOS===');
console.log(protos.join('\n'));
console.log('===ACCESSORS===');
console.log(accessors.join('\n'));
console.log('===READONLY===');
console.log(readonly.join('\n'));
"#;

fn main() {
    let oracle = std::env::var("NODE_JS_PARITY_NODE").unwrap_or_else(|_| "node".into());
    let dir = std::env::temp_dir().join(format!("node-js-arity-{}", std::process::id()));
    std::fs::create_dir_all(&dir).expect("temp dir");
    let script = dir.join("enumerate.js");
    std::fs::write(&script, ENUMERATE).expect("write script");
    let out = Command::new(&oracle)
        .arg(&script)
        .output()
        .unwrap_or_else(|e| panic!("running reference `{oracle}`: {e}"));
    let _ = std::fs::remove_dir_all(&dir);
    if !out.status.success() {
        panic!(
            "reference `{oracle}` failed: {}",
            String::from_utf8_lossy(&out.stderr)
        );
    }
    let version = Command::new(&oracle)
        .arg("--version")
        .output()
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_default();
    let stdout_text = String::from_utf8_lossy(&out.stdout).to_string();
    let (arity_text, rest) = stdout_text
        .split_once("===PROTOS===\n")
        .expect("enumerator emitted no prototype section");
    let (proto_text, rest) = rest
        .split_once("===ACCESSORS===\n")
        .expect("enumerator emitted no accessor section");
    let (accessor_text, readonly_text) = rest
        .split_once("===READONLY===\n")
        .expect("enumerator emitted no read-only section");
    // (constructor, member names) — kept in the engine's order, not sorted.
    let readonly: Vec<(String, Vec<String>)> = readonly_text
        .lines()
        .filter(|l| !l.is_empty())
        .map(|l| {
            let (ctor, names) = l.split_once('\t').unwrap_or((l, ""));
            let names = names
                .split(',')
                .filter(|n| !n.is_empty())
                .map(str::to_string)
                .collect();
            (ctor.to_string(), names)
        })
        .collect();
    let accessors: Vec<(String, Vec<String>)> = accessor_text
        .lines()
        .filter(|l| !l.is_empty())
        .map(|l| {
            let (ctor, names) = l.split_once('\t').unwrap_or((l, ""));
            let names = names
                .split(',')
                .filter(|n| !n.is_empty())
                .map(str::to_string)
                .collect();
            (ctor.to_string(), names)
        })
        .collect();
    let protos: Vec<(String, Vec<String>)> = proto_text
        .lines()
        .filter(|l| !l.is_empty())
        .map(|l| {
            let (ctor, names) = l.split_once('\t').unwrap_or((l, ""));
            let names = names
                .split(',')
                .filter(|n| !n.is_empty())
                .map(str::to_string)
                .collect();
            (ctor.to_string(), names)
        })
        .collect();
    let mut rows: Vec<(String, String, String)> = arity_text
        .lines()
        .filter(|l| !l.is_empty())
        .map(|l| {
            let mut it = l.split('\t');
            (
                it.next().unwrap_or_default().to_string(),
                it.next().unwrap_or_default().to_string(),
                it.next().unwrap_or_default().to_string(),
            )
        })
        .collect();
    // Sorted so the lookup can binary-search, and so a regeneration produces a
    // reviewable diff rather than a reshuffle.
    rows.sort();
    rows.dedup();
    let stdout = std::io::stdout();
    let mut w = stdout.lock();
    writeln!(
        w,
        "//! `name` and `length` of the ECMAScript intrinsic functions."
    )
    .unwrap();
    writeln!(w, "//!").unwrap();
    writeln!(
        w,
        "//! GENERATED by `cargo run --bin gen-arity` from {version} — do not edit by hand."
    )
    .unwrap();
    writeln!(
        w,
        "//! See `src/bin/gen_arity.rs` for what is in the table and what is deliberately not."
    )
    .unwrap();
    writeln!(w).unwrap();
    writeln!(
        w,
        "/// The `name` and `length` of one intrinsic each, sorted by key.\n\
         ///\n\
         /// The key is how [`crate::host::JsObj::Builtin`] spells the function: a bare\n\
         /// global (`parseInt`), a constructor (`Array`), a static (`Array.from`), or a\n\
         /// prototype-method thunk (`@proto:Array:slice`). `name` is repeated rather than\n\
         /// derived because the legacy aliases share one function object with the name of\n\
         /// the method they alias — `String.prototype.trimLeft.name` is `trimStart`.\n\
         pub const BUILTIN_ARITY: &[(&str, &str, u32)] = &["
    )
    .unwrap();
    for (key, name, len) in &rows {
        writeln!(w, "    ({key:?}, {name:?}, {len}),").unwrap();
    }
    writeln!(w, "];").unwrap();
    writeln!(w).unwrap();
    writeln!(
        w,
        "/// The own property names of each intrinsic prototype, in the engine's own\n\
         /// order. A name prefixed `+` is ENUMERABLE — true only for the WebIDL\n\
         /// interfaces (`URL`, `URLSearchParams`), whose members are plain assigned\n\
         /// properties, never for an ECMAScript builtin's.\n\
         ///\n\
         /// Separate from [`BUILTIN_ARITY`] because that table holds FUNCTIONS only:\n\
         /// `Map.prototype.size`, `RegExp.prototype.source` and the twelve\n\
         /// `URL.prototype` components are accessors, so the answer to\n\
         /// `Object.getOwnPropertyNames(Map.prototype)` is not derivable from it.\n\
         /// Sorted by constructor for lookup; the NAMES within a row are not sorted.\n\
         pub const PROTO_MEMBERS: &[(&str, &[&str])] = &["
    )
    .unwrap();
    let mut protos = protos;
    protos.sort();
    for (ctor, names) in &protos {
        let list = names
            .iter()
            .map(|n| format!("{n:?}"))
            .collect::<Vec<_>>()
            .join(", ");
        writeln!(w, "    ({ctor:?}, &[{list}]),").unwrap();
    }
    writeln!(w, "];").unwrap();
    writeln!(
        w,
        "\n/// The ACCESSOR members of each intrinsic prototype — the subset of\n\
         /// [`PROTO_MEMBERS`] whose descriptor carries a `get` rather than a value.\n\
         ///\n\
         /// Needed because the two are not interchangeable on the PROTOTYPE itself:\n\
         /// `Map.prototype.size` runs a brand check against `%Map.prototype%` and\n\
         /// throws, where a data member reads back its value. Neither existing table\n\
         /// records the descriptor kind.\n\
         /// Sorted by constructor; the NAMES within a row are not sorted.\n\
         pub const PROTO_ACCESSORS: &[(&str, &[&str])] = &["
    )
    .unwrap();
    let mut accessors = accessors;
    accessors.sort();
    for (ctor, names) in &accessors {
        let list = names
            .iter()
            .map(|n| format!("{n:?}"))
            .collect::<Vec<_>>()
            .join(", ");
        writeln!(w, "    ({ctor:?}, &[{list}]),").unwrap();
    }
    writeln!(w, "];").unwrap();
    writeln!(
        w,
        "\n/// The NON-WRITABLE data members of each intrinsic prototype.\n\
         ///\n\
         /// An inherited non-writable property refuses an assignment on every\n\
         /// object below it (10.1.9.2), so `o[Symbol.toStringTag] = 'x'` is silently\n\
         /// dropped when `o` inherits from `Map.prototype`. All but\n\
         /// `String.prototype.length` are symbol-keyed.\n\
         /// Sorted by constructor; the NAMES within a row are not sorted.\n\
         pub const PROTO_READONLY: &[(&str, &[&str])] = &["
    )
    .unwrap();
    let mut readonly = readonly;
    readonly.sort();
    for (ctor, names) in &readonly {
        let list = names
            .iter()
            .map(|n| format!("{n:?}"))
            .collect::<Vec<_>>()
            .join(", ");
        writeln!(w, "    ({ctor:?}, &[{list}]),").unwrap();
    }
    writeln!(w, "];").unwrap();
}