ANALYSIS-2026-08.md 29 KB

Amber code base analysis — August 2026

An assessment of lang/base (hand-written kernel) and lang/src (compiled image), with measurements, and a deep evaluation of the "dumb interpreter + packages as nested JSON data" idea sketched in lang/base/kernel-interpreter.js.

All measurements taken on Node v20.9.0, Windows 11, this machine, 2026-08-28.


1. Executive summary

  • lang/base is in good shape: ~1,700 hand-written lines (excluding the generated parser), cleanly layered, already ES6-capability-based despite its ES5 syntax. The weak spots are almost all peripheral: AMD/RequireJS, the generated bulk of lang/src, and a bit-rotted toolchain.
  • Boot cost is dominated by initialize(), not by JS parsing. Measured: 332 ms total for the deploy image in Node, of which 230 ms (70%) is initialize() — and 11,795 of 11,915 Object.defineProperty calls happen there, mostly the selectors × detached-roots DNU copying in copySuperclass. There is a ~10 µs replacement for that 9,000-call storm (§5).
  • Compiled packages are 3.22 MB from 485 KB of .st (6.8×). Per file, roughly ⅓ IDE metadata, ⅓ debug-context scaffolding, ⅓ code+boilerplate — of which only ~9–10% is real translated logic.
  • The sail interpreter idea is sound and worth pursuing, with two caveats found by actually running kernel-interpreter.js (§6): a one-line this-loss bug, and non-local return not yet wired (a working exception-based fix is included, §6.4). Sail-as-JSON is ~1.5× the .st source size, ~6.6× smaller than compiled devel JS. Pure interpretation measured ~92× slower than compiled JS on send-chain arithmetic, so the end state should be tiered: interpret first / on demand, baseline-compile hot methods from sail (§7).
  • The strongest architectural payoff of sail is not load time but replacing Compiler-Interpreter: today the debugger needs the entire compiler front-end, re-parses source: strings, reconstructs the PC from sendIdx counting, and cannot work at all against a deploy-stripped image. A sail interpreter produces real contexts natively (§7.5).

2. Shape of the code base

2.1 lang/base — the kernel

file lines role
boot.js 94 assembles the brikz layers, defers runtime loading until initialize()
brikz.js 68 micro-DI: named briks with deps, rebuildable
kernel-checks.js 93 fail-fast capability asserts (globalThis, native Promise, setPrototypeOf, …)
kernel-fundamentals.js 267 selector registry, package descriptors, method model, behaviorals
kernel-language.js 426 class/metaclass model, traits + composition, class construction
kernel-runtime.js 455 method installation, DNU, detached roots, contexts, send2
junk-drawer.js 135 inherits, selector mangling, small utilities
helpers.js / lang.js / deploy.js / devel.js ~140 public API + package-set manifests
kernel-interpreter.js 48 unwired sketch: the sail interpreter (§6)
parser.js 4,032 generated by pegjs from parser.pegjs

Layering is genuinely clean: fundamentals (no runtime assumptions) → language (class model) → runtime (loaded lazily via runtimeLoadedPromise in boot.js:50). Cross-layer coupling goes through the emit event table, not direct calls. Despite the "targets old ES3" reputation, kernel-checks.js hard-requires ES6 runtime capabilities; only the syntax is ES5.

2.2 lang/src — the compiled image

26 packages; 3,375,006 B ≈ 3.22 MB of .js (92,462 lines) from 496,535 B ≈ 485 KB of .st. Non-test: 2.27 MB. The Compiler-* chain alone (AST, Core, IR, Inlining, Interpreter, Semantic): 730 KB. Largest files: Kernel-Tests (693 KB), Kernel-Collections (368 KB), Kernel-Objects (228 KB), Compiler-IR (215 KB).

Byte anatomy (consistent across files; Kernel-Objects.js measured exactly):

bucket share notes
IDE metadata (source:, args:, referencedClasses:, comments, pragmas:) ~33% pragmas: alone is 10.9 KB in Kernel-Objects because inlineJS: bodies appear three times (source, pragma, code)
debug-context scaffolding (withContext, $ctx1.fill, $ctx1.sendIdx=, pragma markers) ~36% 252 of 286 Kernel-Objects methods are context-wrapped
code + per-method boilerplate ~31% of which real translated statements are only ~9–10% of the file (~21.6 KB in Kernel-Objects.js)

