CYNIC
A strict-only, hardened-by-default ECMAScript engine

Cynic.

A JavaScript engine that has heard enough.

Strict-only, and hardened from the first line — the primordials freeze before your code runs, so a dependency can't redefine Array.prototype out from under you. It already parses, compiles, and runs real JavaScript: async, generators, a module graph, proper tail calls. And WebAssembly, through a second engine built from scratch — called Sarcasm, because of course. Everything the language spent twenty years trying to forget, it forgets on purpose: no with, no sloppy mode, eval off unless you ask. Ready for production? Eventually.

Pre-alpha. Parser, interpreter, generational GC, and a baseline JIT ship — the JIT is on by default now; --no-jit if you miss the interpreter. Read docs/ROADMAP.md before opening issues. Better yet, don't.
§01 · REFUSALS

Things Cynic won't do for you.

Every feature an engine ships is surface it can never take back. Cynic carries less of it by design — the legacy syntax, the sloppy-mode quirks, the deprecated globals, and the sharper tools a hardened runtime is better off without.

REFUSED
with (obj)

The with statement

Allegedly part of the language. We checked. We disagree.

SyntaxError: with? in 2026? please.
RETIRED
0o7 → 07

Legacy octal literals

The 0o prefix exists for a reason. The bare-leading-zero form does not.

SyntaxError: legacy octal: not in strict mode, not anywhere.
DENIED
function f() in if(){}

Function-in-block, sloppy-mode style

Annex B's sloppy-only function-in-block hoisting is gone. Declare it where it lives.

SyntaxError: Annex B not on the menu.
PASS
for (var i in o = {}) {}

Initializer in for-in head

Annex B accident. Doesn't parse here. Hoist your initializer like an adult.

SyntaxError: for-in head is not for assignment.
NEVER
<!-- -->

HTML comments in source

Yes, that's real ECMAScript. Yes, we are pretending it isn't.

SyntaxError: this is a JS file. compose yourself.
NOPE
escape() · unescape()

The pre-URI globals

Use encodeURI / encodeURIComponent. They've been correct since 1999.

ReferenceError: 'escape' is not shipped.
EXPIRED
"hi".bold()

The String.prototype HTML wrappers

All thirteen of them — anchor, bold, fontcolor, … — left at the door. This is a string, not an HTML formatter.

TypeError: 'bold' is not a string method here.
NOPE
d.getYear()

Date.prototype.{getYear, setYear}

The two-digit-year ones. And no, the Annex B aliases (substr, trimLeft, toGMTString) didn't make the cut either. None of Annex B did.

TypeError: 'getYear' is not on this Date.
OFF BY DEFAULT
eval() · new Function("…")

Runtime code construction

Off by default — code is code here, not strings assembled at runtime. Aligns with SES / Hardened JavaScript and removes the supply-chain bait. --allow=eval opens a real evaluator when you mean it; the frozen primordials still box it in. Multi-file scripts use a host hook, not user-reachable.

EvalError by default. --allow=eval if you insist.
STRICT MODE isn't a mode. it's a worldview.
§02 · WHERE WE ARE

Pre-alpha. Not pretending otherwise.

Lexer, parser, bytecode interpreter, generational GC, and a baseline JIT all ship — the JIT on by default, the optimising tier still future. The runtime failures that remain are refusals, not gaps. Below is what works, what kind-of works, and what Cynic declines to do on purpose.

