tjs-lang 0.8.7 → 0.9.1

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.
Files changed (85) hide show
  1. package/CLAUDE.md +19 -5
  2. package/dist/experiments/ambient/probe.d.ts +52 -0
  3. package/dist/index.js +110 -162
  4. package/dist/index.js.map +4 -4
  5. package/dist/scripts/build-editors.d.ts +19 -0
  6. package/dist/src/css/dimensions.d.ts +23 -0
  7. package/dist/src/css/index.d.ts +85 -0
  8. package/dist/src/css/predicates.d.ts +38 -0
  9. package/dist/src/css/shorthands.d.ts +31 -0
  10. package/dist/src/css/style.d.ts +48 -0
  11. package/dist/src/lang/emitters/js-wasm.d.ts +2 -0
  12. package/dist/src/lang/emitters/js.d.ts +9 -1
  13. package/dist/src/lang/index.d.ts +2 -2
  14. package/dist/src/lang/parser-transforms.d.ts +9 -5
  15. package/dist/src/lang/parser-types.d.ts +7 -0
  16. package/dist/src/lang/parser.d.ts +2 -0
  17. package/dist/src/lang/predicate.d.ts +60 -0
  18. package/dist/src/lang/transpiler.d.ts +3 -2
  19. package/dist/src/lang/types.d.ts +16 -0
  20. package/dist/src/schema/index.d.ts +12 -0
  21. package/dist/tjs-browser-from-ts.js +20 -20
  22. package/dist/tjs-browser-from-ts.js.map +3 -3
  23. package/dist/tjs-browser.js +108 -94
  24. package/dist/tjs-browser.js.map +4 -4
  25. package/dist/tjs-css.js +193 -0
  26. package/dist/tjs-css.js.map +7 -0
  27. package/dist/tjs-eval.js +43 -42
  28. package/dist/tjs-eval.js.map +4 -4
  29. package/dist/tjs-from-ts.js +13 -13
  30. package/dist/tjs-from-ts.js.map +2 -2
  31. package/dist/tjs-lang.js +106 -92
  32. package/dist/tjs-lang.js.map +4 -4
  33. package/dist/tjs-runtime.js +10 -0
  34. package/dist/tjs-runtime.js.map +7 -0
  35. package/dist/tjs-schema.js +6 -0
  36. package/dist/tjs-schema.js.map +7 -0
  37. package/dist/tjs-vm.js +55 -54
  38. package/dist/tjs-vm.js.map +4 -4
  39. package/docs/ambient-contracts.md +261 -0
  40. package/docs/type-system-north-star.md +100 -0
  41. package/editors/ace/ajs-mode.js +214 -233
  42. package/editors/codemirror/ajs-language.js +1570 -233
  43. package/editors/editors-build.test.ts +29 -0
  44. package/editors/monaco/ajs-monarch.js +239 -195
  45. package/llms.txt +4 -0
  46. package/package.json +35 -4
  47. package/src/css/css.test.ts +122 -0
  48. package/src/css/dimensions.test.ts +112 -0
  49. package/src/css/dimensions.ts +146 -0
  50. package/src/css/index.ts +243 -0
  51. package/src/css/perf.bench.test.ts +109 -0
  52. package/src/css/predicates.ts +232 -0
  53. package/src/css/property-aware.test.ts +84 -0
  54. package/src/css/shorthands.test.ts +90 -0
  55. package/src/css/shorthands.ts +113 -0
  56. package/src/css/style.test.ts +125 -0
  57. package/src/css/style.ts +134 -0
  58. package/src/index-tsfree.test.ts +58 -0
  59. package/src/lang/emit-verified-predicate.test.ts +95 -0
  60. package/src/lang/emitters/dts.test.ts +31 -0
  61. package/src/lang/emitters/dts.ts +23 -4
  62. package/src/lang/emitters/js-wasm.ts +8 -4
  63. package/src/lang/emitters/js.ts +58 -1
  64. package/src/lang/from-ts.test.ts +1 -1
  65. package/src/lang/generic-verified-predicate.test.ts +39 -0
  66. package/src/lang/index.ts +14 -5
  67. package/src/lang/parser-transforms.ts +113 -15
  68. package/src/lang/parser-types.ts +7 -0
  69. package/src/lang/parser.ts +18 -3
  70. package/src/lang/predicate-evaluator.test.ts +49 -0
  71. package/src/lang/predicate-report.test.ts +78 -0
  72. package/src/lang/predicate.ts +268 -0
  73. package/src/lang/redos-lint.test.ts +81 -0
  74. package/src/lang/transpiler.ts +13 -0
  75. package/src/lang/type-verified-predicate.test.ts +83 -0
  76. package/src/lang/types.ts +17 -0
  77. package/src/lang/typescript-syntax.test.ts +2 -1
  78. package/src/lang/wasm-fallback-warning.test.ts +50 -0
  79. package/src/lang/wasm-intdiv-lint.test.ts +45 -0
  80. package/src/lang/wasm-ready.test.ts +94 -0
  81. package/src/lang/wasm-simd-ops.test.ts +84 -0
  82. package/src/lang/wasm.ts +61 -2
  83. package/src/schema/index.ts +62 -0
  84. package/src/schema/schema.test.ts +66 -0
  85. package/src/use-cases/bootstrap.test.ts +14 -4
