tjs-lang 0.8.6 → 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 (81) hide show
  1. package/CLAUDE.md +12 -4
  2. package/bin/docs.js +3 -2
  3. package/demo/docs.json +2 -2
  4. package/dist/experiments/ambient/probe.d.ts +52 -0
  5. package/dist/index.js +115 -171
  6. package/dist/index.js.map +4 -4
  7. package/dist/scripts/build-editors.d.ts +19 -0
  8. package/dist/src/css/dimensions.d.ts +23 -0
  9. package/dist/src/css/index.d.ts +85 -0
  10. package/dist/src/css/predicates.d.ts +38 -0
  11. package/dist/src/css/shorthands.d.ts +31 -0
  12. package/dist/src/css/style.d.ts +48 -0
  13. package/dist/src/lang/emitters/js.d.ts +9 -1
  14. package/dist/src/lang/index.d.ts +2 -2
  15. package/dist/src/lang/parser-transforms.d.ts +9 -5
  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 +100 -90
  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 +102 -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/editors/ace/ajs-mode.js +214 -233
  41. package/editors/codemirror/ajs-language.js +1570 -233
  42. package/editors/editors-build.test.ts +29 -0
  43. package/editors/monaco/ajs-monarch.js +239 -195
  44. package/llms.txt +4 -0
  45. package/package.json +35 -4
  46. package/src/css/css.test.ts +122 -0
  47. package/src/css/dimensions.test.ts +112 -0
  48. package/src/css/dimensions.ts +146 -0
  49. package/src/css/index.ts +243 -0
  50. package/src/css/perf.bench.test.ts +109 -0
  51. package/src/css/predicates.ts +232 -0
  52. package/src/css/property-aware.test.ts +84 -0
  53. package/src/css/shorthands.test.ts +90 -0
  54. package/src/css/shorthands.ts +113 -0
  55. package/src/css/style.test.ts +125 -0
  56. package/src/css/style.ts +134 -0
  57. package/src/index-tsfree.test.ts +58 -0
  58. package/src/lang/bare-assignments.test.ts +48 -0
  59. package/src/lang/doc-comment-position.test.ts +32 -0
  60. package/src/lang/docs.ts +6 -2
  61. package/src/lang/emit-verified-predicate.test.ts +95 -0
  62. package/src/lang/emitters/dts.test.ts +31 -0
  63. package/src/lang/emitters/dts.ts +23 -4
  64. package/src/lang/emitters/from-ts.ts +4 -1
  65. package/src/lang/emitters/js.ts +33 -1
  66. package/src/lang/from-ts.test.ts +1 -1
  67. package/src/lang/generic-verified-predicate.test.ts +39 -0
  68. package/src/lang/index.ts +14 -5
  69. package/src/lang/parser-transforms.ts +120 -18
  70. package/src/lang/parser.ts +22 -6
  71. package/src/lang/predicate-evaluator.test.ts +49 -0
  72. package/src/lang/predicate-report.test.ts +54 -0
  73. package/src/lang/predicate.ts +268 -0
  74. package/src/lang/redos-lint.test.ts +81 -0
  75. package/src/lang/transpiler.ts +13 -0
  76. package/src/lang/type-verified-predicate.test.ts +83 -0
  77. package/src/lang/types.ts +17 -0
  78. package/src/lang/typescript-syntax.test.ts +2 -1
  79. package/src/schema/index.ts +62 -0
  80. package/src/schema/schema.test.ts +66 -0
  81. package/src/use-cases/bootstrap.test.ts +14 -4
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'bun:test'
2
- import { fromTS } from './index'
2
+ import { fromTS } from './emitters/from-ts'
3
3
 
