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.
- package/CLAUDE.md +12 -4
- package/bin/docs.js +3 -2
- package/demo/docs.json +2 -2
- package/dist/experiments/ambient/probe.d.ts +52 -0
- package/dist/index.js +115 -171
- 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.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.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 +100 -90
- 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 +102 -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/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/bare-assignments.test.ts +48 -0
- package/src/lang/doc-comment-position.test.ts +32 -0
- package/src/lang/docs.ts +6 -2
- 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/from-ts.ts +4 -1
- package/src/lang/emitters/js.ts +33 -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 +120 -18
- package/src/lang/parser.ts +22 -6
- package/src/lang/predicate-evaluator.test.ts +49 -0
- package/src/lang/predicate-report.test.ts +54 -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/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/from-ts.test.ts
CHANGED
|
@@ -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
|
-
|
|
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 = ''
|
|
@@ -1127,12 +1178,15 @@ function transformTypeofKeyword(source: string): string {
|
|
|
1127
1178
|
}
|
|
1128
1179
|
|
|
1129
1180
|
/**
|
|
1130
|
-
* Transform == and != to
|
|
1181
|
+
* Transform == and != to Eq() and NotEq() calls
|
|
1131
1182
|
*
|
|
1132
1183
|
* In TJS normal mode:
|
|
1133
|
-
* a == b ->
|
|
1134
|
-
*
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
)}],
|
|
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
|
-
|
|
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
|
|
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:
|
|
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/,
|
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
|
|
@@ -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
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
})
|