Skip to content

feat(hir): classify new Function/eval call sites + refusal diagnostic (#1678)#1776

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-eval-classifier-1678
May 25, 2026
Merged

feat(hir): classify new Function/eval call sites + refusal diagnostic (#1678)#1776
proggeramlug merged 1 commit into
mainfrom
worktree-eval-classifier-1678

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Phase 0 of #1677 — classify new Function / eval sites + precise refusal

Closes #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 each
new Function / Function(...) / eval(...) site into:

Bucket Trigger Phase 0 behaviour
const-foldable literal / substitution-free template body (or no body) logged, falls through (→ #1679 compiles it)
known-library-codegen originates from fast-json-stringify / ajv / find-my-way logged, falls through (→ #1680/#1681/#1682)
runtime-unknown anything else refused with a precise diagnostic

Only 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/eval ident → GlobalGet(0) → runtime TypeError; new Function(...) → unknown-class class_id=0 empty-object placeholder) with no
indication of why.

Refusal diagnostic

Error: `new Function(...)` is refused at compile time: main.ts:3 (user source).
Perry is an ahead-of-time compiler — it cannot evaluate a code string built
from runtime data. (#1677)

Options:
- Replace the generated function with an ordinary function or closure.
- If the body is a build-time constant string, a future release will compile it natively (#1679).
- If this comes from a code-generating library, only `fast-json-stringify`, `ajv`, and `find-my-way` are recognized so far (#1680/#1681/#1682) — file an issue against #1677 naming the package.
- Set `PERRY_ALLOW_EVAL=1` to restore the legacy (non-functional) behavior for a one-off build.

Includes file:line (resolved from the call's byte offset against the
currently-installed module source) and the originating package name (or
user source), exactly as the acceptance criteria require.

Instrumentation & escape hatch

  • PERRY_EVAL_DIAG=1 logs every classified site to stderr:
    [perry-eval-diag] new Function(...) @ .../ajv/dist/codegen.ts:2 (in package ajv) -> known-library-codegen
  • PERRY_ALLOW_EVAL=1 downgrades the bucket-3 refusal to the legacy
    fall-through for a one-off build (mirrors security: refuse dynamic stdlib dispatch (obj[runtimeVar]()) #503's PERRY_ALLOW_DYNAMIC_STDLIB).

Hooks

Tests / verification

  • 9 unit tests: each bucket, const-vs-package precedence, template literals,
    file:line resolution, preview truncation.
  • Full perry-hir suite green (111+ tests, 0 failures); cargo fmt --all --check
    clean; cargo clippy -p perry-hir clean.
  • Manually verified end-to-end: runtime-unknown new Function/eval refused
    with file:line + provenance; const-foldable sample compiles; an
    ajv-path sample lands in the known-library bucket; the existing
    Function('return this')() fold still prints global_ok: true.

Note: parity/compile-smoke are tag-gated (don't run on PRs). Compiled
package modules run under PERRY_ALLOW_UNIMPLEMENTED=1, so a refusal there
degrades to a linker stub (same as the prior broken behaviour) rather than a
hard build failure — only the user's entry module hard-errors.

…#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).
@proggeramlug proggeramlug merged commit d788823 into main May 25, 2026
10 checks passed
@proggeramlug proggeramlug deleted the worktree-eval-classifier-1678 branch May 25, 2026 10:11
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.

feat(codegen): classify new Function/eval call sites + precise refusal diagnostic

1 participant