4
4
  describe('TypeScript to TJS Transpiler', () => {
5
5
  describe('fromTS with emitTJS', () => {
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Generic-Type predicates verify too — they compose with their type-param checks
3
+ * (`T(x)` → `checkT(x)`), which the verifier accepts as composition with another
4
+ * safe predicate (via `knownPredicates`). Safe → fuel-bounded native guard;
5
+ * unverifiable → raw fallback (TJS ⊇ JS).
6
+ */
7
+ import { describe, it, expect } from 'bun:test'
8
+ import { preprocess } from './parser'
9
+
10
+ const src = (s: string) => preprocess(s).source
11
+
12
+ describe('Generic predicate → verified fuel-bounded guard', () => {
13
+ it('verifies a generic predicate that composes a type-param check', () => {
14
+ const out = src(
15
+ `Generic Box<T> {\n description: 'a boxed value'\n predicate(x, T) { return typeof x === 'object' && x !== null && T(x.value) }\n}`
16
+ )
17
+ expect(out).toContain('const Box = Generic(')
18
+ expect(out).toContain('__fuel') // verified as safe
19
+ expect(out).toContain('checkT(') // type-param composition preserved
20
+ })
21
+
22
+ it('verifies a two-type-param generic predicate', () => {
23
+ const out = src(
24
+ `Generic Pair<T, U> {\n description: 'a pair'\n predicate(x, T, U) { return T(x[0]) && U(x[1]) }\n}`
25
+ )
26
+ expect(out).toContain('__fuel')
27
+ expect(out).toContain('checkT(')
28
+ expect(out).toContain('checkU(')
29
+ })
30
+
31
+ it('falls back for an unverifiable generic predicate (loop)', () => {
32
+ const out = src(
33
+ `Generic AllOf<T> {\n description: 'all match'\n predicate(xs, T) { for (const x of xs) { if (!T(x)) return false } return true }\n}`
34
+ )
35
+ expect(out).not.toContain('__fuel') // not certified
36
+ expect(out).toContain('for (const x of xs)') // raw body preserved
37
+ expect(out).toContain('checkT') // type-param rewrite still applied
38
+ })
39
+ })
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 = ''
@@ -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(
@@ -3126,12 +3220,19 @@ ${branches.join('\n')}
3126
3220
  * where the identifier starts with uppercase (to avoid breaking normal assignments)
3127
3221
  */
3128
3222
  export function transformBareAssignments(source: string): string {
3223
+ // A bare `Foo = …` is auto-const'd only on FIRST assignment. If the name is
3224
+ // already declared (let/const/var/function/class) anywhere in the source, the
3225
+ // statement is a REASSIGNMENT — leave it alone, or we'd emit a duplicate/
3226
+ // shadowing `const` (e.g. `let B = null; … B = x` → wrongly `const B = x`).
3227
+ const declared = new Set<string>()
3228
+ const declRe = /\b(?:let|const|var|function|class)\s+([A-Z][a-zA-Z0-9_]*)/g
3229
+ for (let m; (m = declRe.exec(source)); ) declared.add(m[1])
3230
+
3129
3231
  // Match: start of line/statement, uppercase identifier, =, not ==
3130
- // Negative lookbehind for const/let/var to avoid double-declaring
3131
3232
  return source.replace(
3132
- /(?<=^|[;\n{])\s*([A-Z][a-zA-Z0-9_]*)\s*=(?!=)/gm,
3133
- (match, name) => {
3134
- // Check if already has const/let/var before it
3233
+ /(?<=^|[;\n{])(\s*)([A-Z][a-zA-Z0-9_]*)\s*=(?!=)/gm,
3234
+ (match, _ws, name) => {
3235
+ if (declared.has(name)) return match // reassignment of a declared binding
3135
3236
  return match.replace(name, `const ${name}`)
3136
3237
  }
3137
3238
  )
@@ -3535,11 +3636,12 @@ export function validateNoDate(source: string): string {
3535
3636
  {
3536
3637
  pattern: /\bnew\s+Date\b/,
3537
3638
  message:
3538
- '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)',
3539
3640
  },
3540
3641
  {
3541
3642
  pattern: /\bDate\.now\b/,
3542
- 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)',
3543
3645
  },
3544
3646
  {
3545
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,15 +300,24 @@ 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)
306
312
 
307
- // Transform bare assignments to const declarations
308
- // Foo = ... -> const Foo = ...
309
- source = transformBareAssignments(source)
313
+ // Transform bare assignments to const declarations (native-TJS convenience):
314
+ // Foo = ... -> const Foo = ... Gated by TjsSafeAssign — OFF for plain JS
315
+ // (dialect: 'js'), TS-originated, and VM targets, so a JS reassignment like
316
+ // `B = value` (of an already-declared `let B`) is never rewritten. See
317
+ // PRINCIPLES.md (TJS ⊇ JS): plain JS must pass through unchanged.
318
+ if (tjsModes.tjsSafeAssign) {
319
+ source = transformBareAssignments(source)
320
+ }
310
321
 
311
322
  // Phase 3: cross-file wasm-function composition. When a ModuleLoader is
312
323
  // supplied, resolve `import { ... } from '<spec>'` statements at transpile
@@ -432,6 +443,7 @@ export function preprocess(
432
443
  polymorphicNames: polyResult.polymorphicNames,
433
444
  extensions: extResult.extensions,
434
445
  letAnnotations,
446
+ predicates,
435
447
  }
436
448
  }
437
449
 
@@ -737,7 +749,11 @@ export function extractTDoc(
737
749
  // This preserves full markdown content
738
750
  // Find the LAST /*# ... */ block and verify it immediately precedes the function
739
751
  // (only whitespace and line comments allowed between)
740
- const allDocBlocks = [...beforeFunc.matchAll(/\/\*#([\s\S]*?)\*\//g)]
752
+ // Line-start `/*#` only — a `/*#` after code (or in a string) isn't a doc
753
+ // comment. Lookbehind keeps match.index/length on the `/*#…*/` span.
754
+ const allDocBlocks = [
755
+ ...beforeFunc.matchAll(/(?<=^[ \t]*)\/\*#([\s\S]*?)\*\//gm),
756
+ ]
741
757
  if (allDocBlocks.length > 0) {
742
758
  const lastBlock = allDocBlocks[allDocBlocks.length - 1]
743
759
  const afterBlock = beforeFunc.substring(
@@ -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
+ })