tjs-lang 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CLAUDE.md +99 -33
  2. package/bin/docs.js +4 -1
  3. package/demo/docs.json +104 -22
  4. package/demo/src/examples.test.ts +1 -0
  5. package/demo/src/imports.test.ts +16 -4
  6. package/demo/src/imports.ts +60 -15
  7. package/demo/src/playground-shared.ts +9 -8
  8. package/demo/src/tfs-worker.js +205 -147
  9. package/demo/src/tjs-playground.ts +34 -10
  10. package/demo/src/ts-examples.ts +8 -8
  11. package/demo/src/ts-playground.ts +24 -8
  12. package/dist/index.js +118 -101
  13. package/dist/index.js.map +4 -4
  14. package/dist/src/lang/bool-coercion.d.ts +50 -0
  15. package/dist/src/lang/docs.d.ts +31 -6
  16. package/dist/src/lang/linter.d.ts +8 -0
  17. package/dist/src/lang/parser-transforms.d.ts +18 -0
  18. package/dist/src/lang/parser-types.d.ts +2 -0
  19. package/dist/src/lang/parser.d.ts +3 -0
  20. package/dist/src/lang/runtime.d.ts +34 -0
  21. package/dist/src/lang/types.d.ts +9 -1
  22. package/dist/src/rbac/index.d.ts +1 -1
  23. package/dist/src/vm/runtime.d.ts +1 -1
  24. package/dist/tjs-eval.js +38 -36
  25. package/dist/tjs-eval.js.map +4 -4
  26. package/dist/tjs-from-ts.js +20 -20
  27. package/dist/tjs-from-ts.js.map +3 -3
  28. package/dist/tjs-lang.js +85 -83
  29. package/dist/tjs-lang.js.map +4 -4
  30. package/dist/tjs-vm.js +47 -45
  31. package/dist/tjs-vm.js.map +4 -4
  32. package/llms.txt +79 -0
  33. package/package.json +9 -4
  34. package/src/cli/commands/convert.test.ts +16 -21
  35. package/src/lang/bool-coercion.test.ts +203 -0
  36. package/src/lang/bool-coercion.ts +314 -0
  37. package/src/lang/codegen.test.ts +137 -0
  38. package/src/lang/docs.test.ts +476 -1
  39. package/src/lang/docs.ts +471 -37
  40. package/src/lang/emitters/ast.ts +11 -12
  41. package/src/lang/emitters/dts.test.ts +41 -0
  42. package/src/lang/emitters/dts.ts +9 -0
  43. package/src/lang/emitters/js-tests.ts +9 -4
  44. package/src/lang/emitters/js-wasm.ts +57 -65
  45. package/src/lang/emitters/js.ts +198 -3
  46. package/src/lang/features.test.ts +4 -3
  47. package/src/lang/index.ts +9 -0
  48. package/src/lang/inference.ts +54 -0
  49. package/src/lang/linter.test.ts +104 -1
  50. package/src/lang/linter.ts +124 -1
  51. package/src/lang/module-loader.test.ts +318 -0
  52. package/src/lang/module-loader.ts +419 -0
  53. package/src/lang/parser-params.ts +31 -0
  54. package/src/lang/parser-transforms.ts +640 -0
  55. package/src/lang/parser-types.ts +35 -0
  56. package/src/lang/parser.test.ts +73 -1
  57. package/src/lang/parser.ts +77 -3
  58. package/src/lang/runtime.ts +98 -0
  59. package/src/lang/types.ts +6 -0
  60. package/src/lang/wasm.test.ts +1293 -2
  61. package/src/lang/wasm.ts +470 -87
  62. package/src/linalg/index.tjs +119 -0
  63. package/src/linalg/linalg.test.ts +294 -0
  64. package/src/linalg/vector-search.bench.test.ts +395 -0
  65. package/src/rbac/index.ts +2 -2
  66. package/src/rbac/rules.tjs.d.ts +9 -0
  67. package/src/vm/atoms/batteries.ts +2 -2
  68. package/src/vm/runtime.ts +10 -3
  69. package/dist/src/rbac/rules.d.ts +0 -184
  70. package/src/rbac/rules.js +0 -338
