tjs-lang 0.8.7 → 0.9.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.
Files changed (75) hide show
  1. package/CLAUDE.md +12 -4
  2. package/dist/experiments/ambient/probe.d.ts +52 -0
  3. package/dist/index.js +115 -171
  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.d.ts +9 -1
  12. package/dist/src/lang/index.d.ts +2 -2
  13. package/dist/src/lang/parser-transforms.d.ts +9 -5
  14. package/dist/src/lang/parser.d.ts +2 -0
  15. package/dist/src/lang/predicate.d.ts +60 -0
  16. package/dist/src/lang/transpiler.d.ts +3 -2
  17. package/dist/src/lang/types.d.ts +16 -0
  18. package/dist/src/schema/index.d.ts +12 -0
  19. package/dist/tjs-browser-from-ts.js +20 -20
  20. package/dist/tjs-browser-from-ts.js.map +3 -3
  21. package/dist/tjs-browser.js +100 -90
  22. package/dist/tjs-browser.js.map +4 -4
  23. package/dist/tjs-css.js +193 -0
  24. package/dist/tjs-css.js.map +7 -0
  25. package/dist/tjs-eval.js +43 -42
  26. package/dist/tjs-eval.js.map +4 -4
  27. package/dist/tjs-from-ts.js +13 -13
  28. package/dist/tjs-from-ts.js.map +2 -2
  29. package/dist/tjs-lang.js +102 -92
  30. package/dist/tjs-lang.js.map +4 -4
  31. package/dist/tjs-runtime.js +10 -0
  32. package/dist/tjs-runtime.js.map +7 -0
  33. package/dist/tjs-schema.js +6 -0
  34. package/dist/tjs-schema.js.map +7 -0
  35. package/dist/tjs-vm.js +55 -54
  36. package/dist/tjs-vm.js.map +4 -4
  37. package/docs/ambient-contracts.md +261 -0
  38. package/editors/ace/ajs-mode.js +214 -233
  39. package/editors/codemirror/ajs-language.js +1570 -233
  40. package/editors/editors-build.test.ts +29 -0
  41. package/editors/monaco/ajs-monarch.js +239 -195
  42. package/llms.txt +4 -0
  43. package/package.json +35 -4
  44. package/src/css/css.test.ts +122 -0
  45. package/src/css/dimensions.test.ts +112 -0
  46. package/src/css/dimensions.ts +146 -0
  47. package/src/css/index.ts +243 -0
  48. package/src/css/perf.bench.test.ts +109 -0
  49. package/src/css/predicates.ts +232 -0
  50. package/src/css/property-aware.test.ts +84 -0
  51. package/src/css/shorthands.test.ts +90 -0
  52. package/src/css/shorthands.ts +113 -0
  53. package/src/css/style.test.ts +125 -0
  54. package/src/css/style.ts +134 -0
  55. package/src/index-tsfree.test.ts +58 -0
  56. package/src/lang/emit-verified-predicate.test.ts +95 -0
  57. package/src/lang/emitters/dts.test.ts +31 -0
  58. package/src/lang/emitters/dts.ts +23 -4
  59. package/src/lang/emitters/js.ts +33 -1
  60. package/src/lang/from-ts.test.ts +1 -1
  61. package/src/lang/generic-verified-predicate.test.ts +39 -0
  62. package/src/lang/index.ts +14 -5
  63. package/src/lang/parser-transforms.ts +109 -14
  64. package/src/lang/parser.ts +9 -2
  65. package/src/lang/predicate-evaluator.test.ts +49 -0
  66. package/src/lang/predicate-report.test.ts +54 -0
  67. package/src/lang/predicate.ts +268 -0
  68. package/src/lang/redos-lint.test.ts +81 -0
  69. package/src/lang/transpiler.ts +13 -0
  70. package/src/lang/type-verified-predicate.test.ts +83 -0
  71. package/src/lang/types.ts +17 -0
  72. package/src/lang/typescript-syntax.test.ts +2 -1
  73. package/src/schema/index.ts +62 -0
  74. package/src/schema/schema.test.ts +66 -0
  75. package/src/use-cases/bootstrap.test.ts +14 -4
@@ -42,6 +42,57 @@ import type {
42
42
  TokenizerState,
43
43
  } from './parser-types'
44
44
  import { extractJSValue } from './parser-params'
