Skip to content

refactor(stdlib): remove cron/exponential-backoff/moment/node-forge native bindings - #10795

Closed
proggeramlug wants to merge 3 commits into
mainfrom
remove/9-binding-batch
Closed

proggeramlug wants to merge 3 commits into
mainfrom
remove/9-binding-batch

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 1 of the 9-package native-binding-removal batch probed cheerio, cron, exponential-backoff, ioredis, moment, node-forge, nodemailer, undici, ws at each binding's pinned upstream version (from well_known_bindings.toml), forcing real-source compilation via perry.compilePackages and diffing stdout byte-for-byte against node --experimental-strip-types (Node 26.5.1) on a fixture exercising each package's primary documented use.

Phase 2 (this PR) removes the bindings for the four packages that passed: cron, exponential-backoff, moment, node-forge. The other five stay, each with a concrete blocker below.

Phase 1 results

Package Version Compiles Modules Matches node (stdout) Verdict
cheerio 1.2.0 yes (21 pkgs in compilePackages) 21 no stays — see blocker
cron 4.4.0 yes 2 yes removed
exponential-backoff 3.1.3 yes 1 yes removed
ioredis 5.11.1 yes 9 yes (stderr diverges, benign) stays — shared infra, see below
moment 2.30.1 yes 1 yes removed
node-forge 1.4.0 yes 1 yes removed
nodemailer 9.0.3 yes 1 yes on the narrow smoke path stays — see blocker
undici 8.9.0 yes 112 no stays — see blocker
ws 8.21.1 yes 1 no stays — see blocker

Removed (real npm source now compiles via perry.compilePackages)

  • cron 4.4.0 — fixture: real-time CronJob tick scheduling (asserted via guaranteed count bounds, not an exact tick count, since the phase offset within the first wall-clock second is not reproducible across two separate process launches) + CronTime/nextDate() pattern parsing. 2 modules (cron, luxon). Byte-for-byte identical to node.
  • exponential-backoff 3.1.3 — fixture: backOff() retry-until-success, the retry predicate callback, and the exhausted-attempts rejection path. 1 module. Byte-for-byte identical.
  • moment 2.30.1 — fixture: parse/format/add/subtract/diff/startOf/endOf/duration against fixed UTC dates (never moment() with no args, so output is reproducible across separate runs). 1 module. Byte-for-byte identical.
  • node-forge 1.4.0 — fixture: RSA-2048 keygen, self-signed X.509 build + SHA-256 sign + self-verify, PEM round-trips both ways, SHA-256 digest of a fixed string. (RSA keygen is random, so the fixture asserts derived facts — bit length, round-trip equality, verify result — rather than diffing raw key bytes.) 1 module. Byte-for-byte identical, including the SHA-256 digest.

Removed per package: the perry-ext-<pkg> crate, each package's hidden perry-stdlib-side duplicate (perry-stdlib/src/cron.rs, exponential_backoff.rs, moment.rs — every binding audited in this campaign so far has had one, per #10678), the well_known_bindings.toml binding + upstream-pin rows, NATIVE_MODULE_TABLE dispatch rows (native_table/dates.rs deleted outright — after date-fns/dayjs's earlier removal it held only moment's 27 rows), HIR special-casing (node-forge's dedicated try_node_forge_namespace sub-namespace flattener; single-line dispatch-hint arms for cron/moment elsewhere), js_<pkg>_* FFI declarations, and API-manifest entries (methods/classes + NATIVE_MODULES).

cron's two event-loop liveness FFI symbols (js_cron_timer_tick / js_cron_timer_has_pending) are not removed — the generated entry loop calls them unconditionally every iteration regardless of whether a program uses cron, so they stay as the permanent 0-returning stub (previously the fallback only when the scheduler feature was off).

