Skip to content

feat(runtime): expose gc() as a callable globalThis property - #5712

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
machineloop:feat/gc-callable-global
Jun 27, 2026
Merged

feat(runtime): expose gc() as a callable globalThis property#5712
proggeramlug merged 1 commit into
PerryTS:mainfrom
machineloop:feat/gc-callable-global

Conversation

@machineloop

@machineloop machineloop commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to the typeof gc capability-guard fix (#5711). That change makes the bare-identifier guard if (typeof gc === "function") gc() work, but the other idiomatic capability-guard forms read gc as a value off globalThis:

  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).
if (globalThis.gc) globalThis.gc();   // the Node CLI docs' own example
global.gc?.();                        // Bun's documented optional-chaining form
const g = globalThis.gc; g?.();

These all read globalThis.gc, which Perry never installed — so they were
undefined / silent no-ops even though gc() is fully callable.

This installs gc as a real callable property on the globalThis singleton,
exactly like setTimeout / queueMicrotask, routed to the same
js_gc_collect the bare gc() intrinsic uses.

Changes

  • crates/perry-runtime/src/object/global_this/builtin_thunks.rs — new
    global_this_gc_thunk: calls js_gc_collect, returns undefined. Node's
    optional force argument is accepted (arity 1) but ignored (Perry's gc is
    a full collection).
  • crates/perry-runtime/src/object/global_this_tables.rs — add "gc" to
    GLOBAL_THIS_BUILTIN_FUNCTIONS.
  • crates/perry-runtime/src/object/global_this/populate.rs — install gc
    (non-enumerable; arity 1) as a ClosureHeader-backed value on the singleton.
  • crates/perry-runtime/src/object/global_this.rs — re-export the thunk.

After this, typeof globalThis.gc === "function", globalThis.gc is truthy,
and globalThis.gc() / globalThis.gc?.() / gc?.() /
const g = globalThis.gc; g() all run a real collection.

Related issue

