Context: surfaced by real-app AOT probing (opencode/openclaw) in our internal suite.
Problem
perry refuses new Function(...) (#1677) and eval(...) at compile time, which aborts the entire build. In real apps a single such call in a cold path blocks compilation of the whole program.
Concrete: bundling opencode (bun build) resolved its full import graph (179 modules → 1.24 MB) with no import wall. The first hard failure was one call —
// src/cli/cmd/debug/agent.handler.ts — a --params fallback, never hit in normal use
return new Function(`return (${trimmed})`)();
That single line currently blocks the entire binary.
Proposed default: defer to a runtime error, don't refuse the build
When perry encounters eval / new Function (and other runtime code-eval), compile it to a binding that throws a descriptive error at runtime, and only if it is actually invoked, e.g.:
Error: eval()/new Function() cannot run in an ahead-of-time compiled binary (src/.../agent.handler.ts:NN)
Everything else compiles and runs normally; only the eval path fails, and only when reached.
Compile-time transparency (required, not silent)
The user must be told at compile time exactly what is being deferred and where:
notice: 1 runtime-eval site compiled to a deferred runtime error (throws only if reached):
- new Function(...) src/cli/cmd/debug/agent.handler.ts:NN
(summary count + per-site file:line). The build succeeds, but the user knows the capability is degraded and exactly where.
Strict mode (opt-in hard failure)
A flag/config to restore today's behavior — treat any eval/new Function as a compile-time error that blocks the build — for users who require a guarantee that the binary contains no runtime-eval paths. e.g. CLI --strict-eval (or a broader --strict), or perry.eval: "error" / perry.strict: true in package.json. Default = deferred-runtime-error + notice; strict = block.
Why
Runtime-computed code is the irreducible AOT boundary — but refusing at compile time over-applies it to the whole binary. Deferring to runtime, with a clear compile-time heads-up, lets the (vast majority of an) app that doesn't depend on eval compile and run, while keeping the hard guarantee available via strict mode. This was the single biggest unlock observed when probing real apps.
Related: #1677 (new Function refusal), #668 (require under compile).
Context: surfaced by real-app AOT probing (opencode/openclaw) in our internal suite.
Problem
perry refuses
new Function(...)(#1677) andeval(...)at compile time, which aborts the entire build. In real apps a single such call in a cold path blocks compilation of the whole program.Concrete: bundling opencode (
bun build) resolved its full import graph (179 modules → 1.24 MB) with no import wall. The first hard failure was one call —That single line currently blocks the entire binary.
Proposed default: defer to a runtime error, don't refuse the build
When perry encounters
eval/new Function(and other runtime code-eval), compile it to a binding that throws a descriptive error at runtime, and only if it is actually invoked, e.g.:Everything else compiles and runs normally; only the eval path fails, and only when reached.
Compile-time transparency (required, not silent)
The user must be told at compile time exactly what is being deferred and where:
(summary count + per-site file:line). The build succeeds, but the user knows the capability is degraded and exactly where.
Strict mode (opt-in hard failure)
A flag/config to restore today's behavior — treat any
eval/new Functionas a compile-time error that blocks the build — for users who require a guarantee that the binary contains no runtime-eval paths. e.g. CLI--strict-eval(or a broader--strict), orperry.eval: "error"/perry.strict: trueinpackage.json. Default = deferred-runtime-error + notice; strict = block.Why
Runtime-computed code is the irreducible AOT boundary — but refusing at compile time over-applies it to the whole binary. Deferring to runtime, with a clear compile-time heads-up, lets the (vast majority of an) app that doesn't depend on eval compile and run, while keeping the hard guarantee available via strict mode. This was the single biggest unlock observed when probing real apps.
Related: #1677 (new Function refusal), #668 (require under compile).