@@ -0,0 +1,94 @@
1
+ /**
2
+ * `globalThis.__tjs_wasm_ready()` — an awaitable signal for WASM instantiation.
3
+ *
4
+ * The emitted bootstrap instantiates WASM asynchronously (fire-and-forget), so
5
+ * synchronous code right after transpile+eval used to hit the JS `fallback{}`
6
+ * because `globalThis.__tjs_wasm_N` wasn't set yet — with no way to await it
7
+ * (tosijs-ui feedback UI-#2, "bit us hard"). Now each module pushes its
8
+ * instantiation promise onto `globalThis.__tjs_wasm_pending` and
9
+ * `__tjs_wasm_ready()` awaits them all.
10
+ */
11
+ import { describe, it, expect, beforeEach, afterEach } from 'bun:test'
12
+ import { tjs } from './index'
13
+ import { createRuntime } from './runtime'
14
+
15
+ const g = globalThis as any
16
+ g.__tjs = createRuntime()
17
+
18
+ // The `__tjs_wasm_*` globals are shared process-wide, so other test files that
19
+ // eval a bootstrap can leave them set. Clear before each test so the
20
+ // "undefined before ready" assertion reflects THIS module's state.
21
+ const clearWasmGlobals = () => {
22
+ delete g.__tjs_wasm_pending
23
+ delete g.__tjs_wasm_ready
24
+ delete g.__tjs_wasm_0
25
+ }
26
+
27
+ const KERNEL = `function scale(arr: [0.0], len: 0, f: 0.0) {
28
+ wasm {
29
+ let s = f32x4_splat(f)
30
+ for (let i = 0; i < len; i += 4) { let o = i * 4; f32x4_store(arr, o, f32x4_mul(f32x4_load(arr, o), s)) }
31
+ } fallback { for (let i = 0; i < len; i++) arr[i] *= f }
32
+ return arr
33
+ }`
34
+
35
+ beforeEach(clearWasmGlobals)
36
+ afterEach(clearWasmGlobals)
37
+
38
+ describe('__tjs_wasm_ready', () => {
39
+ it('resolves once WASM is instantiated (was a fallback race before)', async () => {
40
+ const { code } = tjs(KERNEL)
41
+ new Function(code)() // run the bootstrap (fire-and-forget instantiation)
42
+
43
+ expect(typeof g.__tjs_wasm_ready).toBe('function')
44
+ // synchronously right after eval, the module isn't ready yet…
45
+ expect(g.__tjs_wasm_0).toBeUndefined()
46
+
47
+ await g.__tjs_wasm_ready()
48
+
49
+ // …now it is — a sync call here takes the WASM path, not the fallback.
50
+ expect(typeof g.__tjs_wasm_0).toBe('function')
51
+ })
52
+
53
+ it('accumulates pending across modules and awaits all', async () => {
54
+ new Function(tjs(KERNEL).code)()
55
+ const firstReady = g.__tjs_wasm_ready
56
+ new Function(tjs(KERNEL.replace('scale', 'scale2')).code)()
57
+ // same shared helper + a growing pending list, not overwritten per module
58
+ expect(g.__tjs_wasm_ready).toBe(firstReady)
59
+ expect(g.__tjs_wasm_pending.length).toBe(2)
60
+
61
+ await g.__tjs_wasm_ready()
62
+ expect(typeof g.__tjs_wasm_0).toBe('function')
63
+ })
64
+ })
65
+
66
+ describe('__tjs_wasm_enabled toggle (force the JS fallback for benchmarking)', () => {
67
+ // A scalar kernel so input validation passes (a Float32Array isn't
68
+ // `Array.isArray`, which would early-return before the dispatch). We spy on the
69
+ // compiled export to detect which path the dispatch takes.
70
+ const SCALAR = `function pick(x: 0.0) { wasm { return x } fallback { return x } }`
71
+
72
+ it('routes to the fallback when disabled, even though WASM is ready', async () => {
73
+ const pick = new Function(tjs(SCALAR).code + '\nreturn pick')()
74
+ await g.__tjs_wasm_ready()
75
+ const real = g.__tjs_wasm_0
76
+ let wasmCalls = 0
77
+ g.__tjs_wasm_0 = (...a: any[]) => {
78
+ wasmCalls++
79
+ return real(...a)
80
+ }
81
+
82
+ pick(1) // default (enabled) → WASM path
83
+ expect(wasmCalls).toBe(1)
84
+
85
+ g.__tjs_wasm_enabled = false
86
+ pick(1) // forced fallback → WASM not called
87
+ expect(wasmCalls).toBe(1)
88
+
89
+ g.__tjs_wasm_enabled = true
90
+ pick(1) // back to WASM
91
+ expect(wasmCalls).toBe(2)
92
+ delete g.__tjs_wasm_enabled
93
+ })
94
+ })
@@ -0,0 +1,84 @@
1
+ /**
2
+ * f32x4 min/max, lane comparisons (lt/le/gt/ge/eq/ne → mask), and select
3
+ * (branch-free blend) — the intrinsics that unlock DATA-DEPENDENT SIMD (clamp,
4
+ * saturate, SIMD Mandelbrot). Before this, f32x4 was arithmetic-only, so masked
5
+ * SIMD was impossible (tosijs-ui feedback UI-#6). Executed as real WASM and
6
+ * checked lane 0 via `extract_lane` (compares are checked through `select`, since
7
+ * a raw mask isn't a meaningful float).
8
+ */
9
+ import { describe, it, expect } from 'bun:test'
10
+ import { compileToWasm, instantiateWasm, type WasmBlock } from './wasm'
11
+
12
+ /** Compile+instantiate a 2-arg f32 kernel `compute(a, b)` from a block body. */
13
+ async function kernel(body: string, captures: string[]) {
14
+ const block: WasmBlock = { id: 'k', body, captures, start: 0, end: 0 }
15
+ const result = compileToWasm(block)
16
+ expect(result.error).toBeFalsy()
17
+ // SIMD modules import env.memory (for potential v128 load/store); provide one.
18
+ const memory = new WebAssembly.Memory({ initial: 16 })
19
+ const instance = await instantiateWasm(result.bytes, memory)
20
+ return instance.exports.compute as (...a: number[]) => number
21
+ }
22
+
23
+ describe('f32x4 min / max', () => {
24
+ it('max', async () => {
25
+ const f = await kernel(
26
+ 'return f32x4_extract_lane(f32x4_max(f32x4_splat(a), f32x4_splat(b)), 0)',
27
+ ['a', 'b']
28
+ )
29
+ expect(f(3, 5)).toBe(5)
30
+ expect(f(-2, -8)).toBe(-2)
31
+ expect(f(4.5, 4.5)).toBe(4.5)
32
+ })
33
+ it('min', async () => {
34
+ const f = await kernel(
35
+ 'return f32x4_extract_lane(f32x4_min(f32x4_splat(a), f32x4_splat(b)), 0)',
36
+ ['a', 'b']
37
+ )
38
+ expect(f(3, 5)).toBe(3)
39
+ expect(f(-2, -8)).toBe(-8)
40
+ })
41
+ })
42
+
43
+ describe('f32x4 select + comparisons (branch-free lane blend)', () => {
44
+ // select(cmp(a,b), 1, 2) → 1 when the comparison is true (mask lane all-1s), else 2
45
+ const via = (cmp: string) =>
46
+ kernel(
47
+ `return f32x4_extract_lane(f32x4_select(${cmp}(f32x4_splat(a), f32x4_splat(b)), f32x4_splat(1.0), f32x4_splat(2.0)), 0)`,
48
+ ['a', 'b']
49
+ )
50
+
51
+ it('gt', async () => {
52
+ const f = await via('f32x4_gt')
53
+ expect(f(5, 3)).toBe(1)
54
+ expect(f(3, 5)).toBe(2)
55
+ })
56
+ it('lt', async () => {
57
+ const f = await via('f32x4_lt')
58
+ expect(f(3, 5)).toBe(1)
59
+ expect(f(5, 3)).toBe(2)
60
+ })
61
+ it('ge / le at equality', async () => {
62
+ expect(await (await via('f32x4_ge'))(4, 4)).toBe(1)
63
+ expect(await (await via('f32x4_le'))(4, 4)).toBe(1)
64
+ expect(await (await via('f32x4_ge'))(3, 4)).toBe(2)
65
+ })
66
+ it('eq / ne', async () => {
67
+ expect(await (await via('f32x4_eq'))(4, 4)).toBe(1)
68
+ expect(await (await via('f32x4_eq'))(4, 5)).toBe(2)
69
+ expect(await (await via('f32x4_ne'))(4, 5)).toBe(1)
70
+ })
71
+ })
72
+
73
+ describe('clamp via min/max (the canonical use)', () => {
74
+ it('clamps to [lo, hi] on lane 0', async () => {
75
+ // clamp(v, lo, hi) = min(max(v, lo), hi); fix lo=0, hi=3 via constants
76
+ const f = await kernel(
77
+ 'return f32x4_extract_lane(f32x4_min(f32x4_max(f32x4_splat(a), f32x4_splat(0.0)), f32x4_splat(3.0)), 0)',
78
+ ['a', 'b']
79
+ )
80
+ expect(f(-5, 0)).toBe(0)
81
+ expect(f(1.5, 0)).toBe(1.5)
82
+ expect(f(10, 0)).toBe(3)
83
+ })
84
+ })
package/src/lang/wasm.ts CHANGED
@@ -262,6 +262,19 @@ const SimdOp = {
262
262
  f32x4_sub: 0xe5,
263
263
  f32x4_mul: 0xe6,
264
264
  f32x4_div: 0xe7,
265
+ f32x4_min: 0xe8,
266
+ f32x4_max: 0xe9,
267
+
268
+ // f32x4 comparisons → v128 lane mask (all-1s / all-0s per lane)
269
+ f32x4_eq: 0x41,
270
+ f32x4_ne: 0x42,
271
+ f32x4_lt: 0x43,
272
+ f32x4_gt: 0x44,
273
+ f32x4_le: 0x45,
274
+ f32x4_ge: 0x46,
275
+
276
+ // Lane-wise blend: bitselect(a, b, mask) — bits of a where mask=1, else b.
277
+ v128_bitselect: 0x52,
265
278
  } as const