@@ -0,0 +1,119 @@
1
+ /*#
2
+ # tjs-lang/linalg
3
+
4
+ Linear algebra primitives implemented as WebAssembly SIMD kernels.
5
+
6
+ This is the v1 minimum surface — enough to support the canonical
7
+ vector-search demo (cosine similarity via `dot` and `norm_sq`). Future
8
+ versions will add the full vector / matrix / 3D groups documented in
9
+ `wasm-library-plan.md` §5.
10
+
11
+ **Memory model.** Inputs are `Float32Array`s allocated via `wasmBuffer()`
12
+ when zero-copy is desired (recommended for hot loops). Regular
13
+ `Float32Array`s also work — the wasm wrapper copies in/out per call.
14
+ Per the wasm-library plan's memory discipline, these functions never
15
+ allocate; they read from caller-supplied buffers and return scalars.
16
+
17
+ **Length precondition.** `n` must be a multiple of 4 (the SIMD lane
18
+ width). Callers that have non-multiple-of-4 vectors should pad with
19
+ zeros (cheap, doesn't affect dot product or sum-of-squares results).
20
+ */
21
+
22
+ /**
23
+ * Dot product of two f32 vectors. Returns the sum of element-wise products.
24
+ *
25
+ * `a` and `b` must have length `n`; `n` must be a multiple of 4.
26
+ * Returns f64 (sufficient precision; wasm backend's only scalar return type).
27
+ */
28
+ export wasm function dot(a: Float32Array, b: Float32Array, n: i32): f64 {
29
+ let acc = f32x4_splat(0.0)
30
+ for (let i = 0; i < n; i += 4) {
31
+ let off = i * 4
32
+ let av = f32x4_load(a, off)
33
+ let bv = f32x4_load(b, off)
34
+ acc = f32x4_add(acc, f32x4_mul(av, bv))
35
+ }
36
+ return f32x4_extract_lane(acc, 0)
37
+ + f32x4_extract_lane(acc, 1)
38
+ + f32x4_extract_lane(acc, 2)
39
+ + f32x4_extract_lane(acc, 3)
40
+ }
41
+
42
+ /**
43
+ * Squared L2 norm of an f32 vector. Returns the sum of squares.
44
+ *
45
+ * For the actual norm, take `Math.sqrt(norm_sq(a, n))` on the JS side.
46
+ * Returning the squared value avoids the wasm-side sqrt (cheaper) and
47
+ * is sufficient for many use cases (cosine similarity divides by
48
+ * `sqrt(norm_sq(a) * norm_sq(b))`, which can be one sqrt instead of two).
49
+ *
50
+ * `n` must be a multiple of 4.
51
+ */
52
+ export wasm function norm_sq(a: Float32Array, n: i32): f64 {
53
+ let acc = f32x4_splat(0.0)
54
+ for (let i = 0; i < n; i += 4) {
55
+ let off = i * 4
56
+ let av = f32x4_load(a, off)
57
+ acc = f32x4_add(acc, f32x4_mul(av, av))
58
+ }
59
+ return f32x4_extract_lane(acc, 0)
60
+ + f32x4_extract_lane(acc, 1)
61
+ + f32x4_extract_lane(acc, 2)
62
+ + f32x4_extract_lane(acc, 3)
63
+ }
64
+
65
+ /**
66
+ * Dot product against a SLICE of a packed corpus: computes the dot product
67
+ * of `query[0..n]` against `corpus[startIdx..startIdx+n]`. Element indices
68
+ * are f32 indices, not bytes (startIdx * 4 bytes from `corpus`'s base).
69
+ *
70
+ * Designed for the cross-file canonical-demo pattern: a consumer's
71
+ * `wasm function` outer loop iterates over packed corpus rows and calls
72
+ * `dot_at` via wasm-to-wasm `call <index>` instructions — no JS↔wasm
73
+ * boundary in the inner loop.
74
+ *
75
+ * `startIdx` and `n` must each be a multiple of 4. `query` must have at
76
+ * least `n` elements; `corpus` must have at least `startIdx + n` elements.
77
+ */
78
+ export wasm function dot_at(
79
+ corpus: Float32Array,
80
+ startIdx: i32,
81
+ query: Float32Array,
82
+ n: i32
83
+ ): f64 {
84
+ let acc = f32x4_splat(0.0)
85
+ let base = startIdx * 4
86
+ for (let i = 0; i < n; i += 4) {
87
+ let cOff = base + i * 4
88
+ let qOff = i * 4
89
+ let cv = f32x4_load(corpus, cOff)
90
+ let qv = f32x4_load(query, qOff)
91
+ acc = f32x4_add(acc, f32x4_mul(cv, qv))
92
+ }
93
+ return f32x4_extract_lane(acc, 0)
94
+ + f32x4_extract_lane(acc, 1)
95
+ + f32x4_extract_lane(acc, 2)
96
+ + f32x4_extract_lane(acc, 3)
97
+ }
98
+
99
+ /**
100
+ * Sum-of-squares over a SLICE of a packed array: computes
101
+ * `sum(arr[startIdx..startIdx+n] ** 2)`. Mirrors `dot_at`'s pattern for
102
+ * computing the norm of a corpus row without allocating a subarray view.
103
+ *
104
+ * `startIdx` and `n` must each be a multiple of 4. `arr` must have at
105
+ * least `startIdx + n` elements.
106
+ */
107
+ export wasm function norm_sq_at(arr: Float32Array, startIdx: i32, n: i32): f64 {
108
+ let acc = f32x4_splat(0.0)
109
+ let base = startIdx * 4
110
+ for (let i = 0; i < n; i += 4) {
111
+ let off = base + i * 4
112
+ let v = f32x4_load(arr, off)
113
+ acc = f32x4_add(acc, f32x4_mul(v, v))
114
+ }
115
+ return f32x4_extract_lane(acc, 0)
116
+ + f32x4_extract_lane(acc, 1)
117
+ + f32x4_extract_lane(acc, 2)
118
+ + f32x4_extract_lane(acc, 3)
119
+ }
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Tests for tjs-lang/linalg (v1 MVP)
3
+ *
4
+ * Verifies the two functions that unlock the canonical vector-search
5
+ * demo: `dot` (f32x4 dot product) and `norm_sq` (sum of squares).
6
+ *
7
+ * Coverage:
8
+ * - The library file transpiles cleanly to a self-contained .js
9
+ * - Correctness against a JS scalar reference
10
+ * - Phase 3 composition: consumer imports linalg via moduleLoader,
11
+ * calls compose correctly (no JS↔wasm boundary inside the library
12
+ * module — `dot` is local to the consumer's wasm module)
13
+ * - Boundary form: same library imported via dynamic ESM, same results
14
+ */
15
+
16
+ import { describe, it, expect } from 'bun:test'
17
+ import { readFileSync } from 'node:fs'
18
+ import { join } from 'node:path'
19
+ import { tmpdir } from 'node:os'
20
+ import { writeFileSync, unlinkSync } from 'node:fs'
21
+
22
+ const LINALG_PATH = join(import.meta.dir, 'index.tjs')
23
+ const LINALG_SOURCE = readFileSync(LINALG_PATH, 'utf8')
24
+
25
+ /** JS scalar reference for cross-checking wasm results */
26
+ function dotJS(a: Float32Array, b: Float32Array, n: number): number {
27
+ let s = 0
28
+ for (let i = 0; i < n; i++) s += a[i] * b[i]
29
+ return s
30
+ }
31
+
32
+ function normSqJS(a: Float32Array, n: number): number {
33
+ let s = 0
34
+ for (let i = 0; i < n; i++) s += a[i] * a[i]
35
+ return s
36
+ }
37
+
38
+ async function dynamicImportLibrary(transpiled: string): Promise<any> {
39
+ const path = join(
40
+ tmpdir(),
41
+ `linalg-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mjs`
42
+ )
43
+ writeFileSync(path, transpiled)
44
+ try {
45
+ const mod = await import(path)
46
+ // Wait for the async wasm bootstrap inside the module to finish
47
+ await new Promise((r) => setTimeout(r, 100))
48
+ return mod
49
+ } finally {
50
+ try {
51
+ unlinkSync(path)
52
+ } catch {
53
+ /* ignore */
54
+ }
55
+ }
56
+ }
57
+
58
+ describe('tjs-lang/linalg v1', () => {
59
+ it('source file transpiles cleanly: all wasm functions compile', async () => {
60
+ const { tjs } = await import('../lang/index')
61
+ const result = tjs(LINALG_SOURCE, { runTests: false })
62
+
63
+ expect(result.wasmCompiled).toBeDefined()
64
+ // v1 surface: dot, norm_sq, dot_at, norm_sq_at
65
+ expect(result.wasmCompiled).toHaveLength(4)
66
+ expect(result.wasmCompiled!.every((b) => b.success)).toBe(true)
67
+
68
+ const ids = result.wasmCompiled!.map((b) => b.id).sort()
69
+ expect(ids).toEqual([
70
+ '__tjs_wasm_dot',
71
+ '__tjs_wasm_dot_at',
72
+ '__tjs_wasm_norm_sq',
73
+ '__tjs_wasm_norm_sq_at',
74
+ ])
75
+
76
+ // One consolidated WebAssembly.Module per file
77
+ const compileCalls = (result.code.match(/WebAssembly\.compile\(/g) || [])
78
+ .length
79
+ expect(compileCalls).toBe(1)
80
+ })
81
+
82
+ it('boundary form: dynamic import gives a working library', async () => {
83
+ const { tjs } = await import('../lang/index')
84
+ const result = tjs(LINALG_SOURCE, { runTests: false })
85
+ const lib = await dynamicImportLibrary(result.code)
86
+
87
+ expect(typeof lib.dot).toBe('function')
88
+ expect(typeof lib.norm_sq).toBe('function')
89
+
90
+ // Use wasmBuffer for zero-copy memory sharing with wasm
91
+ const wasmBuffer = (globalThis as any).wasmBuffer
92
+ expect(typeof wasmBuffer).toBe('function')
93
+
94
+ const a = wasmBuffer(Float32Array, 8)
95
+ const b = wasmBuffer(Float32Array, 8)
96
+ for (let i = 0; i < 8; i++) {
97
+ a[i] = i + 1 // [1,2,3,4,5,6,7,8]
98
+ b[i] = i + 1 // [1,2,3,4,5,6,7,8]
99
+ }
100
+
101
+ // dot([1..8], [1..8]) = 1 + 4 + 9 + ... + 64 = 204
102
+ expect(lib.dot(a, b, 8)).toBeCloseTo(204, 4)
103
+ // norm_sq([1..8]) = same as dot([1..8], [1..8]) = 204
104
+ expect(lib.norm_sq(a, 8)).toBeCloseTo(204, 4)
105
+ })
106
+
107
+ it('correctness against JS scalar reference (random vectors)', async () => {
108
+ const { tjs } = await import('../lang/index')
109
+ const result = tjs(LINALG_SOURCE, { runTests: false })
110
+ const lib = await dynamicImportLibrary(result.code)
111
+
112
+ const wasmBuffer = (globalThis as any).wasmBuffer
113
+
114
+ // Several sizes (all multiples of 4 — current SIMD precondition)
115
+ for (const n of [4, 16, 64, 128, 256]) {
116
+ const a = wasmBuffer(Float32Array, n)
117
+ const b = wasmBuffer(Float32Array, n)
118
+ for (let i = 0; i < n; i++) {
119
+ a[i] = Math.random() * 2 - 1
120
+ b[i] = Math.random() * 2 - 1
121
+ }
122
+
123
+ // Copy to regular Float32Array for JS reference (otherwise SAB issues)
124
+ const aRef = Float32Array.from(a)
125
+ const bRef = Float32Array.from(b)
126
+
127
+ const wasmDot = lib.dot(a, b, n)
128
+ const jsDot = dotJS(aRef, bRef, n)
129
+ // f32 precision: a few decimal digits of agreement is enough
130
+ expect(wasmDot).toBeCloseTo(jsDot, 3)
131
+
132
+ const wasmNorm = lib.norm_sq(a, n)
133
+ const jsNorm = normSqJS(aRef, n)
134
+ expect(wasmNorm).toBeCloseTo(jsNorm, 3)
135
+ }
136
+ })
137
+
138
+ it('Phase 3 composition: consumer importing linalg works end-to-end', async () => {
139
+ // The canonical Phase 5 + Phase 3 + Phase 0.5 integration test:
140
+ // a consumer imports `dot` from linalg via the moduleLoader, the
141
+ // function is composed into the consumer's wasm module, and calling
142
+ // it from JS produces correct results.
143
+ const { tjs } = await import('../lang/index')
144
+ const { createRuntime } = await import('../lang/runtime')
145
+ const { ModuleLoader, inMemoryFileSystem } = await import(
146
+ '../lang/module-loader'
147
+ )
148
+
149
+ const loader = new ModuleLoader({
150
+ fs: inMemoryFileSystem({ '/proj/linalg.tjs': LINALG_SOURCE }),
151
+ baseDir: '/proj',
152
+ })
153
+
154
+ const consumerSource = `
155
+ import { dot, norm_sq } from './linalg.tjs'
156
+
157
+ function cosine(a, b, n) {
158
+ const d = dot(a, b, n)
159
+ const ma = norm_sq(a, n)
160
+ const mb = norm_sq(b, n)
161
+ if (ma <= 0 || mb <= 0) return 0
162
+ return d / Math.sqrt(ma * mb)
163
+ }
164
+ `
165
+ const result = tjs(consumerSource, {
166
+ moduleLoader: loader,
167
+ filename: '/proj/app.tjs',
168
+ runTests: false,
169
+ })
170
+
171
+ // The consumer's emitted module contains both linalg functions as
172
+ // local exports (Phase 3 acceptance criterion: composed-not-imported).
173
+ expect(result.wasmCompiled).toHaveLength(2)
174
+ const ids = result.wasmCompiled!.map((b) => b.id).sort()
175
+ expect(ids).toEqual(['__tjs_wasm_dot', '__tjs_wasm_norm_sq'])
176
+ const compileCalls = (result.code.match(/WebAssembly\.compile\(/g) || [])
177
+ .length
178
+ expect(compileCalls).toBe(1)
179
+
180
+ // Run the consumer and verify the cosine function works correctly
181
+ const savedTjs = globalThis.__tjs
182
+ try {
183
+ globalThis.__tjs = createRuntime()
184
+ await new Function(
185
+ '__tjs',
186
+ `return (async () => { ${result.code}\n` +
187
+ `globalThis.__test_cosine = cosine;\n` +
188
+ `})();`
189
+ )(globalThis.__tjs)
190
+ await new Promise((r) => setTimeout(r, 100))
191
+
192
+ const wasmBuffer = (globalThis as any).wasmBuffer
193
+ const a = wasmBuffer(Float32Array, 8)
194
+ const b = wasmBuffer(Float32Array, 8)
195
+ for (let i = 0; i < 8; i++) {
196
+ a[i] = i + 1
197
+ b[i] = i + 1
198
+ }
199
+ // cosine(a, a) = 1 (identical vectors)
200
+ const sim = (globalThis as any).__test_cosine(a, b, 8)
201
+ expect(sim).toBeCloseTo(1, 4)
202
+
203
+ // Orthogonal vectors → cosine 0
204
+ const ox = wasmBuffer(Float32Array, 4)
205
+ const oy = wasmBuffer(Float32Array, 4)
206
+ ox[0] = 1; ox[1] = 0; ox[2] = 0; ox[3] = 0
207
+ oy[0] = 0; oy[1] = 1; oy[2] = 0; oy[3] = 0
208
+ const ortho = (globalThis as any).__test_cosine(ox, oy, 4)
209
+ expect(ortho).toBeCloseTo(0, 4)
210
+ } finally {
211
+ globalThis.__tjs = savedTjs
212
+ delete (globalThis as any).__test_cosine
213
+ delete (globalThis as any).wasmBuffer
214
+ }
215
+ })
216
+
217
+ it('boundary and composed forms return identical results', async () => {
218
+ // Same linalg source consumed two ways — verifies the
219
+ // "same source, two distribution forms" claim from the plan
220
+ const { tjs } = await import('../lang/index')
221
+ const { createRuntime } = await import('../lang/runtime')
222
+ const { ModuleLoader, inMemoryFileSystem } = await import(
223
+ '../lang/module-loader'
224
+ )
225
+
226
+ // Boundary form
227
+ const result = tjs(LINALG_SOURCE, { runTests: false })
228
+ const lib = await dynamicImportLibrary(result.code)
229
+ const wasmBuffer = (globalThis as any).wasmBuffer
230
+ const a = wasmBuffer(Float32Array, 16)
231
+ const b = wasmBuffer(Float32Array, 16)
232
+ for (let i = 0; i < 16; i++) {
233
+ a[i] = (i * 0.7 + 0.3) % 1.0
234
+ b[i] = (i * 1.3 + 0.7) % 1.0
235
+ }
236
+ const boundaryDot = lib.dot(a, b, 16)
237
+ const boundaryNormA = lib.norm_sq(a, 16)
238
+ const boundaryNormB = lib.norm_sq(b, 16)
239
+
240
+ // Capture values BEFORE the composed run replaces wasmBuffer in globalThis
241
+ const aValues = Array.from(a)
242
+ const bValues = Array.from(b)
243
+
244
+ // Composed form (Phase 3 path)
245
+ const loader = new ModuleLoader({
246
+ fs: inMemoryFileSystem({ '/proj/linalg.tjs': LINALG_SOURCE }),
247
+ baseDir: '/proj',
248
+ })
249
+ const consumerSource = `
250
+ import { dot, norm_sq } from './linalg.tjs'
251
+ `
252
+ const consumerResult = tjs(consumerSource, {
253
+ moduleLoader: loader,
254
+ filename: '/proj/app.tjs',
255
+ runTests: false,
256
+ })
257
+
258
+ const savedTjs = globalThis.__tjs
259
+ try {
260
+ globalThis.__tjs = createRuntime()
261
+ await new Function(
262
+ '__tjs',
263
+ `return (async () => { ${consumerResult.code}\n` +
264
+ `globalThis.__test_dot = dot;\n` +
265
+ `globalThis.__test_norm_sq = norm_sq;\n` +
266
+ `})();`
267
+ )(globalThis.__tjs)
268
+ await new Promise((r) => setTimeout(r, 100))
269
+
270
+ // Allocate from the new module's wasmBuffer (composed module
271
+ // sets up its own __wasmMem)
272
+ const composedBuffer = (globalThis as any).wasmBuffer
273
+ const a2 = composedBuffer(Float32Array, 16)
274
+ const b2 = composedBuffer(Float32Array, 16)
275
+ for (let i = 0; i < 16; i++) {
276
+ a2[i] = aValues[i]
277
+ b2[i] = bValues[i]
278
+ }
279
+ const composedDot = (globalThis as any).__test_dot(a2, b2, 16)
280
+ const composedNormA = (globalThis as any).__test_norm_sq(a2, 16)
281
+ const composedNormB = (globalThis as any).__test_norm_sq(b2, 16)
282
+
283
+ // Identical results from both distribution forms
284
+ expect(composedDot).toBeCloseTo(boundaryDot, 4)
285
+ expect(composedNormA).toBeCloseTo(boundaryNormA, 4)
286
+ expect(composedNormB).toBeCloseTo(boundaryNormB, 4)
287
+ } finally {
288
+ globalThis.__tjs = savedTjs
289
+ delete (globalThis as any).__test_dot
290
+ delete (globalThis as any).__test_norm_sq
291
+ delete (globalThis as any).wasmBuffer
292
+ }
293
+ })
294
+ })