Not removed, with each blocker

  • cheerio 1.2.0cheerio.load(html) returns a callable object built via Object.assign(initialize, staticMethods, {...}) (a plain function decorated with static/prototype properties — the standard cheerio $ factory shape). typeof $ correctly reports "function", but calling it — $('h2') — throws TypeError: string "h2" is not a function. Minimal repro:

    import * as cheerio from "cheerio";
    const $ = cheerio.load("<h2>hi</h2>");
    console.log(typeof $);   // "function" — correct
    console.log($("h2").text()); // throws

    Real compiler defect (a function value carrying extra properties isn't invocable), not attempted here.

  • ioredis 5.11.1 — functionally correct: stdout is byte-for-byte identical to node (set/get/callback-get/zadd+zrange WITHSCORES/incr against a real local redis, db 15). Two Perry-only stderr lines diverge (Warning: Accessing non-existent property 'default' of module exports inside circular dependency) — a real but stdout-invisible divergence in Perry's CJS circular-require init order; this repo's own parity harness diffs stdout only (run_parity_tests.sh's own comment: "the harness only diffed stdout"), so this doesn't block on this convention, and I'm not otherwise fixing it here.

    Not removed despite passing: perry-ext-ioredis is shared by ioredis, iovalkey, and redis (three separate npm packages, one crate) — the brief only flagged iovalkey, but redis shares it too, per well_known_bindings.toml's own rows. More importantly, the literal string "ioredis" is load-bearing shared infrastructure, not just an npm package name:

    1. crates/perry-codegen/src/ext_registry.rs's EXT_PREFIX_REGISTRY routes every js_ioredis_* emitted symbol back to the well_known_bindings.toml binding key "ioredis" to resolve which .a to auto-link — its own comment: "ioredis, iovalkey, and valkey all share this wrapper... so the single 'ioredis' binding key covers every RESP package that lowers here." Deregistering [bindings.ioredis] breaks this lookup for iovalkey/redis too, not just for the npm package literally named ioredis.
    2. The bare Redis type-name resolves to the canonical module string "ioredis" in 8 HIR/codegen files (static_and_instance.rs, native_new.rs, module_decl.rs, local_natives.rs, misc.rs, fn_decl.rs, expr_function.rs, ir/module.rs) — this is iovalkey/redis's own dispatch path for their (identically-named) Redis class, not something specific to the ioredis npm package.

    iovalkey is explicitly staying (open cosmetic issue Spurious 'non-existent property default inside circular dependency' warnings on stderr for iovalkey — Node emits none; the emulated-warning trigger over-fires #10760) and redis isn't part of this batch either, so I stopped rather than working around this — per the brief's instruction.

  • nodemailer 9.0.3 — confirms the brief's stated known expectation. nodemailer.js unconditionally requires ./smtp-transport, which unconditionally requires ../xoauth2, whose lib/xoauth2/index.js does class XOAuth2 extends Stream against the bare node:stream Stream (destructured const { Stream } = require("stream")) — not Readable/Writable/Duplex/Transform, which fix(runtime): dispatch node:stream super() through any bound-export heritage shape #10649 fixed. new XOAuth2(...) throws TypeError: is not a constructor. A narrower fixture (createTransport({ jsonTransport: true }) + sendMail(), which never touches XOAuth2) compiles and matches node byte-for-byte, but real OAuth2 SMTP auth (Gmail, etc.) is broken. Per the brief: "nodemailer simply does not get removed — do not try to fix it."

  • undici 8.9.0 — real source compiles cleanly (112 modules). request() throws before even attempting a TCP connection. Minimal, package-independent-shaped repro:

    import { request } from "undici";
    await request("http://127.0.0.1:1/x");
    // node: "connect ECONNREFUSED 127.0.0.1:1" (correct — attempted the connect)
    // perry: TypeError: Cannot read properties of undefined (reading 'length')
  • ws 8.21.1 — real source compiles. The server side of the WebSocket handshake is actually correct: a raw curl Upgrade request against a Perry-compiled WebSocketServer gets back HTTP/1.1 101 Switching Protocols with Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= — the exact RFC 6455 example value for that example key, so the handshake math is right. A real ws client (either the compiled one, or an unmodified Node ws client run under Node) successfully reaches open. But the compiled server then throws TypeError: Cannot read properties of undefined (reading 'destroy') before the user-level 'connection' event ever fires — logged in the server process, not surfaced to the client (the client instead just hangs / the client-and-server-in-one-process variant surfaced it as Unexpected server response: 426, presumably because the crash truncated the response mid-write in that ordering).

Counts (re-derived from the resolved tree)

  • Workspace members / crates/ directories / workspace-architecture.json policy entries / sum of decision_counts: 66, all four agree (was 70).
  • scripts/native_result_ledger.py: 356 → 349 rows, 307 → 300 providers.
  • scripts/unrooted_local_shape.py --update-baseline and scripts/string_payload_access_inventory.py --write-baseline re-run (baselines regenerated, not hand-edited).
  • docs/api/perry.d.ts / docs/src/api/reference.md regenerated from a fresh cargo build --release -p perry (2041 → 2026 entries, 122 → 118 modules); docs/src/native-libraries/governance.md's generated table regenerated via python3 scripts/binding_governance.py --table. All three diffs are pure deletions with intact tails — not the destructive-truncation shape.
  • Deleted the four packages' own gap/parity test files (test_gap_backoff_options.ts, test_gap_cron_cronjob.ts, test_gap_moment_methods.ts, test_parity_moment.ts) — each imported its removed npm package directly with no compilePackages, so its only subject was the deleted native stub. Also removed the now-stale test_parity_moment entry from test-parity/known_failures.json (it documented a bug in the shim being deleted).

What was and wasn't run

  • cargo fmt --all -- --check: clean.
  • cargo check --workspace --all-targets --exclude perry-ui-gtk4 under RUSTFLAGS="-D warnings" on the default dev profile: clean.
  • cargo test -p perry-hir -p perry-api-manifest -p perry-codegen: all pass except every_dispatch_entry_has_manifest_counterpart, which is pre-existing on origin/main before this branch's changes (verified via git stash) — 15 unrelated net/http dispatch↔manifest drift entries, nothing to do with this PR.
  • cargo test -p perry --bin perry: 1139 passed, 0 failed, including the three tests that directly named the removed packages (shipped_subset_bindings_are_partial, ext_prefix_binding_keys_resolve_to_wrapper_crates, ext_binding_build_routing_split), all updated to drop node-forge and re-verified green.
  • run_lint_gates.sh with SKIP_COMPILE_GATES=1: 78 of 79 passed. The one failure, "Public benchmark evidence freshness," is the pre-existing red documented in CLAUDE.md/ci: two reds on main fail every PR — gap-suite shard 5 parity regression (test_gap_10430) and a stale public benchmark baseline #10707 — not chased.
  • node scripts/binding_pins.mjs --check under Node 26.5.1: OK, 20 pinned.
  • Not run: the compile tier of run_lint_gates.sh (documented known-red on this Linux host); a full gap sweep (fixed port 17891, unavailable on this shared host, per the standing guidance for this host).

Branched from origin/main at 8e9f6f09e (v0.5.1616); main advanced one merge train to 1a4fa6507e (v0.5.1617) while this was in flight — checked, no file overlap with anything this PR touches.

Summary by CodeRabbit

  • Changes

    • Removed bundled native support for cron, exponential-backoff, moment, and node-forge.
    • These packages now resolve through their real npm source when compiled, rather than bundled implementations.
  • Documentation

    • Updated API references and standard-library documentation to remove the retired module declarations and examples.
    • Added migration notes confirming successful compilation of the upstream package sources.

Rebase note (2026-09-20)

Rebased onto main @ b9ba951ff861c61afb845bfbdfa574cb0fa4080e (train 239) as part of a
4-PR sequential rebase campaign together with #10677, #10680, #10704 — all four
independently rebased onto this same main SHA and pushed together so they are mutually
consistent as of this snapshot. This PR's own base was already main (3 commits behind);
only Cargo.lock conflicted, resolved by taking main's side and resyncing via
cargo metadata --offline (dropped 109 further stale transitive-dep lines).

Recomputed triple (workspace-architecture.json baseline / native_result_ledger.py /
unrooted-local-shape), derived from the resolved tree via the actual tools, not carried
over: workspace_members=66 (decision_counts: externalize=17, keep=44, merge=1, remove=1,
review=3) — already correct as auto-merged, no edit needed; native_result_ledger: 349
rows / 300 providers — already correct as committed, no edit needed;
unrooted-local-shape total: 527 — unchanged (none of cron/exponential-backoff/moment/
node-forge's files carried findings).

These numbers assume main is still at the stated SHA. #10677/#10680/#10704 remove
different crates from the same starting point — if any of them lands before this one, these
counts (and the other three PRs') need re-deriving against the tree as it then stands.

Shared-crate hazard check: grepped ext_registry.rs and well_known_bindings.toml for
cron/exponential-backoff/moment/node-forge. node-cron's own binding was already
removed separately (#466, confirmed via stdlib_features.rs's comment) — only cron itself
still routed through perry-ext-cron, so no shared-crate breakage.

Gates run on this branch: cargo fmt --all -- --check OK; cargo check --workspace --all-targets under -D warnings on the default dev profile (excl. perry-ui-gtk4) —
clean, 0 warnings; run_lint_gates.sh SKIP_COMPILE_GATES=1 — 78 of 79 passed (1
pre-existing: "Public benchmark evidence freshness", #10707, not chased, per the campaign
brief); binding_governance.py --check OK; binding_pins.mjs --check under Node 26.5.1 OK;
check_file_size.sh OK. docs/api/perry.d.ts / docs/src/api/reference.md regenerated from
a fresh perry-dev build and diffed — zero drift (already correct as auto-merged).
Compile tier of run_lint_gates.sh not run (Linux compile tier is known-red per the brief).
No gap sweep run (fixed port 17891, per the brief). No acceptance re-run — the packages'
behavior is unchanged by this rebase, only the base commit moved.

proggeramlug pushed a commit that referenced this pull request Sep 20, 2026
@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: a460befb-6cf4-4bff-a307-56189709b775

📥 Commits

Reviewing files that changed from the base of the PR and between 1a4fa65 and 4aeb292.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • scripts/native_result_ledger.tsv is excluded by !**/*.tsv
📒 Files selected for processing (57)
  • Cargo.toml
  • changelog.d/10795-remove-cron-backoff-moment-forge.md
  • crates/perry-api-manifest/src/entries.rs
  • crates/perry-api-manifest/src/entries/part_1.rs
  • crates/perry-api-manifest/src/entries/part_2.rs
  • crates/perry-api-manifest/tests/stub_inventory.rs
  • crates/perry-codegen/src/ext_registry.rs
  • crates/perry-codegen/src/lower_call/builtin.rs
  • crates/perry-codegen/src/lower_call/native_table/dates.rs
  • crates/perry-codegen/src/lower_call/native_table/media.rs
  • crates/perry-codegen/src/lower_call/native_table/mod.rs
  • crates/perry-codegen/src/lower_call/native_table/utils_crypto.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi/utilities.rs
  • crates/perry-ext-cron/Cargo.toml
  • crates/perry-ext-cron/src/lib.rs
  • crates/perry-ext-exponential-backoff/Cargo.toml
  • crates/perry-ext-exponential-backoff/src/lib.rs
  • crates/perry-ext-moment/Cargo.toml
  • crates/perry-ext-moment/src/lib.rs
  • crates/perry-ext-node-forge/Cargo.toml
  • crates/perry-ext-node-forge/src/crypto.rs
  • crates/perry-ext-node-forge/src/lib.rs
  • crates/perry-ext-node-forge/tests/openssl_e2e.rs
  • crates/perry-hir/src/destructuring/var_decl/native_new.rs
  • crates/perry-hir/src/js_transform/local_natives.rs
  • crates/perry-hir/src/lower/expr_call/mod.rs
  • crates/perry-hir/src/lower/expr_call/native_module.rs
  • crates/perry-hir/src/lower/expr_call/static_and_instance.rs
  • crates/perry-hir/src/lower/module_decl.rs
  • crates/perry-hir/tests/node_forge_namespace_lowering.rs
  • crates/perry-stdlib/Cargo.toml
  • crates/perry-stdlib/src/cron.rs
  • crates/perry-stdlib/src/exponential_backoff.rs
  • crates/perry-stdlib/src/lib.rs
  • crates/perry-stdlib/src/moment.rs
  • crates/perry-ui-android/src/stdlib_stubs.rs
  • crates/perry/src/commands/compile/optimized_libs/tests.rs
  • crates/perry/src/commands/compile/well_known.rs
  • crates/perry/src/commands/stdlib_features.rs
  • crates/perry/well_known_bindings.toml
  • docs/api/perry.d.ts
  • docs/src/api/reference.md
  • docs/src/native-libraries/governance.md
  • docs/src/stdlib/other.md
  • docs/src/stdlib/utilities.md
  • scripts/ci_ext_link_scope.py
  • scripts/gc_runtime_root_holders.json
  • scripts/native_result_ledger.py
  • scripts/string_payload_access_baseline.txt
  • scripts/unrooted_local_shape_baseline.json
  • test-files/test_gap_backoff_options.ts
  • test-files/test_gap_cron_cronjob.ts
  • test-files/test_gap_moment_methods.ts
  • test-files/test_parity_moment.ts
  • test-parity/known_failures.json
  • workspace-architecture.json
💤 Files with no reviewable changes (38)
  • test-files/test_gap_backoff_options.ts
  • crates/perry/src/commands/stdlib_features.rs
  • crates/perry-ext-exponential-backoff/Cargo.toml
  • crates/perry-ext-cron/Cargo.toml
  • crates/perry-hir/src/destructuring/var_decl/native_new.rs
  • test-parity/known_failures.json
  • crates/perry-ext-cron/src/lib.rs
  • test-files/test_parity_moment.ts
  • crates/perry-ext-node-forge/tests/openssl_e2e.rs
  • test-files/test_gap_moment_methods.ts
  • crates/perry-ext-node-forge/src/crypto.rs
  • crates/perry-api-manifest/src/entries/part_2.rs
  • docs/src/stdlib/utilities.md
  • crates/perry-stdlib/src/moment.rs
  • crates/perry-api-manifest/src/entries/part_1.rs
  • crates/perry-codegen/src/lower_call/native_table/dates.rs
  • docs/src/stdlib/other.md
  • crates/perry-stdlib/src/cron.rs
  • crates/perry-codegen/src/lower_call/native_table/mod.rs
  • crates/perry-ext-exponential-backoff/src/lib.rs
  • crates/perry-hir/src/lower/expr_call/mod.rs
  • crates/perry-codegen/src/lower_call/native_table/media.rs
  • test-files/test_gap_cron_cronjob.ts
  • crates/perry-stdlib/src/exponential_backoff.rs
  • crates/perry-hir/src/lower/module_decl.rs
  • crates/perry-ext-node-forge/Cargo.toml
  • crates/perry-ext-moment/Cargo.toml
  • crates/perry-codegen/src/lower_call/builtin.rs
  • scripts/gc_runtime_root_holders.json
  • crates/perry-hir/tests/node_forge_namespace_lowering.rs
  • docs/src/native-libraries/governance.md
  • Cargo.toml
  • crates/perry-codegen/src/lower_call/native_table/utils_crypto.rs
  • crates/perry-ext-node-forge/src/lib.rs
  • crates/perry/well_known_bindings.toml
  • crates/perry-ext-moment/src/lib.rs
  • crates/perry-hir/src/js_transform/local_natives.rs
  • crates/perry-ui-android/src/stdlib_stubs.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The PR removes bundled cron, exponential-backoff, moment, and node-forge support from workspace configuration, runtime implementations, compiler dispatch, API manifests, documentation, tests, and repository inventories.

Changes

Bundled binding removal

Layer / File(s) Summary
Binding registry and workspace contracts
Cargo.toml, crates/perry-api-manifest/..., crates/perry/well_known_bindings.toml, workspace-architecture.json, changelog.d/*
The four bindings are removed from workspace membership, dependency declarations, API manifests, well-known binding resolution, architecture records, and stub inventories.
Runtime implementation removal
crates/perry-ext-*/*, crates/perry-stdlib/..., crates/perry-ui-android/...
The binding crates and duplicate standard-library implementations are deleted. Related features, dependencies, FFI symbols, and Android stubs are removed or retained only for timer liveness symbols.
Compiler and lowering cleanup
crates/perry-codegen/..., crates/perry-hir/..., crates/perry/src/commands/...
Native dispatch tables, FFI declarations, CronJob lowering, Moment chaining, node-forge namespace lowering, prefix routing, and feature mappings no longer reference the removed bindings.
Documentation and validation updates
docs/..., test-files/..., test-parity/..., scripts/...
Generated API documentation, standard-library documentation, gap tests, parity records, compiler tests, GC inventories, native-result ledgers, and baseline counts are updated for the removals.

Priority: ⬇️ Low

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Refactor

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.42% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 13 files. (6 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the removal of the four native bindings, which is the main change in the pull request.
Description check ✅ Passed The description is detailed and on-topic. It explains the motivation, package results, removed components, blockers, generated updates, and test coverage. It omits some template headings, such as Rela…
Full details: Docstring Coverage

Explanation

Docstring coverage is 68.42% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 13 files. (6 skipped: 6 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch remove/9-binding-batch
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

proggeramlug pushed a commit that referenced this pull request Sep 20, 2026
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Queued as the next train, behind #10793+#10787 which are validating now. Reading it, the structure is right and worth saying so explicitly, because it is the shape the campaign needed after #10765.

Probing nine and removing four is the correct order. #10765 had to pull typescript back out at the last minute — removing its binding made perry execute real typescript.js for the first time and it threw (#10772, still open). A phase-1 probe that produces four "removed" and five "stays, here is the blocker" verdicts is how that gets caught before the diff exists rather than after. The five blockers being concrete rather than "didn't get to it" is the part that makes this reviewable.

Three details I noticed that suggest the fixtures were built by someone who had thought about reproducibility, and which I would rather not see simplified away later:

  • cron asserting guaranteed count bounds rather than an exact tick count, because the phase offset within the first wall-clock second is not reproducible across two process launches. There is an open issue (Two more test_gap_ fixtures decide a printed boolean by wall clock (siblings of #10581) #10720) about test_gap_ fixtures that decide a printed boolean by wall clock; this avoids becoming another one.
  • moment never calling moment() with no arguments.
  • node-forge asserting derived facts — bit length, round-trip equality, verify result — rather than diffing raw RSA key bytes, since keygen is random.

On cron specifically: train 231 removed node-cron but deliberately kept cron, because the two share perry-ext-cron and only node-cron's own well-known-binding entry, manifest rows and ("node-cron", "schedule") => "CronJob" HIR tagging came out. This PR removes the other half, so perry-ext-cron should disappear entirely — worth confirming the crate is gone from Cargo.lock and not merely orphaned.

What the train will check, flagged now so none of it is a surprise:

…ative bindings

Removes the perry-ext-cron, perry-ext-exponential-backoff, perry-ext-moment,
and perry-ext-node-forge crates (and each package's hidden perry-stdlib-side
duplicate: cron.rs, exponential_backoff.rs, moment.rs), plus their
well_known_bindings.toml rows, NATIVE_MODULE_TABLE dispatch rows, HIR
special-casing, FFI declarations, and API-manifest entries.

Real npm source for all four now compiles cleanly via perry.compilePackages
and matches node --experimental-strip-types byte-for-byte on a fixture
exercising each package's primary documented use:
  - cron 4.4.0: CronJob tick scheduling + CronTime pattern parsing (2 modules)
  - exponential-backoff 3.1.3: backOff retry/backoff/predicate paths (1 module)
  - moment 2.30.1: parse/format/arithmetic/diff/duration (1 module)
  - node-forge 1.4.0: RSA keygen + X.509 build/sign/verify + PEM round-trip
    (1 module)

cron's two event-loop liveness FFI symbols (js_cron_timer_tick /
js_cron_timer_has_pending) stay as unconditional 0-returning stubs -- the
generated entry loop calls them every iteration regardless of whether a
program uses cron at all.

Five other probed packages in the same 9-package batch are NOT removed:

  - cheerio 1.2.0: cheerio.load() returns a callable object decorated with
    static/prototype properties (Object.assign(initialize, staticMethods,
    {...})); calling it as a function ($('h2')) throws
    'TypeError: string "h2" is not a function' under Perry, even though
    'typeof $' correctly reports "function". Real compiler defect, not
    attempted here.

  - ioredis 5.11.1: functionally correct (stdout byte-for-byte identical to
    node; two Perry-only stderr warnings about a circular '.default'
    access that node's loader doesn't hit for the same import -- benign,
    stdout-only comparison is this repo's convention). NOT removed: the
    literal string "ioredis" is shared infrastructure, not just an npm
    package name. It is (a) the well_known_bindings.toml lookup key that
    EXT_PREFIX_REGISTRY's 'js_ioredis_*' prefix resolves through for
    iovalkey and redis's own static-lib auto-linking (see
    perry-codegen/src/ext_registry.rs's own comment: "ioredis, iovalkey,
    and valkey all share this wrapper... so the single 'ioredis' binding
    key covers every RESP package"), and (b) the internal canonical
    dispatch key that iovalkey/redis's bare 'Redis' type-name resolution
    routes through in 8 HIR/codegen files (static_and_instance.rs,
    native_new.rs, module_decl.rs, local_natives.rs, misc.rs, fn_decl.rs,
    expr_function.rs, ir/module.rs). Deregistering it would break iovalkey
    and redis, which are staying.

  - nodemailer 9.0.3: confirms the known #10649 gap -- nodemailer's
    lib/xoauth2/index.js does 'class XOAuth2 extends Stream' against the
    BARE node:stream Stream (not Readable/Writable/Duplex/Transform, which
    #10649 fixed). 'new XOAuth2(...)' throws 'TypeError: is not a
    constructor'. This is loaded unconditionally on require("nodemailer")
    (nodemailer.js -> smtp-transport -> xoauth2), so any OAuth2 SMTP auth
    (e.g. Gmail) is broken under a real-source compile, even though the
    narrower JSONTransport/sendMail smoke path compiles and runs correctly.

  - undici 8.9.0: real source compiles (112 modules) but request() throws
    "TypeError: Cannot read properties of undefined (reading 'length')"
    before attempting a TCP connect -- minimal repro:
    import { request } from "undici"; await request("http://127.0.0.1:1/x")

  - ws 8.21.1: real source compiles and completes the WebSocket handshake
    correctly (verified byte-for-byte against a raw curl Upgrade request,
    matching RFC 6455's example Sec-WebSocket-Accept), but the compiled
    WebSocketServer then throws "TypeError: Cannot read properties of
    undefined (reading 'destroy')" immediately after the handshake, before
    the user-level 'connection' event ever fires.

Counts re-derived from the resolved tree, not carried forward: workspace
members / API-manifest NATIVE_MODULES / crates all agree at 66;
scripts/native_result_ledger.py: 356->349 rows, 307->300 providers;
scripts/unrooted_local_shape.py --update-baseline; docs/api/perry.d.ts and
docs/src/api/reference.md regenerated from a fresh release build
(2041->2026 entries, 122->118 modules); docs/src/native-libraries/governance.md
regenerated via binding_governance.py --table.

Not run: the compile tier of run_lint_gates.sh (known-red on this Linux
host per CLAUDE.md); a full gap sweep (fixed port, not available on this
shared host). run_lint_gates.sh with SKIP_COMPILE_GATES=1: 78 of 79 passed,
1 pre-existing failure (Public benchmark evidence freshness, #10707/#10573,
unrelated to this change).

# Conflicts:
#	Cargo.lock
proggeramlug pushed a commit that referenced this pull request Sep 20, 2026
… orphaned decimal.js parity fixture

Removes the stray perry-ext-decimal crate entry that survived the rebase's
auto-merge in workspace-architecture.json, resyncs Cargo.lock, and
recomputes native_result_ledger EXPECTED_ROWS/PROVIDERS (344 rows, 295
providers), the unrooted-local-shape baseline, the generated
binding-governance table, and docs/api/perry.d.ts + docs/src/api/reference.md
from a fresh perry-dev build.

crates/perry-hir/src/lower_patterns.rs: detect_native_instance_expr's
new-expression arm went dead. Its match on class_name used to have five live
arms (Big/Decimal/BigNumber from this PR, LRUCache/Command from the already-
landed #10708/#10712) -- with all five gone the fallback-only match triggered
rustc's unreachable_code lint under -D warnings. Simplified the arm to what
it now always evaluates to (None after the local-class shadow check), and
rewrote the function doc comment to explain why the stub is kept rather than
deleted. This is a sequencing interaction the brief calls out explicitly:
this file wasn't touched by mysql2/pg/cron's diffs, but decimal.js landing
after commander/lru-cache emptied a match neither PR could see on its own.

test-files/test_parity_decimal.ts + its test-parity/known_failures.json
entry: the original PR left this fixture behind (unlike #10795, which
deleted its own moment/cron/backoff test files as part of the same removal).
The fixture is now double-dead: decimal.js has no Perry-specific behavior
left to validate, and the file was already skip-listed as a broken oracle
(node itself can't resolve decimal.js post-npm-ci, #8271) before this PR.
proggeramlug pushed a commit that referenced this pull request Sep 20, 2026
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Landed via merge train 240 (#10819) as v0.5.1619c1d9f73e58.

Probing nine packages and removing only the four that passed is the right shape, and it is worth saying so plainly: #10765 had to pull typescript at the last minute because removing its binding made perry execute real typescript.js for the first time and it threw (#10772). A phase-1 probe that yields four "removed" and five "stays, here is the blocker" is how that gets caught before the diff exists.

The fixtures earned their keep too — cron asserting count bounds rather than an exact tick count, moment never calling moment() bare, node-forge asserting derived facts instead of diffing random RSA bytes. #10720 tracks fixtures that decide a printed boolean by wall clock; these deliberately avoid becoming more of them.

Two things the train adds, both found in review.

The release provider broke again. tests/release/packages/next-app-route/provider/stdlib/Cargo.toml still enabled bundled-moment and bundled-exponential-backoff, which this PR deletes. That crate is not a workspace member, so cargo check --workspace --all-targets cannot see it — it breaks only in the nightly release-package smoke. Merge train 231 did exactly this with rate-limit and bundled-dayjs. Second time in a day, so it is now a cheap gate rather than a thing to remember: check_nonworkspace_features.py, proven to discriminate (clean on the fixed tree, exits 1 on the pre-fix tree naming both features). Worth running before your next removal — five more are queued.

I restored test_gap_cron_cronjob.ts, which the PR deleted. After the binding is gone, import { CronJob } from "cron" resolves to the real package — still a devDependency at ^4.4.0 — so the fixture stops testing a shim and starts witnessing what the removal claims. That is the precedent train 231 set by keeping the dayjs and ratelimiter fixtures, and it is what makes the gap gate go red the day real cron diverges.

It is also a fixture with real thought in it: #10581 rewrote its wait as a barrier rather than a deadline after the original raced a 10-second wall clock against a one-per-second schedule, and its header explains why no fallback bound is permitted. That reasoning is not recoverable from a deleted file. It passesgap_cron ran=1 rc=0 against real cron 4.4.0.

Counts were re-derived on the assembled tree rather than carried: ledger 349/300, governance 24 crates, pins 20, workspace policy OK. Cargo.lock regenerated with cargo metadata --offline, all four perry-ext-* crates confirmed gone — perry-ext-cron disappearing completes what train 231 started when it removed node-cron but kept cron.

Validation: ten cheap gates, -D warnings across all targets, five pinned artifacts byte-identical before and after, seven unit suites with an empty failing set, both compiler-output suites at failed_workloads=[], repsel_census rc=0, and a seven-area gap sweep (class 83, import 20, date 16, module 14, crypto 7, require 5, cron 1) with zero unexplained regressions — under a load average that peaked above 200, so any single failure would have been re-run standalone before being believed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants