ata-validator 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/safe-regex.js CHANGED
@@ -226,60 +226,240 @@ function compileProg (ast) {
226
226
  return prog
227
227
  }
228
228
 
229
- function matchClass (instr, c) {
230
- let inside = false
229
+ // Numeric opcodes for the runner. The program is compiled once into flat
230
+ // typed arrays so the inner loop does no property lookups or string compares.
231
+ const OP_CHAR = 0
232
+ const OP_ANY = 1
233
+ const OP_CLASS = 2
234
+ const OP_SPLIT = 3
235
+ const OP_JMP = 4
236
+ const OP_BOL = 5
237
+ const OP_EOL = 6
238
+ const OP_MATCH = 7
239
+
240
+ function classMatcher (instr) {
241
+ // ASCII is answered from a bitmap; anything above 0x7f walks the ranges.
242
+ const bits = new Uint8Array(128)
231
243
  const r = instr.ranges
232
- for (let k = 0; k < r.length; k++) { if (c >= r[k][0] && c <= r[k][1]) { inside = true; break } }
233
- return instr.neg ? !inside : inside
244
+ for (let k = 0; k < r.length; k++) {
245
+ const hi = Math.min(r[k][1], 127)
246
+ for (let c = r[k][0]; c <= hi; c++) bits[c] = 1
247
+ }
248
+ return { bits, ranges: r, neg: instr.neg }
249
+ }
250
+
251
+ function matchClass (cls, c) {
252
+ let inside
253
+ if (c < 128) {
254
+ inside = cls.bits[c] === 1
255
+ } else {
256
+ inside = false
257
+ const r = cls.ranges
258
+ for (let k = 0; k < r.length; k++) { if (c >= r[k][0] && c <= r[k][1]) { inside = true; break } }
259
+ }
260
+ return cls.neg ? !inside : inside
234
261
  }
235
262
 
236
263
  function makeRunner (prog) {
237
264
  const n = prog.length
265
+ const ops = new Uint8Array(n)
266
+ const xs = new Int32Array(n)
267
+ const ys = new Int32Array(n)
268
+ const cs = new Int32Array(n)
269
+ const classes = new Array(n)
270
+ for (let i = 0; i < n; i++) {
271
+ const I = prog[i]
272
+ switch (I.op) {
273
+ case 'char': ops[i] = OP_CHAR; cs[i] = I.c; break
274
+ case 'any': ops[i] = OP_ANY; break
275
+ case 'class': ops[i] = OP_CLASS; classes[i] = classMatcher(I); break
276
+ case 'split': ops[i] = OP_SPLIT; xs[i] = I.x; ys[i] = I.y; break
277
+ case 'jmp': ops[i] = OP_JMP; xs[i] = I.x; break
278
+ case 'bol': ops[i] = OP_BOL; break
279
+ case 'eol': ops[i] = OP_EOL; break
280
+ case 'match': ops[i] = OP_MATCH; break
281
+ }
282
+ }
283
+
238
284
  const lastGen = new Int32Array(n).fill(-1)
239
285
  let gen = 0
240
- const stack = []
286
+ // Each unvisited instruction is popped once and pushes at most two, so the
287
+ // stack never holds more than 2n + 1 entries.
288
+ const stack = new Int32Array(2 * n + 2)
289
+ // Thread lists hold at most one entry per instruction per step.
290
+ let clist = new Int32Array(n)
291
+ let nlist = new Int32Array(n)
292
+ let clen = 0
293
+ let nlen = 0
241
294
 
242
- function addThread (list, pc, pos, len) {
243
- stack.length = 0
244
- stack.push(pc)
245
- while (stack.length) {
246
- const p = stack.pop()
295
+ // Follows epsilon edges from `pc` and records every consuming instruction
296
+ // (or match) reached in `list`. `lastGen` dedupes per step.
297
+ function addThread (list, len0, pc, pos, len) {
298
+ // Most transitions land directly on a consuming instruction; skip the
299
+ // stack walk for those.
300
+ if (ops[pc] <= OP_CLASS || ops[pc] === OP_MATCH) {
301
+ if (lastGen[pc] === gen) return len0
302
+ lastGen[pc] = gen
303
+ list[len0] = pc
304
+ return len0 + 1
305
+ }
306
+ let sp = 0
307
+ stack[sp++] = pc
308
+ let count = len0
309
+ while (sp > 0) {
310
+ const p = stack[--sp]
247
311
  if (lastGen[p] === gen) continue
248
312
  lastGen[p] = gen
249
- const I = prog[p]
250
- switch (I.op) {
251
- case 'jmp': stack.push(I.x); break
252
- case 'split': stack.push(I.y); stack.push(I.x); break
253
- case 'bol': if (pos === 0) stack.push(p + 1); break
254
- case 'eol': if (pos === len) stack.push(p + 1); break
255
- default: list.push(p)
313
+ switch (ops[p]) {
314
+ case OP_JMP: stack[sp++] = xs[p]; break
315
+ case OP_SPLIT: stack[sp++] = ys[p]; stack[sp++] = xs[p]; break
316
+ case OP_BOL: if (pos === 0) stack[sp++] = p + 1; break
317
+ case OP_EOL: if (pos === len) stack[sp++] = p + 1; break
318
+ default: list[count++] = p
256
319
  }
257
320
  }
321
+ return count
258
322
  }
259
323
 
260
- return function test (s) {
324
+ // A pattern is anchored when starting it anywhere but position 0 yields no
325
+ // thread, which is the case for `^...` and its alternations. The probe sits
326
+ // at position 1 of a length-1 string so that only `^` can fail. For anchored
327
+ // patterns the per-position restart below is skipped, and an empty thread
328
+ // list means the match has already failed.
329
+ gen++
330
+ const anchored = addThread(nlist, 0, 0, 1, 1) === 0
331
+
332
+ function testNFA (s) {
261
333
  const len = s.length
262
- let clist = []
263
- let nlist = []
264
334
  gen++
265
- addThread(clist, 0, 0, len)
335
+ clen = addThread(clist, 0, 0, 0, len)
266
336
  for (let pos = 0; pos <= len; pos++) {
267
337
  const c = pos < len ? s.charCodeAt(pos) : -1
268
338
  gen++
269
- nlist.length = 0
270
- for (let k = 0; k < clist.length; k++) {
339
+ nlen = 0
340
+ for (let k = 0; k < clen; k++) {
271
341
  const pc = clist[k]
272
- const I = prog[pc]
273
- if (I.op === 'match') return true
274
- else if (I.op === 'char') { if (c === I.c) addThread(nlist, pc + 1, pos + 1, len) }
275
- else if (I.op === 'any') { if (c !== -1 && c !== 10) addThread(nlist, pc + 1, pos + 1, len) }
276
- else if (I.op === 'class') { if (c !== -1 && matchClass(I, c)) addThread(nlist, pc + 1, pos + 1, len) }
342
+ switch (ops[pc]) {
343
+ case OP_MATCH: return true
344
+ case OP_CHAR: if (c === cs[pc]) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break
345
+ case OP_ANY: if (c !== -1 && c !== 10) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break
346
+ case OP_CLASS: if (c !== -1 && matchClass(classes[pc], c)) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break
347
+ }
348
+ }
349
+ if (pos < len) {
350
+ if (!anchored) nlen = addThread(nlist, nlen, 0, pos + 1, len)
351
+ else if (nlen === 0) return false
277
352
  }
278
- if (pos < len) addThread(nlist, 0, pos + 1, len)
279
353
  const tmp = clist; clist = nlist; nlist = tmp
354
+ clen = nlen
280
355
  }
281
356
  return false
282
357
  }
358
+
359
+ // Lazy DFA on top of the NFA. A DFA state is the set of consuming
360
+ // instructions live at a position; transitions are computed on first use
361
+ // and cached per ASCII character. `^` and `$` depend on position, so the
362
+ // closure is taken with flags for "at start" and "at end", which gives two
363
+ // start states and two transition tables per state. The state count is
364
+ // capped; past the cap the matcher falls back to the NFA walk above, so the
365
+ // time bound stays linear either way.
366
+ const MAX_STATES = 256
367
+ const states = []
368
+ const stateIds = new Map()
369
+ let overflow = false
370
+
371
+ function closure (list, count, pc, atStart, atEnd) {
372
+ // Same walk as addThread, with the position replaced by the two flags.
373
+ let sp = 0
374
+ stack[sp++] = pc
375
+ while (sp > 0) {
376
+ const p = stack[--sp]
377
+ if (lastGen[p] === gen) continue
378
+ lastGen[p] = gen
379
+ switch (ops[p]) {
380
+ case OP_JMP: stack[sp++] = xs[p]; break
381
+ case OP_SPLIT: stack[sp++] = ys[p]; stack[sp++] = xs[p]; break
382
+ case OP_BOL: if (atStart) stack[sp++] = p + 1; break
383
+ case OP_EOL: if (atEnd) stack[sp++] = p + 1; break
384
+ default: list[count++] = p
385
+ }
386
+ }
387
+ return count
388
+ }
389
+
390
+ function internState (list, count) {
391
+ const pcs = Array.from(list.subarray(0, count)).sort((a, b) => a - b)
392
+ const key = pcs.join(',')
393
+ let id = stateIds.get(key)
394
+ if (id !== undefined) return id
395
+ if (states.length >= MAX_STATES) { overflow = true; return -1 }
396
+ id = states.length
397
+ let isMatch = false
398
+ for (let k = 0; k < pcs.length; k++) if (ops[pcs[k]] === OP_MATCH) { isMatch = true; break }
399
+ states.push({ pcs: Int32Array.from(pcs), isMatch, next: new Int32Array(128).fill(-2), nextEnd: new Int32Array(128).fill(-2) })
400
+ stateIds.set(key, id)
401
+ return id
402
+ }
403
+
404
+ function step (state, c, atEnd) {
405
+ gen++
406
+ let count = 0
407
+ const pcs = state.pcs
408
+ for (let k = 0; k < pcs.length; k++) {
409
+ const pc = pcs[k]
410
+ switch (ops[pc]) {
411
+ case OP_CHAR: if (c === cs[pc]) count = closure(nlist, count, pc + 1, false, atEnd); break
412
+ case OP_ANY: if (c !== 10) count = closure(nlist, count, pc + 1, false, atEnd); break
413
+ case OP_CLASS: if (matchClass(classes[pc], c)) count = closure(nlist, count, pc + 1, false, atEnd); break
414
+ }
415
+ }
416
+ if (!anchored) count = closure(nlist, count, 0, false, atEnd)
417
+ return internState(nlist, count)
418
+ }
419
+
420
+ let startEmpty = -2
421
+ let startNonEmpty = -2
422
+
423
+ function startState (atEnd) {
424
+ gen++
425
+ const count = closure(nlist, 0, 0, true, atEnd)
426
+ return internState(nlist, count)
427
+ }
428
+
429
+ function testDFA (s) {
430
+ const len = s.length
431
+ let id
432
+ if (len === 0) {
433
+ if (startEmpty === -2) startEmpty = startState(true)
434
+ id = startEmpty
435
+ } else {
436
+ if (startNonEmpty === -2) startNonEmpty = startState(false)
437
+ id = startNonEmpty
438
+ }
439
+ if (id < 0) return testNFA(s)
440
+ let state = states[id]
441
+ for (let pos = 0; pos < len; pos++) {
442
+ if (state.isMatch) return true
443
+ const c = s.charCodeAt(pos)
444
+ const atEnd = pos + 1 === len
445
+ let nid
446
+ if (c < 128) {
447
+ const table = atEnd ? state.nextEnd : state.next
448
+ nid = table[c]
449
+ if (nid === -2) { nid = step(state, c, atEnd); table[c] = nid }
450
+ } else {
451
+ nid = step(state, c, atEnd)
452
+ }
453
+ if (nid < 0) return testNFA(s)
454
+ state = states[nid]
455
+ if (anchored && state.pcs.length === 0) return false
456
+ }
457
+ return state.isMatch
458
+ }
459
+
460
+ return function test (s) {
461
+ return overflow ? testNFA(s) : testDFA(s)
462
+ }
283
463
  }
284
464
 
285
465
  function compileSafe (pattern) {
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.6.2';
10
+ module.exports = '1.7.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -44,7 +44,7 @@
44
44
  "release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
45
45
  "build": "cmake-js build --target ata",
46
46
  "rebuild": "cmake-js rebuild --target ata",
47
- "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
47
+ "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
48
48
  "bench:size": "node benchmark/bench_aot_size.mjs",
49
49
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
50
50
  "test:compat": "node tests/test_compat.js",
@@ -112,12 +112,12 @@
112
112
  "LICENSE"
113
113
  ],
114
114
  "optionalDependencies": {
115
- "@ata-validator/native-darwin-arm64": "1.6.2",
116
- "@ata-validator/native-linux-x64-gnu": "1.6.2",
117
- "@ata-validator/native-linux-arm64-gnu": "1.6.2",
118
- "@ata-validator/native-linux-x64-musl": "1.6.2",
119
- "@ata-validator/native-linux-arm64-musl": "1.6.2",
120
- "@ata-validator/native-win32-x64": "1.6.2"
115
+ "@ata-validator/native-darwin-arm64": "1.7.0",
116
+ "@ata-validator/native-linux-x64-gnu": "1.7.0",
117
+ "@ata-validator/native-linux-arm64-gnu": "1.7.0",
118
+ "@ata-validator/native-linux-x64-musl": "1.7.0",
119
+ "@ata-validator/native-linux-arm64-musl": "1.7.0",
120
+ "@ata-validator/native-win32-x64": "1.7.0"
121
121
  },
122
122
  "peerDependencies": {
123
123
  "yaml": "^2.0.0"