tjs-lang 0.7.8 → 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 +9 -0
- package/demo/docs.json +64 -16
- package/demo/src/ts-examples.ts +8 -8
- package/package.json +7 -3
- package/src/lang/docs.test.ts +148 -0
- package/src/lang/docs.ts +49 -15
- package/src/lang/emitters/js-wasm.ts +57 -65
- package/src/lang/emitters/js.ts +16 -1
- package/src/lang/features.test.ts +4 -3
- package/src/lang/index.ts +9 -0
- package/src/lang/module-loader.test.ts +318 -0
- package/src/lang/module-loader.ts +419 -0
- package/src/lang/parser-transforms.ts +336 -0
- package/src/lang/parser-types.ts +33 -0
- package/src/lang/parser.ts +43 -2
- 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/demo/src/ts-examples.ts
CHANGED
|
@@ -423,14 +423,14 @@ console.log('(2 + 3) * 4 =', calc.add(2).add(3).multiply(4).getResult())
|
|
|
423
423
|
description:
|
|
424
424
|
'Complete example showing the TS -> TJS -> JS value proposition',
|
|
425
425
|
group: 'advanced',
|
|
426
|
-
code:
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
426
|
+
code: `/*#
|
|
427
|
+
## The Full Picture
|
|
428
|
+
|
|
429
|
+
TypeScript promises type safety.
|
|
430
|
+
TJS delivers it at RUNTIME.
|
|
431
|
+
|
|
432
|
+
This is what "TS keeps its promise" means.
|
|
433
|
+
*/
|
|
434
434
|
|
|
435
435
|
// Define your types with standard TypeScript syntax
|
|
436
436
|
interface Product {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tjs-lang",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Type-safe JavaScript dialect with runtime validation, sandboxed VM execution, and AI agent orchestration. Transpiles TypeScript to validated JS with fuel-metered execution for untrusted code.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -53,6 +53,10 @@
|
|
|
53
53
|
"types": "./dist/src/batteries/index.d.ts",
|
|
54
54
|
"default": "./dist/tjs-batteries.js"
|
|
55
55
|
},
|
|
56
|
+
"./linalg": {
|
|
57
|
+
"bun": "./src/linalg/index.tjs",
|
|
58
|
+
"default": "./dist/tjs-linalg.js"
|
|
59
|
+
},
|
|
56
60
|
"./src": "./src/index.ts",
|
|
57
61
|
"./editors/monaco": "./editors/monaco/ajs-monarch.js",
|
|
58
62
|
"./editors/codemirror": "./editors/codemirror/ajs-language.js",
|
|
@@ -131,8 +135,8 @@
|
|
|
131
135
|
"functions:build": "cd functions && npm run build",
|
|
132
136
|
"functions:deploy": "cd functions && npm run deploy",
|
|
133
137
|
"functions:serve": "cd functions && npm run serve",
|
|
134
|
-
"deploy:hosting": "firebase deploy --only hosting",
|
|
135
|
-
"deploy": "
|
|
138
|
+
"deploy:hosting": "bun run build:demo && firebase deploy --only hosting",
|
|
139
|
+
"deploy": "bun run build:demo && bun run functions:deploy && firebase deploy --only hosting",
|
|
136
140
|
"start": "bun run build:demo && bun run dev"
|
|
137
141
|
},
|
|
138
142
|
"dependencies": {
|
package/src/lang/docs.test.ts
CHANGED
|
@@ -157,6 +157,154 @@ function second(x: 0): 0 { return x }
|
|
|
157
157
|
})
|
|
158
158
|
})
|
|
159
159
|
|
|
160
|
+
describe('JSDoc-style doc blocks', () => {
|
|
161
|
+
it('extracts /** */ blocks and strips leading asterisks', () => {
|
|
162
|
+
const source = `
|
|
163
|
+
/**
|
|
164
|
+
* # Title
|
|
165
|
+
*
|
|
166
|
+
* Body line 1
|
|
167
|
+
* Body line 2
|
|
168
|
+
*/
|
|
169
|
+
`
|
|
170
|
+
const result = generateDocs(source)
|
|
171
|
+
|
|
172
|
+
expect(result.items).toHaveLength(1)
|
|
173
|
+
const doc = result.items[0] as any
|
|
174
|
+
expect(doc.type).toBe('doc')
|
|
175
|
+
expect(doc.content).toBe('# Title\n\nBody line 1\nBody line 2')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('handles single-line JSDoc', () => {
|
|
179
|
+
const source = `/** A short note. */`
|
|
180
|
+
const result = generateDocs(source)
|
|
181
|
+
|
|
182
|
+
const doc = result.items[0] as any
|
|
183
|
+
expect(doc.type).toBe('doc')
|
|
184
|
+
expect(doc.content).toBe('A short note.')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('preserves markdown lists and tables', () => {
|
|
188
|
+
const source = `
|
|
189
|
+
/**
|
|
190
|
+
* ## Options
|
|
191
|
+
*
|
|
192
|
+
* | Flag | Meaning |
|
|
193
|
+
* |------|---------|
|
|
194
|
+
* | \`-v\` | verbose |
|
|
195
|
+
*
|
|
196
|
+
* - first
|
|
197
|
+
* - second
|
|
198
|
+
*/
|
|
199
|
+
`
|
|
200
|
+
const result = generateDocs(source)
|
|
201
|
+
|
|
202
|
+
const doc = result.items[0] as any
|
|
203
|
+
expect(doc.content).toContain('## Options')
|
|
204
|
+
expect(doc.content).toContain('| Flag | Meaning |')
|
|
205
|
+
expect(doc.content).toContain('- first')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('leaves @param / @returns as plain markdown', () => {
|
|
209
|
+
const source = `
|
|
210
|
+
/**
|
|
211
|
+
* Square the input.
|
|
212
|
+
*
|
|
213
|
+
* @param x - the input
|
|
214
|
+
* @returns the squared value
|
|
215
|
+
*/
|
|
216
|
+
function square(x: 0): 0 { return x * x }
|
|
217
|
+
`
|
|
218
|
+
const result = generateDocs(source)
|
|
219
|
+
|
|
220
|
+
const doc = result.items[0] as any
|
|
221
|
+
expect(doc.content).toContain('@param x - the input')
|
|
222
|
+
expect(doc.content).toContain('@returns the squared value')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('skips JSDoc inside function bodies', () => {
|
|
226
|
+
const source = `
|
|
227
|
+
function outer() {
|
|
228
|
+
/**
|
|
229
|
+
* Should not be extracted — inside a body.
|
|
230
|
+
*/
|
|
231
|
+
return 1
|
|
232
|
+
}
|
|
233
|
+
`
|
|
234
|
+
const result = generateDocs(source)
|
|
235
|
+
|
|
236
|
+
// Only the function itself, no doc item
|
|
237
|
+
const docs = result.items.filter((i) => i.type === 'doc')
|
|
238
|
+
expect(docs).toHaveLength(0)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('skips empty JSDoc blocks', () => {
|
|
242
|
+
const source = `
|
|
243
|
+
/**
|
|
244
|
+
*
|
|
245
|
+
*/
|
|
246
|
+
function f() {}
|
|
247
|
+
`
|
|
248
|
+
const result = generateDocs(source)
|
|
249
|
+
|
|
250
|
+
const docs = result.items.filter((i) => i.type === 'doc')
|
|
251
|
+
expect(docs).toHaveLength(0)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('does not treat /* ... */ as a doc comment', () => {
|
|
255
|
+
const source = `
|
|
256
|
+
/* just a regular block comment */
|
|
257
|
+
function f() {}
|
|
258
|
+
`
|
|
259
|
+
const result = generateDocs(source)
|
|
260
|
+
|
|
261
|
+
const docs = result.items.filter((i) => i.type === 'doc')
|
|
262
|
+
expect(docs).toHaveLength(0)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('interleaves JSDoc with functions in document order', () => {
|
|
266
|
+
const source = `
|
|
267
|
+
/**
|
|
268
|
+
* # First
|
|
269
|
+
*/
|
|
270
|
+
function first(x: 0): 0 { return x }
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* # Second
|
|
274
|
+
*/
|
|
275
|
+
function second(x: 0): 0 { return x }
|
|
276
|
+
`
|
|
277
|
+
const result = generateDocs(source)
|
|
278
|
+
|
|
279
|
+
expect(result.items).toHaveLength(4)
|
|
280
|
+
expect(result.items[0].type).toBe('doc')
|
|
281
|
+
expect((result.items[0] as any).content).toContain('# First')
|
|
282
|
+
expect(result.items[1].type).toBe('function')
|
|
283
|
+
expect(result.items[2].type).toBe('doc')
|
|
284
|
+
expect((result.items[2] as any).content).toContain('# Second')
|
|
285
|
+
expect(result.items[3].type).toBe('function')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('coexists with /*# blocks in the same file', () => {
|
|
289
|
+
const source = `
|
|
290
|
+
/*#
|
|
291
|
+
## TJS-native
|
|
292
|
+
*/
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* ## JSDoc-native
|
|
296
|
+
*/
|
|
297
|
+
function f(x: 0): 0 { return x }
|
|
298
|
+
`
|
|
299
|
+
const result = generateDocs(source)
|
|
300
|
+
|
|
301
|
+
const docs = result.items.filter((i) => i.type === 'doc') as any[]
|
|
302
|
+
expect(docs).toHaveLength(2)
|
|
303
|
+
expect(docs[0].content).toContain('TJS-native')
|
|
304
|
+
expect(docs[1].content).toContain('JSDoc-native')
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
160
308
|
describe('markdown output', () => {
|
|
161
309
|
it('renders doc blocks as plain markdown', () => {
|
|
162
310
|
const source = `
|
package/src/lang/docs.ts
CHANGED
|
@@ -120,6 +120,34 @@ export type DocItem =
|
|
|
120
120
|
members: string[] // constructor / method signatures, no bodies
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// Dedent a block of text by the smallest leading-whitespace indent
|
|
124
|
+
// found across non-empty lines.
|
|
125
|
+
function dedent(content: string): string {
|
|
126
|
+
const lines = content.split('\n')
|
|
127
|
+
const minIndent = lines
|
|
128
|
+
.filter((line) => line.trim().length > 0)
|
|
129
|
+
.reduce((min, line) => {
|
|
130
|
+
const indent = line.match(/^(\s*)/)?.[1].length || 0
|
|
131
|
+
return Math.min(min, indent)
|
|
132
|
+
}, Infinity)
|
|
133
|
+
if (minIndent === 0 || minIndent === Infinity) return content
|
|
134
|
+
return lines.map((line) => line.slice(minIndent)).join('\n')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Strip the leading ` * ` from each line of a JSDoc comment body.
|
|
138
|
+
// The first line (between `/**` and the first newline) is left alone —
|
|
139
|
+
// content can appear there directly. Empty `*`-only lines become blank.
|
|
140
|
+
function stripJSDocAsterisks(content: string): string {
|
|
141
|
+
const lines = content.split('\n')
|
|
142
|
+
return lines
|
|
143
|
+
.map((line, i) => {
|
|
144
|
+
if (i === 0) return line
|
|
145
|
+
// Remove optional leading whitespace, then `*`, then optional single space
|
|
146
|
+
return line.replace(/^[ \t]*\*[ \t]?/, '')
|
|
147
|
+
})
|
|
148
|
+
.join('\n')
|
|
149
|
+
}
|
|
150
|
+
|
|
123
151
|
/**
|
|
124
152
|
* Generate documentation from TJS source
|
|
125
153
|
*
|
|
@@ -137,8 +165,12 @@ export function generateDocs(source: string): DocResult {
|
|
|
137
165
|
// shown in `/*# ... */` doc blocks as real declarations.
|
|
138
166
|
const isInComment = computeInComment(source)
|
|
139
167
|
|
|
140
|
-
// Find all doc blocks, functions, and classes; sort by position
|
|
168
|
+
// Find all doc blocks, functions, and classes; sort by position.
|
|
169
|
+
// Two doc-block flavors are recognized:
|
|
170
|
+
// /*# ... */ — TJS native: content is markdown verbatim
|
|
171
|
+
// /** ... */ — JSDoc: each line's leading ` * ` is stripped, then markdown
|
|
141
172
|
const docPattern = /\/\*#([\s\S]*?)\*\//g
|
|
173
|
+
const jsdocPattern = /\/\*\*([\s\S]*?)\*\//g
|
|
142
174
|
// Match the START of a function declaration. Params (which can contain
|
|
143
175
|
// nested parens like `fn = (x) => x`) are captured by balanced-paren
|
|
144
176
|
// scanning below, NOT by this regex.
|
|
@@ -155,24 +187,26 @@ export function generateDocs(source: string): DocResult {
|
|
|
155
187
|
continue
|
|
156
188
|
}
|
|
157
189
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
190
|
+
const content = dedent(match[1]).trim()
|
|
191
|
+
if (!content) continue
|
|
192
|
+
|
|
193
|
+
matches.push({
|
|
194
|
+
type: 'doc',
|
|
195
|
+
index: match.index,
|
|
196
|
+
data: content,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
while ((match = jsdocPattern.exec(source)) !== null) {
|
|
201
|
+
if (braceDepthAt[match.index] !== 0) continue
|
|
202
|
+
|
|
203
|
+
const content = dedent(stripJSDocAsterisks(match[1])).trim()
|
|
204
|
+
if (!content) continue
|
|
171
205
|
|
|
172
206
|
matches.push({
|
|
173
207
|
type: 'doc',
|
|
174
208
|
index: match.index,
|
|
175
|
-
data: content
|
|
209
|
+
data: content,
|
|
176
210
|
})
|
|
177
211
|
}
|
|
178
212
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TJS WASM Bootstrap Generation
|
|
3
3
|
*
|
|
4
|
-
* Compiles
|
|
4
|
+
* Compiles the file's WASM blocks into a single WebAssembly.Module with
|
|
5
|
+
* one exported function per block, then emits JavaScript that compiles
|
|
6
|
+
* and instantiates the module once at startup. This is the foundation
|
|
7
|
+
* for cross-file wasm composition (see wasm-library-plan.md, Phase 3) —
|
|
8
|
+
* once everything's in one module, intra-module calls cost nothing.
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
|
-
import {
|
|
11
|
+
import { compileBlocksToModule } from '../wasm'
|
|
8
12
|
import type { WasmBlock } from '../parser'
|
|
9
13
|
|
|
10
14
|
export function generateWasmBootstrap(blocks: WasmBlock[]): {
|
|
@@ -16,75 +20,61 @@ export function generateWasmBootstrap(blocks: WasmBlock[]): {
|
|
|
16
20
|
byteLength?: number
|
|
17
21
|
}[]
|
|
18
22
|
} {
|
|
19
|
-
const
|
|
20
|
-
id: string
|
|
21
|
-
success: boolean
|
|
22
|
-
error?: string
|
|
23
|
-
byteLength?: number
|
|
24
|
-
}[] = []
|
|
25
|
-
const compiledBlocks: {
|
|
26
|
-
id: string
|
|
27
|
-
base64: string
|
|
28
|
-
captures: string[]
|
|
29
|
-
needsMemory: boolean
|
|
30
|
-
wat: string
|
|
31
|
-
}[] = []
|
|
23
|
+
const compiled = compileBlocksToModule(blocks)
|
|
32
24
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
compiledBlocks.push({
|
|
39
|
-
id: block.id,
|
|
40
|
-
base64,
|
|
41
|
-
captures: block.captures,
|
|
42
|
-
needsMemory: result.needsMemory ?? false,
|
|
43
|
-
wat: result.wat ?? '',
|
|
44
|
-
})
|
|
45
|
-
results.push({
|
|
46
|
-
id: block.id,
|
|
47
|
-
success: true,
|
|
48
|
-
byteLength: result.bytes.length,
|
|
49
|
-
})
|
|
50
|
-
} else {
|
|
51
|
-
results.push({
|
|
52
|
-
id: block.id,
|
|
53
|
-
success: false,
|
|
54
|
-
error: result.error,
|
|
55
|
-
})
|
|
25
|
+
// Map per-block status to the public result shape (preserves input order)
|
|
26
|
+
const exportById = new Map(compiled.exports.map((e) => [e.id, e]))
|
|
27
|
+
const results = compiled.results.map((r) => {
|
|
28
|
+
if (!r.success) {
|
|
29
|
+
return { id: r.id, success: false, error: r.error }
|
|
56
30
|
}
|
|
57
|
-
|
|
31
|
+
const exp = exportById.get(r.id)!
|
|
32
|
+
return {
|
|
33
|
+
id: r.id,
|
|
34
|
+
success: true,
|
|
35
|
+
byteLength: compiled.bytes.length,
|
|
36
|
+
// Per-export byte length isn't meaningful in the consolidated module;
|
|
37
|
+
// we report the total module size for every successful block.
|
|
38
|
+
_exportName: exp.exportName,
|
|
39
|
+
}
|
|
40
|
+
})
|
|
58
41
|
|
|
59
|
-
if (
|
|
42
|
+
if (compiled.exports.length === 0) {
|
|
60
43
|
return { code: '', results }
|
|
61
44
|
}
|
|
62
45
|
|
|
63
|
-
//
|
|
64
|
-
const watComments =
|
|
65
|
-
.map((
|
|
66
|
-
const watLines =
|
|
67
|
-
return `/**\n * WASM: ${
|
|
46
|
+
// WAT comment block — one section per included function
|
|
47
|
+
const watComments = compiled.exports
|
|
48
|
+
.map((e) => {
|
|
49
|
+
const watLines = e.wat.split('\n').map((line) => ` * ${line}`)
|
|
50
|
+
return `/**\n * WASM: ${e.id} (export: ${e.exportName})\n${watLines.join(
|
|
51
|
+
'\n'
|
|
52
|
+
)}\n */`
|
|
68
53
|
})
|
|
69
54
|
.join('\n')
|
|
70
55
|
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
56
|
+
// Per-export metadata embedded in the bootstrap.
|
|
57
|
+
// id = original block id (becomes globalThis[id])
|
|
58
|
+
// n = export name in the composed module (instance.exports[n])
|
|
59
|
+
// c = capture annotations for type-aware wrapping
|
|
60
|
+
// m = whether this export uses memory (must be true if any other
|
|
61
|
+
// export uses it, since memory is shared at module level)
|
|
62
|
+
const exportData = compiled.exports
|
|
74
63
|
.map(
|
|
75
|
-
(
|
|
76
|
-
`{id:${JSON.stringify(
|
|
77
|
-
|
|
78
|
-
)},c:${JSON.stringify(
|
|
64
|
+
(e) =>
|
|
65
|
+
`{id:${JSON.stringify(e.id)},n:${JSON.stringify(
|
|
66
|
+
e.exportName
|
|
67
|
+
)},c:${JSON.stringify(e.captures)},m:${e.needsMemory}}`
|
|
79
68
|
)
|
|
80
69
|
.join(',')
|
|
81
70
|
|
|
82
|
-
|
|
83
|
-
const anyNeedsMemory =
|
|
71
|
+
const moduleBase64 = btoa(String.fromCharCode(...compiled.bytes))
|
|
72
|
+
const anyNeedsMemory = compiled.needsMemory
|
|
84
73
|
|
|
85
74
|
const code = `${watComments}
|
|
86
75
|
;(async()=>{
|
|
87
|
-
const
|
|
76
|
+
const __wasmExports=[${exportData}];
|
|
77
|
+
const __wasmModuleB64=${JSON.stringify(moduleBase64)};
|
|
88
78
|
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};
|
|
89
79
|
const __parseType=c=>{const m=c.match(/^(\\w+)\\s*:\\s*(\\w+)$/);if(!m)return{n:c,t:'f64',a:false};const[,n,ts]=m;const at={Float32Array:'f32',Float64Array:'f64',Int32Array:'i32',Uint8Array:'i32'};if(at[ts])return{n,t:'i32',a:true,at:ts};return{n,t:'f64',a:false}};
|
|
90
80
|
${
|
|
@@ -94,30 +84,32 @@ let __woff=0;
|
|
|
94
84
|
globalThis.wasmBuffer=function(Ctor,len){const bytes=len*Ctor.BYTES_PER_ELEMENT;const align=Math.max(Ctor.BYTES_PER_ELEMENT,16);__woff=(__woff+align-1)&~(align-1);const arr=new Ctor(__wasmMem.buffer,__woff,len);__woff+=bytes;return arr};`
|
|
95
85
|
: ''
|
|
96
86
|
}
|
|
97
|
-
|
|
98
|
-
|
|
87
|
+
const __wasmInst=await WebAssembly.instantiate(await WebAssembly.compile(__b64ToBytes(__wasmModuleB64)),${
|
|
88
|
+
anyNeedsMemory ? '{env:{memory:__wasmMem}}' : '{}'
|
|
89
|
+
});
|
|
90
|
+
for(const{id,n,c,m}of __wasmExports){
|
|
91
|
+
const compute=__wasmInst.exports[n];
|
|
99
92
|
const params=c.map(__parseType);
|
|
100
93
|
const hasArrays=params.some(p=>p.a);
|
|
101
|
-
const mem=m?__wasmMem:null;
|
|
102
|
-
const imp=mem?{env:{memory:mem}}:{};
|
|
103
|
-
const inst=await WebAssembly.instantiate(await WebAssembly.compile(bytes),imp);
|
|
104
|
-
const compute=inst.exports.compute;
|
|
105
94
|
if(!hasArrays){globalThis[id]=compute;continue}
|
|
106
95
|
globalThis[id]=function(...args){
|
|
107
|
-
const mv=new Uint8Array(
|
|
96
|
+
const mv=new Uint8Array(__wasmMem.buffer);let off=__woff;const ptrs=[];
|
|
108
97
|
for(let i=0;i<params.length;i++){const p=params[i],a=args[i];
|
|
109
98
|
if(p.a&&a?.buffer){
|
|
110
|
-
if(a.buffer===
|
|
99
|
+
if(a.buffer===__wasmMem.buffer){ptrs.push(a.byteOffset)}
|
|
111
100
|
else{const ab=new Uint8Array(a.buffer,a.byteOffset,a.byteLength);off=(off+15)&~15;mv.set(ab,off);ptrs.push(off);off+=ab.length}
|
|
112
101
|
} else ptrs.push(a)}
|
|
113
102
|
const r=compute(...ptrs);off=__woff;
|
|
114
103
|
for(let i=0;i<params.length;i++){const p=params[i],a=args[i];
|
|
115
104
|
if(p.a&&a?.buffer){
|
|
116
|
-
if(a.buffer===
|
|
105
|
+
if(a.buffer===__wasmMem.buffer) continue;
|
|
117
106
|
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}}
|
|
118
107
|
return r};
|
|
119
108
|
}})();
|
|
120
109
|
`.trim()
|
|
121
110
|
|
|
122
|
-
|
|
111
|
+
// Strip the temporary _exportName field before returning to caller.
|
|
112
|
+
const publicResults = results.map(({ _exportName: _, ...rest }) => rest)
|
|
113
|
+
|
|
114
|
+
return { code, results: publicResults }
|
|
123
115
|
}
|
package/src/lang/emitters/js.ts
CHANGED
|
@@ -101,6 +101,15 @@ export interface TJSTranspileOptions {
|
|
|
101
101
|
* Used when tests depend on imported modules.
|
|
102
102
|
*/
|
|
103
103
|
resolvedImports?: Record<string, string>
|
|
104
|
+
/**
|
|
105
|
+
* Optional ModuleLoader for cross-file `wasm function` composition (Phase 3
|
|
106
|
+
* of the wasm-library plan). When provided, `import { dot } from
|
|
107
|
+
* 'tjs-lang/linalg'`-style imports are resolved at transpile time and any
|
|
108
|
+
* matching `wasm function` declarations are composed into this file's
|
|
109
|
+
* consolidated WebAssembly.Module. When omitted, imports are preserved
|
|
110
|
+
* verbatim (default behavior — runtime resolves them).
|
|
111
|
+
*/
|
|
112
|
+
moduleLoader?: any
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
/** Result of running tests at transpile time */
|
|
@@ -631,13 +640,19 @@ export function transpileToJS(
|
|
|
631
640
|
} = parse(cleanSource, {
|
|
632
641
|
filename,
|
|
633
642
|
colonShorthand: true,
|
|
643
|
+
moduleLoader: options.moduleLoader,
|
|
634
644
|
})
|
|
635
645
|
|
|
636
646
|
// Find ALL functions in the program
|
|
637
647
|
const functions = findAllFunctions(program)
|
|
638
648
|
|
|
639
649
|
// Preprocess source (handles TJS syntax transformations)
|
|
640
|
-
|
|
650
|
+
// Pass through the moduleLoader so Phase 3 cross-file wasm composition
|
|
651
|
+
// sees imported `wasm function` declarations.
|
|
652
|
+
const preprocessed = preprocess(cleanSource, {
|
|
653
|
+
moduleLoader: options.moduleLoader,
|
|
654
|
+
filename,
|
|
655
|
+
})
|
|
641
656
|
|
|
642
657
|
// Apply the same source-level equality transforms to extracted test/mock
|
|
643
658
|
// bodies so they observe the module's TJS semantics (e.g. structural ==).
|
|
@@ -1524,9 +1524,10 @@ function double(x: 0, y: 0) {
|
|
|
1524
1524
|
expect(result.wasmCompiled?.[0].success).toBe(true)
|
|
1525
1525
|
expect(result.wasmCompiled?.[0].byteLength).toBeGreaterThan(0)
|
|
1526
1526
|
|
|
1527
|
-
// Output should contain base64-encoded WASM
|
|
1528
|
-
|
|
1529
|
-
expect(result.code).toContain('
|
|
1527
|
+
// Output should contain base64-encoded WASM. After Phase 0.5 consolidation
|
|
1528
|
+
// the emitter ships one module per file and a per-export metadata table.
|
|
1529
|
+
expect(result.code).toContain('__wasmExports')
|
|
1530
|
+
expect(result.code).toContain('__wasmModuleB64')
|
|
1530
1531
|
|
|
1531
1532
|
// Execute with async function to allow WASM instantiation
|
|
1532
1533
|
const fn = new Function(
|
package/src/lang/index.ts
CHANGED
|
@@ -74,6 +74,15 @@ export {
|
|
|
74
74
|
type FunctionTypeInfo,
|
|
75
75
|
type ParamTypeInfo,
|
|
76
76
|
} from './docs'
|
|
77
|
+
export {
|
|
78
|
+
ModuleLoader,
|
|
79
|
+
inMemoryFileSystem,
|
|
80
|
+
type ModuleLoaderOptions,
|
|
81
|
+
type FileSystem,
|
|
82
|
+
type LoadedModule,
|
|
83
|
+
type ImportEntry,
|
|
84
|
+
type ExportEntry,
|
|
85
|
+
} from './module-loader'
|
|
77
86
|
export {
|
|
78
87
|
extractTests,
|
|
79
88
|
assertFunction,
|