The receipts
Cynic says
What that means
test262 · parser
scoped to the corpus Cynic targets
100% attempted.
Every fixture in scope either parses or correctly rejects. Spec coverage is the next number to grow.
test262 · runtime
parse → compile → execute; every failure classified, CI gates on zero engine gaps
Done arguing.
Every remaining failure is a refusal — sloppy mode, Annex B — not a bug. The audit registry says so, and CI checks the registry.
In-tree unit tests
tests-first, allegedly
A lot.
Quadruple digits. Nobody's bragging.
Promises & async/await
full chaining; pending-await via JSGenerator
Works.
Yes, even the part where it suspends.
Async generators
yield-await chaining via promise reactions
Works.
Surprisingly. We checked.
yield* delegation · for await of
sync + async generators; IteratorClose on every abrupt path the spec describes
Works.
We learned §15.5.5 by failing it, then we stopped failing it.
Top-level await
thenable adoption + suspension across module boundaries
Works.
Async modules suspend. Sibling modules don't block. The spec said so.
RegExp · full ECMA-262
backrefs, named groups, lookaround, u/v flags
Works.
Perlex — Cynic's own matcher. The vendored regex engine got retired.
WebAssembly
the WebAssembly JS API — compile / instantiate, Module / Instance / Memory / Table / Global; SIMD, reference types, memory64, multi-memory, tail calls, exception handling, cross-module linking
Works.
Cynic does WebAssembly too. With Sarcasm — its own from-scratch engine, 100 % of the spec testsuite it scores — plus Spasm, its baseline JIT, on by default. Off unless --allow=wasm.
Real Symbol & BigInt primitives
NaN-boxed, pointer-tagged; well-known symbols, registry, arbitrary-precision BigInt
Works.
Primitives. Not polyfills wearing a costume.
TypedArrays + ArrayBuffer + DataView
full %TypedArray%.prototype surface
Works.
All the views. All the bytes. None of the drama.
Proxy traps
get / set / has / deleteProperty / defineProperty / ownKeys / getPrototypeOf / setPrototypeOf / isExtensible / preventExtensions / apply / construct
Works.
Every trap the spec wrote down. Apologies to your debugger.
Real module graph
cyclic imports, indirect-binding TDZ, namespace [[Get]] ReferenceError on uninit, dynamic import()
Works.
Cyclic. TDZ on indirect imports. The Namespace [[Get]] ReferenceError the spec asked for.
Dynamic import()
host-loader-backed; non-Promise & settled-Promise cases through await
Works.
Returns a real Promise. The loader is up to the host. The body is up to the spec.
Import attributes · with { type: "json" }
JSON & text module records, attribute-keyed module cache, static + dynamic import()
Works.
JSON modules without the assertion-vs-attribute drama. Cache keys on the type.
ES2025 collection & Promise additions
Set union / intersection / difference / symmetricDifference / isSubsetOf / isSupersetOf / isDisjointFrom · Promise.try · Promise.withResolvers
Works.
Shipped. Each one cost the runtime score nothing and the spec a little dignity.
Temporal
all eight types + Temporal.Now; ISO + UTC/offset by default, real calendars and IANA zones with -Dintl=stub / full
Works.
The Date replacement the spec spent a decade on. Nanoseconds, no mutation, no month-is-zero-indexed.
Map / WeakMap getOrInsert (upsert)
Stage 4 (2026-05); default-on, no flag
Works.
Graduated to Stage 4 this month — the flag came off, the method stayed.
Explicit Resource Management · using / await using
Symbol.dispose / Symbol.asyncDispose, DisposableStack / AsyncDisposableStack, SuppressedError; disposal on every abrupt path
Works.
Scopes clean up after themselves. The finally you keep forgetting to write.
Experimental · Iterator.zip · ShadowRealm
joint-iteration (Stage 3) + ShadowRealm (Stage 2.7), behind --enable=<name> or --enable-experimental; off by default. Each scored as its own isolated test262 sweep.
Opt-in.
Pre-Stage-4: shippable, but the committee can still change its mind. Cynic takes the trade; embedders decline by default.
WeakRef · FinalizationRegistry
§26.1 / §26.2; real weak references — major sweep clears dead targets and queues registry callbacks
Works.
Genuine weakness. The collector finally honours its end of the contract.
Garbage collector
generational mark-sweep — short-lived objects collected without scanning the whole heap
Works.
Most objects die young. The collector finally agrees.
Hardened by default
primordials frozen at realm init; native harden(); the override mistake fixed via synthetic accessors; --unhardened opts out
Works.
No lockdown() call, no import. Your dependency's prototype patch throws before it ships a backdoor.
Proper Tail Calls (PTC)
ECMA-262 §15.10; return f(x) reuses the caller's frame instead of pushing a fresh one
Works.
Second JS engine ever to ship this. JavaScriptCore was first in 2016. V8 tried and walked it back. SpiderMonkey's tracking bug is open since 2015.
Inline property caches
monomorphic shape ICs on lda_property / sta_property / call_method + proto-load; /perf & /profile harness scaffolded
Works.
Reads land on the slot in one shape compare. No speculation involved.
Intl · the internationalization API
CLDR-backed NumberFormat / DateTimeFormat / PluralRules / DisplayNames / ListFormat behind -Dintl=full, with embedded IANA tzdata; the default build still ships without a locale database
Opt-in.
Real CLDR output at -Dintl=full — currency accounting parentheses, metazone names, plural rules per locale. Segmenter and the lunisolar calendars are the tail.
SharedArrayBuffer · Atomics
shared-memory primitives — Atomics.wait / notify / waitAsync, cross-agent notify, a multi-agent realm pool; the SES isolation boundary stays intact
Works.
Was out by policy, then a design problem. Now it just works — wait, notify, waitAsync, across agents.
JIT tiers
Bistromath (JS) and Spasm (wasm) — baseline JITs — on by default; --no-jit opts out. The optimising tier (Ohaimark) is still future.
In progress.
There's a JIT now, and it's done hiding — on by default. The optimising tier is future Cynic's problem.

Full per-bucket scoreboard, history, and per-day deltas live in test262-results.md.