//! 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();
}