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
package/src/lang/index.ts CHANGED
@@ -47,6 +47,8 @@ export {
47
47
  export {
48
48
  verifyPredicate,
49
49
  compilePredicate,
50
+ emitVerifiedPredicate,
51
+ createPredicateEvaluator,
50
52
  suggest,
51
53
  effectfulFromAtoms,
52
54
  formatPredicateDiagnostics,
@@ -55,6 +57,8 @@ export {
55
57
  type PredicateVerifyResult,
56
58
  type VerifyPredicateOptions,
57
59
  type CompilePredicateOptions,
60
+ type EmitPredicateResult,
61
+ type PredicateEvaluatorOptions,
58
62
  type Suggestion,
59
63
  type SuggestOptions,
60
64
  } from './predicate'
@@ -80,11 +84,16 @@ export {
80
84
  typeDescriptorToTS,
81
85
  type GenerateDTSOptions,
82
86
  } from './emitters/dts'
83
- export {
84
- fromTS,
85
- type FromTSOptions,
86
- type FromTSResult,
87
- } from './emitters/from-ts'
87
+ // `fromTS` is intentionally NOT re-exported here: it pulls the TypeScript
88
+ // compiler (~4-10MB, only a devDependency) into the main `tjs-lang` entry, which
89
+ // crashes Node consumers that don't have `typescript` installed with
90
+ // `Cannot find package 'typescript'` (and pulls TS at import time, breaking
91
+ // constrained runtimes like Cloud Run). Import the value from the dedicated,
92
+ // documented subpath instead:
93
+ // import { fromTS } from 'tjs-lang/lang/from-ts'
94
+ // The types are safe to re-export — `export type` is erased at build time, so it
95
+ // adds no runtime dependency.
96
+ export type { FromTSOptions, FromTSResult } from './emitters/from-ts'
88
97
  export * from './inference'
89
98
  export { Schema } from './schema'
90
99
  export {
@@ -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 = ''
@@ -209,7 +260,10 @@ export function extractWasmBlocks(source: string): {
209
260
  ? `globalThis.${block.id}(${captureArgs})`
210
261
  : `globalThis.${block.id}()`
211
262
 
212
- const dispatch = `(globalThis.${block.id} ? ${wasmCall} : (() => {${fallbackCode}})())`
263
+ // `globalThis.__tjs_wasm_enabled === false` forces the fallback path even
264
+ // when the WASM is instantiated — a public toggle for A/B benchmarking
265
+ // (WASM vs JS) without poking the internal `__tjs_wasm_<id>` globals (UI-#3).
266
+ const dispatch = `((globalThis.__tjs_wasm_enabled !== false && globalThis.${block.id}) ? ${wasmCall} : (() => {${fallbackCode}})())`
213
267
 
214
268
  result += dispatch
215
269
  i = matchEnd
@@ -1127,12 +1181,15 @@ function transformTypeofKeyword(source: string): string {
1127
1181
  }
1128
1182
 
1129
1183
  /**
1130
- * Transform == and != to Is() and IsNot() calls
1184
+ * Transform == and != to Eq() and NotEq() calls
1131
1185
  *
1132
1186
  * In TJS normal mode:
1133
- * a == b -> Is(a, b) (structural equality)
1134
- * a != b -> IsNot(a, b) (structural inequality)
1187
+ * a == b -> Eq(a, b) (footgun-free ===: unwraps boxed primitives,
1188
+ * null == undefined, but does NOT coerce types and
1189
+ * is NOT structural — see PRINCIPLES/guides)
1190
+ * a != b -> NotEq(a, b)
1135
1191
  * a === b -> a === b (identity, unchanged)
1192
+ * (For deep structural equality use the explicit `Is`/`IsNot` operators.)
1136
1193
  *
1137
1194
  * Uses a two-pass algorithm:
1138
1195
  * 1. Find all == and != positions (outside strings/comments/regex)
@@ -1620,7 +1677,10 @@ function findRightOperandBoundary(
1620
1677
  *
1621
1678
  * When predicate + example: auto-generate type guard from example
1622
1679
  */
1623
- export function transformTypeDeclarations(source: string): string {
1680
+ export function transformTypeDeclarations(
1681
+ source: string,
1682
+ report?: PredicateVerification[]
1683
+ ): string {
1624
1684
  let result = ''
1625
1685
  let i = 0
1626
1686
 
@@ -1737,17 +1797,41 @@ export function transformTypeDeclarations(source: string): string {
1737
1797
  // Build Type() call with appropriate arguments
1738
1798
  // Type(description, predicateOrExample, example?, default?)
1739
1799
  if (predicateMatch && example) {
1740
- // Predicate + example
1800
+ // Predicate + example: the example is the base schema, the predicate
1801
+ // refines it. Verify the predicate → fuel-bounded native guard; the
1802
+ // example schema check stays as an outer gate (constructed once via an
1803
+ // IIFE, not per call). Falls back to the raw arrow when unverifiable.
1741
1804
  const params = predicateMatch[1].trim()
1742
1805
  const body = predicateMatch[2].trim()
1743
1806
  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})`
1807
+ const schemaGate = `globalThis.__tjs?.validate(${params}, globalThis.__tjs?.infer(${example}))`
1808
+ const guard = verifiedGuardExpr(
1809
+ typeName,
1810
+ 'Type',
1811
+ params,
1812
+ body,
1813
+ undefined,
1814
+ report
1815
+ )
1816
+ const fn = guard
1817
+ ? `(__g => (${params}) => (${schemaGate} ? __g(${params}) : false))(${guard})`
1818
+ : `(${params}) => { if (!${schemaGate}) return false; ${body} }`
1819
+ result += `const ${typeName} = Type('${description}', ${fn}, ${example}${defaultArg})`
1745
1820
  } else if (predicateMatch) {
1746
- // Predicate only
1821
+ // Predicate only: verify → fuel-bounded native guard, else raw arrow.
1747
1822
  const params = predicateMatch[1].trim()
1748
1823
  const body = predicateMatch[2].trim()
1749
1824
  const defaultArg = defaultValue ? `, undefined, ${defaultValue}` : ''
1750
- result += `const ${typeName} = Type('${description}', (${params}) => { ${body} }${defaultArg})`
1825
+ const guard = verifiedGuardExpr(
1826
+ typeName,
1827
+ 'Type',
1828
+ params,
1829
+ body,
1830
+ undefined,
1831
+ report
1832
+ )
1833
+ const fn = guard ?? `(${params}) => { ${body} }`
1834
+ result += `const ${typeName} = Type('${description}', ${fn}${defaultArg})`
1751
1835
  } else if (example) {
1752
1836
  // Example only (becomes validation schema)
1753
1837
  const defaultArg = defaultValue ? `, ${defaultValue}` : ''
@@ -1938,7 +2022,10 @@ export function transformFunctionPredicateDeclarations(source: string): string {
1938
2022
  * const Pair = Generic(['T', 'U'], (obj, checkT, checkU) => { ... }, '...')
1939
2023
  * const Container = Generic(['T', ['U', '']], (obj, checkT, checkU) => { ... }, '...')
1940
2024
  */
1941
- export function transformGenericDeclarations(source: string): string {
2025
+ export function transformGenericDeclarations(
2026
+ source: string,
2027
+ report?: PredicateVerification[]
2028
+ ): string {
1942
2029
  let result = ''
1943
2030
  let i = 0
1944
2031
 
@@ -2034,11 +2121,21 @@ export function transformGenericDeclarations(source: string): string {
2034
2121
  )
2035
2122
  })
2036
2123
 
2124
+ // Verify the (type-param-rewritten) body, composing with the type-param
2125
+ // checks; safe → fuel-bounded native guard, else the raw arrow.
2126
+ const paramList = [valueParam, ...typeCheckParams].join(', ')
2127
+ const guard = verifiedGuardExpr(
2128
+ genericName,
2129
+ 'Generic',
2130
+ paramList,
2131
+ body,
2132
+ typeCheckParams,
2133
+ report
2134
+ )
2135
+ const fn = guard ?? `(${paramList}) => { ${body} }`
2037
2136
  result += `const ${genericName} = Generic([${typeParams.join(
2038
2137
  ', '
2039
- )}], (${valueParam}, ${typeCheckParams.join(
2040
- ', '
2041
- )}) => { ${body} }, '${description}')`
2138
+ )}], ${fn}, '${description}')`
2042
2139
  } else {
2043
2140
  // No predicate - create a generic that always passes
2044
2141
  result += `const ${genericName} = Generic([${typeParams.join(
@@ -3542,11 +3639,12 @@ export function validateNoDate(source: string): string {
3542
3639
  {
3543
3640
  pattern: /\bnew\s+Date\b/,
3544
3641
  message:
3545
- 'new Date() is not allowed in TjsDate mode. Use Timestamp.now() or Timestamp.from()',
3642
+ '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
3643
  },
3547
3644
  {
3548
3645
  pattern: /\bDate\.now\b/,
3549
- message: 'Date.now() is not allowed in TjsDate mode. Use Timestamp.now()',
3646
+ message:
3647
+ '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
3648
  },
3551
3649
  {
3552
3650
  pattern: /\bDate\.parse\b/,
@@ -196,6 +196,13 @@ export interface TjsModes {
196
196
  tjsNoVar: boolean
197
197
  /** TjsSafeAssign: let declarations need an initializer or `: example` annotation; literal undefined/null/void 0 assigned to typed lets is flagged */
198
198
  tjsSafeAssign: boolean
199
+ /**
200
+ * TjsStrict: the author explicitly opted into strict semantics (via the
201
+ * `TjsStrict` directive). Distinct from "all modes on" (native TJS's default) —
202
+ * only `true` when the directive is present. Escalates soft diagnostics (e.g. an
203
+ * unverifiable `Type`/`Generic` predicate) from a warning to a transpile error.
204
+ */
205
+ tjsStrict: boolean
199
206
  }
200
207
 
201
208
  /**
@@ -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
@@ -156,6 +158,7 @@ export function preprocess(
156
158
  tjsSafeEval: false,
157
159
  tjsNoVar: false,
158
160
  tjsSafeAssign: false,
161
+ tjsStrict: false,
159
162
  }
160
163
  : {
161
164
  tjsEquals: true,
@@ -166,6 +169,10 @@ export function preprocess(
166
169
  tjsSafeEval: false, // opt-in only (adds import)
167
170
  tjsNoVar: true,
168
171
  tjsSafeAssign: true,
172
+ // Native TJS has all modes on by default, but is NOT "strict" unless the
173
+ // author writes the `TjsStrict` directive — that opt-in is what escalates
174
+ // e.g. unverifiable predicates from a warning to a hard error.
175
+ tjsStrict: false,
169
176
  }
170
177
 
171
178
  // Safety: native TJS defaults to 'inputs' (runtime default),
@@ -201,7 +208,8 @@ export function preprocess(
201
208
  const directive = match[2]
202
209
 
203
210
  if (directive === 'TjsStrict') {
204
- // Enable all TJS modes
211
+ // Enable all TJS modes + mark strict (the author's explicit opt-in, which
212
+ // escalates soft diagnostics like unverifiable predicates to hard errors).
205
213
  tjsModes.tjsEquals = true
206
214
  tjsModes.tjsClass = true
207
215
  tjsModes.tjsDate = true
@@ -209,6 +217,7 @@ export function preprocess(
209
217
  tjsModes.tjsNoVar = true
210
218
  tjsModes.tjsStandard = true
211
219
  tjsModes.tjsSafeAssign = true
220
+ tjsModes.tjsStrict = true
212
221
  } else if (directive === 'TjsCompat') {
213
222
  // Disable all TJS modes (JS-compatible)
214
223
  tjsModes.tjsEquals = false
@@ -298,8 +307,12 @@ export function preprocess(
298
307
  // Generic Bar<T, U> { ... } -> const Bar = Generic(...)
299
308
  // Union Dir 'up' | 'down' -> const Dir = Union(...)
300
309
  // Enum Status { Pending, Active, Done } -> const Status = Enum(...)
301
- source = transformTypeDeclarations(source)
302
- source = transformGenericDeclarations(source)
310
+ // Collect per-predicate verification status (Type/Generic predicate bodies:
311
+ // verified → native guard, or fell back to a raw function). Surfaced on the
312
+ // transpile result so tools can flag unverifiable predicates.
313
+ const predicates: PredicateVerification[] = []
314
+ source = transformTypeDeclarations(source, predicates)
315
+ source = transformGenericDeclarations(source, predicates)
303
316
  source = transformFunctionPredicateDeclarations(source)
304
317
  source = transformUnionDeclarations(source)
305
318
  source = transformEnumDeclarations(source)
@@ -437,6 +450,7 @@ export function preprocess(
437
450
  polymorphicNames: polyResult.polymorphicNames,
438
451
  extensions: extResult.extensions,
439
452
  letAnnotations,
453
+ predicates,
440
454
  }
441
455
  }
442
456
 
@@ -512,6 +526,7 @@ export function parse(
512
526
  tjsSafeEval: false,
513
527
  tjsNoVar: false,
514
528
  tjsSafeAssign: false,
529
+ tjsStrict: false,
515
530
  } as TjsModes,
516
531
  }
517
532
 
@@ -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,78 @@
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
+ })
55
+
56
+ describe('TjsStrict escalates an unverifiable predicate to a transpile error', () => {
57
+ const bad =
58
+ "Type Bad 'bad' { predicate(xs) { for (const x of xs) {} return true } }"
59
+
60
+ it('warns (does not throw) without the directive', () => {
61
+ const r = tjs(bad)
62
+ expect(r.predicates?.[0]?.verified).toBe(false)
63
+ expect(r.warnings?.length).toBeGreaterThan(0)
64
+ })
65
+
66
+ it('throws under TjsStrict, naming the predicate + reason', () => {
67
+ expect(() => tjs('TjsStrict\n' + bad)).toThrow(
68
+ /TjsStrict.*could not be verified/s
69
+ )
70
+ expect(() => tjs('TjsStrict\n' + bad)).toThrow(/Bad/)
71
+ })
72
+
73
+ it('does NOT throw under TjsStrict when the predicate verifies', () => {
74
+ const r = tjs("TjsStrict\nType Pos 'p' { predicate(x) { return x > 0 } }")
75
+ expect(r.predicates?.[0]?.verified).toBe(true)
76
+ expect(r.warnings).toBeUndefined()
77
+ })
78
+ })