engine: 'interpreter' — a per-validator way to keep a schema off code generation - #43
Open
d4tocchini wants to merge 2 commits into
Open
engine: 'interpreter' — a per-validator way to keep a schema off code generation#43d4tocchini wants to merge 2 commits into
d4tocchini wants to merge 2 commits into
Conversation
… generation
The default engine turns a schema into JavaScript source (new Function).
That is the right trade for a schema the application wrote. A schema that
arrives at runtime — a plugin declaring the shape of its own config, a
tenant uploading a contract — is input, and until now the only way to
validate against one without executing source derived from it was the
process-wide ATA_FORCE_NAPI switch.
new Validator(schema, { engine: 'interpreter' }) skips the JS compile and
the isValidObject fast path exactly as ATA_FORCE_NAPI does, neither reads
nor writes the shared compile cache, takes the closure mutators instead of
buildPreprocessCodegen (generated source with default values embedded),
and refuses any value other than 'auto' or 'interpreter' with a TypeError.
engine() reports 'interpreter' for such a validator.
tests/test_engine_option.js holds the verdicts equal across engines and
proves, with Function guarded, that the interpreter validator never builds
a function from source while the default one does.
Claude-Session: https://claude.ai/code/session_01SN64ywhLhSjK8ZzfRCkvkZ
d4tocchini
force-pushed
the
feat/engine-option
branch
from
September 10, 2026 20:40
b9c2faf to
a0b036e
Compare
A default fills a property the instance does not carry. Both engines asked `key in data` to find out, which also answers for what the instance inherits from Object.prototype: a property named `constructor` never received its default, and one named `__proto__` sent its nested defaults to Object.prototype itself, since the interpreter's applier walked data.__proto__ as the parent and wrote the child defaults there. Both engines now ask Object.hasOwn — the codegen preprocessor's inlined check and the interpreter's closure applier. tests/test_defaults_own_keys.js holds it for either engine: a `constructor` property receives its default, and a default under `__proto__` leaves Object.prototype alone. Claude-Session: https://claude.ai/code/session_01SN64ywhLhSjK8ZzfRCkvkZ
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.
engine: 'interpreter'— a per-validator way to keep a schema off code generationWhy
The default engine turns a schema into JavaScript source (
new Function). That is the right trade for a schema the application wrote: it is code, reviewed like code. A schema that arrives at runtime is not — a plugin declaring the shape of its own config, a tenant uploading a contract, a form builder. Today the only way to validate against such a schema without executing source derived from it is the process-wide, undocumentedATA_FORCE_NAPIswitch, which also takes every trusted schema in the process off the fast engines and, despite the name, does not require the addon.The eval-free interpreted engine already exists and already answers every schema where
new Functionis blocked (CSP pages, Workers). This makes it selectable per validator.What
new Validator(schema, { engine: 'interpreter' }):_ensureCompiledand theisValidObjectfast path in_ensureCodegen, exactly asATA_FORCE_NAPIdoes — the existing branches then route object validation to the interpreter with or without the native addon;buildPreprocessCodegen, which is generated source too, with the schema'sdefaultvalues embedded;'auto'or'interpreter'with aTypeError. A misspelling falling through to the generator would be the silent-acceptance failureAGENTS.mdwarns about.engine()reports'interpreter'for such a validator. Buffer APIs (isValid,countValid,batchIsValid) are native-only and unaffected.Tests
tests/test_engine_option.js, in thenpm testchain aftertest_engine_routing.js:validate,isValidObject,validateJSONfor the default and the interpreter validator of the same schema (the default one already in the compile cache);'interpretor','native',nullthrow;Functionguarded the waytest_no_eval.jsdoes: a fresh schema withdefault,patternand nesting validates on the interpreter validator with zero attempts to build a function from source, while the default validator for a copy of the same schema does reach for the generator — so the guard is proven live.Passes with and without the native addon. Full
npm test,node tests/test_no_eval.js(1299/1299, 927/927, 1133/1133) green.Docs
docs/API.mdoptions table andv.engine(), README options block,index.d.ts(ValidatorOptions.engine), CHANGELOG under Unreleased.No new dependencies. No change for callers that do not pass
engine.Second commit: a default fills an own key or an absent one
Found while using the interpreter engine for runtime-supplied schemas. A default fills a property the instance does not carry, and both engines asked
key in datato find out, which also answers for what the instance inherits fromObject.prototype: a property namedconstructornever received its default, and one named__proto__sent its nested defaults toObject.prototypeitself — the interpreter's closure applier walkeddata.__proto__as the parent and wrote the child defaults there. Both engines now askObject.hasOwn(the codegen preprocessor's inlined check and the interpreter's applier).tests/test_defaults_own_keys.jsholds it for either engine; CHANGELOG under Unreleased. Independent of the engine option; happy to split it into its own PR if preferred.