Skip to content

fix(hir): perry.define of process.env.* folds at lowering, not just tree-shake (#5009)#5014

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-define-process-env-5009
Jun 11, 2026
Merged

fix(hir): perry.define of process.env.* folds at lowering, not just tree-shake (#5009)#5014
proggeramlug merged 1 commit into
mainfrom
worktree-fix-define-process-env-5009

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Fixes #5009perry.define of process.env.NODE_ENV (and any process.env.*) was silently ignored in every default build. As a result, compiled packages that branch on process.env.NODE_ENV (React, react-reconciler, scheduler, …) selected their development builds, and react-reconciler's dev build then threw TypeError: value is not a function during render — the last wall before yoga for ink (#348).

Root cause (not #4993 as the issue guessed)

#4993 only touched runtime files; process.env.X still lowered to Expr::EnvGet. The real bug is broader: the define was only consulted by the env_fold branch pruner, which is gated on tree_shake — and tree-shaking is off by default (PERRY_TREE_SHAKE=1 / perry.experiments.treeShake: true only). So in a normal build the define did nothing and process.env.NODE_ENV stayed a live runtime EnvGet → js_getenv_value lookup (undefined when unset).

Fix

Fold a defined process.env.<NAME> read to its literal at the single HIR lowering point that would otherwise emit Expr::EnvGet — independent of tree-shaking and in every context (branch conditions, ternaries, closures). esbuild-style define semantics: the define wins over the runtime environment.

  • perry-hir/ir/constants.rsEnvDefine enum + ENV_DEFINES thread-local (mirrors the existing COMPILE_PACKAGES_OVERRIDE pattern; rayon-safe per worker) with set_env_defines / clear_env_defines / env_define_lookup.
  • perry-hir/lower/expr_member.rsenv_define_literal() substitutes the literal in both the process.env.X and process.env["X"] arms before emitting EnvGet.
  • perry/compile/collect_modules.rsenv_defines_for_lowering() strips the process.env. prefix from ctx.define; installed before each lower_module_full, cleared after.

Composes cleanly with env_fold: the substitution yields a string literal that try_const already folds, so branch-elimination still works under tree-shake; the tree-shake-only node_modules NODE_ENV → "production" default is unchanged. Keys without an explicit define still produce a runtime EnvGet.

Verification

  • Reproduced the bug with a react-reconciler-shaped compilePackages fixture; after the fix the production build is selected and the define wins over a conflicting runtime NODE_ENV=development.
  • No regression: without a define, runtime env reads work as before; fix(runtime): node:process default import & globalThis.process expose the full process object (#4987) #4993's test_gap_process_import_4987.ts is still byte-identical to node --experimental-strip-types.
  • New tests: crates/perry/tests/issue_5009_define_process_env.rs (2 integration) + a unit test for env_defines_for_lowering. All pass.

The secondary item in the issue (react-reconciler's development build hits a value is not a function codegen gap) is out of scope here — production is the target build and now selected.

🤖 Generated with Claude Code

…ree-shake (#5009)

perry.define of process.env.NODE_ENV was ignored in every default build:
the define was only consulted by the env_fold branch pruner, which is gated
on tree_shake — and tree-shaking is off by default. So process.env.NODE_ENV
stayed a live runtime EnvGet lookup (undefined when unset), and React /
react-reconciler / scheduler selected their development builds (crashing ink
#348 with 'value is not a function').

Fold a defined process.env.<NAME> read to its literal at the single HIR
lowering point that would otherwise emit Expr::EnvGet — independent of
tree-shaking and in every context (branch conditions, ternaries, closures).
esbuild-style define semantics: the define wins over the runtime environment.

- perry-hir/ir/constants.rs: EnvDefine enum + ENV_DEFINES thread-local
  (mirrors COMPILE_PACKAGES_OVERRIDE; rayon-safe) with set/clear/lookup.
- perry-hir/lower/expr_member.rs: env_define_literal() substitutes the
  literal in both the process.env.X and process.env["X"] arms before EnvGet.
- perry/compile/collect_modules.rs: env_defines_for_lowering() strips the
  process.env. prefix; set before each lower_module_full, cleared after.

Composes with env_fold: substitution yields a string literal that try_const
already folds, so branch-elimination still works under tree-shake; the
tree-shake-only node_modules NODE_ENV->production default is unchanged.

Tests: issue_5009_define_process_env.rs (react-reconciler-shaped fixture:
production selected, define wins over runtime env; no-define still reads
runtime env) + unit test for env_defines_for_lowering mapping.
@proggeramlug proggeramlug merged commit 0599316 into main Jun 11, 2026
11 of 13 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-define-process-env-5009 branch June 11, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant