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.
- package/CLAUDE.md +99 -33
- package/bin/docs.js +4 -1
- package/demo/docs.json +104 -22
- package/demo/src/examples.test.ts +1 -0
- package/demo/src/imports.test.ts +16 -4
- package/demo/src/imports.ts +60 -15
- package/demo/src/playground-shared.ts +9 -8
- package/demo/src/tfs-worker.js +205 -147
- package/demo/src/tjs-playground.ts +34 -10
- package/demo/src/ts-examples.ts +8 -8
- package/demo/src/ts-playground.ts +24 -8
- package/dist/index.js +118 -101
- package/dist/index.js.map +4 -4
- package/dist/src/lang/bool-coercion.d.ts +50 -0
- package/dist/src/lang/docs.d.ts +31 -6
- package/dist/src/lang/linter.d.ts +8 -0
- package/dist/src/lang/parser-transforms.d.ts +18 -0
- package/dist/src/lang/parser-types.d.ts +2 -0
- package/dist/src/lang/parser.d.ts +3 -0
- package/dist/src/lang/runtime.d.ts +34 -0
- package/dist/src/lang/types.d.ts +9 -1
- package/dist/src/rbac/index.d.ts +1 -1
- package/dist/src/vm/runtime.d.ts +1 -1
- package/dist/tjs-eval.js +38 -36
- package/dist/tjs-eval.js.map +4 -4
- package/dist/tjs-from-ts.js +20 -20
- package/dist/tjs-from-ts.js.map +3 -3
- package/dist/tjs-lang.js +85 -83
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +47 -45
- package/dist/tjs-vm.js.map +4 -4
- package/llms.txt +79 -0
- package/package.json +9 -4
- package/src/cli/commands/convert.test.ts +16 -21
- package/src/lang/bool-coercion.test.ts +203 -0
- package/src/lang/bool-coercion.ts +314 -0
- package/src/lang/codegen.test.ts +137 -0
- package/src/lang/docs.test.ts +476 -1
- package/src/lang/docs.ts +471 -37
- package/src/lang/emitters/ast.ts +11 -12
- package/src/lang/emitters/dts.test.ts +41 -0
- package/src/lang/emitters/dts.ts +9 -0
- package/src/lang/emitters/js-tests.ts +9 -4
- package/src/lang/emitters/js-wasm.ts +57 -65
- package/src/lang/emitters/js.ts +198 -3
- package/src/lang/features.test.ts +4 -3
- package/src/lang/index.ts +9 -0
- package/src/lang/inference.ts +54 -0
- package/src/lang/linter.test.ts +104 -1
- package/src/lang/linter.ts +124 -1
- package/src/lang/module-loader.test.ts +318 -0
- package/src/lang/module-loader.ts +419 -0
- package/src/lang/parser-params.ts +31 -0
- package/src/lang/parser-transforms.ts +640 -0
- package/src/lang/parser-types.ts +35 -0
- package/src/lang/parser.test.ts +73 -1
- package/src/lang/parser.ts +77 -3
- package/src/lang/runtime.ts +98 -0
- package/src/lang/types.ts +6 -0
- package/src/lang/wasm.test.ts +1293 -2
- package/src/lang/wasm.ts +470 -87
- package/src/linalg/index.tjs +119 -0
- package/src/linalg/linalg.test.ts +294 -0
- package/src/linalg/vector-search.bench.test.ts +395 -0
- package/src/rbac/index.ts +2 -2
- package/src/rbac/rules.tjs.d.ts +9 -0
- package/src/vm/atoms/batteries.ts +2 -2
- package/src/vm/runtime.ts +10 -3
- package/dist/src/rbac/rules.d.ts +0 -184
- 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
|
+
})
|