The //>>excludeStart("ide"/"ctx"/"imports") pragmas are consumed only by the r.js optimizer configs (cli, sdk, grunt-init-project) at bundle time — nothing strips at devel load time. Emitted code is strictly ES5 syntax (var, function, no arrows/let/class anywhere outside metadata strings), with ES6 Promise in the $pkg.isReady import prologue.

2.3 Load pipeline

deploy.js pulls 11 packages (Kernel-Helpers, -Objects, -Classes, -Methods, -Collections, -Dag, -Infrastructure, -Promises, -Exceptions, -Announcements, Platform-Services) = 1,336,017 B that must be fetched, parsed, and executed before initialize() can run AmberBootstrapInitialization._run(). devel.js adds 6 more, lang.js the 7-package compiler chain. Everything is eager: every addMethod runs, every selector registers, then initialize() installs everything onto prototypes.


3. Strong spots

  • Small, layered, evented kernel. A headless/reflection-less image is architecturally already possible; the seams exist.
  • instantiateFn indirection (kernel-fundamentals.js:135, kernel-runtime.js:224): methods carry a factory, not a raw function. This is 80% of the hook a lazy execution scheme needs — it just fires at class-init time instead of first-send time.
  • Pragma stripping machinery exists — tiered payloads are designed in, merely unexercised at devel load time.
  • Trait composition via prototype chains (buildCompositionChain) is compact, and traitMethodChanged propagates per-selector rather than rebuilding compositions.
  • Wrapper-free JS interop via detached roots + DNU handlers — Amber's distinctive feature, coherently implemented.
  • Discipline: strict mode, Object.create(null) dictionaries, hasOwnProperty hygiene, fail-fast checks.

4. Weak spots

  • Eager everything at boot — quantified in §5; the dominant cost is not bytes but registration work.
  • AMD/RequireJS is a dead ecosystem; the generated config.js (duplicated shim deps from amd-config-builder) is a symptom. Amber can't be consumed from ESM-native tooling without ceremony.
  • Compiler-Interpreter is heavy and fragile: hard AMD deps on Compiler-AST/Core/Semantic; AIContext >> initializeAST re-parses the source: string via Compiler new; PC recovery re-walks the AST counting sendIdx occurrences of evaluatedSelector; ASTDebugger >> stepInto is shouldBeImplemented; and none of it can work against a deploy-stripped image (no source:).
  • The ~36% context-scaffolding tax is paid on every devel-mode call.
  • Bit-rotted toolchain: pegjs 0.10 (dead; successor is peggy), grunt + jshint, es6-promise polyfill still loaded by index.html even though kernel-checks.js asserts native Promise, "engines": ">=4.0.0".
  • Object.create(globalThis) as the globals object and patching of native prototypes: works, but precludes two Amber instances per realm and invites subtle shadowing. This is a design identity, not a bug — named here as the constraint it is.
  • Factories run more often than methods exist: measured 1,630 instantiateFn calls for 1,345 addMethods — trait aliasing re-instantiates per user (aliased() in kernel-language.js copies instantiateFn).

5. Deep dive: where boot time actually goes

Instrumented boot of the deploy image (RequireJS in Node, counters patched around Object.defineProperty and the api registration functions):

phases (ms):   kernelLoad 30.4 | packageLoadAndExec 72.2 | initialize 229.6 | total 332.2
counters:      addClass 99 | addTrait 13 | addMethod 1,345 | methodFactoryCalls 1,630
               defineProperty 11,915 — of which 11,795 during initialize()
census:        893 selectors | 10 detached roots | 1,501 local methods (incl. class side)
               deploy set on disk: 1,336,017 B

