Biblia

Biblia is the intermediate representation Cruft's built-in methods are written in, one node per ECMAScript abstract operation and one step per numbered specification step. It lowers mechanically to the Rust that runs Array.prototype.map and roughly 226 other builtins, and a linter checks each section against the specification's step list.

Every JavaScript engine has to turn the ECMAScript specification's English algorithm steps into executable code. Cruft does it through Biblia, an intermediate representation that keeps the specification itself as the canonical text the engine is checked against. Its nodes are the specification's own abstract operations, and its steps keep the specification's own step numbers. A linter then checks the implementation against the specification structurally, step by step. It is an unusual approach: no mainstream engine keeps the specification's step structure as checkable data the way Cruft does.

The problem every engine solves differently

ECMA-262 defines a builtin like Array.prototype.map as a numbered list of steps: "1. Let O be ? ToObject(this value). 2. Let len be ? LengthOfArrayLike(O). ...". Every engine transcribes those steps into its own form. V8 uses Torque, a purpose-built language whose syntax resembles the specification's pseudocode but is written by hand. SpiderMonkey implements many builtins as self-hosted JavaScript. JSC writes them in C++. In all three, the builtin's source is an artifact in the engine's own language, and the correspondence back to the specification's numbered steps lives in comments and in the author's memory. Nothing mechanical can tell you that step 3 was dropped in transcription.

Cruft keeps that correspondence as data.

What the IR models

The IR is shaped like the specification rather than like a general compiler IR.

  • An IRFunction is one specification section, carrying its section number (for example Array.prototype.map, §23.1.3.20).
  • A Step is one numbered specification step, and it carries the specification's own step identifier as a string ("1", "6.c.ii", "3.throw"). Steps map one-to-one onto the specification's numbered list.
  • The IR's vocabulary is the specification's abstract operations, as first-class nodes: ToObject, LengthOfArrayLike, Get, Call, CreateDataPropertyOrThrow, ArraySpeciesCreate, IsCallable, SameValue, ToPropertyKey, and roughly seventy more. Throws are typed by error class (TypeError, RangeError, ReferenceError, SyntaxError), matching the specification's "throw a TypeError exception", and internal slots are modeled in their [[Slot]] form.

The effect is that the IR for a builtin reads as a close transcription of the specification's algorithm, with every step tagged by its specification step number.

Specification to IR to Rust

The pipeline has two stages that ship today.

Into the IR. Each builtin is written into the IR one section at a time, step by step, with each step tagged by its specification step number. This is a transcription: the specification is the reference the author works from, not a text the tooling reads automatically (see Limitations).

Out to Rust. A lowering step turns the IR into Rust source. Each abstract operation becomes a fixed runtime call, each typed throw becomes the matching error, and the specification's ? (ReturnIfAbrupt) becomes Rust's ? error propagation on a Result. The next section walks a whole builtin through that step in detail.

The lowered Rust is compiled into the engine's builtin dispatch. When a program calls Array.prototype.map, it runs the code that was lowered from the IR.

How the lowering works

Lowering is mechanical. Every IR node maps to one fixed piece of Rust, there is no analysis and no optimization, and the same IR always produces the same output. Understanding it comes down to two rules and one worked example.

Every lowered builtin has the same shape: a Rust function that takes the running engine and the JavaScript call, and returns either a value or an error.

pub fn array_prototype_map(rt: &mut Runtime, this: Value, args: &[Value])
    -> Result<Value, RuntimeError>

rt is the engine, this and args are the call's receiver and arguments, and Value is the engine's JavaScript value type. Two rules turn the IR inside that function into Rust:

  1. Each abstract operation is a method on rt. ToObject lowers to rt.to_object(...), LengthOfArrayLike to rt.length_of_array_like(...), Get to rt.spec_get(...), Call to rt.call_function(...). The IR node names the specification operation; the lowering knows the one runtime method that implements it.
  2. The specification's ? becomes Rust's ?. In the specification, ? Foo() means "if Foo returns an abrupt completion, stop and return it." Rust's ? operator on a Result means exactly that. So every fallible operation lowers to a call ending in ?, and an error propagates straight out as the RuntimeError half of the return type. No completion-record type is needed; Rust's error propagation carries it.

Here are the first four steps of Array.prototype.map (§23.1.3.20) as the specification writes them:

1. Let O be ? ToObject(this value).
2. Let len be ? LengthOfArrayLike(O).
3. If IsCallable(callbackfn) is false, throw a TypeError exception.
4. Let A be ? ArraySpeciesCreate(O, len).

In the IR, one step per statement, each tagged with its step number:

step 1:  let O   = ToObject(this)
step 2:  let len = LengthOfArrayLike(O)
step 3:  if not IsCallable(callbackfn): throw TypeError
step 4:  let A   = ArraySpeciesCreate(O, len)

And the Rust the lowering emits for those four steps:

// step 1
let mut o = rt.to_object(&this.clone())?;
// step 2
let mut len: usize = rt.length_of_array_like(&o.clone())?;
// step 3
if !rt.is_callable(&callbackfn.clone()) {
    return Err(RuntimeError::TypeError(
        "Array.prototype.map: callback is not callable".into()));
}
// step 4
let mut a = rt.array_species_create(&o.clone(), len.clone())?;