45
+ import { emitVerifiedPredicate, formatPredicateDiagnostics } from './predicate'
46
+ import type { PredicateVerification } from './types'
47
+
48
+ /**
49
+ * If a `Type`/`FunctionPredicate` predicate body verifies as predicate-safe
50
+ * (pure, synchronous, no loops/IO), return a self-contained fuel-bounded guard
51
+ * **expression** to inline in place of the raw arrow; otherwise return null so
52
+ * the caller falls back to the unverified body (never rejects — see
53
+ * PRINCIPLES.md, TJS ⊇ JS). This is the transpile-time consumer of the predicate
54
+ * engine: a safe predicate "earns" the native fast path and DoS-safe validation.
55
+ */
56
+ function verifiedGuardExpr(
57
+ name: string,
58
+ kind: 'Type' | 'Generic',
59
+ params: string,
60
+ body: string,
61
+ knownPredicates?: string[],
62
+ report?: PredicateVerification[]
63
+ ): string | null {
64
+ // Synthesize a named declaration the verifier can analyze. The body has
65
+ // already been through the TJS equality/typeof rewrites (Eq/NotEq/Is/IsNot/
66
+ // TypeOf), all whitelisted as pure in the verifier. `knownPredicates` lets a
67
+ // Generic predicate compose with its type-param checks (`checkT(x[0])`) — the
68
+ // verifier treats those calls as composition with another safe predicate.
69
+ //
70
+ // The entry function is named per-declaration (`__pred_<Type>`), not a fixed
71
+ // `__pred`: the guard IIFEs are inlined into the module, and two identically-
72
+ // named nested `function __pred`s would be picked up by the polymorphic-merge
73
+ // transform (which runs after this one) as ambiguous overloads. Type/Generic
74
+ // names are unique within a module, so this is collision-free and deterministic.
75
+ const entry = `__pred_${name}`
76
+ const fnSource = `function ${entry}(${params}) { ${body} }`
77
+ const r = emitVerifiedPredicate(
78
+ fnSource,
79
+ entry,
80
+ knownPredicates ? { knownPredicates: new Set(knownPredicates) } : {}
81
+ )
82
+ if (r.safe) {
83
+ report?.push({ name, kind, verified: true })
84
+ return r.code!
85
+ }
86
+ report?.push({
87
+ name,
88
+ kind,
89
+ verified: false,
90
+ // The verifier names the synthesized entry `__pred_<Name>`; surface the
91
+ // declaration name in the user-facing reason instead.
92
+ reason: formatPredicateDiagnostics(r.diagnostics).replace(/__pred_/g, ''),
93
+ })
94
+ return null
95
+ }
45
96
 
