tjs-lang 0.8.7 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CLAUDE.md +19 -5
  2. package/dist/experiments/ambient/probe.d.ts +52 -0
  3. package/dist/index.js +110 -162
  4. package/dist/index.js.map +4 -4
  5. package/dist/scripts/build-editors.d.ts +19 -0
  6. package/dist/src/css/dimensions.d.ts +23 -0
  7. package/dist/src/css/index.d.ts +85 -0
  8. package/dist/src/css/predicates.d.ts +38 -0
  9. package/dist/src/css/shorthands.d.ts +31 -0
  10. package/dist/src/css/style.d.ts +48 -0
  11. package/dist/src/lang/emitters/js-wasm.d.ts +2 -0
  12. package/dist/src/lang/emitters/js.d.ts +9 -1
  13. package/dist/src/lang/index.d.ts +2 -2
  14. package/dist/src/lang/parser-transforms.d.ts +9 -5
  15. package/dist/src/lang/parser-types.d.ts +7 -0
  16. package/dist/src/lang/parser.d.ts +2 -0
  17. package/dist/src/lang/predicate.d.ts +60 -0
  18. package/dist/src/lang/transpiler.d.ts +3 -2
  19. package/dist/src/lang/types.d.ts +16 -0
  20. package/dist/src/schema/index.d.ts +12 -0
  21. package/dist/tjs-browser-from-ts.js +20 -20
  22. package/dist/tjs-browser-from-ts.js.map +3 -3
  23. package/dist/tjs-browser.js +108 -94
  24. package/dist/tjs-browser.js.map +4 -4
  25. package/dist/tjs-css.js +193 -0
  26. package/dist/tjs-css.js.map +7 -0
  27. package/dist/tjs-eval.js +43 -42
  28. package/dist/tjs-eval.js.map +4 -4
  29. package/dist/tjs-from-ts.js +13 -13
  30. package/dist/tjs-from-ts.js.map +2 -2
  31. package/dist/tjs-lang.js +106 -92
  32. package/dist/tjs-lang.js.map +4 -4
  33. package/dist/tjs-runtime.js +10 -0
  34. package/dist/tjs-runtime.js.map +7 -0
  35. package/dist/tjs-schema.js +6 -0
  36. package/dist/tjs-schema.js.map +7 -0
  37. package/dist/tjs-vm.js +55 -54
  38. package/dist/tjs-vm.js.map +4 -4
  39. package/docs/ambient-contracts.md +261 -0
  40. package/docs/type-system-north-star.md +100 -0
  41. package/editors/ace/ajs-mode.js +214 -233
  42. package/editors/codemirror/ajs-language.js +1570 -233
  43. package/editors/editors-build.test.ts +29 -0
  44. package/editors/monaco/ajs-monarch.js +239 -195
  45. package/llms.txt +4 -0
  46. package/package.json +35 -4
  47. package/src/css/css.test.ts +122 -0
  48. package/src/css/dimensions.test.ts +112 -0
  49. package/src/css/dimensions.ts +146 -0
  50. package/src/css/index.ts +243 -0
  51. package/src/css/perf.bench.test.ts +109 -0
  52. package/src/css/predicates.ts +232 -0
  53. package/src/css/property-aware.test.ts +84 -0
  54. package/src/css/shorthands.test.ts +90 -0
  55. package/src/css/shorthands.ts +113 -0
  56. package/src/css/style.test.ts +125 -0
  57. package/src/css/style.ts +134 -0
  58. package/src/index-tsfree.test.ts +58 -0
  59. package/src/lang/emit-verified-predicate.test.ts +95 -0
  60. package/src/lang/emitters/dts.test.ts +31 -0
  61. package/src/lang/emitters/dts.ts +23 -4
  62. package/src/lang/emitters/js-wasm.ts +8 -4
  63. package/src/lang/emitters/js.ts +58 -1
  64. package/src/lang/from-ts.test.ts +1 -1
  65. package/src/lang/generic-verified-predicate.test.ts +39 -0
  66. package/src/lang/index.ts +14 -5
  67. package/src/lang/parser-transforms.ts +113 -15
  68. package/src/lang/parser-types.ts +7 -0
  69. package/src/lang/parser.ts +18 -3
  70. package/src/lang/predicate-evaluator.test.ts +49 -0
  71. package/src/lang/predicate-report.test.ts +78 -0
  72. package/src/lang/predicate.ts +268 -0
  73. package/src/lang/redos-lint.test.ts +81 -0
  74. package/src/lang/transpiler.ts +13 -0
  75. package/src/lang/type-verified-predicate.test.ts +83 -0
  76. package/src/lang/types.ts +17 -0
  77. package/src/lang/typescript-syntax.test.ts +2 -1
  78. package/src/lang/wasm-fallback-warning.test.ts +50 -0
  79. package/src/lang/wasm-intdiv-lint.test.ts +45 -0
  80. package/src/lang/wasm-ready.test.ts +94 -0
  81. package/src/lang/wasm-simd-ops.test.ts +84 -0
  82. package/src/lang/wasm.ts +61 -2
  83. package/src/schema/index.ts +62 -0
  84. package/src/schema/schema.test.ts +66 -0
  85. package/src/use-cases/bootstrap.test.ts +14 -4
@@ -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
+ })
@@ -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 {
@@ -109,11 +109,30 @@ function functionDeclToTS(
109
109
  exported: boolean,
110
110
  isDefault: boolean
111
111
  ): string {
112
- const params = Object.entries(info.params)
113
- .map(([pName, p]) => {
114
- const optional = !p.required
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 optional ? `${pName}?: ${tsType}` : `${pName}: ${tsType}`
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
- ;(async()=>{
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
  }
@@ -59,7 +59,11 @@ import {
59
59
  transformEqualityToStructural,
60
60
  transformIsOperators,
61
61
  } from '../parser-transforms'
62
- import type { TypeDescriptor, ParameterDescriptor } from '../types'
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,
@@ -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
+ })