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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tjs-lang/css` phase 4 — recursive style-object structure + the `$predicate`
|
|
3
|
+
* JSON-Schema. The thesis end-to-end: one serializable schema, validated two
|
|
4
|
+
* ways — naive (structure only) vs predicate-aware (the full recursive grammar).
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from 'bun:test'
|
|
7
|
+
import {
|
|
8
|
+
compilePredicateSchema,
|
|
9
|
+
validatePredicateSchema,
|
|
10
|
+
} from '../lang/predicate-schema'
|
|
11
|
+
import {
|
|
12
|
+
isStyleObject,
|
|
13
|
+
isStyleValue,
|
|
14
|
+
isCssProperty,
|
|
15
|
+
isSelectorOrAtRule,
|
|
16
|
+
cssStyleSchema,
|
|
17
|
+
cssColorSchema,
|
|
18
|
+
verifyCss,
|
|
19
|
+
} from './index'
|
|
20
|
+
|
|
21
|
+
describe('the combined style source verifies safe', () => {
|
|
22
|
+
it('color + dimension + recursive structure all predicate-safe', () => {
|
|
23
|
+
const r = verifyCss()
|
|
24
|
+
if (!r.safe) console.error(r.diagnostics)
|
|
25
|
+
expect(r.safe).toBe(true)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('key classification', () => {
|
|
30
|
+
it('isCssProperty', () => {
|
|
31
|
+
expect(isCssProperty('color')).toBe(true)
|
|
32
|
+
expect(isCssProperty('-webkit-box-shadow')).toBe(true)
|
|
33
|
+
expect(isCssProperty('--brand')).toBe(true)
|
|
34
|
+
expect(isCssProperty('&:hover')).toBe(false)
|
|
35
|
+
expect(isCssProperty('.card')).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
it('isSelectorOrAtRule', () => {
|
|
38
|
+
expect(isSelectorOrAtRule('&:hover')).toBe(true)
|
|
39
|
+
expect(isSelectorOrAtRule('.card')).toBe(true)
|
|
40
|
+
expect(isSelectorOrAtRule('> a')).toBe(true)
|
|
41
|
+
expect(isSelectorOrAtRule('@media (min-width: 600px)')).toBe(true)
|
|
42
|
+
expect(isSelectorOrAtRule('[data-x]')).toBe(true)
|
|
43
|
+
expect(isSelectorOrAtRule('color')).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('isStyleObject validates the recursive structure', () => {
|
|
48
|
+
it('accepts a nested style object', () => {
|
|
49
|
+
const spec = {
|
|
50
|
+
color: 'red',
|
|
51
|
+
padding: '10px',
|
|
52
|
+
'--gap': '4px',
|
|
53
|
+
'&:hover': { color: '#00f', background: 'var(--surface)' },
|
|
54
|
+
'@media (min-width: 600px)': {
|
|
55
|
+
display: 'grid',
|
|
56
|
+
'.card': { boxShadow: '0 1px 2px rgba(0,0,0,0.2)' },
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
expect(isStyleObject(spec)).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
it('rejects a non-object', () => {
|
|
62
|
+
expect(isStyleObject('red')).toBe(false)
|
|
63
|
+
expect(isStyleObject(null)).toBe(false)
|
|
64
|
+
expect(isStyleObject(42)).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
it('rejects a property whose value is the wrong type', () => {
|
|
67
|
+
expect(isStyleObject({ color: { nested: 'x' } })).toBe(false) // property → not an object
|
|
68
|
+
expect(isStyleObject({ color: '' })).toBe(false) // empty value
|
|
69
|
+
})
|
|
70
|
+
it('rejects a selector whose value is not a nested rule', () => {
|
|
71
|
+
expect(isStyleObject({ '&:hover': 'red' })).toBe(false)
|
|
72
|
+
})
|
|
73
|
+
it('rejects a key that is neither property nor selector', () => {
|
|
74
|
+
expect(isStyleObject({ '': 'red' })).toBe(false)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('isStyleValue (leaf)', () => {
|
|
79
|
+
it('accepts known + permissive values', () => {
|
|
80
|
+
expect(isStyleValue('red')).toBe(true) // color
|
|
81
|
+
expect(isStyleValue('10px')).toBe(true) // dimension
|
|
82
|
+
expect(isStyleValue('1px solid red')).toBe(true) // shorthand → permissive tail
|
|
83
|
+
expect(isStyleValue(0)).toBe(true) // finite number
|
|
84
|
+
})
|
|
85
|
+
it('rejects empty / non-primitive', () => {
|
|
86
|
+
expect(isStyleValue('')).toBe(false)
|
|
87
|
+
expect(isStyleValue({})).toBe(false)
|
|
88
|
+
expect(isStyleValue(Infinity)).toBe(false)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('$predicate schema — progressive enhancement', () => {
|
|
93
|
+
const schema = cssStyleSchema()
|
|
94
|
+
const good = { color: 'red', '&:hover': { color: 'blue' } }
|
|
95
|
+
const structurallyOkButBadGrammar = { ' not a prop ': 'red' } // is an object, but a bad key
|
|
96
|
+
|
|
97
|
+
it('naive validator (structure only) checks just `type: object`', () => {
|
|
98
|
+
const naive = compilePredicateSchema(schema, { ignorePredicates: true })
|
|
99
|
+
expect(naive(good).valid).toBe(true)
|
|
100
|
+
// a plain object passes structure even with an invalid key...
|
|
101
|
+
expect(naive(structurallyOkButBadGrammar).valid).toBe(true)
|
|
102
|
+
// ...but a non-object still fails the structural `type`.
|
|
103
|
+
expect(naive('nope').valid).toBe(false)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('predicate-aware validator runs the full recursive grammar', () => {
|
|
107
|
+
const aware = compilePredicateSchema(schema)
|
|
108
|
+
expect(aware(good).valid).toBe(true)
|
|
109
|
+
// now the bad key is caught by $predicate where the naive one passed it
|
|
110
|
+
expect(aware(structurallyOkButBadGrammar).valid).toBe(false)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('validatePredicateSchema one-shot works for a color node', () => {
|
|
114
|
+
expect(validatePredicateSchema(cssColorSchema(), '#3a3').valid).toBe(true)
|
|
115
|
+
expect(validatePredicateSchema(cssColorSchema(), 'notacolor').valid).toBe(
|
|
116
|
+
false
|
|
117
|
+
)
|
|
118
|
+
// naive sees only `type: string`
|
|
119
|
+
expect(
|
|
120
|
+
validatePredicateSchema(cssColorSchema(), 'notacolor', {
|
|
121
|
+
ignorePredicates: true,
|
|
122
|
+
}).valid
|
|
123
|
+
).toBe(true)
|
|
124
|
+
})
|
|
125
|
+
})
|
package/src/css/style.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS style-object structure (phase 4) — the recursive, open shape TS structural
|
|
3
|
+
* types and JSON Schema can't express, and the `$predicate` schemas that carry
|
|
4
|
+
* it. This is the thesis deliverable: a JSON-Schema node whose *structural* part
|
|
5
|
+
* (`type: 'object'`) a naive validator checks, and whose `$predicate` a
|
|
6
|
+
* predicate-aware validator runs to validate the whole recursive CSS structure —
|
|
7
|
+
* progressive enhancement, one serializable artifact.
|
|
8
|
+
*
|
|
9
|
+
* The combined source concatenates the color + dimension leaf clusters and adds
|
|
10
|
+
* the structure predicates, so `isStyleValue`/`isStyleObject` compose the leaves
|
|
11
|
+
* directly. `isStyleObject` is LAST — the entry the `$predicate` schema runs.
|
|
12
|
+
*
|
|
13
|
+
* Value precision is intentionally two-tier (as in the PoC): the *structure* is
|
|
14
|
+
* validated strictly (keys must be CSS properties or selectors/at-rules; a
|
|
15
|
+
* property maps to a value, a selector to a nested rule), while a generic leaf
|
|
16
|
+
* value falls back to "non-empty string or finite number" so a valid-but-
|
|
17
|
+
* unmodelled value (a shorthand, a not-yet-covered property) is never rejected.
|
|
18
|
+
* Per-property precision comes from the specific value schemas (`cssColorSchema`
|
|
19
|
+
* etc.) and, later, property-aware validation (phase 3 shorthands).
|
|
20
|
+
*/
|
|
21
|
+
import type { PredicateSchema } from '../lang/predicate-schema'
|
|
22
|
+
import { CSS_COLOR_SOURCE } from './predicates'
|
|
23
|
+
import { CSS_DIMENSION_SOURCE } from './dimensions'
|
|
24
|
+
import { CSS_SHORTHAND_FRAGMENT } from './shorthands'
|
|
25
|
+
|
|
26
|
+
/** The recursive structure predicates, layered on the leaf clusters. */
|
|
27
|
+
const CSS_STRUCTURE_FRAGMENT = `
|
|
28
|
+
// Properties whose value grammar is CLOSED — a value that fails the leaf
|
|
29
|
+
// predicate is genuinely invalid, so it's safe to enforce (normalized:
|
|
30
|
+
// lower-cased, dashes stripped, so 'background-color' === 'backgroundColor').
|
|
31
|
+
var CSS_COLOR_PROPS = ['color','backgroundcolor','bordercolor','bordertopcolor','borderrightcolor','borderbottomcolor','borderleftcolor','outlinecolor','caretcolor','columnrulecolor','textdecorationcolor','accentcolor','fill','stroke','floodcolor','stopcolor','lightingcolor']
|
|
32
|
+
|
|
33
|
+
function isCssProperty(k) {
|
|
34
|
+
// custom property (--foo) OR a standard/vendor-prefixed property (-webkit-…).
|
|
35
|
+
return typeof k === 'string' && /^(--[a-z0-9-]+|-?[a-z][a-z0-9-]*)$/i.test(k)
|
|
36
|
+
}
|
|
37
|
+
function isSelectorOrAtRule(k) {
|
|
38
|
+
if (typeof k !== 'string' || k.length === 0) { return false }
|
|
39
|
+
if (k.startsWith('@')) { return true }
|
|
40
|
+
return /[ :>~+.#&*\\[\\]]/.test(k)
|
|
41
|
+
}
|
|
42
|
+
function isStyleValue(val) {
|
|
43
|
+
if (typeof val === 'number') { return isFinite(val) }
|
|
44
|
+
if (typeof val !== 'string' || val.length === 0) { return false }
|
|
45
|
+
// Precise recognition for known leaves; permissive tail keeps the whole
|
|
46
|
+
// structure total (a shorthand / unmodelled value is a non-empty string).
|
|
47
|
+
return isColorValue(val) || isDimension(val) || isGlobalKeyword(val)
|
|
48
|
+
|| isCssVar(val) || isCssCalc(val) || val.length > 0
|
|
49
|
+
}
|
|
50
|
+
function cssPropNorm(prop) {
|
|
51
|
+
return typeof prop === 'string' ? prop.toLowerCase().replace(/-/g, '') : ''
|
|
52
|
+
}
|
|
53
|
+
// Property-AWARE value check: tighten only the closed value grammars (color,
|
|
54
|
+
// animation, transition) — a wrong value there is a real error; everything else
|
|
55
|
+
// stays permissive (keyword-heavy props like width/display accept idents we don't
|
|
56
|
+
// enumerate, so enforcing would false-reject valid CSS). Universal escapes
|
|
57
|
+
// (global keyword / var() / calc()) are valid on any property.
|
|
58
|
+
function isStyleValueFor(prop, val) {
|
|
59
|
+
if (isGlobalKeyword(val) || isCssVar(val) || isCssCalc(val)) { return true }
|
|
60
|
+
var p = cssPropNorm(prop)
|
|
61
|
+
if (CSS_COLOR_PROPS.includes(p)) { return isColorValue(val) }
|
|
62
|
+
if (p === 'animation') { return isAnimation(val) }
|
|
63
|
+
if (p === 'transition') { return isTransition(val) }
|
|
64
|
+
return isStyleValue(val)
|
|
65
|
+
}
|
|
66
|
+
function isStyleEntry(pair) {
|
|
67
|
+
var k = pair[0]
|
|
68
|
+
var val = pair[1]
|
|
69
|
+
if (isSelectorOrAtRule(k)) { return isStyleObject(val) }
|
|
70
|
+
if (isCssProperty(k)) { return isStyleValueFor(k, val) }
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
function isStyleObject(o) {
|
|
74
|
+
if (typeof o !== 'object' || o === null) { return false }
|
|
75
|
+
return Object.entries(o).every(isStyleEntry)
|
|
76
|
+
}
|
|
77
|
+
`
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The full CSS style predicate cluster: color + dimension + shorthand leaves +
|
|
81
|
+
* recursive, property-aware structure. Entry = `isStyleObject` (last function).
|
|
82
|
+
* Serializable — this is exactly what a `$predicate` schema carries.
|
|
83
|
+
*/
|
|
84
|
+
export const CSS_STYLE_SOURCE =
|
|
85
|
+
CSS_COLOR_SOURCE +
|
|
86
|
+
'\n' +
|
|
87
|
+
CSS_DIMENSION_SOURCE +
|
|
88
|
+
'\n' +
|
|
89
|
+
CSS_SHORTHAND_FRAGMENT +
|
|
90
|
+
'\n' +
|
|
91
|
+
CSS_STRUCTURE_FRAGMENT
|
|
92
|
+
|
|
93
|
+
/** Entry predicates exported by the style cluster (for compile/verify). */
|
|
94
|
+
export const CSS_STYLE_ENTRIES = [
|
|
95
|
+
'isCssProperty',
|
|
96
|
+
'isSelectorOrAtRule',
|
|
97
|
+
'isStyleValue',
|
|
98
|
+
'isStyleValueFor',
|
|
99
|
+
'isStyleObject',
|
|
100
|
+
] as const
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A `$predicate` JSON-Schema for a CSS style object (à la a tosijs styleSpec):
|
|
104
|
+
* naive validators check only `type: 'object'`; predicate-aware validators run
|
|
105
|
+
* `isStyleObject`, validating the whole recursive structure (nested selectors,
|
|
106
|
+
* property/value shapes). Progressive enhancement from one serializable schema.
|
|
107
|
+
*/
|
|
108
|
+
export function cssStyleSchema(): PredicateSchema {
|
|
109
|
+
return {
|
|
110
|
+
type: 'object',
|
|
111
|
+
title: 'CSSStyleObject',
|
|
112
|
+
description:
|
|
113
|
+
'An open, recursive CSS style object (properties + nested selectors/at-rules).',
|
|
114
|
+
$predicate: CSS_STYLE_SOURCE,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A `$predicate` schema for a single CSS **color** value (entry `isColorValue`,
|
|
120
|
+
* so a trailing `!important` is tolerated). Naive validators check `type:
|
|
121
|
+
* 'string'`; aware ones validate the color grammar.
|
|
122
|
+
*/
|
|
123
|
+
export function cssColorSchema(): PredicateSchema {
|
|
124
|
+
return { type: 'string', title: 'CSSColor', $predicate: CSS_COLOR_SOURCE }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A `$predicate` schema for a CSS **dimension** value (length/%/angle/time/
|
|
129
|
+
* number; entry `isDimension`). No structural `type` — a dimension may be a
|
|
130
|
+
* string (`10px`) or a number — so aware validators do all the work.
|
|
131
|
+
*/
|
|
132
|
+
export function cssDimensionSchema(): PredicateSchema {
|
|
133
|
+
return { title: 'CSSDimension', $predicate: CSS_DIMENSION_SOURCE }
|
|
134
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reproduction + guard for the "`import 'tjs-lang'` crashes without the TypeScript
|
|
3
|
+
* compiler" bug (snowfox production feedback).
|
|
4
|
+
*
|
|
5
|
+
* The main entry (`src/index.ts`) does `export * from './lang'`, which reaches
|
|
6
|
+
* `src/lang/index.ts`. If that statically re-exports `fromTS`, the whole
|
|
7
|
+
* TypeScript compiler (~4-10MB) is dragged into the module graph. `typescript`
|
|
8
|
+
* is only a devDependency (externalized in the build), so a Node consumer who
|
|
9
|
+
* runs `import 'tjs-lang'` without it installed gets
|
|
10
|
+
* `Cannot find package 'typescript' imported from dist/index.js` — and it also
|
|
11
|
+
* pulls TS at import time, which breaks constrained runtimes (Cloud Run).
|
|
12
|
+
*
|
|
13
|
+
* The fix: the TS compiler must be reachable ONLY via the explicit
|
|
14
|
+
* `tjs-lang/lang/from-ts` subpath, never through the main entry. This test
|
|
15
|
+
* bundles the main entry with esbuild (same externals as `scripts/build.ts`'s
|
|
16
|
+
* `index` target) and asserts `typescript` never appears as an import specifier.
|
|
17
|
+
*/
|
|
18
|
+
import { describe, it, expect } from 'bun:test'
|
|
19
|
+
import { buildSync } from 'esbuild'
|
|
20
|
+
|
|
21
|
+
const HERE = import.meta.dir
|
|
22
|
+
|
|
23
|
+
/** All static + dynamic import specifiers in a bundle (bare, relative, or URL). */
|
|
24
|
+
function importSpecifiers(src: string): string[] {
|
|
25
|
+
const found: string[] = []
|
|
26
|
+
for (const m of src.matchAll(
|
|
27
|
+
/(?:^|[;\n}])import\s*(?:[^"';]*?from)?\s*["']([^"']+)["']/g
|
|
28
|
+
))
|
|
29
|
+
found.push(m[1])
|
|
30
|
+
for (const m of src.matchAll(/import\(\s*["']([^"']+)["']\s*\)/g))
|
|
31
|
+
found.push(m[1])
|
|
32
|
+
return [...new Set(found)]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("import 'tjs-lang' must not pull in the TypeScript compiler", () => {
|
|
36
|
+
it('main entry bundle has no static `typescript` import', () => {
|
|
37
|
+
// Mirror scripts/build.ts's `index` target externals. `typescript` is
|
|
38
|
+
// externalized there, so if the main entry reaches it, esbuild leaves a
|
|
39
|
+
// bare `import "typescript"` in the output — which is exactly the crash.
|
|
40
|
+
const out = buildSync({
|
|
41
|
+
entryPoints: [`${HERE}/index.ts`],
|
|
42
|
+
bundle: true,
|
|
43
|
+
format: 'esm',
|
|
44
|
+
platform: 'neutral',
|
|
45
|
+
write: false,
|
|
46
|
+
external: [
|
|
47
|
+
'acorn',
|
|
48
|
+
'tosijs-schema',
|
|
49
|
+
'typescript',
|
|
50
|
+
'node:readline',
|
|
51
|
+
'node:fs',
|
|
52
|
+
'node:path',
|
|
53
|
+
],
|
|
54
|
+
}).outputFiles[0].text
|
|
55
|
+
|
|
56
|
+
expect(importSpecifiers(out)).not.toContain('typescript')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `emitVerifiedPredicate` — the transpile-time source emitter
|
|
3
|
+
* that turns a verified-safe predicate cluster into a self-contained,
|
|
4
|
+
* fuel-bounded guard expression (the foundation of wiring predicates into
|
|
5
|
+
* `Type`/`FunctionPredicate`). Distinct from `compilePredicate`, which evals to
|
|
6
|
+
* live closures; here we assert the emitted *string* is self-contained and
|
|
7
|
+
* behaves correctly when evaluated.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect } from 'bun:test'
|
|
10
|
+
import { emitVerifiedPredicate } from './predicate'
|
|
11
|
+
|
|
12
|
+
/** Evaluate an emitted guard expression to a callable, in an empty scope. */
|
|
13
|
+
function guardFrom(code: string): (...a: any[]) => boolean {
|
|
14
|
+
// Indirect eval: the emitted code is a self-contained IIFE expression.
|
|
15
|
+
return new Function(`return (${code})`)()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('emitVerifiedPredicate', () => {
|
|
19
|
+
it('emits a self-contained guard for a safe predicate', () => {
|
|
20
|
+
const r = emitVerifiedPredicate(
|
|
21
|
+
'function isPos(x) { return x > 0 }',
|
|
22
|
+
'isPos'
|
|
23
|
+
)
|
|
24
|
+
expect(r.safe).toBe(true)
|
|
25
|
+
expect(r.diagnostics).toEqual([])
|
|
26
|
+
expect(typeof r.code).toBe('string')
|
|
27
|
+
|
|
28
|
+
const guard = guardFrom(r.code!)
|
|
29
|
+
expect(guard(5)).toBe(true)
|
|
30
|
+
expect(guard(-1)).toBe(false)
|
|
31
|
+
expect(guard(0)).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('coerces the guard result to a real boolean', () => {
|
|
35
|
+
// Truthy/falsy body values must come back as strict booleans.
|
|
36
|
+
const r = emitVerifiedPredicate(
|
|
37
|
+
'function nonEmpty(s) { return s && s.length }',
|
|
38
|
+
'nonEmpty'
|
|
39
|
+
)
|
|
40
|
+
const guard = guardFrom(r.code!)
|
|
41
|
+
expect(guard('hi')).toBe(true)
|
|
42
|
+
expect(guard('')).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('supports composition + array methods (fuel-bounded iteration)', () => {
|
|
46
|
+
const src =
|
|
47
|
+
'function isWord(s) { return typeof s === "string" && s.length > 0 }\n' +
|
|
48
|
+
'function allWords(xs) { return Array.isArray(xs) && xs.every(isWord) }'
|
|
49
|
+
const r = emitVerifiedPredicate(src, 'allWords')
|
|
50
|
+
expect(r.safe).toBe(true)
|
|
51
|
+
const guard = guardFrom(r.code!)
|
|
52
|
+
expect(guard(['a', 'b'])).toBe(true)
|
|
53
|
+
expect(guard(['a', ''])).toBe(false)
|
|
54
|
+
expect(guard('nope')).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('rejects an unsafe predicate (loop) with diagnostics, no code', () => {
|
|
58
|
+
const r = emitVerifiedPredicate(
|
|
59
|
+
'function f(xs) { for (const x of xs) { if (x < 0) return false } return true }',
|
|
60
|
+
'f'
|
|
61
|
+
)
|
|
62
|
+
expect(r.safe).toBe(false)
|
|
63
|
+
expect(r.code).toBeUndefined()
|
|
64
|
+
expect(r.diagnostics.length).toBeGreaterThan(0)
|
|
65
|
+
expect(r.diagnostics[0].message).toMatch(/loop/i)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('rejects an impure predicate (effectful call)', () => {
|
|
69
|
+
const r = emitVerifiedPredicate('function f(x) { return fetch(x) }', 'f')
|
|
70
|
+
expect(r.safe).toBe(false)
|
|
71
|
+
expect(r.code).toBeUndefined()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('reports a missing entry name as unsafe', () => {
|
|
75
|
+
const r = emitVerifiedPredicate(
|
|
76
|
+
'function isPos(x) { return x > 0 }',
|
|
77
|
+
'nope'
|
|
78
|
+
)
|
|
79
|
+
expect(r.safe).toBe(false)
|
|
80
|
+
expect(r.diagnostics[0].message).toMatch(/not found/i)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('returns false (does not throw) on a runaway input — DoS-safe guard', () => {
|
|
84
|
+
// Unbounded recursion: with a tiny fuel budget it must exhaust and the guard
|
|
85
|
+
// answers `false` rather than hanging or throwing to the caller.
|
|
86
|
+
const r = emitVerifiedPredicate(
|
|
87
|
+
'function deep(n) { return deep(n + 1) }',
|
|
88
|
+
'deep',
|
|
89
|
+
{ fuel: 100 }
|
|
90
|
+
)
|
|
91
|
+
expect(r.safe).toBe(true)
|
|
92
|
+
const guard = guardFrom(r.code!)
|
|
93
|
+
expect(guard(0)).toBe(false)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -166,6 +166,37 @@ export function greet(name = 'world'): '' {
|
|
|
166
166
|
expect(dts).toContain('name?: string')
|
|
167
167
|
})
|
|
168
168
|
|
|
169
|
+
// #11: a bare (untyped) param is a required *position* in a signature, not an
|
|
170
|
+
// optional *contract* — runtime `required:false` (wild-west JS) must not leak
|
|
171
|
+
// into dts optionality, or the auto .d.ts is INVALID TS (ts1016: an optional
|
|
172
|
+
// param can't precede a required one), not merely loose.
|
|
173
|
+
it('emits bare params as required positions (valid TS, not a?: before b)', () => {
|
|
174
|
+
const dts = generateDTS(
|
|
175
|
+
transpileToJS('export function f(a, b: 0) { return a }', {
|
|
176
|
+
runTests: false,
|
|
177
|
+
}),
|
|
178
|
+
'export function f(a, b: 0) { return a }'
|
|
179
|
+
)
|
|
180
|
+
expect(dts).toContain('f(a: any, b: number)')
|
|
181
|
+
expect(dts).not.toContain('a?:') // was `f(a?: any, b: number)` — invalid TS
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('keeps deliberate optionals (default / ?) optional', () => {
|
|
185
|
+
const src = "export function g(a = 1, b?: '') { return a }"
|
|
186
|
+
const dts = generateDTS(transpileToJS(src, { runTests: false }), src)
|
|
187
|
+
expect(dts).toContain('a?: number')
|
|
188
|
+
expect(dts).toContain('b?: string')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('demotes an optional-before-required to required (no ts1016)', () => {
|
|
192
|
+
// `h(a = 1, b)` — a is a default (optional) but a required position (bare b)
|
|
193
|
+
// follows it, so a must be emitted required to keep the signature valid.
|
|
194
|
+
const src = 'export function h(a = 1, b) { return a }'
|
|
195
|
+
const dts = generateDTS(transpileToJS(src, { runTests: false }), src)
|
|
196
|
+
expect(dts).toContain('h(a: number, b: any)')
|
|
197
|
+
expect(dts).not.toContain('a?:')
|
|
198
|
+
})
|
|
199
|
+
|
|
169
200
|
it('should handle multiple parameters and return types', () => {
|
|
170
201
|
const source = `
|
|
171
202
|
export function add(a: 0, b: 0): 0 {
|
package/src/lang/emitters/dts.ts
CHANGED
|
@@ -109,11 +109,30 @@ function functionDeclToTS(
|
|
|
109
109
|
exported: boolean,
|
|
110
110
|
isDefault: boolean
|
|
111
111
|
): string {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
// DTS optionality is NOT the runtime `required` flag. Runtime `required` is a
|
|
113
|
+
// *contract* check: a bare (untyped) JS param is wild-west — `required:false`
|
|
114
|
+
// so the runtime doesn't reject omitting it — but it's still a required
|
|
115
|
+
// *position* in the signature, not an optional *contract*. A param is emitted
|
|
116
|
+
// optional only if (a) it's a DELIBERATE optional — a default value or `?`
|
|
117
|
+
// marker, both of which set `default` (a bare param has no `default` at all) —
|
|
118
|
+
// and (b) no required param follows it (TS forbids an optional param before a
|
|
119
|
+
// required one; ts1016). Deriving optionality from `!required` leaked wild-west
|
|
120
|
+
// omittability into the dts and produced INVALID TS (#11).
|
|
121
|
+
const entries = Object.entries(info.params)
|
|
122
|
+
const optionalFlags: boolean[] = new Array(entries.length)
|
|
123
|
+
let requiredFollows = false
|
|
124
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
125
|
+
const p = entries[i][1] as (typeof entries)[number][1] & {
|
|
126
|
+
default?: unknown
|
|
127
|
+
}
|
|
128
|
+
const deliberateOptional = !p.required && p.default !== undefined
|
|
129
|
+
optionalFlags[i] = deliberateOptional && !requiredFollows
|
|
130
|
+
if (!optionalFlags[i]) requiredFollows = true
|
|
131
|
+
}
|
|
132
|
+
const params = entries
|
|
133
|
+
.map(([pName, p], i) => {
|
|
115
134
|
const tsType = typeDescriptorToTS(p.type)
|
|
116
|
-
return
|
|
135
|
+
return optionalFlags[i] ? `${pName}?: ${tsType}` : `${pName}: ${tsType}`
|
|
117
136
|
})
|
|
118
137
|
.join(', ')
|
|
119
138
|
|
|
@@ -19,6 +19,8 @@ export function generateWasmBootstrap(blocks: WasmBlock[]): {
|
|
|
19
19
|
error?: string
|
|
20
20
|
byteLength?: number
|
|
21
21
|
}[]
|
|
22
|
+
/** Compile-time lints (e.g. i32/i32 integer-division) across all blocks. */
|
|
23
|
+
warnings: string[]
|
|
22
24
|
} {
|
|
23
25
|
const compiled = compileBlocksToModule(blocks)
|
|
24
26
|
|
|
@@ -40,7 +42,7 @@ export function generateWasmBootstrap(blocks: WasmBlock[]): {
|
|
|
40
42
|
})
|
|
41
43
|
|
|
42
44
|
if (compiled.exports.length === 0) {
|
|
43
|
-
return { code: '', results }
|
|
45
|
+
return { code: '', results, warnings: compiled.warnings }
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
// WAT comment block — one section per included function
|
|
@@ -72,7 +74,9 @@ export function generateWasmBootstrap(blocks: WasmBlock[]): {
|
|
|
72
74
|
const anyNeedsMemory = compiled.needsMemory
|
|
73
75
|
|
|
74
76
|
const code = `${watComments}
|
|
75
|
-
;(
|
|
77
|
+
;(globalThis.__tjs_wasm_pending??=[]);
|
|
78
|
+
globalThis.__tjs_wasm_ready??=(()=>Promise.all(globalThis.__tjs_wasm_pending));
|
|
79
|
+
globalThis.__tjs_wasm_pending.push((async()=>{
|
|
76
80
|
const __wasmExports=[${exportData}];
|
|
77
81
|
const __wasmModuleB64=${JSON.stringify(moduleBase64)};
|
|
78
82
|
const __b64ToBytes=s=>{const b=atob(s),a=new Uint8Array(b.length);for(let i=0;i<b.length;i++)a[i]=b.charCodeAt(i);return a};
|
|
@@ -105,11 +109,11 @@ for(const{id,n,c,m}of __wasmExports){
|
|
|
105
109
|
if(a.buffer===__wasmMem.buffer) continue;
|
|
106
110
|
const ab=new Uint8Array(a.buffer,a.byteOffset,a.byteLength);off=(off+15)&~15;ab.set(mv.slice(off,off+ab.length));off+=ab.length}}
|
|
107
111
|
return r};
|
|
108
|
-
}})();
|
|
112
|
+
}})().catch(()=>{}));
|
|
109
113
|
`.trim()
|
|
110
114
|
|
|
111
115
|
// Strip the temporary _exportName field before returning to caller.
|
|
112
116
|
const publicResults = results.map(({ _exportName: _, ...rest }) => rest)
|
|
113
117
|
|
|
114
|
-
return { code, results: publicResults }
|
|
118
|
+
return { code, results: publicResults, warnings: compiled.warnings }
|
|
115
119
|
}
|
package/src/lang/emitters/js.ts
CHANGED
|
@@ -59,7 +59,11 @@ import {
|
|
|
59
59
|
transformEqualityToStructural,
|
|
60
60
|
transformIsOperators,
|
|
61
61
|
} from '../parser-transforms'
|
|
62
|
-
import type {
|
|
62
|
+
import type {
|
|
63
|
+
TypeDescriptor,
|
|
64
|
+
ParameterDescriptor,
|
|
65
|
+
PredicateVerification,
|
|
66
|
+
} from '../types'
|
|
63
67
|
import { inferTypeFromValue, parseParameter } from '../inference'
|
|
64
68
|
import { extractTests } from '../tests'
|
|
65
69
|
import {
|
|
@@ -137,6 +141,14 @@ export interface TJSTranspileResult {
|
|
|
137
141
|
metadata: Record<string, TJSTypeInfo>
|
|
138
142
|
/** Any warnings during transpilation */
|
|
139
143
|
warnings?: string[]
|
|
144
|
+
/**
|
|
145
|
+
* Per-`Type`/`Generic` predicate verification status: `verified` → compiled to
|
|
146
|
+
* a fuel-bounded, DoS-safe native guard; `!verified` → fell back to a plain
|
|
147
|
+
* function (valid, but not fuel-bounded / not safe on untrusted data — its
|
|
148
|
+
* `reason` is the verifier diagnostic). Unverified entries are also mirrored
|
|
149
|
+
* into `warnings`. Lets tools flag unverifiable predicates.
|
|
150
|
+
*/
|
|
151
|
+
predicates?: PredicateVerification[]
|
|
140
152
|
/** Generated test runner code (if tests were present) - DEPRECATED, tests now run at transpile time */
|
|
141
153
|
testRunner?: string
|
|
142
154
|
/** Number of tests extracted */
|
|
@@ -668,6 +680,32 @@ export function transpileToJS(
|
|
|
668
680
|
filename,
|
|
669
681
|
})
|
|
670
682
|
|
|
683
|
+
// Mirror unverifiable Type/Generic predicates into `warnings` (they still
|
|
684
|
+
// compile — as plain functions — but aren't fuel-bounded / safe on untrusted
|
|
685
|
+
// data). The full per-predicate status is returned as `predicates`. Under the
|
|
686
|
+
// explicit `TjsStrict` opt-in these escalate to a transpile error (the subset
|
|
687
|
+
// invariant: warn by default, error only when the author asked for strict).
|
|
688
|
+
const unverified = preprocessed.predicates.filter((p) => !p.verified)
|
|
689
|
+
const predicateMsg = (p: PredicateVerification) =>
|
|
690
|
+
`${p.kind} '${
|
|
691
|
+
p.name
|
|
692
|
+
}': predicate is not verifiable, compiled as a plain function (not fuel-bounded, not safe on untrusted data)${
|
|
693
|
+
p.reason ? ` — ${p.reason.replace(/\s+/g, ' ').trim()}` : ''
|
|
694
|
+
}`
|
|
695
|
+
if (unverified.length && preprocessed.tjsModes.tjsStrict) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`TjsStrict: ${
|
|
698
|
+
unverified.length
|
|
699
|
+
} predicate(s) could not be verified:\n${unverified
|
|
700
|
+
.map((p) => ` ${predicateMsg(p)}`)
|
|
701
|
+
.join('\n')}`
|
|
702
|
+
)
|
|
703
|
+
}
|
|
704
|
+
for (const p of unverified) warnings.push(predicateMsg(p))
|
|
705
|
+
const predicateReport = preprocessed.predicates.length
|
|
706
|
+
? preprocessed.predicates
|
|
707
|
+
: undefined
|
|
708
|
+
|
|
671
709
|
// Apply the same source-level equality transforms to extracted test/mock
|
|
672
710
|
// bodies so they observe the module's TJS semantics (e.g. structural ==).
|
|
673
711
|
// Test bodies are extracted as raw text before parse(), so they would
|
|
@@ -1158,6 +1196,7 @@ export function transpileToJS(
|
|
|
1158
1196
|
metadata: allTypes,
|
|
1159
1197
|
testResults,
|
|
1160
1198
|
testCount: testResults?.length,
|
|
1199
|
+
predicates: predicateReport,
|
|
1161
1200
|
}
|
|
1162
1201
|
}
|
|
1163
1202
|
|
|
@@ -1171,6 +1210,23 @@ export function transpileToJS(
|
|
|
1171
1210
|
code = wasmBootstrap.code + '\n' + code
|
|
1172
1211
|
}
|
|
1173
1212
|
wasmCompiled = wasmBootstrap.results
|
|
1213
|
+
// Surface WASM compile failures as warnings (they were only in
|
|
1214
|
+
// `wasmCompiled` before, so a `wasm{}` block that can't compile fell back to
|
|
1215
|
+
// its `fallback{}` SILENTLY — the worst failure mode for a perf feature).
|
|
1216
|
+
// The full status stays on `wasmCompiled`; this just makes it visible.
|
|
1217
|
+
for (const w of wasmCompiled) {
|
|
1218
|
+
if (!w.success) {
|
|
1219
|
+
warnings.push(
|
|
1220
|
+
`wasm{} block '${
|
|
1221
|
+
w.id
|
|
1222
|
+
}' did not compile — running the fallback{} (JS)${
|
|
1223
|
+
w.error ? `: ${w.error}` : ''
|
|
1224
|
+
}`
|
|
1225
|
+
)
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
// Compile-time wasm lints (e.g. i32/i32 integer division — UI-#4).
|
|
1229
|
+
for (const w of wasmBootstrap.warnings) warnings.push(`wasm{}: ${w}`)
|
|
1174
1230
|
}
|
|
1175
1231
|
|
|
1176
1232
|
return {
|
|
@@ -1178,6 +1234,7 @@ export function transpileToJS(
|
|
|
1178
1234
|
types: allTypes,
|
|
1179
1235
|
metadata: allTypes, // alias for runtime compatibility
|
|
1180
1236
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
1237
|
+
predicates: predicateReport,
|
|
1181
1238
|
testRunner: tests.length > 0 ? testRunner : undefined,
|
|
1182
1239
|
testCount: tests.length > 0 ? tests.length : undefined,
|
|
1183
1240
|
testResults,
|
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
|
+
})
|