266
279
 
267
280
  /** Reverse lookup for SIMD opcodes */
@@ -1160,6 +1173,18 @@ function compileBinaryExpr(
1160
1173
  return [Op.f64_const, ...encodeF64(0)]
1161
1174
  }
1162
1175
 
1176
+ // Lint the i32/i32 division footgun: `/` with two integer operands (loop vars,
1177
+ // `0`-annotated params, int literals) does TRUNCATING integer division, and the
1178
+ // coercion to f64 only happens at the *next* operator — so `x / w - 0.5` is
1179
+ // silently `0 - 0.5` for all `x < w`. Warn once per block; add `+ 0.0` to an
1180
+ // operand to force f64 division. (UI-#4 — this is a warning, not an error:
1181
+ // integer division may be intended.)
1182
+ if (node.operator === '/' && opType === 'i32') {
1183
+ const msg =
1184
+ "integer division: '/' with two i32 operands truncates (result coerces to f64 only at the next operator). Add `+ 0.0` to an operand to force f64 division."
1185
+ if (!ctx.warnings.includes(msg)) ctx.warnings.push(msg)
1186
+ }
1187
+
1163
1188
  return [...leftCode, ...rightCode, opcode]
1164
1189
  }
1165
1190
 
@@ -1808,8 +1833,17 @@ function compileSIMDCall(
1808
1833
  case 'f32x4_add':
1809
1834
  case 'f32x4_sub':
1810
1835
  case 'f32x4_mul':
1811
- case 'f32x4_div': {
1812
- // Binary v128 op: f32x4_add(a, b) → v128
1836
+ case 'f32x4_div':
1837
+ case 'f32x4_min':
1838
+ case 'f32x4_max':
1839
+ case 'f32x4_eq':
1840
+ case 'f32x4_ne':
1841
+ case 'f32x4_lt':
1842
+ case 'f32x4_gt':
1843
+ case 'f32x4_le':
1844
+ case 'f32x4_ge': {
1845
+ // Binary v128 op: f(a, b) → v128. The comparisons (eq/ne/lt/gt/le/ge)
1846
+ // return a v128 lane mask (all-1s where true, all-0s else).
1813
1847
  code.push(...compileExpression(args[0], ctx))
1814
1848
  code.push(...compileExpression(args[1], ctx))
1815
1849
  const opMap: Record<string, number> = {
@@ -1817,11 +1851,36 @@ function compileSIMDCall(
1817
1851
  f32x4_sub: SimdOp.f32x4_sub,
1818
1852
  f32x4_mul: SimdOp.f32x4_mul,
1819
1853
  f32x4_div: SimdOp.f32x4_div,
1854
+ f32x4_min: SimdOp.f32x4_min,
1855
+ f32x4_max: SimdOp.f32x4_max,
1856
+ f32x4_eq: SimdOp.f32x4_eq,
1857
+ f32x4_ne: SimdOp.f32x4_ne,
1858
+ f32x4_lt: SimdOp.f32x4_lt,
1859
+ f32x4_gt: SimdOp.f32x4_gt,
1860
+ f32x4_le: SimdOp.f32x4_le,
1861
+ f32x4_ge: SimdOp.f32x4_ge,
1820
1862
  }
1821
1863
  code.push(...encodeSIMD(opMap[name]))
1822
1864
  return code
1823
1865
  }
1824
1866
 
1867
+ case 'f32x4_select': {
1868
+ // Branch-free lane blend: f32x4_select(mask, a, b) → a where the mask lane
1869
+ // is true (all-1s, e.g. from a compare), b where false. Maps to
1870
+ // v128.bitselect(a, b, mask) — bits of a where mask=1, of b where mask=0.
1871
+ if (args.length !== 3) {
1872
+ ctx.errors.push(
1873
+ `f32x4_select expects (mask, a, b) — 3 args, got ${args.length}`
1874
+ )
1875
+ return [Op.f64_const, ...encodeF64(0)]
1876
+ }
1877
+ code.push(...compileExpression(args[1], ctx)) // a (bitselect v1)
1878
+ code.push(...compileExpression(args[2], ctx)) // b (bitselect v2)
1879
+ code.push(...compileExpression(args[0], ctx)) // mask
1880
+ code.push(...encodeSIMD(SimdOp.v128_bitselect))
1881
+ return code
1882
+ }
1883
+
1825
1884
  case 'f32x4_neg':
1826
1885
  case 'f32x4_sqrt': {
1827
1886
  // Unary v128 op: f32x4_neg(a) → v128
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `tjs-lang/schema` — tosijs-schema, pre-wired with predicate support.
3
+ *
4
+ * A drop-in for `tosijs-schema` that additionally evaluates the `$predicate`
5
+ * keyword: importing this module registers tjs-lang's verified predicate engine
6
+ * as the evaluator, so JSON-Schema validation is predicate-aware **out of the
7
+ * box** — no manual `setPredicateEvaluator` call. Everything tosijs-schema
8
+ * exports is re-exported here, so this is the only import a consumer needs:
9
+ *
10
+ * ```ts
11
+ * import { s, validate } from 'tjs-lang/schema'
12
+ * // a schema whose node carries `$predicate` now validates computationally
13
+ * ```
14
+ *
15
+ * Why it lives here and not in tosijs-schema: tjs-lang depends on tosijs-schema,
16
+ * so the reverse would be a circular dependency. This entry sits on the side of
17
+ * the graph that may know about both — it's the "batteries-included" packaging
18
+ * of the two.
19
+ *
20
+ * Registration is a global (tosijs-schema keeps one evaluator), so importing
21
+ * this module makes `$predicate` work for tosijs-schema usage app-wide, even in
22
+ * code that imports `tosijs-schema` directly. Call `setPredicateEvaluator(null)`
23
+ * to opt back out, or `installPredicateSupport(opts)` to re-install with custom
24
+ * options (fuel budget, atom-aware effects, etc.).
25
+ */
26
+ import { setPredicateEvaluator, getPredicateEvaluator } from 'tosijs-schema'
27
+ import {
28
+ createPredicateEvaluator,
29
+ type PredicateEvaluatorOptions,
30
+ } from '../lang/predicate'
31
+
32
+ // The full tosijs-schema surface (s, validate, filter, diff, types,
33
+ // setPredicateEvaluator, getPredicateEvaluator, PredicateEvaluator, …).
34
+ export * from 'tosijs-schema'
35
+ // The evaluator factory, for advanced/custom wiring.
36
+ export {
37
+ createPredicateEvaluator,
38
+ type PredicateEvaluatorOptions,
39
+ } from '../lang/predicate'
40
+
41
+ let installed = false
42
+
43
+ /**
44
+ * Register tjs-lang's predicate engine as tosijs-schema's `$predicate` evaluator.
45
+ * Called automatically when this module is imported; exposed so callers can
46
+ * re-install with custom {@link PredicateEvaluatorOptions} (e.g. a smaller fuel
47
+ * budget, or an atom-aware effectful set) after opting out.
48
+ */
49
+ export function installPredicateSupport(
50
+ opts?: PredicateEvaluatorOptions
51
+ ): void {
52
+ setPredicateEvaluator(createPredicateEvaluator(opts))
53
+ installed = true
54
+ }
55
+
56
+ /** True once predicate support has been installed (and not since cleared). */
57
+ export function predicateSupportInstalled(): boolean {
58
+ return installed && getPredicateEvaluator() !== null
59
+ }
60
+
61
+ // Batteries included: wire the engine up on import.
62
+ installPredicateSupport()
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `tjs-lang/schema` — tosijs-schema pre-wired with predicate support. Importing
3
+ * the module should make `$predicate` validation work with no manual wiring, and
4
+ * re-export the whole tosijs-schema surface. Runs against the real published
5
+ * tosijs-schema (node_modules), so it also guards the version/API contract.
6
+ */
7
+ import { describe, it, expect, afterAll } from 'bun:test'
8
+ import {
9
+ validate,
10
+ s,
11
+ setPredicateEvaluator,
12
+ getPredicateEvaluator,
13
+ installPredicateSupport,
14
+ predicateSupportInstalled,
15
+ createPredicateEvaluator,
16
+ } from './index'
17
+ import { cssStyleSchema, cssColorSchema } from '../css'
18
+
19
+ // Leave the global evaluator registered for other suites (import order is
20
+ // nondeterministic); this module's contract is "installed after import".
21
+ afterAll(() => installPredicateSupport())
22
+
23
+ describe('tjs-lang/schema is batteries-included', () => {
24
+ it('registers the predicate evaluator on import', () => {
25
+ expect(predicateSupportInstalled()).toBe(true)
26
+ expect(typeof getPredicateEvaluator()).toBe('function')
27
+ })
28
+
29
+ it('re-exports the tosijs-schema surface', () => {
30
+ expect(typeof validate).toBe('function')
31
+ expect(typeof s).toBe('object') // the builder proxy
32
+ expect(typeof setPredicateEvaluator).toBe('function')
33
+ expect(typeof createPredicateEvaluator).toBe('function')
34
+ })
35
+
36
+ it('validates a $predicate node out of the box (CSS color)', () => {
37
+ const color = cssColorSchema()
38
+ expect(validate('#3a3', color)).toBe(true)
39
+ expect(validate('notacolor', color)).toBe(false) // predicate ran, no manual wiring
40
+ })
41
+
42
+ it('validates the recursive CSS style structure', () => {
43
+ const style = cssStyleSchema()
44
+ expect(
45
+ validate({ color: 'red', '&:hover': { color: 'var(--accent)' } }, style)
46
+ ).toBe(true)
47
+ expect(validate({ ' bad key ': 'red' }, style)).toBe(false)
48
+ expect(validate('not-an-object', style)).toBe(false)
49
+ })
50
+
51
+ it('opting out (setPredicateEvaluator(null)) falls back to structural only', () => {
52
+ setPredicateEvaluator(null)
53
+ const color = cssColorSchema()
54
+ // naive: only `type: string` is checked, so a non-color string passes
55
+ expect(validate('notacolor', color)).toBe(true)
56
+ // re-install for the remaining assertions
57
+ installPredicateSupport()
58
+ expect(validate('notacolor', color)).toBe(false)
59
+ })
60
+
61
+ it('installPredicateSupport accepts custom options (fuel budget)', () => {
62
+ installPredicateSupport({ fuel: 500_000 })
63
+ expect(predicateSupportInstalled()).toBe(true)
64
+ expect(validate('#fff', cssColorSchema())).toBe(true)
65
+ })
66
+ })
@@ -14,6 +14,7 @@
14
14
  import { describe, it, expect } from 'bun:test'
15
15
  import { fromTS } from '../lang/emitters/from-ts'
16
16
  import { tjs } from '../lang'
17
+ import { emitVerifiedPredicate } from '../lang/predicate'
17
18
  import * as fs from 'fs'
18
19
  import * as path from 'path'
19
20
 
@@ -528,10 +529,16 @@ describe('Bootstrap Canary', () => {
528
529
  // Step 2: Execute the combined transpiled parser modules
529
530
  const execStart = performance.now()
530
531
 
531
- const parserModule = new Function(`
532
+ // parser-transforms.ts calls emitVerifiedPredicate (from predicate.ts, an
533
+ // acorn-backed engine module not in this parser-only bundle) — inject the
534
+ // native one so the self-hosted preprocess can verify Type/Generic predicates.
535
+ const parserModule = new Function(
536
+ 'emitVerifiedPredicate',
537
+ `
532
538
  ${combinedCode}
533
539
  return { preprocess };
534
- `)()
540
+ `
541
+ )(emitVerifiedPredicate)
535
542
  const execTime = performance.now() - execStart
536
543
 
537
544
  expect(typeof parserModule.preprocess).toBe('function')
@@ -639,10 +646,13 @@ describe('Bootstrap Canary', () => {
639
646
  combinedCode += stripped + '\n'
640
647
  }
641
648
 
642
- const bootstrappedParser = new Function(`
649
+ const bootstrappedParser = new Function(
650
+ 'emitVerifiedPredicate',
651
+ `
643
652
  ${combinedCode}
644
653
  return { preprocess };
645
- `)()
654
+ `
655
+ )(emitVerifiedPredicate)
646
656
 
647
657
  // Import native parser
648
658
  const nativeParser = require('../lang/parser')