n/a — completes the --expose-gc-family capability-guard idioms for Perry's
always-available gc. Builds on the typeof gc fix (#5711).

Test plan

# Each guard form must ACTUALLY collect (200MB dead garbage/round, then the form):
$ perry compile gc_forms.ts -o gc_forms && ./gc_forms
typeof globalThis.gc   = function
globalThis.gc truthy   = yes
A if(globalThis.gc) globalThis.gc():  212 MB
B globalThis.gc?.():                  219 MB
C gc?.():                             240 MB     # all bounded (collecting), not climbing 200/round

$ cargo build --release -p perry-runtime          # clean
$ cargo test -p perry-runtime --lib global_this    # 18 passed
$ cargo fmt --check -p perry-runtime              # clean

No regression: setTimeout / parseInt / fetch remain installed callable
globals; timers still fire.

(Known minor gap, out of scope: the bare-identifier rebind const f = gc; f()
still reports typeof f === "boolean"gc lowers to an ExternFuncRef value
like setTimeout does, but unlike setTimeout doesn't materialize to a closure
in that position. It is not one of the documented guard idioms and is left as a
separate follow-up.)

  • cargo build --release clean — built -p perry-runtime (and -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 cargo test -p perry-runtime --lib global_this (18/18) plus the end-to-end guard-forms check; full workspace deferred to CI (perry-ui needs gdk-pixbuf).
  • (if user-facing) Added or updated a test under test-files/ or a #[test]global_this builtin install covered by perry-runtime tests; the guard forms verified end to end above.
  • (if CLI / stdlib / runtime API changed) Updated docs/src/ — n/a, an internal global-builtin install, no new public API surface.
  • (if touching a platform UI backend) Built -p perry-ui-<backend> — n/a.

Screenshots / output

n/a — see the bounded-RSS guard-forms output 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 — feat(runtime): …
  • I've read CONTRIBUTING.md and agree to the Code of Conduct

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Added a new globalThis.gc() function to the runtime.
    • Calling globalThis.gc() triggers garbage collection and returns undefined.
    • Supports an optional argument (e.g., globalThis.gc(force)), which is accepted but ignored.
    • Enables common capability-guard usage patterns such as checking and invoking globalThis.gc safely.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a15566d7-5200-4dc4-ae41-92cb6f80824c

📥 Commits

Reviewing files that changed from the base of the PR and between fb49b05 and 881bab8.

📒 Files selected for processing (4)
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/global_this/builtin_thunks.rs
  • crates/perry-runtime/src/object/global_this/populate.rs
  • crates/perry-runtime/src/object/global_this_tables.rs
✅ Files skipped from review due to trivial changes (2)
  • crates/perry-runtime/src/object/global_this/populate.rs
  • crates/perry-runtime/src/object/global_this_tables.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/global_this/builtin_thunks.rs

📝 Walkthrough

Walkthrough

The runtime adds a callable globalThis.gc, including the thunk that triggers garbage collection and the wiring that exposes it through the global built-in function tables and population path.

Changes

GlobalThis gc builtin exposure

Layer / File(s) Summary
Thunk and re-export
crates/perry-runtime/src/object/global_this/builtin_thunks.rs, crates/perry-runtime/src/object/global_this.rs
Adds global_this_gc_thunk, which accepts an ignored force argument, calls the GC collector, returns undefined, and is re-exported from global_this.rs.
Builtin registration
crates/perry-runtime/src/object/global_this_tables.rs, crates/perry-runtime/src/object/global_this/populate.rs
Adds gc to the global builtin function list and installs it as a non-enumerable globalThis.gc callable with arity 1.

Sequence Diagram(s)

sequenceDiagram
  participant globalThis
  participant global_this_gc_thunk
  participant js_gc_collect

  globalThis->>global_this_gc_thunk: gc([force])
  global_this_gc_thunk->>js_gc_collect: collect garbage
  js_gc_collect-->>global_this_gc_thunk: done
  global_this_gc_thunk-->>globalThis: undefined
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A bunny hopped through global night,
and found a gc beacon bright.
Thump-thump, the heap went clean and neat,
then undefined landed on its feet.
🐇✨

Possibly related PRs

  • PerryTS/perry#5711: Updates HIR typeof gc lowering so capability-guard checks can recognize gc as callable.
  • PerryTS/perry#5714: Changes the underlying GC collection behavior that globalThis.gc invokes.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main change: exposing gc() on globalThis as a callable property.
Description check ✅ Passed The description follows the required template and includes a summary, concrete changes, related issue, test plan, screenshots/output, and checklist.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Follow-up to the `typeof gc` capability-guard fix. That change makes the
bare-identifier guard `if (typeof gc === "function") gc()` work, but the
other idiomatic capability-guard forms read `gc` as a VALUE:

  if (globalThis.gc) globalThis.gc();   // Node CLI docs' own example
  global.gc?.();                        // Bun's documented form
  const g = globalThis.gc; g?.();

These all read `globalThis.gc`, which Perry never installed, so they were
`undefined` / no-ops even though `gc()` is callable.

Install `gc` as a real callable property on the globalThis singleton (a
ClosureHeader-backed value, like `setTimeout`/`queueMicrotask`), routed to
the same `js_gc_collect` the bare `gc()` intrinsic uses. Non-enumerable; the
optional Node `force` argument is accepted but ignored (Perry's gc is a full
collection). Now `typeof globalThis.gc === "function"`, `globalThis.gc` is
truthy, and `globalThis.gc()` / `globalThis.gc?.()` / `gc?.()` all run a real
collection.
@machineloop
machineloop force-pushed the feat/gc-callable-global branch from fb49b05 to 881bab8 Compare June 27, 2026 02:10
@proggeramlug
proggeramlug merged commit 8f5d2d1 into PerryTS:main Jun 27, 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