The two line up one for one. ? ToObject(this value) is rt.to_object(&this.clone())?, with the trailing ? standing for the specification's ReturnIfAbrupt. "throw a TypeError" is a typed RuntimeError::TypeError. Nothing was inferred or reordered: each step became its fixed snippet, and the // step N comments carry the specification's own numbering into the output, so the generated file can be read against the specification line by line.

The loop is the same idea nested one level. Step 6, "Repeat, while k < len", is a Rust while, and each numbered sub-step inside it lowers to one statement. Shown with two mechanical details removed for readability, the per-read .clone() calls visible above and the garbage-collection root guards the emitter wraps around allocating calls:

// step 6 — Repeat, while k < len
while k < len {
    // step 6.a — Pk = ToString(k)
    let mut pk = k.to_string();
    // step 6.b — kPresent = HasProperty(O, Pk)
    let mut k_present = rt.has_property_via_throw(&o, &pk)?;
    // step 6.c — If kPresent is true, then
    if k_present {
        // step 6.c.i — kValue = Get(O, Pk)
        let mut k_value = rt.spec_get(&o, &pk)?;
        // step 6.c.ii — mappedValue = Call(callbackfn, thisArg, «kValue, k, O»)
        let mapped = rt.call_function(
            callbackfn, this_arg,
            vec![k_value, Value::Number(k as f64), o.clone()])?;
        // step 6.c.iii — CreateDataPropertyOrThrow(A, Pk, mappedValue)
        rt.create_data_property_or_throw(&a, &pk, mapped)?;
    }
    // step 6.d — Set k to k + 1
    k += 1;
}
// step 7 — Return A
return Ok(a);

Every line traces to a specification step: Repeat is the while, Get(O, Pk) is rt.spec_get, the callback invocation is rt.call_function, and Return A is return Ok(a). That is the whole of it. An algorithm's steps become a function's statements, its abstract operations become runtime method calls, and its error handling becomes Rust's ?.

The lowering runs as an offline developer step, not at program start. It writes every lowered function into one generated Rust file, headed "Do not edit by hand; modify the IR," and that file is committed and compiled into the engine like any other source. Invoking Array.prototype.map from JavaScript runs the array_prototype_map function above. Because the file is generated and committed rather than produced during the build, regenerating it after a section changes is a manual step, which is the drift noted under Limitations.

The linter: specification correspondence as a check

Because every step keeps its specification step identifier, the implementation can be compared against the specification's step list mechanically. The linter walks the IR for a section against the list of that section's specification steps and reports a missing step, a missing abstract-operation call, or a throw whose error class does not match the specification.

This is the property that sets the tier apart. In an engine whose builtins are written in Torque, self-hosted JavaScript, or C++, "does this implementation follow the specification's steps in order, with the right operations and the right throws" is a question a human answers by reading. Here it is a check the tooling can run, because the step structure is preserved as data rather than flattened into ordinary code.

What runs through it today

Biblia covers a large part of the ECMA-262 builtin library, and those operations are wired into the engine's live dispatch (roughly 226 of them). The covered surface includes most of Array.prototype, the Object statics and prototype methods, Reflect, the full Promise static and combinator family, Math, Number, JSON, most of String.prototype, Map and Set, a large Date surface, and core conversions such as ToPrimitive.

It does not cover everything, by design. User-authored JavaScript runs through Parsimony, Distil, and Exegesis, not this tier. The language-semantics core, the Node, Bun, and web-platform host surfaces, and a handful of performance-critical operations that are deliberately kept as direct code are all outside it. The tier is a growing subset of the specification's builtins. It does not cover the whole engine.

Limitations

  • Transcription is manual. The algorithm bodies are written into the IR by the team, not ingested from the specification text. An emu-alg parser exists but currently operates on a single embedded fixture and feeds the linter, not code generation. There is no automated path from specification text to IR today.
  • The linter checks against authored step lists. The specification-side step list each section is checked against is written alongside that section, so the linter currently guards the project's own transcription against internal drift rather than against a live specification checkout.
  • Lowering emits text. The IR is lowered to Rust by string emission and the result is committed as a generated source file, regenerated when sections change. It is not a macro-based, hygienic codegen pipeline, and the committed file can drift from the IR sources if it is not regenerated.
  • Completion machinery is not modeled structurally. There are no Completion Records, Reference Records, or Property Descriptor records as IR types, and no explicit ?/!/Assert nodes; abruptness rides Rust's ? operator. Some engine concerns (typed loop indices, garbage-collection rooting) sit alongside the specification nodes in the IR.
  • It is a correctness tier. There are no optimization passes; the tier exists to keep builtins faithful to the specification, and coverage is a subset that grows section by section.

Why it matters

Biblia gives Cruft two independent roads to correctness for the builtins it covers. One is behavioral: test262 exercises the operations from the outside. The other is structural: the linter checks that the implementation follows the specification's steps, in order, with the right operations and throws. When a specification revision changes an algorithm, its step-level diff can be walked directly against the IR for the affected section. That combination, a specification-shaped intermediate form plus a mechanical correspondence check, is what makes the tier worth its cost.

See Distil for where the IR sits in the engine, and Cruft Core for the engine as a whole.