Skip to content

engine: 'interpreter' — a per-validator way to keep a schema off code generation - #43

Open
d4tocchini wants to merge 2 commits into
ata-core:masterfrom
d4tocchini:feat/engine-option
Open

engine: 'interpreter' — a per-validator way to keep a schema off code generation#43
d4tocchini wants to merge 2 commits into
ata-core:masterfrom
d4tocchini:feat/engine-option

Conversation

@d4tocchini

@d4tocchini d4tocchini commented Sep 10, 2026

Copy link
Copy Markdown

engine: 'interpreter' — a per-validator way to keep a schema off code generation

Why

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, undocumented ATA_FORCE_NAPI switch, 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 Function is blocked (CSP pages, Workers). This makes it selectable per validator.

What

new Validator(schema, { engine: 'interpreter' }):

  • skips the JS compile in _ensureCompiled and the isValidObject fast path in _ensureCodegen, exactly as ATA_FORCE_NAPI does — the existing branches then route object validation to the interpreter with or without the native addon;
  • neither reads nor writes the shared compile cache (a cached function for the same schema string from a trusted validator must not answer for an untrusted one);
  • takes the closure mutators instead of buildPreprocessCodegen, which is generated source too, with the schema's default values embedded;
  • refuses any value other than 'auto' or 'interpreter' with a TypeError. A misspelling falling through to the generator would be the silent-acceptance failure AGENTS.md warns 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 the npm test chain after test_engine_routing.js:

  • same verdicts and failing paths on validate, isValidObject, validateJSON for the default and the interpreter validator of the same schema (the default one already in the compile cache);
  • defaults + coercion still apply on the interpreter validator (closure mutators);
  • 'interpretor', 'native', null throw;
  • with Function guarded the way test_no_eval.js does: a fresh schema with default, pattern and 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.md options table and v.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 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 — the interpreter's closure 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 applier). tests/test_defaults_own_keys.js holds it for either engine; CHANGELOG under Unreleased. Independent of the engine option; happy to split it into its own PR if preferred.

… 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
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
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.

1 participant