Skip to content

fix(hir): report typeof gc as "function" so the optional-gc capability guard runs - #5711

Merged
TheHypnoo merged 1 commit into
PerryTS:mainfrom
machineloop:fix/typeof-gc-is-function
Jun 26, 2026
Merged

fix(hir): report typeof gc as "function" so the optional-gc capability guard runs#5711
TheHypnoo merged 1 commit into
PerryTS:mainfrom
machineloop:fix/typeof-gc-is-function

Conversation

@machineloop

@machineloop machineloop commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

An allocation-heavy program that gates collection on the idiomatic optional-gc
capability guard never collected on Perry — RSS grew unbounded (to ~physical
RAM) while an identical Node run (with --expose-gc) stayed flat.

The guard if (typeof gc === "function") gc() is standard precisely because
gc is optional: Node only exposes it under --expose-gc (where typeof gc
is otherwise "undefined"), and Bun likewise behind --expose-gc. The
Node CLI docs for --expose-gc
show the same capability guard (if (globalThis.gc) globalThis.gc()).

Perry's gc() is always a real, callable builtin (a call-intrinsic routed
to js_gc_collect). But the HIR typeof fold deliberately excluded gc
(arm_unary.rs: "gc is excluded — it's undefined in Node without
--expose-gc"
), so typeof gc reported a non-"function" value while gc()
was callable. The guard was therefore always false, and programs that use it
to bound memory silently ran with no manual collection.

  1. Node.js — the --expose-gc CLI flag docs: https://nodejs.org/api/cli.html#--expose-gc (which itself shows the capability-guard idiom if (globalThis.gc) globalThis.gc()).
  2. Bun — the Bun.gc API reference: https://bun.com/reference/bun/gc (documents --expose-gc exposing gc() on the global object, and the global.gc?.() form).

Changes

  • crates/perry-hir/src/lower/lower_expr/arm_unary.rs — include gc in the
    typeof"function" fold (alongside setTimeout, queueMicrotask,
    structuredClone, …). Since Perry's gc is genuinely available, typeof gc
    must be "function" so the capability guard runs the collector. Direct
    gc() calls are unchanged.

The complementary value-form guards (if (globalThis.gc) gc(),
global.gc?.()) read gc as a property of globalThis; those are addressed
in a follow-up that installs gc as a real callable global value.

Related issue

n/a — gc() capability-guard compatibility (the --expose-gc family of
idioms).

Test plan

# A 200MB-of-dead-garbage-per-round loop guarded by `if (typeof gc === "function") gc()`.
# Before: typeof gc !== "function" → guard false → RSS climbs 405 → 3168 MB.
# After:
$ perry compile repro.ts -o repro && ./repro
round 1: 405 MB --gc()--> 411 MB
round 6: 468 MB --gc()--> 468 MB
round 8: 469 MB --gc()--> 469 MB     # bounded plateau, not climbing

$ cargo test -p perry-hir typeof          # 5 passed
$ cargo fmt --check -p perry-hir          # clean

typeof gc now reports "function"; the guard runs gc() each round and RSS
stays bounded. No regression to the other builtins (typeof setTimeout /
parseInt / fetch remain "function"; timers still fire).

  • cargo build --release clean — built -p perry; the full-workspace build needs the GTK/gdk-pixbuf libs for perry-ui-*, which CI provides.
  • cargo test --workspace --exclude perry-ui-ios --exclude perry-ui-tvos --exclude perry-ui-watchos --exclude perry-ui-gtk4 --exclude perry-ui-android --exclude perry-ui-windows passes — ran the affected crate (cargo test -p perry-hir typeof, 5/5) plus the end-to-end repro; full workspace deferred to CI (perry-ui needs gdk-pixbuf).
  • (if user-facing) Added or updated a test under test-files/ or a #[test] — the typeof fold is covered by perry-hir typeof tests; behavior verified end to end above.
  • (if CLI / stdlib / runtime API changed) Updated docs/src/ — n/a.
  • (if touching a platform UI backend) Built -p perry-ui-<backend> — n/a.

Screenshots / output

n/a — see the repro RSS plateau in the test plan.

Checklist

  • I have NOT bumped the workspace version or edited CLAUDE.md / CHANGELOG.md (maintainer handles these at merge)
  • My commits follow the loose feat: / fix: / docs: / chore: prefix convention — fix(hir): …
  • I've read CONTRIBUTING.md and agree to the Code of Conduct

Summary by CodeRabbit

  • Bug Fixes
    • typeof gc now returns "function" for bare identifier checks, matching expected runtime behavior.
    • Updated handling so the built-in gc is treated as always available and callable.

…ity guard runs

Programs gate the optional `gc()` builtin behind the idiomatic capability
guard `if (typeof gc === "function") gc()` — written that way because Node
only exposes `gc` under `--expose-gc` (where `typeof gc === "undefined"`
otherwise) and Bun behind `--expose-gc` too. See the Node CLI docs, which
show `if (globalThis.gc) globalThis.gc()`.

Perry's `gc()` is ALWAYS a real, callable builtin (a call-intrinsic routed
to `js_gc_collect`), yet the HIR typeof fold deliberately EXCLUDED `gc`
(arm_unary.rs: "gc is excluded — it's undefined in Node without
--expose-gc"), folding `typeof gc` to a non-"function" value. So the guard
was always false: an allocation-heavy program that gates collection on it
never collected and RSS grew unbounded (to ~physical RAM), while an
identical Node run with --expose-gc stayed flat.

Include `gc` in the typeof-"function" fold (matching `setTimeout`,
`queueMicrotask`, etc.). `typeof gc` now reports "function", so the guard
runs the collector and memory stays bounded. Direct `gc()` calls are
unchanged.
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The unary typeof lowering now folds bare gc to "function" when it is not locally bound. The surrounding comment was updated to describe Perry’s always-available gc builtin.

Changes

typeof folding for gc

Layer / File(s) Summary
Comment and fold list update
crates/perry-hir/src/lower/lower_expr/arm_unary.rs
The typeof fast-path comment and the bare-identifier fold set both include gc, so unbound gc lowers to "function".

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

A bunny hopped by with a happy twitch,
and gc got its functional hitch.
Thump went the paws, so neat, so spry,
typeof gc now answers “function” high.
🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific and accurately summarizes the main change: fixing typeof gc to return "function".
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.
Description check ✅ Passed The description follows the required template and includes summary, changes, related issue, test plan, screenshots, and checklist.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@TheHypnoo
TheHypnoo merged commit 1fde7c6 into PerryTS:main Jun 26, 2026
15 checks passed
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