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
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
1184
|
+
* Transform == and != to Eq() and NotEq() calls
|
|
1131
1185
|
*
|
|
1132
1186
|
* In TJS normal mode:
|
|
1133
|
-
* a == b ->
|
|
1134
|
-
*
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
)}],
|
|
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
|
|
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:
|
|
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/,
|
package/src/lang/parser-types.ts
CHANGED
|
@@ -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
|
/**
|
package/src/lang/parser.ts
CHANGED
|
@@ -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
|
-
|
|
302
|
-
|
|
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
|
+
})
|