tjs-lang 0.7.8 → 0.8.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 +14 -1
- package/CONTEXT.md +4 -0
- package/demo/docs.json +66 -696
- package/demo/src/ts-examples.ts +8 -8
- package/dist/eslint.config.d.ts +2 -0
- package/dist/index.js +137 -135
- package/dist/index.js.map +4 -4
- package/dist/src/lang/emitters/js-wasm.d.ts +5 -1
- package/dist/src/lang/emitters/js.d.ts +9 -0
- package/dist/src/lang/index.d.ts +1 -0
- package/dist/src/lang/module-loader.d.ts +125 -0
- package/dist/src/lang/parser-transforms.d.ts +79 -0
- package/dist/src/lang/parser-types.d.ts +33 -0
- package/dist/src/lang/wasm.d.ts +67 -1
- package/dist/tjs-batteries.js +2 -2
- package/dist/tjs-batteries.js.map +2 -2
- package/dist/tjs-eval.js +39 -37
- package/dist/tjs-eval.js.map +3 -3
- package/dist/tjs-from-ts.js +2 -2
- package/dist/tjs-from-ts.js.map +2 -2
- package/dist/tjs-lang.js +102 -102
- package/dist/tjs-lang.js.map +3 -3
- package/dist/tjs-vm.js +50 -48
- package/dist/tjs-vm.js.map +3 -3
- package/docs/README.md +2 -0
- package/docs/lm-studio-setup.md +143 -0
- package/docs/universal-endpoint.md +122 -0
- package/llms.txt +8 -2
- package/package.json +11 -6
- package/src/batteries/audit.ts +3 -3
- package/src/batteries/llm.ts +8 -3
- package/src/builder.ts +0 -3
- package/src/cli/commands/test.ts +1 -1
- package/src/lang/docs.test.ts +148 -1
- package/src/lang/docs.ts +49 -15
- package/src/lang/emitters/from-ts.ts +1 -1
- package/src/lang/emitters/js-wasm.ts +57 -65
- package/src/lang/emitters/js.ts +16 -2
- package/src/lang/features.test.ts +4 -3
- package/src/lang/index.ts +9 -0
- package/src/lang/linter.ts +1 -1
- package/src/lang/module-loader.test.ts +322 -0
- package/src/lang/module-loader.ts +418 -0
- package/src/lang/parser-params.ts +1 -1
- package/src/lang/parser-transforms.ts +339 -9
- package/src/lang/parser-types.ts +33 -0
- package/src/lang/parser.ts +43 -2
- package/src/lang/perf.test.ts +10 -4
- package/src/lang/runtime.ts +0 -1
- 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 +300 -0
- package/src/linalg/vector-search.bench.test.ts +416 -0
- package/src/types/Type.ts +6 -6
- package/src/use-cases/asymmetric-client-server.test.ts +0 -2
- package/src/use-cases/client-server.test.ts +1 -1
- package/src/use-cases/unbundled-imports.test.ts +0 -1
- package/src/vm/runtime.ts +3 -3
- package/src/vm/vm.ts +3 -1
- package/dist/examples/modules/dist/main.d.ts +0 -34
- package/dist/examples/modules/dist/math.d.ts +0 -120
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical wasm-library acceptance test: vector-search inline vs composed.
|
|
3
|
+
*
|
|
4
|
+
* This is the test that proves the conceptual goal from
|
|
5
|
+
* `wasm-library-plan.md` § "Canonical end-to-end demo". The same
|
|
6
|
+
* cosine-similarity workload is run two ways:
|
|
7
|
+
*
|
|
8
|
+
* Inline baseline: one big `wasm {}` block computing dot/magA/magB
|
|
9
|
+
* together — what the original `wasm-vector-search.md`
|
|
10
|
+
* playground example does today.
|
|
11
|
+
*
|
|
12
|
+
* Composed: a JS outer loop calling imported `dot` and `norm_sq`
|
|
13
|
+
* from `tjs-lang/linalg`. The library's wasm functions
|
|
14
|
+
* are composed into the consumer's wasm module via the
|
|
15
|
+
* Phase 3 ModuleLoader path.
|
|
16
|
+
*
|
|
17
|
+
* Acceptance criteria (matches the plan):
|
|
18
|
+
* 1. Correctness: both implementations pick the same best index across
|
|
19
|
+
* a randomized corpus. ✓ asserted.
|
|
20
|
+
* 2. Performance: within ~5% of the inline baseline. Timing is reported
|
|
21
|
+
* for inspection but not asserted as a hard limit
|
|
22
|
+
* (engine variance makes hard thresholds flaky in CI).
|
|
23
|
+
* 3. Module shape: composed-not-imported. Verified by Phase 3 tests in
|
|
24
|
+
* wasm.test.ts; not re-checked here.
|
|
25
|
+
* 4. Boundary form: same library works for non-tjs consumers. Verified
|
|
26
|
+
* by Phase 4 tests; not re-checked here.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { describe, it, expect } from 'bun:test'
|
|
30
|
+
import { readFileSync } from 'node:fs'
|
|
31
|
+
import { join } from 'node:path'
|
|
32
|
+
|
|
33
|
+
const LINALG_SOURCE = readFileSync(join(import.meta.dir, 'index.tjs'), 'utf8')
|
|
34
|
+
|
|
35
|
+
// The inline baseline — single wasm{} block computing dot, magA, magB
|
|
36
|
+
// together. Mirrors what guides/examples/tjs/wasm-vector-search.md does.
|
|
37
|
+
const INLINE_SOURCE = `
|
|
38
|
+
function inlineSearch(corpus: Float32Array, query: Float32Array, count: 0, dim: 0) {
|
|
39
|
+
return wasm {
|
|
40
|
+
let bestIdx = 0
|
|
41
|
+
let bestScore = -2.0
|
|
42
|
+
|
|
43
|
+
for (let v = 0; v < count; v++) {
|
|
44
|
+
let dotAcc = f32x4_splat(0.0)
|
|
45
|
+
let magAAcc = f32x4_splat(0.0)
|
|
46
|
+
let magBAcc = f32x4_splat(0.0)
|
|
47
|
+
|
|
48
|
+
for (let j = 0; j < dim; j += 4) {
|
|
49
|
+
let qOff = j * 4
|
|
50
|
+
let cOff = (v * dim + j) * 4
|
|
51
|
+
let a = f32x4_load(query, qOff)
|
|
52
|
+
let b = f32x4_load(corpus, cOff)
|
|
53
|
+
dotAcc = f32x4_add(dotAcc, f32x4_mul(a, b))
|
|
54
|
+
magAAcc = f32x4_add(magAAcc, f32x4_mul(a, a))
|
|
55
|
+
magBAcc = f32x4_add(magBAcc, f32x4_mul(b, b))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let dot = f32x4_extract_lane(dotAcc, 0) + f32x4_extract_lane(dotAcc, 1)
|
|
59
|
+
+ f32x4_extract_lane(dotAcc, 2) + f32x4_extract_lane(dotAcc, 3)
|
|
60
|
+
let magA = f32x4_extract_lane(magAAcc, 0) + f32x4_extract_lane(magAAcc, 1)
|
|
61
|
+
+ f32x4_extract_lane(magAAcc, 2) + f32x4_extract_lane(magAAcc, 3)
|
|
62
|
+
let magB = f32x4_extract_lane(magBAcc, 0) + f32x4_extract_lane(magBAcc, 1)
|
|
63
|
+
+ f32x4_extract_lane(magBAcc, 2) + f32x4_extract_lane(magBAcc, 3)
|
|
64
|
+
|
|
65
|
+
let mA = Math.sqrt(magA)
|
|
66
|
+
let mB = Math.sqrt(magB)
|
|
67
|
+
if (mA > 0.000001) {
|
|
68
|
+
if (mB > 0.000001) {
|
|
69
|
+
let score = dot / (mA * mB)
|
|
70
|
+
if (score > bestScore) {
|
|
71
|
+
bestScore = score
|
|
72
|
+
bestIdx = v
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return bestIdx
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
`
|
|
81
|
+
|
|
82
|
+
// Composed, JS-outer-loop: outer iteration is JS calling imported linalg
|
|
83
|
+
// kernels. Each row costs 2 JS↔wasm boundary crossings (dot + norm_sq).
|
|
84
|
+
const COMPOSED_JS_LOOP_SOURCE = `
|
|
85
|
+
import { dot, norm_sq } from './linalg.tjs'
|
|
86
|
+
|
|
87
|
+
function composedJsSearch(corpus, query, count, dim) {
|
|
88
|
+
const magA = Math.sqrt(norm_sq(query, dim))
|
|
89
|
+
if (magA < 0.000001) return 0
|
|
90
|
+
|
|
91
|
+
let bestIdx = 0
|
|
92
|
+
let bestScore = -2
|
|
93
|
+
|
|
94
|
+
for (let v = 0; v < count; v++) {
|
|
95
|
+
const row = corpus.subarray(v * dim, (v + 1) * dim)
|
|
96
|
+
const d = dot(query, row, dim)
|
|
97
|
+
const magB = Math.sqrt(norm_sq(row, dim))
|
|
98
|
+
if (magB > 0.000001) {
|
|
99
|
+
const score = d / (magA * magB)
|
|
100
|
+
if (score > bestScore) {
|
|
101
|
+
bestScore = score
|
|
102
|
+
bestIdx = v
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return bestIdx
|
|
107
|
+
}
|
|
108
|
+
`
|
|
109
|
+
|
|
110
|
+
// Composed, WASM-outer-loop: outer iteration is itself a `wasm function`
|
|
111
|
+
// that calls imported `dot_at` / `norm_sq_at` via wasm-to-wasm
|
|
112
|
+
// `call <index>` instructions. NO JS↔wasm boundary in the inner loop —
|
|
113
|
+
// the whole workload runs inside one wasm call. This is the Phase 1.5
|
|
114
|
+
// payoff in action.
|
|
115
|
+
const COMPOSED_WASM_LOOP_SOURCE = `
|
|
116
|
+
import { dot_at, norm_sq_at } from './linalg.tjs'
|
|
117
|
+
|
|
118
|
+
wasm function composedWasmSearch(
|
|
119
|
+
corpus: Float32Array,
|
|
120
|
+
query: Float32Array,
|
|
121
|
+
count: i32,
|
|
122
|
+
dim: i32
|
|
123
|
+
): f64 {
|
|
124
|
+
let magQ = norm_sq_at(query, 0, dim)
|
|
125
|
+
if (magQ < 0.000001) return 0.0
|
|
126
|
+
let mA = Math.sqrt(magQ)
|
|
127
|
+
|
|
128
|
+
let bestIdx = 0
|
|
129
|
+
let bestScore = -2.0
|
|
130
|
+
|
|
131
|
+
for (let v = 0; v < count; v++) {
|
|
132
|
+
let startIdx = v * dim
|
|
133
|
+
let d = dot_at(corpus, startIdx, query, dim)
|
|
134
|
+
let magB = norm_sq_at(corpus, startIdx, dim)
|
|
135
|
+
if (magB > 0.000001) {
|
|
136
|
+
let mB = Math.sqrt(magB)
|
|
137
|
+
let score = d / (mA * mB)
|
|
138
|
+
if (score > bestScore) {
|
|
139
|
+
bestScore = score
|
|
140
|
+
bestIdx = v
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return bestIdx
|
|
145
|
+
}
|
|
146
|
+
`
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Compile one source and load it into a fresh globalThis.__tjs context,
|
|
150
|
+
* exposing the named search function (and its wasmBuffer) on globalThis
|
|
151
|
+
* under unique keys for the benchmark to pick up.
|
|
152
|
+
*
|
|
153
|
+
* Each variant gets its own wasm module + own __wasmMem, so wasmBuffer
|
|
154
|
+
* allocations stay isolated.
|
|
155
|
+
*/
|
|
156
|
+
async function loadVariant(
|
|
157
|
+
code: string,
|
|
158
|
+
fnName: string,
|
|
159
|
+
varName: string
|
|
160
|
+
): Promise<{
|
|
161
|
+
search: (
|
|
162
|
+
corpus: Float32Array,
|
|
163
|
+
query: Float32Array,
|
|
164
|
+
count: number,
|
|
165
|
+
dim: number
|
|
166
|
+
) => number
|
|
167
|
+
wasmBuffer: (Ctor: any, len: number) => any
|
|
168
|
+
}> {
|
|
169
|
+
await new Function(
|
|
170
|
+
'__tjs',
|
|
171
|
+
`return (async () => { ${code}\n` +
|
|
172
|
+
`globalThis.__${varName}_search = ${fnName};\n` +
|
|
173
|
+
`globalThis.__${varName}_wasmBuffer = globalThis.wasmBuffer;\n` +
|
|
174
|
+
`})();`
|
|
175
|
+
)(globalThis.__tjs)
|
|
176
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
177
|
+
const search = (globalThis as any)[`__${varName}_search`]
|
|
178
|
+
const wasmBuffer = (globalThis as any)[`__${varName}_wasmBuffer`]
|
|
179
|
+
if (typeof search !== 'function') {
|
|
180
|
+
throw new Error(`${varName} search function not registered`)
|
|
181
|
+
}
|
|
182
|
+
if (typeof wasmBuffer !== 'function') {
|
|
183
|
+
throw new Error(`${varName} wasmBuffer not available`)
|
|
184
|
+
}
|
|
185
|
+
return { search, wasmBuffer }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
describe('Canonical demo: vector-search across three forms', () => {
|
|
189
|
+
// Compares THREE implementations of the same cosine-similarity workload:
|
|
190
|
+
// - inline: one big wasm{} block (no boundary crossings)
|
|
191
|
+
// - composedJs: imported linalg + JS outer loop (2 crossings per row)
|
|
192
|
+
// - composedWasm: imported linalg + wasm-function outer loop calling
|
|
193
|
+
// dot_at/norm_sq_at via wasm `call <index>` (1 crossing
|
|
194
|
+
// for the whole workload)
|
|
195
|
+
//
|
|
196
|
+
// The point: composedWasm should match (or beat) inline. If it does,
|
|
197
|
+
// the perf criterion from the wasm-library plan is proven.
|
|
198
|
+
it('all three forms agree on best index; composed-wasm matches inline perf', async () => {
|
|
199
|
+
const { tjs } = await import('../lang/index')
|
|
200
|
+
const { createRuntime } = await import('../lang/runtime')
|
|
201
|
+
const { ModuleLoader, inMemoryFileSystem } = await import(
|
|
202
|
+
'../lang/module-loader'
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// Compile each source (composed versions share a loader pointing at linalg)
|
|
206
|
+
const inlineResult = tjs(INLINE_SOURCE, { runTests: false })
|
|
207
|
+
expect(inlineResult.wasmCompiled!.every((b) => b.success)).toBe(true)
|
|
208
|
+
|
|
209
|
+
const loader = new ModuleLoader({
|
|
210
|
+
fs: inMemoryFileSystem({ '/proj/linalg.tjs': LINALG_SOURCE }),
|
|
211
|
+
baseDir: '/proj',
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const composedJsResult = tjs(COMPOSED_JS_LOOP_SOURCE, {
|
|
215
|
+
moduleLoader: loader,
|
|
216
|
+
filename: '/proj/app.tjs',
|
|
217
|
+
runTests: false,
|
|
218
|
+
})
|
|
219
|
+
expect(composedJsResult.wasmCompiled!.every((b) => b.success)).toBe(true)
|
|
220
|
+
|
|
221
|
+
const composedWasmResult = tjs(COMPOSED_WASM_LOOP_SOURCE, {
|
|
222
|
+
moduleLoader: loader,
|
|
223
|
+
filename: '/proj/app.tjs',
|
|
224
|
+
runTests: false,
|
|
225
|
+
})
|
|
226
|
+
expect(composedWasmResult.wasmCompiled!.every((b) => b.success)).toBe(true)
|
|
227
|
+
|
|
228
|
+
const savedTjs = globalThis.__tjs
|
|
229
|
+
try {
|
|
230
|
+
// ---- Inline ----
|
|
231
|
+
globalThis.__tjs = createRuntime()
|
|
232
|
+
const inline = await loadVariant(
|
|
233
|
+
inlineResult.code,
|
|
234
|
+
'inlineSearch',
|
|
235
|
+
'inline'
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
// ---- Composed, JS outer loop ----
|
|
239
|
+
globalThis.__tjs = createRuntime()
|
|
240
|
+
const composedJs = await loadVariant(
|
|
241
|
+
composedJsResult.code,
|
|
242
|
+
'composedJsSearch',
|
|
243
|
+
'composedJs'
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
// ---- Composed, WASM outer loop ----
|
|
247
|
+
globalThis.__tjs = createRuntime()
|
|
248
|
+
const composedWasm = await loadVariant(
|
|
249
|
+
composedWasmResult.code,
|
|
250
|
+
'composedWasmSearch',
|
|
251
|
+
'composedWasm'
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
// ---- Workload configs ----
|
|
255
|
+
// Each config: { dim, count, label }. Sized to keep the test under
|
|
256
|
+
// a few seconds in CI but large enough for SIMD to matter.
|
|
257
|
+
const configs = [
|
|
258
|
+
{ dim: 128, count: 500, label: '500x128' },
|
|
259
|
+
{ dim: 256, count: 500, label: '500x256' },
|
|
260
|
+
{ dim: 128, count: 2000, label: '2000x128' },
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
const timings: {
|
|
264
|
+
label: string
|
|
265
|
+
inlineMs: number
|
|
266
|
+
composedJsMs: number
|
|
267
|
+
composedWasmMs: number
|
|
268
|
+
bestIdx: number
|
|
269
|
+
}[] = []
|
|
270
|
+
|
|
271
|
+
for (const cfg of configs) {
|
|
272
|
+
const total = cfg.count * cfg.dim
|
|
273
|
+
|
|
274
|
+
// Allocate corpus/query in EACH variant's wasm memory so the
|
|
275
|
+
// wasmBuffer fast path is hit on all three runs.
|
|
276
|
+
const inlineCorpus = inline.wasmBuffer(Float32Array, total)
|
|
277
|
+
const inlineQuery = inline.wasmBuffer(Float32Array, cfg.dim)
|
|
278
|
+
const composedJsCorpus = composedJs.wasmBuffer(Float32Array, total)
|
|
279
|
+
const composedJsQuery = composedJs.wasmBuffer(Float32Array, cfg.dim)
|
|
280
|
+
const composedWasmCorpus = composedWasm.wasmBuffer(Float32Array, total)
|
|
281
|
+
const composedWasmQuery = composedWasm.wasmBuffer(Float32Array, cfg.dim)
|
|
282
|
+
|
|
283
|
+
// Seed all three with the same values
|
|
284
|
+
for (let i = 0; i < total; i++) {
|
|
285
|
+
const v = Math.random() * 2 - 1
|
|
286
|
+
inlineCorpus[i] = v
|
|
287
|
+
composedJsCorpus[i] = v
|
|
288
|
+
composedWasmCorpus[i] = v
|
|
289
|
+
}
|
|
290
|
+
for (let i = 0; i < cfg.dim; i++) {
|
|
291
|
+
const v = Math.random() * 2 - 1
|
|
292
|
+
inlineQuery[i] = v
|
|
293
|
+
composedJsQuery[i] = v
|
|
294
|
+
composedWasmQuery[i] = v
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Warm up all three (JIT)
|
|
298
|
+
const warmCount = Math.min(100, cfg.count)
|
|
299
|
+
for (let w = 0; w < 3; w++) {
|
|
300
|
+
inline.search(inlineCorpus, inlineQuery, warmCount, cfg.dim)
|
|
301
|
+
composedJs.search(
|
|
302
|
+
composedJsCorpus,
|
|
303
|
+
composedJsQuery,
|
|
304
|
+
warmCount,
|
|
305
|
+
cfg.dim
|
|
306
|
+
)
|
|
307
|
+
composedWasm.search(
|
|
308
|
+
composedWasmCorpus,
|
|
309
|
+
composedWasmQuery,
|
|
310
|
+
warmCount,
|
|
311
|
+
cfg.dim
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Time inline
|
|
316
|
+
const inlineStart = performance.now()
|
|
317
|
+
const inlineIdx = inline.search(
|
|
318
|
+
inlineCorpus,
|
|
319
|
+
inlineQuery,
|
|
320
|
+
cfg.count,
|
|
321
|
+
cfg.dim
|
|
322
|
+
)
|
|
323
|
+
const inlineMs = performance.now() - inlineStart
|
|
324
|
+
|
|
325
|
+
// Time composed JS-outer-loop
|
|
326
|
+
const composedJsStart = performance.now()
|
|
327
|
+
const composedJsIdx = composedJs.search(
|
|
328
|
+
composedJsCorpus,
|
|
329
|
+
composedJsQuery,
|
|
330
|
+
cfg.count,
|
|
331
|
+
cfg.dim
|
|
332
|
+
)
|
|
333
|
+
const composedJsMs = performance.now() - composedJsStart
|
|
334
|
+
|
|
335
|
+
// Time composed wasm-outer-loop
|
|
336
|
+
const composedWasmStart = performance.now()
|
|
337
|
+
const composedWasmIdx = composedWasm.search(
|
|
338
|
+
composedWasmCorpus,
|
|
339
|
+
composedWasmQuery,
|
|
340
|
+
cfg.count,
|
|
341
|
+
cfg.dim
|
|
342
|
+
)
|
|
343
|
+
const composedWasmMs = performance.now() - composedWasmStart
|
|
344
|
+
|
|
345
|
+
// All three implementations must agree on best index
|
|
346
|
+
expect(composedJsIdx).toBe(inlineIdx)
|
|
347
|
+
expect(composedWasmIdx).toBe(inlineIdx)
|
|
348
|
+
|
|
349
|
+
timings.push({
|
|
350
|
+
label: cfg.label,
|
|
351
|
+
inlineMs,
|
|
352
|
+
composedJsMs,
|
|
353
|
+
composedWasmMs,
|
|
354
|
+
bestIdx: inlineIdx,
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Report (visible in test output)
|
|
359
|
+
console.log(
|
|
360
|
+
'\n=== Vector-search: inline / composed-JS-loop / composed-WASM-loop ==='
|
|
361
|
+
)
|
|
362
|
+
console.log(
|
|
363
|
+
' config | inline | composed-JS | ratio | composed-WASM | ratio'
|
|
364
|
+
)
|
|
365
|
+
console.log(
|
|
366
|
+
' -------------|----------|-------------|--------|---------------|-------'
|
|
367
|
+
)
|
|
368
|
+
for (const t of timings) {
|
|
369
|
+
const jsRatio = t.composedJsMs / t.inlineMs
|
|
370
|
+
const wasmRatio = t.composedWasmMs / t.inlineMs
|
|
371
|
+
console.log(
|
|
372
|
+
` ${t.label.padEnd(12)} | ${t.inlineMs
|
|
373
|
+
.toFixed(2)
|
|
374
|
+
.padStart(8)} | ${t.composedJsMs
|
|
375
|
+
.toFixed(2)
|
|
376
|
+
.padStart(11)} | ${jsRatio
|
|
377
|
+
.toFixed(2)
|
|
378
|
+
.padStart(6)}x | ${t.composedWasmMs
|
|
379
|
+
.toFixed(2)
|
|
380
|
+
.padStart(13)} | ${wasmRatio.toFixed(2).padStart(5)}x`
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// The composed-WASM path should match inline within a small factor.
|
|
385
|
+
// Engine variance means hard thresholds are flaky; we use a wide
|
|
386
|
+
// 3× ceiling that catches catastrophic regressions while tolerating
|
|
387
|
+
// JIT-warmup noise and CI-environment variability. Observed ratios
|
|
388
|
+
// are typically 1.0–1.3× — i.e., parity with inline.
|
|
389
|
+
for (const t of timings) {
|
|
390
|
+
const wasmRatio = t.composedWasmMs / t.inlineMs
|
|
391
|
+
expect(wasmRatio).toBeLessThan(3.0)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// The composed-JS path is expected to be slower than composed-WASM
|
|
395
|
+
// (boundary-crossing tax). This is the "before/after" demonstration:
|
|
396
|
+
// composed-WASM must be at least 2× faster than composed-JS for the
|
|
397
|
+
// wasm-to-wasm optimization to be considered "working." In practice
|
|
398
|
+
// the gap is much larger (5–10×).
|
|
399
|
+
for (const t of timings) {
|
|
400
|
+
expect(t.composedJsMs).toBeGreaterThan(t.composedWasmMs * 2)
|
|
401
|
+
}
|
|
402
|
+
} finally {
|
|
403
|
+
globalThis.__tjs = savedTjs
|
|
404
|
+
for (const v of ['inline', 'composedJs', 'composedWasm']) {
|
|
405
|
+
delete (globalThis as any)[`__${v}_search`]
|
|
406
|
+
delete (globalThis as any)[`__${v}_wasmBuffer`]
|
|
407
|
+
}
|
|
408
|
+
delete (globalThis as any).wasmBuffer
|
|
409
|
+
for (const key of Object.keys(globalThis)) {
|
|
410
|
+
if (key.startsWith('__tjs_wasm_')) {
|
|
411
|
+
delete (globalThis as any)[key]
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
})
|
package/src/types/Type.ts
CHANGED
|
@@ -686,7 +686,7 @@ export interface FunctionPredicateSpec {
|
|
|
686
686
|
}
|
|
687
687
|
|
|
688
688
|
/** A runtime type that validates function signatures */
|
|
689
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
689
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
690
690
|
export interface FunctionPredicateType extends RuntimeType<Function> {
|
|
691
691
|
/** Parameter specification */
|
|
692
692
|
readonly params: Record<string, any>
|
|
@@ -755,7 +755,7 @@ function kindOfExample(example: unknown): string | null {
|
|
|
755
755
|
*/
|
|
756
756
|
export function FunctionPredicate(
|
|
757
757
|
name: string,
|
|
758
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
758
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
759
759
|
specOrFn: FunctionPredicateSpec | Function | (string | [string, TypeParam])[],
|
|
760
760
|
specBuilder?: (...typeArgs: any[]) => FunctionPredicateSpec
|
|
761
761
|
): FunctionPredicateType | GenericFunctionPredicateType {
|
|
@@ -794,18 +794,18 @@ export function FunctionPredicate(
|
|
|
794
794
|
return factory
|
|
795
795
|
}
|
|
796
796
|
|
|
797
|
-
/* eslint-disable @typescript-eslint/
|
|
797
|
+
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
|
798
798
|
return _createFunctionPredicate(
|
|
799
799
|
name,
|
|
800
800
|
specOrFn as FunctionPredicateSpec | Function
|
|
801
801
|
)
|
|
802
|
-
/* eslint-enable @typescript-eslint/
|
|
802
|
+
/* eslint-enable @typescript-eslint/no-unsafe-function-type */
|
|
803
803
|
}
|
|
804
804
|
|
|
805
805
|
/** Internal: create a non-generic FunctionPredicateType */
|
|
806
806
|
function _createFunctionPredicate(
|
|
807
807
|
name: string,
|
|
808
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
808
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
809
809
|
specOrFn: FunctionPredicateSpec | Function
|
|
810
810
|
): FunctionPredicateType {
|
|
811
811
|
let params: Record<string, any> = {}
|
|
@@ -853,7 +853,7 @@ function _createFunctionPredicate(
|
|
|
853
853
|
// Structural validation: check arity and __tjs metadata
|
|
854
854
|
const expectedArity = Object.keys(params).length
|
|
855
855
|
if (expectedArity > 0) {
|
|
856
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
856
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
857
857
|
const fn = value as Function
|
|
858
858
|
const meta = (fn as any).__tjs
|
|
859
859
|
if (meta?.params) {
|
|
@@ -82,7 +82,6 @@ describe('Use Case: Asymmetric Client-Server', () => {
|
|
|
82
82
|
|
|
83
83
|
// Note: In a real scenario, client imports definitions (types/schema) but not heavy deps.
|
|
84
84
|
// Here we reuse the atom definitions from 'batteries.ts' for the builder schema.
|
|
85
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
86
85
|
const { coreAtoms } = require('../runtime')
|
|
87
86
|
const clientBuilder = Agent.custom({
|
|
88
87
|
...coreAtoms,
|
|
@@ -154,7 +153,6 @@ describe('Use Case: Asymmetric Client-Server', () => {
|
|
|
154
153
|
llmPredictBattery,
|
|
155
154
|
})
|
|
156
155
|
|
|
157
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
158
156
|
const { coreAtoms } = require('../runtime')
|
|
159
157
|
const logic = Agent.custom({ ...coreAtoms, storeVectorize })
|
|
160
158
|
.step({ op: 'storeVectorize', text: 'fail' })
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
|
|
15
15
|
import { fromTS } from '../lang/emitters/from-ts'
|
|
16
16
|
import { tjs } from '../lang'
|
|
17
|
-
import { createRuntime } from '../lang/runtime'
|
|
18
17
|
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs'
|
|
19
18
|
import { join } from 'path'
|
|
20
19
|
|
package/src/vm/runtime.ts
CHANGED
|
@@ -2202,7 +2202,7 @@ export const fetch = defineAtom(
|
|
|
2202
2202
|
}
|
|
2203
2203
|
} catch (e: any) {
|
|
2204
2204
|
if (e.message.includes('allowedFetchDomains')) throw e
|
|
2205
|
-
throw new Error(`Invalid URL: ${url}
|
|
2205
|
+
throw new Error(`Invalid URL: ${url}`, { cause: e })
|
|
2206
2206
|
}
|
|
2207
2207
|
}
|
|
2208
2208
|
|
|
@@ -2476,7 +2476,7 @@ export const transpileCode = defineAtom(
|
|
|
2476
2476
|
try {
|
|
2477
2477
|
return ctx.capabilities.code.transpile(resolvedCode)
|
|
2478
2478
|
} catch (e: any) {
|
|
2479
|
-
throw new Error(`Code transpilation failed: ${e.message}
|
|
2479
|
+
throw new Error(`Code transpilation failed: ${e.message}`, { cause: e })
|
|
2480
2480
|
}
|
|
2481
2481
|
},
|
|
2482
2482
|
{ docs: 'Transpile AsyncJS code to AST', cost: 1 }
|
|
@@ -2538,7 +2538,7 @@ export const runCode = defineAtom(
|
|
|
2538
2538
|
try {
|
|
2539
2539
|
ast = ctx.capabilities.code.transpile(resolvedCode)
|
|
2540
2540
|
} catch (e: any) {
|
|
2541
|
-
throw new Error(`Code transpilation failed: ${e.message}
|
|
2541
|
+
throw new Error(`Code transpilation failed: ${e.message}`, { cause: e })
|
|
2542
2542
|
}
|
|
2543
2543
|
|
|
2544
2544
|
if (ast.op !== 'seq') {
|
package/src/vm/vm.ts
CHANGED
|
@@ -94,7 +94,9 @@ export class AgentVM<M extends Record<string, Atom<any, any>>> {
|
|
|
94
94
|
try {
|
|
95
95
|
ast = transpile(astOrToken).ast as BaseNode
|
|
96
96
|
} catch (e: any) {
|
|
97
|
-
throw new Error(`AJS transpilation failed: ${e.message}
|
|
97
|
+
throw new Error(`AJS transpilation failed: ${e.message}`, {
|
|
98
|
+
cause: e,
|
|
99
|
+
})
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
} else {
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export function calculate(x?: number, y?: number): any;
|
|
2
|
-
export namespace calculate {
|
|
3
|
-
namespace __tjs {
|
|
4
|
-
namespace params {
|
|
5
|
-
namespace x {
|
|
6
|
-
export namespace type {
|
|
7
|
-
let kind: string;
|
|
8
|
-
}
|
|
9
|
-
export let required: boolean;
|
|
10
|
-
let _default: null;
|
|
11
|
-
export { _default as default };
|
|
12
|
-
}
|
|
13
|
-
namespace y {
|
|
14
|
-
export namespace type_1 {
|
|
15
|
-
let kind_1: string;
|
|
16
|
-
export { kind_1 as kind };
|
|
17
|
-
}
|
|
18
|
-
export { type_1 as type };
|
|
19
|
-
let required_1: boolean;
|
|
20
|
-
export { required_1 as required };
|
|
21
|
-
let _default_1: null;
|
|
22
|
-
export { _default_1 as default };
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
namespace returns {
|
|
26
|
-
export namespace type_2 {
|
|
27
|
-
let kind_2: string;
|
|
28
|
-
export { kind_2 as kind };
|
|
29
|
-
}
|
|
30
|
-
export { type_2 as type };
|
|
31
|
-
}
|
|
32
|
-
let source: string;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
export function add(a?: number, b?: number): any;
|
|
2
|
-
export namespace add {
|
|
3
|
-
namespace __tjs {
|
|
4
|
-
namespace params {
|
|
5
|
-
namespace a {
|
|
6
|
-
export namespace type {
|
|
7
|
-
let kind: string;
|
|
8
|
-
}
|
|
9
|
-
export let required: boolean;
|
|
10
|
-
let _default: null;
|
|
11
|
-
export { _default as default };
|
|
12
|
-
}
|
|
13
|
-
namespace b {
|
|
14
|
-
export namespace type_1 {
|
|
15
|
-
let kind_1: string;
|
|
16
|
-
export { kind_1 as kind };
|
|
17
|
-
}
|
|
18
|
-
export { type_1 as type };
|
|
19
|
-
let required_1: boolean;
|
|
20
|
-
export { required_1 as required };
|
|
21
|
-
let _default_1: null;
|
|
22
|
-
export { _default_1 as default };
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
namespace returns {
|
|
26
|
-
export namespace type_2 {
|
|
27
|
-
let kind_2: string;
|
|
28
|
-
export { kind_2 as kind };
|
|
29
|
-
}
|
|
30
|
-
export { type_2 as type };
|
|
31
|
-
}
|
|
32
|
-
let source: string;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
export function subtract(a?: number, b?: number): any;
|
|
36
|
-
export namespace subtract {
|
|
37
|
-
export namespace __tjs_1 {
|
|
38
|
-
export namespace params_1 {
|
|
39
|
-
export namespace a_1 {
|
|
40
|
-
export namespace type_3 {
|
|
41
|
-
let kind_3: string;
|
|
42
|
-
export { kind_3 as kind };
|
|
43
|
-
}
|
|
44
|
-
export { type_3 as type };
|
|
45
|
-
let required_2: boolean;
|
|
46
|
-
export { required_2 as required };
|
|
47
|
-
let _default_2: null;
|
|
48
|
-
export { _default_2 as default };
|
|
49
|
-
}
|
|
50
|
-
export { a_1 as a };
|
|
51
|
-
export namespace b_1 {
|
|
52
|
-
export namespace type_4 {
|
|
53
|
-
let kind_4: string;
|
|
54
|
-
export { kind_4 as kind };
|
|
55
|
-
}
|
|
56
|
-
export { type_4 as type };
|
|
57
|
-
let required_3: boolean;
|
|
58
|
-
export { required_3 as required };
|
|
59
|
-
let _default_3: null;
|
|
60
|
-
export { _default_3 as default };
|
|
61
|
-
}
|
|
62
|
-
export { b_1 as b };
|
|
63
|
-
}
|
|
64
|
-
export { params_1 as params };
|
|
65
|
-
export namespace returns_1 {
|
|
66
|
-
export namespace type_5 {
|
|
67
|
-
let kind_5: string;
|
|
68
|
-
export { kind_5 as kind };
|
|
69
|
-
}
|
|
70
|
-
export { type_5 as type };
|
|
71
|
-
}
|
|
72
|
-
export { returns_1 as returns };
|
|
73
|
-
let source_1: string;
|
|
74
|
-
export { source_1 as source };
|
|
75
|
-
}
|
|
76
|
-
export { __tjs_1 as __tjs };
|
|
77
|
-
}
|
|
78
|
-
export function multiply(a?: number, b?: number): any;
|
|
79
|
-
export namespace multiply {
|
|
80
|
-
export namespace __tjs_2 {
|
|
81
|
-
export namespace params_2 {
|
|
82
|
-
export namespace a_2 {
|
|
83
|
-
export namespace type_6 {
|
|
84
|
-
let kind_6: string;
|
|
85
|
-
export { kind_6 as kind };
|
|
86
|
-
}
|
|
87
|
-
export { type_6 as type };
|
|
88
|
-
let required_4: boolean;
|
|
89
|
-
export { required_4 as required };
|
|
90
|
-
let _default_4: null;
|
|
91
|
-
export { _default_4 as default };
|
|
92
|
-
}
|
|
93
|
-
export { a_2 as a };
|
|
94
|
-
export namespace b_2 {
|
|
95
|
-
export namespace type_7 {
|
|
96
|
-
let kind_7: string;
|
|
97
|
-
export { kind_7 as kind };
|
|
98
|
-
}
|
|
99
|
-
export { type_7 as type };
|
|
100
|
-
let required_5: boolean;
|
|
101
|
-
export { required_5 as required };
|
|
102
|
-
let _default_5: null;
|
|
103
|
-
export { _default_5 as default };
|
|
104
|
-
}
|
|
105
|
-
export { b_2 as b };
|
|
106
|
-
}
|
|
107
|
-
export { params_2 as params };
|
|
108
|
-
export namespace returns_2 {
|
|
109
|
-
export namespace type_8 {
|
|
110
|
-
let kind_8: string;
|
|
111
|
-
export { kind_8 as kind };
|
|
112
|
-
}
|
|
113
|
-
export { type_8 as type };
|
|
114
|
-
}
|
|
115
|
-
export { returns_2 as returns };
|
|
116
|
-
let source_2: string;
|
|
117
|
-
export { source_2 as source };
|
|
118
|
-
}
|
|
119
|
-
export { __tjs_2 as __tjs };
|
|
120
|
-
}
|