feat(hir): classify new Function/eval call sites + refusal diagnostic (#1678)#1776
Merged
Conversation
…#1678) Phase 0 of #1677 (AOT-first eval/new Function strategy). Establishes the single decision point every later phase builds on: a classifier that buckets each `new Function` / `Function(...)` / `eval(...)` site into 1. const-foldable — literal/substitution-free body (→ #1679) 2. known-library-codegen — from fast-json-stringify / ajv / find-my-way (the Fastify JIT trio; → #1680/#1681/#1682) 3. runtime-unknown — genuinely runtime-dynamic code string Only the runtime-unknown bucket is refused, with a precise diagnostic that names the surface, file:line, and originating package (or "user source"). Buckets 1 and 2 keep their existing placeholder lowering so the phases that own them can swap it in without a behaviour change here — Phase 0 is pure analysis + reporting, it never compiles, folds, or evaluates anything. Before this, both shapes silently fell through to broken lowerings (a bare `Function`/`eval` ident → GlobalGet(0) sentinel → runtime TypeError, and `new Function(...)` → an unknown-class class_id=0 empty-object placeholder) with no indication of why. New module crates/perry-hir/src/eval_classifier.rs (pure classification + diagnostic + instrumentation), hooked at the two Function-shape lowering sites (expr_new for `new Function`, expr_call for `Function(...)`/`eval`). The `Function('return this')()` globalThis fold (#957/#959) runs first and short-circuits, so it is unaffected. Instrumentation: PERRY_EVAL_DIAG=1 logs every classified site (surface, file:line, package, bucket, body preview) to stderr. Escape hatch: PERRY_ALLOW_EVAL=1 downgrades the bucket-3 refusal to the legacy fall-through for a one-off build (mirrors #503's PERRY_ALLOW_DYNAMIC_STDLIB). Tests: 9 unit tests covering each bucket + provenance + line resolution + preview truncation. Verified end-to-end on direct `new Function`/`eval` samples (refused, with file:line + provenance), a const-foldable sample (passes through), an ajv-path sample (known-library bucket), and the existing `Function('return this')()` fold (still works).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 0 of #1677 — classify
new Function/evalsites + precise refusalCloses #1678.
What
Adds the single decision point every later phase of #1677 builds on: a
classifier (
crates/perry-hir/src/eval_classifier.rs) that buckets eachnew Function/Function(...)/eval(...)site into:fast-json-stringify/ajv/find-my-wayOnly the runtime-unknown bucket is refused. Buckets 1 & 2 keep their existing
placeholder lowering so the phases that own them can swap it in without a
behaviour change. Phase 0 is pure analysis + reporting — it never compiles,
folds, or evaluates anything (per the issue's out-of-scope note).
Before this, both shapes silently fell through to broken lowerings (bare
Function/evalident →GlobalGet(0)→ runtimeTypeError;new Function(...)→ unknown-classclass_id=0empty-object placeholder) with noindication of why.
Refusal diagnostic
Includes
file:line(resolved from the call's byte offset against thecurrently-installed module source) and the originating package name (or
user source), exactly as the acceptance criteria require.Instrumentation & escape hatch
PERRY_EVAL_DIAG=1logs every classified site to stderr:[perry-eval-diag] new Function(...) @ .../ajv/dist/codegen.ts:2 (in packageajv) -> known-library-codegenPERRY_ALLOW_EVAL=1downgrades the bucket-3 refusal to the legacyfall-through for a one-off build (mirrors security: refuse dynamic stdlib dispatch (
obj[runtimeVar]()) #503'sPERRY_ALLOW_DYNAMIC_STDLIB).Hooks
expr_new.rs—new Function(...)(body = last arg).expr_call/intrinsics.rs::check_eval_function_call—Function(...)/eval(...),run after the
Function('return this')()globalThis fold (feat(security): #498 — pin SHA-256 ofperry.nativeLibraryprebuilt archives inperry.lock#957/fix(#957): CJS IIFE .call(this) + IndexUpdate codegen #959) sothat fold short-circuits first and is unaffected. Shadowed
Function/evalbindings (local/func/imported) are left alone.
Tests / verification
file:lineresolution, preview truncation.perry-hirsuite green (111+ tests, 0 failures);cargo fmt --all --checkclean;
cargo clippy -p perry-hirclean.new Function/evalrefusedwith
file:line+ provenance; const-foldable sample compiles; anajv-path sample lands in the known-library bucket; the existingFunction('return this')()fold still printsglobal_ok: true.Note:
parity/compile-smokeare tag-gated (don't run on PRs). Compiledpackage modules run under
PERRY_ALLOW_UNIMPLEMENTED=1, so a refusal theredegrades to a linker stub (same as the prior broken behaviour) rather than a
hard build failure — only the user's entry module hard-errors.