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
|
@@ -223,6 +223,342 @@ export function extractWasmBlocks(source: string): {
|
|
|
223
223
|
return { source: result, blocks }
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Extract top-level `wasm function NAME(params): RetType { body }` declarations.
|
|
228
|
+
*
|
|
229
|
+
* Unlike `extractWasmBlocks` (which finds `wasm { ... }` blocks nested inside
|
|
230
|
+
* regular tjs functions with auto-captured variables), this extractor finds
|
|
231
|
+
* top-level wasm function declarations with explicit parameters and an
|
|
232
|
+
* optional return type. The body is the wasm-subset source.
|
|
233
|
+
*
|
|
234
|
+
* Each declaration becomes a `WasmBlock` whose `captures` array holds the
|
|
235
|
+
* function's parameters with their type annotations (e.g. `['a: Float32Array',
|
|
236
|
+
* 'b: Float32Array', 'n: i32']`). The block ID is derived from the function
|
|
237
|
+
* name (`__tjs_wasm_<name>`), so the JS-side wrapper can reference it.
|
|
238
|
+
*
|
|
239
|
+
* The declaration is replaced in source with a regular JS function that
|
|
240
|
+
* forwards its args to the wasm export, preserving the `export` modifier
|
|
241
|
+
* if present:
|
|
242
|
+
*
|
|
243
|
+
* (export)? wasm function dot(a: Float32Array, b: Float32Array, n: f64): f64 {
|
|
244
|
+
* <wasm-source>
|
|
245
|
+
* }
|
|
246
|
+
*
|
|
247
|
+
* becomes:
|
|
248
|
+
*
|
|
249
|
+
* (export)? function dot(a, b, n) { return globalThis.__tjs_wasm_dot(a, b, n) }
|
|
250
|
+
*
|
|
251
|
+
* This runs BEFORE `extractWasmBlocks` so its output (a regular JS function
|
|
252
|
+
* wrapper) isn't disturbed by the inline-block scanner.
|
|
253
|
+
*
|
|
254
|
+
* Return-type note: the underlying wasm bytecode builder currently emits f64
|
|
255
|
+
* or void return types only. The declared `: RetType` annotation is parsed
|
|
256
|
+
* and preserved on the block, but not yet validated against the backend's
|
|
257
|
+
* capabilities — `: f64` and omitted-return work today; other types (`: i32`,
|
|
258
|
+
* `: f32`, `: v128`) will be supported when the bytecode builder grows
|
|
259
|
+
* per-function return-type encoding.
|
|
260
|
+
*/
|
|
261
|
+
export function extractWasmFunctions(source: string): {
|
|
262
|
+
source: string
|
|
263
|
+
blocks: WasmBlock[]
|
|
264
|
+
} {
|
|
265
|
+
const blocks: WasmBlock[] = []
|
|
266
|
+
let result = ''
|
|
267
|
+
let i = 0
|
|
268
|
+
|
|
269
|
+
while (i < source.length) {
|
|
270
|
+
// Match: `(export )?wasm function NAME(` (with leading whitespace allowed)
|
|
271
|
+
// The leading `\b` ensures we don't match inside identifiers like `mywasm`.
|
|
272
|
+
const declRe = /^\b(export\s+)?wasm\s+function\s+(\w+)\s*\(/
|
|
273
|
+
const m = source.slice(i).match(declRe)
|
|
274
|
+
if (!m) {
|
|
275
|
+
result += source[i]
|
|
276
|
+
i++
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const hasExport = !!m[1]
|
|
281
|
+
const name = m[2]
|
|
282
|
+
const matchStart = i
|
|
283
|
+
|
|
284
|
+
// After the opening `(`: scan balanced parens for the params block
|
|
285
|
+
const parensStart = i + m[0].length
|
|
286
|
+
let parenDepth = 1
|
|
287
|
+
let j = parensStart
|
|
288
|
+
while (j < source.length && parenDepth > 0) {
|
|
289
|
+
if (source[j] === '(') parenDepth++
|
|
290
|
+
else if (source[j] === ')') parenDepth--
|
|
291
|
+
j++
|
|
292
|
+
}
|
|
293
|
+
if (parenDepth !== 0) {
|
|
294
|
+
// Unmatched parens — leave source alone and move on
|
|
295
|
+
result += source[i]
|
|
296
|
+
i++
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
let paramsSource = source.slice(parensStart, j - 1)
|
|
300
|
+
// j now points just past the closing `)`
|
|
301
|
+
|
|
302
|
+
// Phase 2: detect `(!` — reserved syntax for unsafe wasm functions
|
|
303
|
+
// (will allow host-import calls, side-effecting globals, etc.).
|
|
304
|
+
// Implementation deferred; for now we reject with a clear message so
|
|
305
|
+
// users either remove the bang or know to wait.
|
|
306
|
+
const unsafeMatch = paramsSource.match(/^\s*!/)
|
|
307
|
+
if (unsafeMatch) {
|
|
308
|
+
throw new SyntaxError(
|
|
309
|
+
`Unsafe wasm functions (with \`!\` marker) are reserved for a ` +
|
|
310
|
+
`future phase. Remove the bang from \`wasm function ${name}\` ` +
|
|
311
|
+
`to declare it as a regular (pure) wasm function, or wait until ` +
|
|
312
|
+
`the unsafe variant is implemented.`,
|
|
313
|
+
locAt(source, matchStart)
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Optional return-type annotation: `: TYPE` (TYPE is a single identifier).
|
|
318
|
+
// Pointer-style annotations like `Ptr<f32>` are reserved for a follow-up.
|
|
319
|
+
let returnType: string | undefined
|
|
320
|
+
let afterReturnType = j
|
|
321
|
+
const retMatch = source.slice(j).match(/^\s*:\s*(\w+)/)
|
|
322
|
+
if (retMatch) {
|
|
323
|
+
returnType = retMatch[1]
|
|
324
|
+
afterReturnType = j + retMatch[0].length
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Expect `{` next (with leading whitespace)
|
|
328
|
+
const braceMatch = source.slice(afterReturnType).match(/^\s*\{/)
|
|
329
|
+
if (!braceMatch) {
|
|
330
|
+
// Not a wasm function decl after all — pass through
|
|
331
|
+
result += source[i]
|
|
332
|
+
i++
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Find body via balanced braces
|
|
337
|
+
const bodyStart = afterReturnType + braceMatch[0].length
|
|
338
|
+
let braceDepth = 1
|
|
339
|
+
let k = bodyStart
|
|
340
|
+
while (k < source.length && braceDepth > 0) {
|
|
341
|
+
if (source[k] === '{') braceDepth++
|
|
342
|
+
else if (source[k] === '}') braceDepth--
|
|
343
|
+
k++
|
|
344
|
+
}
|
|
345
|
+
if (braceDepth !== 0) {
|
|
346
|
+
result += source[i]
|
|
347
|
+
i++
|
|
348
|
+
continue
|
|
349
|
+
}
|
|
350
|
+
const body = source.slice(bodyStart, k - 1)
|
|
351
|
+
// k now points just past the closing `}`
|
|
352
|
+
|
|
353
|
+
// Parse params into the existing `captures` shape used by the wasm
|
|
354
|
+
// compiler: each entry is either `name` or `name: type`.
|
|
355
|
+
const captures = parseWasmFunctionParams(paramsSource)
|
|
356
|
+
|
|
357
|
+
const id = `__tjs_wasm_${name}`
|
|
358
|
+
const block: WasmBlock = {
|
|
359
|
+
id,
|
|
360
|
+
name, // enables Phase 3 cross-file matching against imported symbols
|
|
361
|
+
returnType, // undefined when the user wrote no `: T` annotation
|
|
362
|
+
body,
|
|
363
|
+
captures,
|
|
364
|
+
start: matchStart,
|
|
365
|
+
end: k,
|
|
366
|
+
}
|
|
367
|
+
blocks.push(block)
|
|
368
|
+
|
|
369
|
+
// Generate the JS wrapper function. The wasm runtime sets globalThis[id]
|
|
370
|
+
// (with type-aware marshalling already baked into the wrapper); we just
|
|
371
|
+
// forward args. Preserve `export` if present.
|
|
372
|
+
const argNames = captures.map((c) => c.split(':')[0].trim())
|
|
373
|
+
const exportKeyword = hasExport ? 'export ' : ''
|
|
374
|
+
const wrapper = `${exportKeyword}function ${name}(${argNames.join(
|
|
375
|
+
', '
|
|
376
|
+
)}) { return globalThis.${id}(${argNames.join(', ')}) }`
|
|
377
|
+
|
|
378
|
+
result += wrapper
|
|
379
|
+
i = k
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return { source: result, blocks }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Phase 3: cross-file wasm-function composition.
|
|
387
|
+
*
|
|
388
|
+
* Scans the consumer's source for `import { name1, name2, ... } from 'spec'`
|
|
389
|
+
* statements. For each, resolves the spec via the supplied ModuleLoader; if
|
|
390
|
+
* the imported file is tjs/ts and one of the imported names corresponds to a
|
|
391
|
+
* `wasm function` declared there, the function's WasmBlock is pulled into the
|
|
392
|
+
* consumer's compilation and replaced in source by a local JS wrapper. The
|
|
393
|
+
* import statement is rewritten to remove the satisfied names (or removed
|
|
394
|
+
* entirely if every imported name was wasm-composed).
|
|
395
|
+
*
|
|
396
|
+
* This is the heart of the cross-file composition story: imported wasm
|
|
397
|
+
* functions become local functions in the consumer's single WebAssembly.Module
|
|
398
|
+
* (via the Phase 0.5 consolidated-module path). The library's transpiled .js
|
|
399
|
+
* is NOT involved — the source is consumed at transpile time.
|
|
400
|
+
*
|
|
401
|
+
* Caller is responsible for providing a configured ModuleLoader. When no
|
|
402
|
+
* loader is supplied (the common case before Phase 3 is fully wired up), this
|
|
403
|
+
* function returns the source unchanged with an empty blocks array.
|
|
404
|
+
*
|
|
405
|
+
* @param source the consumer's source (after extractWasmFunctions on its own
|
|
406
|
+
* wasm functions, before transformParenExpressions)
|
|
407
|
+
* @param options.loader the ModuleLoader to resolve imports through
|
|
408
|
+
* @param options.importerPath the path of the file being transpiled (used as
|
|
409
|
+
* the resolver's importer context); optional
|
|
410
|
+
* @returns updated source + the list of imported wasm function blocks
|
|
411
|
+
*/
|
|
412
|
+
export function composeImportedWasmFunctions(
|
|
413
|
+
source: string,
|
|
414
|
+
options: {
|
|
415
|
+
loader?: {
|
|
416
|
+
load(specifier: string, importerPath?: string): {
|
|
417
|
+
parseResult: { wasmBlocks: WasmBlock[] }
|
|
418
|
+
} | null
|
|
419
|
+
}
|
|
420
|
+
importerPath?: string
|
|
421
|
+
}
|
|
422
|
+
): { source: string; blocks: WasmBlock[] } {
|
|
423
|
+
const { loader, importerPath } = options
|
|
424
|
+
if (!loader) return { source, blocks: [] }
|
|
425
|
+
|
|
426
|
+
const composedBlocks: WasmBlock[] = []
|
|
427
|
+
// Track imported names that have been composed, so we don't pull the same
|
|
428
|
+
// wasm function in twice if it's imported from multiple specifiers.
|
|
429
|
+
const composedNames = new Set<string>()
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Pull a wasm function into the consumer's composition and recursively
|
|
433
|
+
* pull in any other wasm functions it calls from the same source module.
|
|
434
|
+
*
|
|
435
|
+
* Transitive walking is required for correctness: if `outer` calls
|
|
436
|
+
* `inner` and the consumer only imports `outer`, we still need `inner`
|
|
437
|
+
* in the consumer's wasm module — otherwise outer's body's
|
|
438
|
+
* `call <inner-index>` instruction would reference a function index
|
|
439
|
+
* that doesn't exist. The wasm compiler would (correctly) reject the
|
|
440
|
+
* call at body-compile time.
|
|
441
|
+
*
|
|
442
|
+
* The body scan uses word-boundary regex against the known wasm-function
|
|
443
|
+
* names in the source module. False positives (matching a method call
|
|
444
|
+
* `obj.inner(x)` or a shadowed local) only cause bloat — not correctness
|
|
445
|
+
* problems — and TJS wasm bodies don't use either of those forms anyway.
|
|
446
|
+
*/
|
|
447
|
+
function pullInTransitively(
|
|
448
|
+
block: WasmBlock,
|
|
449
|
+
sourceModuleFns: Map<string, WasmBlock>
|
|
450
|
+
): void {
|
|
451
|
+
if (composedNames.has(block.id)) return
|
|
452
|
+
composedBlocks.push(block)
|
|
453
|
+
composedNames.add(block.id)
|
|
454
|
+
// Scan the body for calls to other wasm functions in this source module
|
|
455
|
+
for (const [name, target] of sourceModuleFns) {
|
|
456
|
+
if (name === block.name) continue // self isn't transitive
|
|
457
|
+
if (composedNames.has(target.id)) continue
|
|
458
|
+
const callRe = new RegExp(`\\b${name}\\s*\\(`)
|
|
459
|
+
if (callRe.test(block.body)) {
|
|
460
|
+
pullInTransitively(target, sourceModuleFns)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Match `import { ... } from 'spec'` or `import { ... } from "spec"`.
|
|
466
|
+
// Default imports, namespace imports, and side-effect imports are left
|
|
467
|
+
// alone — only named-bindings are candidates for wasm composition.
|
|
468
|
+
// Multiline imports are supported via the [^}]*? non-greedy match.
|
|
469
|
+
const importRe = /^(\s*)import\s*\{([^}]*?)\}\s*from\s*(['"])([^'"]+)\3\s*;?\s*$/gm
|
|
470
|
+
|
|
471
|
+
const replaced = source.replace(
|
|
472
|
+
importRe,
|
|
473
|
+
(match, indent: string, bindings: string, _quote, spec: string) => {
|
|
474
|
+
const mod = loader.load(spec, importerPath)
|
|
475
|
+
if (!mod) return match // not a loadable tjs/ts module — leave as-is
|
|
476
|
+
|
|
477
|
+
const importedWasmFunctions = new Map<string, WasmBlock>()
|
|
478
|
+
for (const b of mod.parseResult.wasmBlocks) {
|
|
479
|
+
if (b.name) importedWasmFunctions.set(b.name, b)
|
|
480
|
+
}
|
|
481
|
+
if (importedWasmFunctions.size === 0) return match // no wasm here
|
|
482
|
+
|
|
483
|
+
// Parse the bindings list: `{ a, b as c, d }`
|
|
484
|
+
// Each binding is `name` or `name as local` (we keep `local` as the
|
|
485
|
+
// local name; for wasm-composed names, `local` must equal the wasm
|
|
486
|
+
// function name since the local wrapper uses the original name).
|
|
487
|
+
const parts = bindings
|
|
488
|
+
.split(',')
|
|
489
|
+
.map((p) => p.trim())
|
|
490
|
+
.filter((p) => p.length > 0)
|
|
491
|
+
|
|
492
|
+
const wrappers: string[] = []
|
|
493
|
+
const remainingBindings: string[] = []
|
|
494
|
+
|
|
495
|
+
for (const part of parts) {
|
|
496
|
+
const m = part.match(/^(\w+)(?:\s+as\s+(\w+))?$/)
|
|
497
|
+
if (!m) {
|
|
498
|
+
// Couldn't parse — be conservative and keep it as-is
|
|
499
|
+
remainingBindings.push(part)
|
|
500
|
+
continue
|
|
501
|
+
}
|
|
502
|
+
const imported = m[1]
|
|
503
|
+
const local = m[2] ?? m[1]
|
|
504
|
+
const wasmBlock = importedWasmFunctions.get(imported)
|
|
505
|
+
if (!wasmBlock) {
|
|
506
|
+
// Not a wasm function in the source module — keep the import
|
|
507
|
+
remainingBindings.push(part)
|
|
508
|
+
continue
|
|
509
|
+
}
|
|
510
|
+
// Composed: pull the block in (with any transitive callees from
|
|
511
|
+
// the same source module) and emit a local wrapper that uses the
|
|
512
|
+
// LOCAL name (in case of `as` renames) but forwards to the wasm
|
|
513
|
+
// export's original id. Transitive walking is what makes
|
|
514
|
+
// `import { outer } from 'lib'` work when outer internally calls
|
|
515
|
+
// inner — both end up in the consumer's composed module.
|
|
516
|
+
pullInTransitively(wasmBlock, importedWasmFunctions)
|
|
517
|
+
const argNames = wasmBlock.captures.map((c) =>
|
|
518
|
+
c.split(':')[0].trim()
|
|
519
|
+
)
|
|
520
|
+
wrappers.push(
|
|
521
|
+
`function ${local}(${argNames.join(
|
|
522
|
+
', '
|
|
523
|
+
)}) { return globalThis.${wasmBlock.id}(${argNames.join(', ')}) }`
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const wrapperBlock = wrappers.join('\n')
|
|
528
|
+
|
|
529
|
+
if (remainingBindings.length === 0) {
|
|
530
|
+
// All names were composed — drop the import statement entirely
|
|
531
|
+
return wrapperBlock ? `${indent}${wrapperBlock}` : `${indent}`
|
|
532
|
+
}
|
|
533
|
+
// Some names remain — keep them as a smaller import
|
|
534
|
+
const trimmedImport = `${indent}import { ${remainingBindings.join(
|
|
535
|
+
', '
|
|
536
|
+
)} } from '${spec}'`
|
|
537
|
+
return wrapperBlock
|
|
538
|
+
? `${trimmedImport}\n${indent}${wrapperBlock}`
|
|
539
|
+
: trimmedImport
|
|
540
|
+
}
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
return { source: replaced, blocks: composedBlocks }
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Split a parameter list source into individual param strings.
|
|
548
|
+
* Returns entries like `name` (no type) or `name: type` (with type).
|
|
549
|
+
* Type values are single identifiers (`i32`, `Float32Array`, etc.) — generic
|
|
550
|
+
* forms like `Ptr<f32>` are reserved for a follow-up phase.
|
|
551
|
+
*/
|
|
552
|
+
function parseWasmFunctionParams(paramsSource: string): string[] {
|
|
553
|
+
const trimmed = paramsSource.trim()
|
|
554
|
+
if (!trimmed) return []
|
|
555
|
+
// Simple comma split is fine for the current type syntax (no generics yet).
|
|
556
|
+
return trimmed
|
|
557
|
+
.split(',')
|
|
558
|
+
.map((s) => s.trim())
|
|
559
|
+
.filter((s) => s.length > 0)
|
|
560
|
+
}
|
|
561
|
+
|
|
226
562
|
/** Check if an identifier is a WASM SIMD intrinsic (not a captured variable) */
|
|
227
563
|
function isWasmIntrinsic(name: string): boolean {
|
|
228
564
|
return name.startsWith('f32x4_') || name.startsWith('v128_')
|
package/src/lang/parser-types.ts
CHANGED
|
@@ -15,6 +15,17 @@ export interface ParseOptions {
|
|
|
15
15
|
* When true, skips == to Is() transformation since the VM handles == correctly.
|
|
16
16
|
*/
|
|
17
17
|
vmTarget?: boolean
|
|
18
|
+
/**
|
|
19
|
+
* Optional ModuleLoader for cross-file `wasm function` composition (Phase 3).
|
|
20
|
+
* When provided, imports are resolved at transpile time and matching wasm
|
|
21
|
+
* functions are composed into the consumer's WebAssembly.Module. When
|
|
22
|
+
* omitted, imports are preserved verbatim (the default — runtime resolves
|
|
23
|
+
* them as before).
|
|
24
|
+
*
|
|
25
|
+
* Type is left as `any` here to avoid a circular import with module-loader.ts;
|
|
26
|
+
* callers should pass a `ModuleLoader` instance.
|
|
27
|
+
*/
|
|
28
|
+
moduleLoader?: any
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
/**
|
|
@@ -37,6 +48,21 @@ export interface ParseOptions {
|
|
|
37
48
|
export interface WasmBlock {
|
|
38
49
|
/** Unique ID for this block */
|
|
39
50
|
id: string
|
|
51
|
+
/**
|
|
52
|
+
* Declared function name (only set for top-level `wasm function NAME(...)`
|
|
53
|
+
* declarations — Phase 1+). Used by Phase 3 cross-file composition to
|
|
54
|
+
* match an imported symbol against a wasm function declaration. Inline
|
|
55
|
+
* `wasm {}` blocks have no name and don't participate in composition.
|
|
56
|
+
*/
|
|
57
|
+
name?: string
|
|
58
|
+
/**
|
|
59
|
+
* Declared return-type annotation, e.g. `'f64'`. Only set for top-level
|
|
60
|
+
* `wasm function NAME(...): RetType` declarations; presence/absence is
|
|
61
|
+
* used to determine `hasReturn` BEFORE the body is compiled, so the
|
|
62
|
+
* function index map can be built up-front for wasm-to-wasm calls.
|
|
63
|
+
* Inline blocks have no declared return type.
|
|
64
|
+
*/
|
|
65
|
+
returnType?: string
|
|
40
66
|
/** The body (JS subset that compiles to WASM, also used as fallback) */
|
|
41
67
|
body: string
|
|
42
68
|
/** Explicit fallback body (only if different from body) */
|
|
@@ -83,6 +109,13 @@ export interface PreprocessOptions {
|
|
|
83
109
|
* Default: false (transform == to Is() for TJS code running in regular JS)
|
|
84
110
|
*/
|
|
85
111
|
vmTarget?: boolean
|
|
112
|
+
/**
|
|
113
|
+
* Optional ModuleLoader for cross-file `wasm function` composition (Phase 3).
|
|
114
|
+
* See ParseOptions.moduleLoader for details.
|
|
115
|
+
*/
|
|
116
|
+
moduleLoader?: any
|
|
117
|
+
/** Path of the file being preprocessed (used as importer context). */
|
|
118
|
+
filename?: string
|
|
86
119
|
}
|
|
87
120
|
|
|
88
121
|
/**
|
package/src/lang/parser.ts
CHANGED
|
@@ -31,6 +31,8 @@ import { transformParenExpressions } from './parser-params'
|
|
|
31
31
|
import {
|
|
32
32
|
transformTryWithoutCatch,
|
|
33
33
|
extractWasmBlocks,
|
|
34
|
+
extractWasmFunctions,
|
|
35
|
+
composeImportedWasmFunctions,
|
|
34
36
|
transformIsOperators,
|
|
35
37
|
insertAsiProtection,
|
|
36
38
|
transformEqualityToStructural,
|
|
@@ -262,6 +264,14 @@ export function preprocess(
|
|
|
262
264
|
source = letAnnoResult.source
|
|
263
265
|
const letAnnotations = letAnnoResult.annotations
|
|
264
266
|
|
|
267
|
+
// Extract `wasm function NAME(...) { ... }` declarations EARLY, before
|
|
268
|
+
// any source-level transforms that would mangle wasm-body text. In
|
|
269
|
+
// particular, the equality transforms below rewrite `==` to `Eq()` and
|
|
270
|
+
// `Is`/`IsNot` to function calls — wasm bodies use literal operators
|
|
271
|
+
// and shouldn't be affected.
|
|
272
|
+
const wasmFunctions = extractWasmFunctions(source)
|
|
273
|
+
source = wasmFunctions.source
|
|
274
|
+
|
|
265
275
|
// Transform Is/IsNot infix operators to function calls
|
|
266
276
|
// a Is b -> Is(a, b)
|
|
267
277
|
// a IsNot b -> IsNot(a, b)
|
|
@@ -290,6 +300,18 @@ export function preprocess(
|
|
|
290
300
|
// Foo = ... -> const Foo = ...
|
|
291
301
|
source = transformBareAssignments(source)
|
|
292
302
|
|
|
303
|
+
// Phase 3: cross-file wasm-function composition. When a ModuleLoader is
|
|
304
|
+
// supplied, resolve `import { ... } from '<spec>'` statements at transpile
|
|
305
|
+
// time. Any imported names that correspond to `wasm function` declarations
|
|
306
|
+
// in the source module get pulled into the consumer's wasm module, with
|
|
307
|
+
// the import statement rewritten to a local JS wrapper. No loader supplied
|
|
308
|
+
// = no behavior change (imports stay verbatim, runtime resolves them).
|
|
309
|
+
const importedWasm = composeImportedWasmFunctions(source, {
|
|
310
|
+
loader: options.moduleLoader,
|
|
311
|
+
importerPath: options.filename,
|
|
312
|
+
})
|
|
313
|
+
source = importedWasm.source
|
|
314
|
+
|
|
293
315
|
// Unified paren expression transformer
|
|
294
316
|
// Handles: function params, arrow params, return types, safe/unsafe markers
|
|
295
317
|
// Model: open paren can be ( or (? or (!, close can be ) or )-> or )-? or )-!
|
|
@@ -324,9 +346,24 @@ export function preprocess(
|
|
|
324
346
|
source = polyResult.source
|
|
325
347
|
|
|
326
348
|
// Extract WASM blocks: wasm(args) { ... } fallback { ... }
|
|
349
|
+
// `wasm function` declarations are already extracted earlier in the pipeline
|
|
350
|
+
// (see above, before transformParenExpressions). This finds the remaining
|
|
351
|
+
// inline `wasm { ... }` blocks inside regular tjs functions.
|
|
327
352
|
const wasmBlocks = extractWasmBlocks(source)
|
|
328
353
|
source = wasmBlocks.source
|
|
329
354
|
|
|
355
|
+
// Combine all flavors of wasm blocks for the downstream emitter.
|
|
356
|
+
// They're indistinguishable from the compiler's perspective — all have
|
|
357
|
+
// an id, body, captures, and need the same module composition treatment.
|
|
358
|
+
// - wasmFunctions: top-level `wasm function NAME(...)` decls in this file
|
|
359
|
+
// - importedWasm: cross-file `wasm function`s pulled in via Phase 3
|
|
360
|
+
// - wasmBlocks: inline `wasm { ... }` blocks nested in tjs functions
|
|
361
|
+
const allWasmBlocks = [
|
|
362
|
+
...wasmFunctions.blocks,
|
|
363
|
+
...importedWasm.blocks,
|
|
364
|
+
...wasmBlocks.blocks,
|
|
365
|
+
]
|
|
366
|
+
|
|
330
367
|
// Extract and run test blocks: test 'desc'? { body }
|
|
331
368
|
// Tests run at transpile time and are stripped from output
|
|
332
369
|
const testResult = extractAndRunTests(source, options.dangerouslySkipTests)
|
|
@@ -381,7 +418,7 @@ export function preprocess(
|
|
|
381
418
|
requiredParams,
|
|
382
419
|
unsafeFunctions,
|
|
383
420
|
safeFunctions,
|
|
384
|
-
wasmBlocks:
|
|
421
|
+
wasmBlocks: allWasmBlocks,
|
|
385
422
|
tests: testResult.tests,
|
|
386
423
|
testErrors: testResult.errors,
|
|
387
424
|
polymorphicNames: polyResult.polymorphicNames,
|
|
@@ -433,7 +470,11 @@ export function parse(
|
|
|
433
470
|
letAnnotations,
|
|
434
471
|
tjsModes,
|
|
435
472
|
} = colonShorthand
|
|
436
|
-
? preprocess(source, {
|
|
473
|
+
? preprocess(source, {
|
|
474
|
+
vmTarget,
|
|
475
|
+
moduleLoader: options.moduleLoader,
|
|
476
|
+
filename: options.filename,
|
|
477
|
+
})
|
|
437
478
|
: {
|
|
438
479
|
source,
|
|
439
480
|
returnType: undefined,
|