Reading of the numbers:

  • Parsing/executing 1.3 MB of package JS costs 72 ms. initialize() costs 230 ms — 3× more. The common intuition "the payload is what's slow" is wrong for Amber-on-V8; the registration storm is what's slow.
  • The defineProperty arithmetic during initialize() matches the code exactly: 893 DNU handlers onto the root prototype (RuntimeSelectorsBrik.installNewSelectors) + 10 detached roots × 893 selectors copied by copySuperclass (kernel-runtime.js:165) ≈ 9,823, plus ~1,900 real method installs ≈ 11.8 k. copySuperclass alone is ~75% of the boot's property-definition work — and every later selectorsAdded event fans out to all detached roots again.
  • For comparison: JSON.parse of a 201 KB sail-flavoured nested-array blob (the size of the deploy set's .st sources) takes 2.4 ms.

5.1 Quick win independent of any redesign: prototype-chain insertion

copySuperclass exists to make detached roots (String, Number, Array, …) appear to inherit from their Amber superclass. In 2026, that can be actual inheritance: insert the Amber superclass's prototype into the builtin's chain once, instead of copying every selector forever.

// in detachClass / setClassConstructor, replacing copySuperclass:
Object.setPrototypeOf(klass.fn.prototype, klass.superclass.fn.prototype);

Measured feasibility (Node 20): insertion of 4 builtin prototypes takes 10.3 µs (vs ~9,000 defineProperty calls ≈ most of 230 ms), natives keep working (toUpperCase, sort, toFixed verified), Amber methods and DNU handlers are reached through the chain, and 3M message sends through the inserted layer run at ~18 ns/send (allocation included). The historical reason for the copying workaround — no reliable setPrototypeOf on builtins in ES3/ES5 browsers — is gone; kernel-checks.js already asserts setPrototypeOf. It also deletes the selectorsAdded fan-out and the propagateMethodChange copying for detached roots.

Caveats to verify against the real image: Function.prototype (BlockClosure) and Error.prototype chains get an extra hop for all JS code in the realm (the same objects already carry hundreds of copied _-methods today, so the delta is the lookup hop, not the pollution); engines deoptimize code that ran before the chain mutation, so do all insertions before user code warms up — i.e. exactly where FrameBindingBrik runs today.

5.2 Engine support for setPrototypeOf on builtin roots (researched 2026-08)

Which objects may legally refuse. Per the ES spec, the only immutable prototype exotic objects are Object.prototype (made so by an ES2017-era normative change, implemented in engines 2015–2016) plus, in browsers, window and location. Every prototype Amber uses as a detached root — String/Number/Boolean/Date/Array/RegExp/Error/Function/Promise .prototype — is an ordinary object with the ordinary, mutable [[SetPrototypeOf]]. MDN states this explicitly ("other built-in prototype objects, such as Function.prototype and Array.prototype, are not protected"). Amber never needs to touch Object.prototype itself — inserted chains simply terminate there — so the immutability rule is never in play. No correctness bug about mutating builtin prototypes surfaced in MDN/engine-tracker searches; the documented concerns are exclusively about performance.

Since when (per mdn/browser-compat-data):

mechanism Chrome Edge Firefox Safari IE Opera Node Deno
Object.setPrototypeOf 34 (2014) 12 31 (2014) 9 (2015) 11 21 0.12.0 (2015) 1.0
legacy __proto__ setter 1 12 1 3 11 10.5 0.10.0 flag-gated
globalThis (Amber's actual floor, asserted by kernel-checks.js) 71 (2018) 79 65 (2019) 12.1 (2019) — 58 12 (2019) 1.0

The decisive row is the third one: Amber's own boot asserts globalThis (and native Promise), so every engine that can run Amber at all is 4–5 years newer than the engines that introduced setPrototypeOf. Within Amber's support envelope the operation is available unconditionally — browser and Node alike (amber-cli's real floor is Node 12 because of the same assert, regardless of the stale "engines": ">=4" field; setPrototypeOf itself has been in Node since 0.12). The only runtimes that would need the current copying fallback are IE ≤ 10-era engines, which Amber already cannot boot on.

Empirical proof, this machine. A 30-check harness (all nine root prototypes: insertion accepted, Amber-style dispatch through the layer, natives intact — string/array/date/regexp/error/function/promise operations, instanceof, typeof, JSON, coercions — for..in enumeration hygiene, symmetric restore, and Object.prototype/window correctly throwing TypeError): all pass on Node 20.9 (V8) and on Chromium 148 in a real browser page. SpiderMonkey/JSC were not run here; given spec + BCD the expectation is identical, and the harness is trivially pasteable into their consoles for confirmation.

One measured performance caveat: holey arrays. After inserting a layer into Array.prototype's chain, V8's "no elements on the prototype chain" fast path can no longer vouch for the chain, and reading holes consults it: a hole-reading loop degraded ~6× (9 ms → 54 ms) on Node while the same loop over a packed array was unaffected. This matters to Amber because Array class >> new: is <inlineJS: 'return new Array(anInteger)'> (Kernel-Collections.st:1258) — a holey array. Cleanest fix: allocate new Array(anInteger).fill(null) — packed elements (fast path preserved, holes never read through the chain) and closer to Smalltalk semantics, where Array new: 5 answers a nil-filled array. Alternatively, keep Array alone on the copying path and insert chains only for the other roots. Browser-context timing right after mutation also showed transient deoptimization on packed loops (re-JIT recovers), reinforcing: do all insertions once, at boot, before anything warms up.


6. Deep dive: the sail interpreter (lang/base/kernel-interpreter.js)

The 48-line sketch is a chain-threading interpreter over nested arrays. It was exercised against a minimal facade (sendFn/runBlock); everything below is verified by running it, not just by reading it.

6.1 The encoding, as implied by the engine

Sail is a message chain encoding — each element of a chain is evaluated with self = the previous element's value; a chain's value is its last element's:

form meaning
non-array literal, becomes the new threaded value
[0, e1, e2, …] sub-chain: evaluate e1…en threading self; value = last
[n, key, cont…] (n>0) read contexts[n][key], then thread it through cont… — reads are fused with their continuation; a bare read must be terminated, e.g. [1,"tmp",[]] (an empty array is the identity expression)
[-n, key] write threaded value into contexts[n][key] (assignment-as-chain-element)
["sel", a1, …] send sel to threaded value; args evaluated as a chain themselves
[["sel", flags], …] flagged send: flags&1 → pass contexts[0].sendTag (super-send channel); flags&2 → answer the receiver (cascade)
["{}", e1, …] dynamic array {…}
["^", …] return threaded value
["[]", x, y, body…] block closure; slots 1–2 are reserved (arity/temp-names fit naturally); body starts at index 3; invocation pushes arguments and a temps object, so a block's params are [k, 0], [k, 1], … where k = its arg-context index
["$…", …] facade escape — the natural hook for interpreter-level inlining of ifTrue:/whileTrue:/and: etc.

Verified working on the engine as committed (via the 4-arg entry point): send chains, context read/write, dynamic arrays, block creation + invocation, cascade flag, super/sendTag flag, method-level ^ mid-body.

The chain-threading design is a genuinely good fit for Smalltalk: unary chains (self class name first isVowel) cost 2 array slots each, and statements compose as [0, …] groups. Scope resolution is already done at encode time (numeric context indices), which is what makes a trivial baseline compiler possible later (§7.4).

6.2 Defect: the 3-arg entry point loses the facade

kernel-interpreter.js:45:

return index == null ? interpret(contexts, [0, sail], self, 1) : …

The recursive call is a plain call, so this (the facade) becomes globalThis; any send then throws facade.sendFn is not a function (reproduced). One-line fix: interpret.call(this, contexts, [0, sail], self, 1).

6.3 Defect: ^ in a block does not return from the home method

Each block invocation creates a fresh engine (engine(newContexts, facade) inside the "[]" closure), with its own returnValue cell. A ^ inside a block therefore terminates only the block's own chain; the home method's doChain never sees it and continues executing.

Reproduced with findFirstEven: coll = coll do: [:e | e even ifTrue: [^e]]. ^nil over [1,3,4,5]: Smalltalk semantics require 4; the engine as committed answers nil. This is not fixable inside facade.runBlock alone — by the time the block's value reaches runBlock, "returned via ^" and "fell off the end" are indistinguishable. A signal must cross the block→home boundary.

6.4 Working fix: exception-based NLR with a home cell (prototype)

The home method activation owns a home cell; blocks share it (they pass it into the engines they spawn); ^ throws; the home activation catches its own token; finally kills the cell so escaped blocks fail correctly.

function NonLocalReturn (home, value) { this.home = home; this.value = value; }

function engine (contexts, facade, home) {
    return function doChain (sail, self, i) {
        for (var args = [], l = sail.length; i < l; ++i) {
            args.push(self = doExpr(sail[i], self));
        }
        return args;

        function doExpr (sail, self) {
            if (!Array.isArray(sail)) return sail;
            var selector = sail[0], flags = 0;
            if (typeof selector === "number")
                return selector === 0 ? doChain(sail, self, 1).pop() :
                    selector < 0 ? (contexts[-selector][sail[1]] = self) :
                        doChain(sail, contexts[selector][sail[1]], 2).pop();
            if (Array.isArray(selector)) { flags = selector[1]; selector = selector[0]; }
            if (typeof selector !== "string") return self;
            if (selector[0] === "$") return facade[selector](sail, self, flags);
            if (selector === "^") {
                if (!home.alive) throw new Error("BlockContext>>cannotReturn: home context already returned");
                throw new NonLocalReturn(home, self);
            }
            if (selector === "[]") return function () {
                var newContexts = contexts.slice();
                var argContext = arguments;
                return facade.runBlock(function () {
                    newContexts.push(argContext, {});
                    return engine(newContexts, facade, home)(sail, this, 3).pop();
                }, newContexts, this);
            };
            var args = doChain(sail, self, 1);
            if (selector === "{}") return args;
            var result = facade.sendFn(
                self, selector, args, flags & 1 ? contexts[0].sendTag : null);
            return flags & 2 ? self : result;
        }
    };
}

return function interpret (contexts, sail, self, index) {
    var home = { alive: true };
    try {
        return engine(contexts, this, home)(
            index == null ? [0, sail] : sail, self, index == null ? 1 : index).pop();
    } catch (e) {
        if (e instanceof NonLocalReturn && e.home === home) return e.value;
        throw e;
    } finally {
        home.alive = false;
    }
};

All tests pass on this variant, including the ones the committed sketch cannot express: NLR out of a nested block (findFirstEven → 4), NLR unwinding through an intermediate interpreted method (its own home doesn't match, so it rethrows — ^7 inside a block passed to another method skips that method's +100), and an escaped block whose home already returned raising cannotReturn. This is the same $early-throw strategy the AOT compiler uses (Compiler-IR.st emits var $early={}; try {…} catch(e){if(e===$early)…}), so semantics stay aligned. Note the throw-always design also covers method-level ^; if that ever shows up in profiles, the home engine can keep the cheap returnValue path for its own chain and reserve the throw for block-originated returns.

6.5 Interpreter speed, measured

Same workload, fixed engine vs hand-written JS (V8, warmed):

workload interpreted hand-written JS ratio
send-chain arithmetic (x+4)*2+x, 10⁶ activations 329 ms 3.6 ms ~92×
findFirstEven incl. NLR throw, 2×10⁵ activations 2,147 ms 1,512 ms 1.4×

Two lessons: (a) the "10–100×" folk estimate is right, and the ~330 ns per interpreted activation is perfectly fine for UI callbacks, doIts, and cold code; (b) where exceptions dominate (NLR-heavy code), interpretation overhead is noise — the expensive thing is the throw, which compiled Amber pays too.


7. Deep dive: packages as sail/JSON data — the design that falls out

7.1 Size, measured on real methods

method .st source sail JSON compiled devel .js
ProtoObject >> printOn: 147 B 215 B 1,418 B
findFirstEven: 69 B 98 B —

Sail JSON ≈ 1.45× the .st source, ≈ 6.6× smaller than devel JS. Extrapolated: the whole image's code as sail ≈ 700 KB (vs 3.22 MB), the deploy set ≈ 290 KB (vs 1.34 MB) — before gzip, which loves sail's repetitive structure. messageSends need not be stored at all: the loader can derive a package's selector set by walking the sail arrays (or store it per package as one deduplicated list, which is cheaper than per-method arrays anyway). IDE metadata (source, args, comments, protocols) moves to a sidecar file along the exact boundary excludeIdeData already defines, fetched only when an IDE asks.

7.2 What actually gets faster

From §5, the boot budget is ~30% parse/exec and ~70% registration. Data-form packages attack both:

  • parse: JSON.parse at ~2.4 ms/200 KB replaces JS parse+exec at ~72 ms/1.3 MB, and can run in a worker / be cached in IndexedDB;
  • registration: with methods as data, "install" = register the selector (still eager — DNU needs it) + put a trampoline on the prototype that on first call materializes the method and replaces itself:
// installAmberMethodIntoAmberClass, lazy variant
function trampoline (method, klass) {
    var fn = function () {
        var real = materialize(method, klass);   // interpret-mode wrapper or baseline-compiled
        installMethodOfJsObject(klass.fn.prototype, method.jsSelector, real);
        return real.apply(this, arguments);
    };
    fn.a$tramp = method;                          // recognizable to tooling
    return fn;
}

This is a call-time-lazy instantiateFn — the model already has the slot. Combined with §5.1 (prototype insertion kills the copying storm), boot work becomes: parse JSON + create 112 classes/traits + one cheap closure per method. Estimated well under 50 ms for today's deploy image, and — more importantly — it stops scaling with image size: a minimal kernel can be live while the rest of the image streams in, because a send to a not-yet-loaded package's method is just a DNU that can await the package.

7.3 The two-format trap to avoid

Don't keep AOT .js and sail as two authoritative formats — they will drift. The clean end state: sail is the package format (what amberc emits, what transports commit), and JS is a cache derived from sail (baseline-compiled at runtime, or ahead-of-time for deploy builds where CSP forbids new Function — see §7.4). During transition, keep emitting both from the same IR so nothing regresses.

7.4 Tiered execution: the baseline compiler is small

Because sail already has resolved scope indices and linearized chains, a sail→JS translator is a few hundred lines of base code with no dependency on Compiler-*: each chain element maps 1:1 to an expression, blocks map to closures, ^ maps to the $early throw, flagged sends map to sendTag/receiver-return. Strategy:

  • first call → interpret (fast to start, no codegen);
  • Nth call (or first call, configurable) → new Function the baseline JS and swap it onto the prototype;
  • strict-CSP environments → interpreter stays the executor (it is semantically complete); deploy builds precompile the cache ahead of time.

The 92× figure from §6.5 is the cost of not doing this for hot code; the 1.4× figure shows how little it matters for cold/exceptional paths.

7.5 The debugger payoff

Compiler-Interpreter today: needs Compiler-AST/Core/Semantic loaded, re-parses source: per method, recovers the PC by re-walking the AST counting sendIdx occurrences, stepInto unimplemented, impossible on stripped images. A sail interpreter is a debugger substrate: contexts, receivers, temps, and the current node are its ordinary working state; stepInto is natural (descend instead of send); and it works on exactly the artifact that was deployed, because sail is the deployed artifact. The $ctx scaffolding (36% of every devel file, plus per-call runtime cost) then has a deletion path: run-code-you-want-to-debug interpreted instead of instrumenting all compiled code forever.

7.6 What stays JS

  • inlineJS: methods — sail can't express them; they live in a small per- package .js companion (or the JSON carries the JS string and non-CSP environments Function it; CSP environments need the companion file).
  • imports: — stays a loader concern; the JSON manifest lists them, the loader resolves them (today's $pkg.isReady promise moves into the loader).
  • Package deps — plain data in the JSON manifest instead of the define() array; this is also the exit ramp from AMD, since data packages have no module format at all. Only the ~10 hand-written base files still need a module story (ESM with an AMD shim during transition).

8. Recommended order of work

  1. Land the two kernel-interpreter fixes (§6.2 this-loss; §6.4 NLR) — they're prerequisites for anything sail-shaped, and small.
  2. Prototype-chain insertion for detached roots (§5.1) — independent, measurable (~200 ms off browser boot is plausible), removes the selectorsAdded/copySuperclass machinery.
  3. Cheap hygiene: drop es6-promise (kernel-checks already asserts native Promise), pegjs→peggy, jshint→eslint, honest engines field.
  4. Sail backend in the compiler (IRSailTranslator beside IRJSTranslator) + JSON package loader in base + trampoline-lazy install (§7.2). Keep AOT .js emission until parity is proven.
  5. Baseline sail→JS compiler in base (§7.4) for steady-state speed.
  6. Debugger on the sail interpreter (§7.5); retire Compiler-Interpreter's re-parse/PC-reconstruction machinery.
  7. ESM for base; packages are data by then, so AMD dies quietly.
  8. Opportunistic: raise AOT codegen to ~ES2017 (arrows for blocks are safe — Amber blocks don't use JS this) whenever IRJSTranslator is next touched.

Leave alone: the detached-root/native-prototype design (it is Amber), brikz (obscure but small and correct — document it instead), the trait engine.


Appendix: how the numbers were produced

Three throwaway Node scripts (run from a session scratchpad, not committed):

  • boot-profile.js — RequireJS in Node with baseUrl at the repo root and the amber/amber/core path mapping; wraps Object.defineProperty and the api.addMethod/addClass/addTrait/method functions between loading amber/boot and amber/deploy; times kernel load / package load+exec / amber.initialize(); then censuses api.traitsOrClasses and api.allSelectors(). Also times JSON.parse on a sail-shaped blob sized to the deploy set's .st sources.
  • proto-insert.js — inserts a synthetic Amber layer into String/Number/Boolean/Array prototype chains via Object.setPrototypeOf, verifies native methods still work, times the insertion and 3M sends through the inserted layer.
  • sail-lab.js — loads the real lang/base/kernel-interpreter.js via a 20-line AMD shim, drives it with a minimal facade (sendFn switch + runBlock), runs the semantics/defect tests of §6, the fixed engine of §6.4 with its NLR test battery, the size measurements of §7.1 (extracting the real printOn: emission from Kernel-Objects.js for comparison), and the §6.5 benchmarks.