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.
- package/CLAUDE.md +19 -5
- package/dist/experiments/ambient/probe.d.ts +52 -0
- package/dist/index.js +110 -162
- package/dist/index.js.map +4 -4
- package/dist/scripts/build-editors.d.ts +19 -0
- package/dist/src/css/dimensions.d.ts +23 -0
- package/dist/src/css/index.d.ts +85 -0
- package/dist/src/css/predicates.d.ts +38 -0
- package/dist/src/css/shorthands.d.ts +31 -0
- package/dist/src/css/style.d.ts +48 -0
- package/dist/src/lang/emitters/js-wasm.d.ts +2 -0
- package/dist/src/lang/emitters/js.d.ts +9 -1
- package/dist/src/lang/index.d.ts +2 -2
- package/dist/src/lang/parser-transforms.d.ts +9 -5
- package/dist/src/lang/parser-types.d.ts +7 -0
- package/dist/src/lang/parser.d.ts +2 -0
- package/dist/src/lang/predicate.d.ts +60 -0
- package/dist/src/lang/transpiler.d.ts +3 -2
- package/dist/src/lang/types.d.ts +16 -0
- package/dist/src/schema/index.d.ts +12 -0
- package/dist/tjs-browser-from-ts.js +20 -20
- package/dist/tjs-browser-from-ts.js.map +3 -3
- package/dist/tjs-browser.js +108 -94
- package/dist/tjs-browser.js.map +4 -4
- package/dist/tjs-css.js +193 -0
- package/dist/tjs-css.js.map +7 -0
- package/dist/tjs-eval.js +43 -42
- package/dist/tjs-eval.js.map +4 -4
- package/dist/tjs-from-ts.js +13 -13
- package/dist/tjs-from-ts.js.map +2 -2
- package/dist/tjs-lang.js +106 -92
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-runtime.js +10 -0
- package/dist/tjs-runtime.js.map +7 -0
- package/dist/tjs-schema.js +6 -0
- package/dist/tjs-schema.js.map +7 -0
- package/dist/tjs-vm.js +55 -54
- package/dist/tjs-vm.js.map +4 -4
- package/docs/ambient-contracts.md +261 -0
- package/docs/type-system-north-star.md +100 -0
- package/editors/ace/ajs-mode.js +214 -233
- package/editors/codemirror/ajs-language.js +1570 -233
- package/editors/editors-build.test.ts +29 -0
- package/editors/monaco/ajs-monarch.js +239 -195
- package/llms.txt +4 -0
- package/package.json +35 -4
- package/src/css/css.test.ts +122 -0
- package/src/css/dimensions.test.ts +112 -0
- package/src/css/dimensions.ts +146 -0
- package/src/css/index.ts +243 -0
- package/src/css/perf.bench.test.ts +109 -0
- package/src/css/predicates.ts +232 -0
- package/src/css/property-aware.test.ts +84 -0
- package/src/css/shorthands.test.ts +90 -0
- package/src/css/shorthands.ts +113 -0
- package/src/css/style.test.ts +125 -0
- package/src/css/style.ts +134 -0
- package/src/index-tsfree.test.ts +58 -0
- package/src/lang/emit-verified-predicate.test.ts +95 -0
- package/src/lang/emitters/dts.test.ts +31 -0
- package/src/lang/emitters/dts.ts +23 -4
- package/src/lang/emitters/js-wasm.ts +8 -4
- package/src/lang/emitters/js.ts +58 -1
- package/src/lang/from-ts.test.ts +1 -1
- package/src/lang/generic-verified-predicate.test.ts +39 -0
- package/src/lang/index.ts +14 -5
- package/src/lang/parser-transforms.ts +113 -15
- package/src/lang/parser-types.ts +7 -0
- package/src/lang/parser.ts +18 -3
- package/src/lang/predicate-evaluator.test.ts +49 -0
- package/src/lang/predicate-report.test.ts +78 -0
- package/src/lang/predicate.ts +268 -0
- package/src/lang/redos-lint.test.ts +81 -0
- package/src/lang/transpiler.ts +13 -0
- package/src/lang/type-verified-predicate.test.ts +83 -0
- package/src/lang/types.ts +17 -0
- package/src/lang/typescript-syntax.test.ts +2 -1
- package/src/lang/wasm-fallback-warning.test.ts +50 -0
- package/src/lang/wasm-intdiv-lint.test.ts +45 -0
- package/src/lang/wasm-ready.test.ts +94 -0
- package/src/lang/wasm-simd-ops.test.ts +84 -0
- package/src/lang/wasm.ts +61 -2
- package/src/schema/index.ts +62 -0
- package/src/schema/schema.test.ts +66 -0
- 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
|
-
|
|
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
|
-
|
|
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')
|