46
97
  export function transformTryWithoutCatch(source: string): string {
47
98
  let result = ''
@@ -1127,12 +1178,15 @@ function transformTypeofKeyword(source: string): string {
1127
1178
  }
1128
1179
 
1129
1180
  /**
1130
- * Transform == and != to Is() and IsNot() calls
1181
+ * Transform == and != to Eq() and NotEq() calls
1131
1182
  *
1132
1183
  * In TJS normal mode:
1133
- * a == b -> Is(a, b) (structural equality)
1134
- * a != b -> IsNot(a, b) (structural inequality)
1184
+ * a == b -> Eq(a, b) (footgun-free ===: unwraps boxed primitives,
1185
+ * null == undefined, but does NOT coerce types and
1186
+ * is NOT structural — see PRINCIPLES/guides)
1187
+ * a != b -> NotEq(a, b)
1135
1188
  * a === b -> a === b (identity, unchanged)
1189
+ * (For deep structural equality use the explicit `Is`/`IsNot` operators.)
1136
1190
  *
1137
1191
  * Uses a two-pass algorithm:
1138
1192
  * 1. Find all == and != positions (outside strings/comments/regex)
@@ -1620,7 +1674,10 @@ function findRightOperandBoundary(
1620
1674
  *
1621
1675
  * When predicate + example: auto-generate type guard from example
1622
1676
  */
1623
- export function transformTypeDeclarations(source: string): string {
1677
+ export function transformTypeDeclarations(
1678
+ source: string,
1679
+ report?: PredicateVerification[]
1680
+ ): string {
1624
1681
  let result = ''
1625
1682
  let i = 0
1626
1683
 
@@ -1737,17 +1794,41 @@ export function transformTypeDeclarations(source: string): string {
1737
1794
  // Build Type() call with appropriate arguments
1738
1795
  // Type(description, predicateOrExample, example?, default?)
1739
1796
  if (predicateMatch && example) {
1740
- // Predicate + example
1797
+ // Predicate + example: the example is the base schema, the predicate
1798
+ // refines it. Verify the predicate → fuel-bounded native guard; the
1799
+ // example schema check stays as an outer gate (constructed once via an
1800
+ // IIFE, not per call). Falls back to the raw arrow when unverifiable.
1741
1801
  const params = predicateMatch[1].trim()
1742
1802
  const body = predicateMatch[2].trim()
1743
1803
  const defaultArg = defaultValue ? `, ${defaultValue}` : ''
1744
- result += `const ${typeName} = Type('${description}', (${params}) => { if (!globalThis.__tjs?.validate(${params}, globalThis.__tjs?.infer(${example}))) return false; ${body} }, ${example}${defaultArg})`
1804
+ const schemaGate = `globalThis.__tjs?.validate(${params}, globalThis.__tjs?.infer(${example}))`
1805
+ const guard = verifiedGuardExpr(
1806
+ typeName,
1807
+ 'Type',
1808
+ params,
1809
+ body,
1810
+ undefined,
1811
+ report
1812
+ )
1813
+ const fn = guard
1814
+ ? `(__g => (${params}) => (${schemaGate} ? __g(${params}) : false))(${guard})`
1815
+ : `(${params}) => { if (!${schemaGate}) return false; ${body} }`
1816
+ result += `const ${typeName} = Type('${description}', ${fn}, ${example}${defaultArg})`
1745
1817
  } else if (predicateMatch) {
1746
- // Predicate only
1818
+ // Predicate only: verify → fuel-bounded native guard, else raw arrow.
1747
1819
  const params = predicateMatch[1].trim()
1748
1820
  const body = predicateMatch[2].trim()
1749
1821
  const defaultArg = defaultValue ? `, undefined, ${defaultValue}` : ''
1750
- result += `const ${typeName} = Type('${description}', (${params}) => { ${body} }${defaultArg})`
1822
+ const guard = verifiedGuardExpr(
1823
+ typeName,
1824
+ 'Type',
1825
+ params,
1826
+ body,
1827
+ undefined,
1828
+ report
1829
+ )
1830
+ const fn = guard ?? `(${params}) => { ${body} }`
1831
+ result += `const ${typeName} = Type('${description}', ${fn}${defaultArg})`
1751
1832
  } else if (example) {
1752
1833
  // Example only (becomes validation schema)
1753
1834
  const defaultArg = defaultValue ? `, ${defaultValue}` : ''
@@ -1938,7 +2019,10 @@ export function transformFunctionPredicateDeclarations(source: string): string {
1938
2019
  * const Pair = Generic(['T', 'U'], (obj, checkT, checkU) => { ... }, '...')
1939
2020
  * const Container = Generic(['T', ['U', '']], (obj, checkT, checkU) => { ... }, '...')
1940
2021
  */
1941
- export function transformGenericDeclarations(source: string): string {
2022
+ export function transformGenericDeclarations(
2023
+ source: string,
2024
+ report?: PredicateVerification[]
2025
+ ): string {
1942
2026
  let result = ''
1943
2027
  let i = 0
1944
2028
 
@@ -2034,11 +2118,21 @@ export function transformGenericDeclarations(source: string): string {
2034
2118
  )
2035
2119
  })
2036
2120
 
2121
+ // Verify the (type-param-rewritten) body, composing with the type-param
2122
+ // checks; safe → fuel-bounded native guard, else the raw arrow.
2123
+ const paramList = [valueParam, ...typeCheckParams].join(', ')
2124
+ const guard = verifiedGuardExpr(
2125
+ genericName,
2126
+ 'Generic',
2127
+ paramList,
2128
+ body,
2129
+ typeCheckParams,
2130
+ report
2131
+ )
2132
+ const fn = guard ?? `(${paramList}) => { ${body} }`
2037
2133
  result += `const ${genericName} = Generic([${typeParams.join(
2038
2134
  ', '
2039
- )}], (${valueParam}, ${typeCheckParams.join(
2040
- ', '
2041
- )}) => { ${body} }, '${description}')`
2135
+ )}], ${fn}, '${description}')`
2042
2136
  } else {
2043
2137
  // No predicate - create a generic that always passes
2044
2138
  result += `const ${genericName} = Generic([${typeParams.join(
@@ -3542,11 +3636,12 @@ export function validateNoDate(source: string): string {
3542
3636
  {
3543
3637
  pattern: /\bnew\s+Date\b/,
3544
3638
  message:
3545
- 'new Date() is not allowed in TjsDate mode. Use Timestamp.now() or Timestamp.from()',
3639
+ 'new Date() is not allowed in TjsDate mode. Use Timestamp.now()/Timestamp.from() for wall-clock dates, or performance.now() for a monotonic counter (timing, id generation)',
3546
3640
  },
3547
3641
  {
3548
3642
  pattern: /\bDate\.now\b/,
3549
- message: 'Date.now() is not allowed in TjsDate mode. Use Timestamp.now()',
3643
+ message:
3644
+ 'Date.now() is not allowed in TjsDate mode. Use Timestamp.now() for a wall-clock date, or performance.now() for a monotonic counter (timing, id generation)',
3550
3645
  },
3551
3646
  {
3552
3647
  pattern: /\bDate\.parse\b/,
@@ -8,6 +8,7 @@
8
8
  import * as acorn from 'acorn'
9
9
  import type { Program, FunctionDeclaration } from 'acorn'
10
10
  import { SyntaxError } from './types'
11
+ import type { PredicateVerification } from './types'
11
12
 
12
13
  // Re-export types so external callers don't need to change imports
13
14
  export type {
@@ -124,6 +125,7 @@ export function preprocess(
124
125
  polymorphicNames: Set<string>
125
126
  extensions: Map<string, Set<string>>
126
127
  letAnnotations: Map<string, string>
128
+ predicates: PredicateVerification[]
127
129
  } {
128
130
  const originalSource = source
129
131
  let moduleSafety: 'none' | 'inputs' | 'all' | undefined
@@ -298,8 +300,12 @@ export function preprocess(
298
300
  // Generic Bar<T, U> { ... } -> const Bar = Generic(...)
299
301
  // Union Dir 'up' | 'down' -> const Dir = Union(...)
300
302
  // Enum Status { Pending, Active, Done } -> const Status = Enum(...)
301
- source = transformTypeDeclarations(source)
302
- source = transformGenericDeclarations(source)
303
+ // Collect per-predicate verification status (Type/Generic predicate bodies:
304
+ // verified → native guard, or fell back to a raw function). Surfaced on the
305
+ // transpile result so tools can flag unverifiable predicates.
306
+ const predicates: PredicateVerification[] = []
307
+ source = transformTypeDeclarations(source, predicates)
308
+ source = transformGenericDeclarations(source, predicates)
303
309
  source = transformFunctionPredicateDeclarations(source)
304
310
  source = transformUnionDeclarations(source)
305
311
  source = transformEnumDeclarations(source)
@@ -437,6 +443,7 @@ export function preprocess(
437
443
  polymorphicNames: polyResult.polymorphicNames,
438
444
  extensions: extResult.extensions,
439
445
  letAnnotations,
446
+ predicates,
440
447
  }
441
448
  }
442
449
 
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `createPredicateEvaluator` — the pluggable `(source, value) => boolean` bridge
3
+ * a zero-dep, predicate-aware JSON-Schema validator (tosijs-schema) injects to
4
+ * run `$predicate` sources. Compiles+caches per source; fails closed on an
5
+ * unverifiable source (never a thrown error, never a silent pass).
6
+ */
7
+ import { describe, it, expect } from 'bun:test'
8
+ import { createPredicateEvaluator } from './predicate'
9
+
10
+ const POS = 'function isPos(x) { return typeof x === "number" && x > 0 }'
11
+ const LOOPY =
12
+ 'function bad(xs) { for (const x of xs) { if (x < 0) return false } return true }'
13
+
14
+ describe('createPredicateEvaluator', () => {
15
+ it('evaluates a safe source against values', () => {
16
+ const evaluate = createPredicateEvaluator()
17
+ expect(evaluate(POS, 5)).toBe(true)
18
+ expect(evaluate(POS, -1)).toBe(false)
19
+ expect(evaluate(POS, 'x')).toBe(false)
20
+ })
21
+
22
+ it('caches: compiles a source once, reuses across calls', () => {
23
+ let unsafeCalls = 0
24
+ const evaluate = createPredicateEvaluator({
25
+ onUnsafe: () => unsafeCalls++,
26
+ })
27
+ // many evaluations of the same source — still one compile, zero warnings
28
+ for (let i = 0; i < 100; i++) expect(evaluate(POS, i + 1)).toBe(true)
29
+ expect(unsafeCalls).toBe(0)
30
+ })
31
+
32
+ it('fails closed on an unverifiable source (returns false, warns once)', () => {
33
+ const unsafe: string[] = []
34
+ const evaluate = createPredicateEvaluator({
35
+ onUnsafe: (src) => unsafe.push(src),
36
+ })
37
+ // a loop can't be certified predicate-safe → every value is invalid
38
+ expect(evaluate(LOOPY, [1, 2, 3])).toBe(false)
39
+ expect(evaluate(LOOPY, [1, 2, 3])).toBe(false)
40
+ // ...and the failure is reported exactly once (cached)
41
+ expect(unsafe.length).toBe(1)
42
+ })
43
+
44
+ it('fails closed on a runaway (fuel) rather than throwing', () => {
45
+ const evaluate = createPredicateEvaluator({ fuel: 100 })
46
+ const recur = 'function deep(n) { return deep(n + 1) }'
47
+ expect(evaluate(recur, 0)).toBe(false)
48
+ })
49
+ })
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The `tjs()` result surfaces per-`Type`/`Generic` predicate verification status
3
+ * (`result.predicates`) so tools can flag unverifiable predicates, and mirrors
4
+ * the unverified ones into `result.warnings`. (#9 / the #5 warn-on-fallback tail.)
5
+ */
6
+ import { describe, it, expect } from 'bun:test'
7
+ import { tjs } from './index'
8
+
9
+ describe('predicate verification is reported on the transpile result', () => {
10
+ it('a safe Type predicate → verified, no warning', () => {
11
+ const r = tjs("Type Pos 'positive' { predicate(x) { return x > 0 } }")
12
+ expect(r.predicates).toEqual([
13
+ { name: 'Pos', kind: 'Type', verified: true },
14
+ ])
15
+ expect(r.warnings).toBeUndefined()
16
+ })
17
+
18
+ it('an unverifiable Type predicate → not verified + reason + warning', () => {
19
+ const r = tjs(
20
+ "Type Bad 'bad' { predicate(xs) { for (const x of xs) { if (x<0) return false } return true } }"
21
+ )
22
+ const p = r.predicates?.[0]
23
+ expect(p).toMatchObject({ name: 'Bad', kind: 'Type', verified: false })
24
+ expect(p?.reason).toMatch(/loop/i)
25
+ expect(p?.reason).not.toContain('__pred_') // internal name not leaked
26
+ expect(r.warnings?.some((w) => /Bad.*not verifiable/.test(w))).toBe(true)
27
+ })
28
+
29
+ it('reports Generic predicates too', () => {
30
+ const r = tjs(
31
+ "Generic Box<T> { description: 'b', predicate(x, T) { return T(x.value) } }"
32
+ )
33
+ expect(r.predicates?.[0]).toMatchObject({
34
+ name: 'Box',
35
+ kind: 'Generic',
36
+ verified: true,
37
+ })
38
+ })
39
+
40
+ it('reports each predicate in a multi-predicate module', () => {
41
+ const r = tjs(
42
+ "Type A 'a' { predicate(x) { return x > 0 } }\n" +
43
+ "Type B 'b' { predicate(xs) { for (const x of xs) {} return true } }"
44
+ )
45
+ const byName = Object.fromEntries(
46
+ (r.predicates ?? []).map((p) => [p.name, p.verified])
47
+ )
48
+ expect(byName).toEqual({ A: true, B: false })
49
+ })
50
+
51
+ it('omits `predicates` when there are none', () => {
52
+ expect(tjs('function f() { return 1 }').predicates).toBeUndefined()
53
+ })
54
+ })
@@ -67,6 +67,16 @@ const PURE_GLOBALS = new Set([
67
67
  'Boolean',
68
68
  'Array',
69
69
  'Object',
70
+ // TJS-injected pure helpers. Native-TJS predicate bodies are rewritten before
71
+ // they reach the verifier: `==`/`!=` → `Eq`/`NotEq`, the explicit `Is`/`IsNot`
72
+ // operators → `Is`/`IsNot`, `typeof x` → `TypeOf(x)`. All are pure, total
73
+ // functions (footgun-free equality / structural equality / safe typeof), so a
74
+ // predicate that uses TJS equality still verifies as predicate-safe.
75
+ 'Eq',
76
+ 'NotEq',
77
+ 'Is',
78
+ 'IsNot',
79
+ 'TypeOf',
70
80
  ])
71
81
 
72
82
  /** Namespaces whose static methods are pure (with effectful exceptions below). */
@@ -82,6 +92,101 @@ const PURE_NAMESPACES = new Set([
82
92
  /** Static members that are NOT pure even on a pure namespace. */
83
93
  const EFFECTFUL_STATICS = new Set(['Math.random', 'Date.now'])
84
94
 
95
+ /**
96
+ * A match at `pattern[pos]` for an *unbounded* quantifier (`*`, `+`, `{n,}` and
97
+ * their lazy `?` variants) — the repetitions that drive backtracking blowups.
98
+ * `?`, `{n}` and `{n,m}` are bounded, so they don't count. Returns the consumed
99
+ * length, or 0 if there's no unbounded quantifier here.
100
+ */
101
+ function unboundedQuantifierLen(pattern: string, pos: number): number {
102
+ const c = pattern[pos]
103
+ let len = 0
104
+ if (c === '*' || c === '+') {
105
+ len = 1
106
+ } else if (c === '{') {
107
+ // `{n,}` is unbounded; `{n}` / `{n,m}` are bounded.
108
+ const m = pattern.slice(pos).match(/^\{\d+,\}/)
109
+ if (m) len = m[0].length
110
+ }
111
+ if (len === 0) return 0
112
+ // A trailing `?` makes it lazy — still unbounded, still backtracks.
113
+ if (pattern[pos + len] === '?') len++
114
+ return len
115
+ }
116
+
117
+ /**
118
+ * Conservative ReDoS detector: flags a regex whose **star height is ≥ 2** — an
119
+ * unbounded quantifier nested inside a group that is itself unbounded-quantified
120
+ * (the classic `(a+)+`, `(a*)*`, `([a-z]+)*`, `(.*)*` exponential-backtracking
121
+ * shapes). A predicate verifier should fail closed: over-flagging a safe pattern
122
+ * only costs it its "verified" badge (it still runs), whereas certifying a
123
+ * dangerous one is a broken safety promise.
124
+ *
125
+ * Not caught (known limitation, documented): *polynomial* ReDoS from adjacent
126
+ * overlapping quantifiers (`\d+\d+$`, `a.*a.*a`) and alternation-overlap
127
+ * (`(a|a)*`). The exponential class above is the one the safety story commits to.
128
+ *
129
+ * @returns a reason string if risky, else null.
130
+ */
131
+ function reDoSRisk(pattern: string): string | null {
132
+ // Per-group frame: did this group contain an unbounded quantifier?
133
+ const stack: Array<{ hadUnbounded: boolean }> = []
134
+ let i = 0
135
+ let inClass = false
136
+ while (i < pattern.length) {
137
+ const c = pattern[i]
138
+ if (c === '\\') {
139
+ i += 2 // skip an escaped char (regex escapes are 2 chars here)
140
+ continue
141
+ }
142
+ if (inClass) {
143
+ if (c === ']') inClass = false
144
+ i++
145
+ continue
146
+ }
147
+ if (c === '[') {
148
+ inClass = true
149
+ i++
150
+ continue
151
+ }
152
+ if (c === '(') {
153
+ stack.push({ hadUnbounded: false })
154
+ i++
155
+ continue
156
+ }
157
+ if (c === ')') {
158
+ const frame = stack.pop() ?? { hadUnbounded: false }
159
+ const qlen = unboundedQuantifierLen(pattern, i + 1)
160
+ if (qlen > 0) {
161
+ // This group is itself unbounded-repeated. If it already contained an
162
+ // unbounded quantifier, that's star height ≥ 2 → catastrophic.
163
+ if (frame.hadUnbounded)
164
+ return 'an unbounded quantifier is nested inside another (e.g. `(a+)+`)'
165
+ // The parent group now contains an unbounded repetition (this group).
166
+ if (stack.length) stack[stack.length - 1].hadUnbounded = true
167
+ i += 1 + qlen
168
+ continue
169
+ }
170
+ // No quantifier on this group, but if it contained an unbounded
171
+ // repetition, that repetition lives in the parent's scope too — propagate
172
+ // so `((a+))+` is caught the same as `(a+)+`.
173
+ if (frame.hadUnbounded && stack.length)
174
+ stack[stack.length - 1].hadUnbounded = true
175
+ i++
176
+ continue
177
+ }
178
+ // An unbounded quantifier on a plain atom at the current nesting level.
179
+ const qlen = unboundedQuantifierLen(pattern, i)
180
+ if (qlen > 0) {
181
+ if (stack.length) stack[stack.length - 1].hadUnbounded = true
182
+ i += qlen
183
+ continue
184
+ }
185
+ i++
186
+ }
187
+ return null
188
+ }
189
+
85
190
  /**
86
191
  * Instance methods known to be pure regardless of receiver type. A method call
87
192
  * whose method name isn't here (and whose receiver isn't a pure namespace) is
@@ -255,6 +360,22 @@ export function verifyPredicate(
255
360
  ForStatement: loop,
256
361
  ForInStatement: loop,
257
362
  ForOfStatement: loop,
363
+ Literal(n: any) {
364
+ // Regex literals are the one primitive fuel can't bound: a single
365
+ // `.match`/`.test`/`.replace` is opaque to the function-entry fuel hook,
366
+ // so a catastrophic-backtracking pattern could hang on hostile input.
367
+ // Certifying it "safe" would be a false guarantee, so flag it. (Dynamic
368
+ // `RegExp(...)` is already rejected — `RegExp` isn't a pure global and
369
+ // `new` is banned — so only literals need analysis.)
370
+ if (n.regex && typeof n.regex.pattern === 'string') {
371
+ const risk = reDoSRisk(n.regex.pattern)
372
+ if (risk)
373
+ flag(
374
+ `regex /${n.regex.pattern}/ risks catastrophic backtracking (ReDoS): ${risk}. A single match is not fuel-bounded, so it can't be certified predicate-safe — simplify the pattern or validate without it.`,
375
+ n
376
+ )
377
+ }
378
+ },
258
379
  CallExpression(n: any) {
259
380
  const callee = n.callee
260
381
  // Bare call: f(...)
@@ -548,3 +669,150 @@ export function compilePredicate(
548
669
  }
549
670
  return wrapped
550
671
  }
672
+
673
+ export interface EmitPredicateResult {
674
+ /** True iff the cluster passed predicate-safety verification. */
675
+ safe: boolean
676
+ /**
677
+ * When `safe`, a self-contained JS **expression** that evaluates to the guard
678
+ * function `(...args) => boolean`. Inline it directly into transpiler output —
679
+ * it carries its own fuel counter (no global `__fuel`, no runtime dependency
680
+ * on the predicate engine or the `PredicateFuelExhausted` class). Undefined
681
+ * when `!safe`.
682
+ */
683
+ code?: string
684
+ /** Verifier diagnostics (the reasons, when `!safe`). */
685
+ diagnostics: PredicateDiagnostic[]
686
+ }
687
+
688
+ /**
689
+ * Verify a predicate cluster and, if safe, emit a **self-contained source
690
+ * expression** for its guard — the transpile-time counterpart to
691
+ * `compilePredicate` (which evals to live closures). This is what lets a
692
+ * verified `Type`/`FunctionPredicate` predicate compile to a fuel-bounded native
693
+ * guard in standalone output: no import of this module, no shared runtime.
694
+ *
695
+ * Fuel model mirrors `compilePredicate` (function-entry `__fuel()` bounds all
696
+ * iteration since loops are rejected), but because a guard answers a boolean
697
+ * question, a runaway input **returns `false`** ("not a valid instance of this
698
+ * type") instead of throwing — DoS-safe validation that never crashes the caller.
699
+ * A deep-recursion stack overflow is the same runaway signal, normalized the
700
+ * same way.
701
+ *
702
+ * The runtime effectful-global shadow that `compilePredicate` applies is omitted
703
+ * here on purpose: a `safe` cluster provably references no effectful global (the
704
+ * static verifier guarantees it), so the shadow would only bloat emitted output.
705
+ *
706
+ * @param source the predicate cluster (one or more `function` declarations)
707
+ * @param entryName which declared function is the guard entry point
708
+ * @param opts verify options + `fuel` budget (default 1,000,000)
709
+ */
710
+ export function emitVerifiedPredicate(
711
+ source: string,
712
+ entryName: string,
713
+ opts: CompilePredicateOptions = {}
714
+ ): EmitPredicateResult {
715
+ const result = verifyPredicate(source, opts)
716
+ if (!result.safe) {
717
+ return { safe: false, diagnostics: result.diagnostics }
718
+ }
719
+ if (!result.predicates.includes(entryName)) {
720
+ return {
721
+ safe: false,
722
+ diagnostics: [
723
+ {
724
+ predicate: entryName,
725
+ message: `entry predicate '${entryName}' not found in the verified cluster`,
726
+ line: 0,
727
+ column: 0,
728
+ },
729
+ ],
730
+ }
731
+ }
732
+
733
+ const budget = opts.fuel ?? 1_000_000
734
+ const instrumented = injectFuel(source)
735
+
736
+ // A self-contained IIFE: fuel counter in a closure, guard entry re-armed per
737
+ // top-level call, runaway (fuel or stack) → `false`.
738
+ const code =
739
+ `(() => {` +
740
+ `let __f = 0;` +
741
+ `const __fuel = () => { if (--__f < 0) throw new RangeError('tjs:predicate-fuel'); };` +
742
+ `${instrumented}` +
743
+ `return (...__a) => {` +
744
+ `__f = ${budget};` +
745
+ `try { return !!${entryName}(...__a); }` +
746
+ `catch (e) {` +
747
+ `if (e instanceof RangeError && /tjs:predicate-fuel|stack/i.test(e.message)) return false;` +
748
+ `throw e;` +
749
+ `}` +
750
+ `};` +
751
+ `})()`
752
+
753
+ return { safe: true, code, diagnostics: [] }
754
+ }
755
+
756
+ // --- $predicate evaluator (#6: the tosijs-schema integration point) ---------
757
+
758
+ export interface PredicateEvaluatorOptions extends CompilePredicateOptions {
759
+ /**
760
+ * Called once per source that fails to verify/compile. Default: `console.warn`.
761
+ * The evaluator fails **closed** on such a source (returns `false`), so an
762
+ * unverifiable `$predicate` can never validate a value as `true`.
763
+ */
764
+ onUnsafe?: (source: string, error: Error) => void
765
+ }
766
+
767
+ /**
768
+ * Build a pluggable `$predicate` evaluator: `(source, value) => boolean`.
769
+ *
770
+ * This is the bridge for a predicate-aware JSON-Schema validator that lives in
771
+ * another package (e.g. `tosijs-schema`, which cannot depend on `tjs-lang` — the
772
+ * dependency runs the other way). Such a validator stays zero-dep and exposes a
773
+ * hook; a consumer that has this engine injects an evaluator built here.
774
+ *
775
+ * Each distinct source is verified + compiled **once** and cached, so repeated
776
+ * validation is just a native call. Semantics match `compilePredicateSchema`:
777
+ * the LAST top-level function is the entry; a source that isn't predicate-safe
778
+ * (or throws / exhausts fuel at runtime) yields `false` — never a thrown error
779
+ * mid-validation, never a silent pass.
780
+ */
781
+ export function createPredicateEvaluator(
782
+ opts: PredicateEvaluatorOptions = {}
783
+ ): (source: string, value: unknown) => boolean {
784
+ const { onUnsafe, ...compileOpts } = opts
785
+ const cache = new Map<string, ((value: unknown) => boolean) | null>()
786
+ const warned = new Set<string>()
787
+
788
+ return (source: string, value: unknown): boolean => {
789
+ let fn = cache.get(source)
790
+ if (fn === undefined) {
791
+ try {
792
+ const names = verifyPredicate(source, compileOpts).predicates
793
+ const entry = names[names.length - 1]
794
+ if (!entry) throw new Error('$predicate declares no function')
795
+ const mod = compilePredicate(source, [entry], compileOpts)
796
+ fn = mod[entry] as (value: unknown) => boolean
797
+ } catch (e) {
798
+ fn = null // fail closed
799
+ if (!warned.has(source)) {
800
+ warned.add(source)
801
+ const err = e instanceof Error ? e : new Error(String(e))
802
+ if (onUnsafe) onUnsafe(source, err)
803
+ else
804
+ console.warn(
805
+ `[tjs-lang] $predicate not verifiable — failing closed: ${err.message}`
806
+ )
807
+ }
808
+ }
809
+ cache.set(source, fn)
810
+ }
811
+ if (fn === null) return false
812
+ try {
813
+ return fn(value) === true
814
+ } catch {
815
+ return false // fuel exhaustion / runtime miss → not valid
816
+ }
817
+ }
818
+ }