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.
Files changed (62) hide show
  1. package/CLAUDE.md +14 -1
  2. package/CONTEXT.md +4 -0
  3. package/demo/docs.json +66 -696
  4. package/demo/src/ts-examples.ts +8 -8
  5. package/dist/eslint.config.d.ts +2 -0
  6. package/dist/index.js +137 -135
  7. package/dist/index.js.map +4 -4
  8. package/dist/src/lang/emitters/js-wasm.d.ts +5 -1
  9. package/dist/src/lang/emitters/js.d.ts +9 -0
  10. package/dist/src/lang/index.d.ts +1 -0
  11. package/dist/src/lang/module-loader.d.ts +125 -0
  12. package/dist/src/lang/parser-transforms.d.ts +79 -0
  13. package/dist/src/lang/parser-types.d.ts +33 -0
  14. package/dist/src/lang/wasm.d.ts +67 -1
  15. package/dist/tjs-batteries.js +2 -2
  16. package/dist/tjs-batteries.js.map +2 -2
  17. package/dist/tjs-eval.js +39 -37
  18. package/dist/tjs-eval.js.map +3 -3
  19. package/dist/tjs-from-ts.js +2 -2
  20. package/dist/tjs-from-ts.js.map +2 -2
  21. package/dist/tjs-lang.js +102 -102
  22. package/dist/tjs-lang.js.map +3 -3
  23. package/dist/tjs-vm.js +50 -48
  24. package/dist/tjs-vm.js.map +3 -3
  25. package/docs/README.md +2 -0
  26. package/docs/lm-studio-setup.md +143 -0
  27. package/docs/universal-endpoint.md +122 -0
  28. package/llms.txt +8 -2
  29. package/package.json +11 -6
  30. package/src/batteries/audit.ts +3 -3
  31. package/src/batteries/llm.ts +8 -3
  32. package/src/builder.ts +0 -3
  33. package/src/cli/commands/test.ts +1 -1
  34. package/src/lang/docs.test.ts +148 -1
  35. package/src/lang/docs.ts +49 -15
  36. package/src/lang/emitters/from-ts.ts +1 -1
  37. package/src/lang/emitters/js-wasm.ts +57 -65
  38. package/src/lang/emitters/js.ts +16 -2
  39. package/src/lang/features.test.ts +4 -3
  40. package/src/lang/index.ts +9 -0
  41. package/src/lang/linter.ts +1 -1
  42. package/src/lang/module-loader.test.ts +322 -0
  43. package/src/lang/module-loader.ts +418 -0
  44. package/src/lang/parser-params.ts +1 -1
  45. package/src/lang/parser-transforms.ts +339 -9
  46. package/src/lang/parser-types.ts +33 -0
  47. package/src/lang/parser.ts +43 -2
  48. package/src/lang/perf.test.ts +10 -4
  49. package/src/lang/runtime.ts +0 -1
  50. package/src/lang/wasm.test.ts +1293 -2
  51. package/src/lang/wasm.ts +470 -87
  52. package/src/linalg/index.tjs +119 -0
  53. package/src/linalg/linalg.test.ts +300 -0
  54. package/src/linalg/vector-search.bench.test.ts +416 -0
  55. package/src/types/Type.ts +6 -6
  56. package/src/use-cases/asymmetric-client-server.test.ts +0 -2
  57. package/src/use-cases/client-server.test.ts +1 -1
  58. package/src/use-cases/unbundled-imports.test.ts +0 -1
  59. package/src/vm/runtime.ts +3 -3
  60. package/src/vm/vm.ts +3 -1
  61. package/dist/examples/modules/dist/main.d.ts +0 -34
  62. package/dist/examples/modules/dist/math.d.ts +0 -120
@@ -223,6 +223,344 @@ 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
+ const 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(
417
+ specifier: string,
418
+ importerPath?: string
419
+ ): {
420
+ parseResult: { wasmBlocks: WasmBlock[] }
421
+ } | null
422
+ }
423
+ importerPath?: string
424
+ }
425
+ ): { source: string; blocks: WasmBlock[] } {
426
+ const { loader, importerPath } = options
427
+ if (!loader) return { source, blocks: [] }
428
+
429
+ const composedBlocks: WasmBlock[] = []
430
+ // Track imported names that have been composed, so we don't pull the same
431
+ // wasm function in twice if it's imported from multiple specifiers.
432
+ const composedNames = new Set<string>()
433
+
434
+ /**
435
+ * Pull a wasm function into the consumer's composition and recursively
436
+ * pull in any other wasm functions it calls from the same source module.
437
+ *
438
+ * Transitive walking is required for correctness: if `outer` calls
439
+ * `inner` and the consumer only imports `outer`, we still need `inner`
440
+ * in the consumer's wasm module — otherwise outer's body's
441
+ * `call <inner-index>` instruction would reference a function index
442
+ * that doesn't exist. The wasm compiler would (correctly) reject the
443
+ * call at body-compile time.
444
+ *
445
+ * The body scan uses word-boundary regex against the known wasm-function
446
+ * names in the source module. False positives (matching a method call
447
+ * `obj.inner(x)` or a shadowed local) only cause bloat — not correctness
448
+ * problems — and TJS wasm bodies don't use either of those forms anyway.
449
+ */
450
+ function pullInTransitively(
451
+ block: WasmBlock,
452
+ sourceModuleFns: Map<string, WasmBlock>
453
+ ): void {
454
+ if (composedNames.has(block.id)) return
455
+ composedBlocks.push(block)
456
+ composedNames.add(block.id)
457
+ // Scan the body for calls to other wasm functions in this source module
458
+ for (const [name, target] of sourceModuleFns) {
459
+ if (name === block.name) continue // self isn't transitive
460
+ if (composedNames.has(target.id)) continue
461
+ const callRe = new RegExp(`\\b${name}\\s*\\(`)
462
+ if (callRe.test(block.body)) {
463
+ pullInTransitively(target, sourceModuleFns)
464
+ }
465
+ }
466
+ }
467
+
468
+ // Match `import { ... } from 'spec'` or `import { ... } from "spec"`.
469
+ // Default imports, namespace imports, and side-effect imports are left
470
+ // alone — only named-bindings are candidates for wasm composition.
471
+ // Multiline imports are supported via the [^}]*? non-greedy match.
472
+ const importRe =
473
+ /^(\s*)import\s*\{([^}]*?)\}\s*from\s*(['"])([^'"]+)\3\s*;?\s*$/gm
474
+
475
+ const replaced = source.replace(
476
+ importRe,
477
+ (match, indent: string, bindings: string, _quote, spec: string) => {
478
+ const mod = loader.load(spec, importerPath)
479
+ if (!mod) return match // not a loadable tjs/ts module — leave as-is
480
+
481
+ const importedWasmFunctions = new Map<string, WasmBlock>()
482
+ for (const b of mod.parseResult.wasmBlocks) {
483
+ if (b.name) importedWasmFunctions.set(b.name, b)
484
+ }
485
+ if (importedWasmFunctions.size === 0) return match // no wasm here
486
+
487
+ // Parse the bindings list: `{ a, b as c, d }`
488
+ // Each binding is `name` or `name as local` (we keep `local` as the
489
+ // local name; for wasm-composed names, `local` must equal the wasm
490
+ // function name since the local wrapper uses the original name).
491
+ const parts = bindings
492
+ .split(',')
493
+ .map((p) => p.trim())
494
+ .filter((p) => p.length > 0)
495
+
496
+ const wrappers: string[] = []
497
+ const remainingBindings: string[] = []
498
+
499
+ for (const part of parts) {
500
+ const m = part.match(/^(\w+)(?:\s+as\s+(\w+))?$/)
501
+ if (!m) {
502
+ // Couldn't parse — be conservative and keep it as-is
503
+ remainingBindings.push(part)
504
+ continue
505
+ }
506
+ const imported = m[1]
507
+ const local = m[2] ?? m[1]
508
+ const wasmBlock = importedWasmFunctions.get(imported)
509
+ if (!wasmBlock) {
510
+ // Not a wasm function in the source module — keep the import
511
+ remainingBindings.push(part)
512
+ continue
513
+ }
514
+ // Composed: pull the block in (with any transitive callees from
515
+ // the same source module) and emit a local wrapper that uses the
516
+ // LOCAL name (in case of `as` renames) but forwards to the wasm
517
+ // export's original id. Transitive walking is what makes
518
+ // `import { outer } from 'lib'` work when outer internally calls
519
+ // inner — both end up in the consumer's composed module.
520
+ pullInTransitively(wasmBlock, importedWasmFunctions)
521
+ const argNames = wasmBlock.captures.map((c) => c.split(':')[0].trim())
522
+ wrappers.push(
523
+ `function ${local}(${argNames.join(', ')}) { return globalThis.${
524
+ wasmBlock.id
525
+ }(${argNames.join(', ')}) }`
526
+ )
527
+ }
528
+
529
+ const wrapperBlock = wrappers.join('\n')
530
+
531
+ if (remainingBindings.length === 0) {
532
+ // All names were composed — drop the import statement entirely
533
+ return wrapperBlock ? `${indent}${wrapperBlock}` : `${indent}`
534
+ }
535
+ // Some names remain — keep them as a smaller import
536
+ const trimmedImport = `${indent}import { ${remainingBindings.join(
537
+ ', '
538
+ )} } from '${spec}'`
539
+ return wrapperBlock
540
+ ? `${trimmedImport}\n${indent}${wrapperBlock}`
541
+ : trimmedImport
542
+ }
543
+ )
544
+
545
+ return { source: replaced, blocks: composedBlocks }
546
+ }
547
+
548
+ /**
549
+ * Split a parameter list source into individual param strings.
550
+ * Returns entries like `name` (no type) or `name: type` (with type).
551
+ * Type values are single identifiers (`i32`, `Float32Array`, etc.) — generic
552
+ * forms like `Ptr<f32>` are reserved for a follow-up phase.
553
+ */
554
+ function parseWasmFunctionParams(paramsSource: string): string[] {
555
+ const trimmed = paramsSource.trim()
556
+ if (!trimmed) return []
557
+ // Simple comma split is fine for the current type syntax (no generics yet).
558
+ return trimmed
559
+ .split(',')
560
+ .map((s) => s.trim())
561
+ .filter((s) => s.length > 0)
562
+ }
563
+
226
564
  /** Check if an identifier is a WASM SIMD intrinsic (not a captured variable) */
227
565
  function isWasmIntrinsic(name: string): boolean {
228
566
  return name.startsWith('f32x4_') || name.startsWith('v128_')
@@ -3040,15 +3378,7 @@ export function transformPolymorphicConstructors(
3040
3378
  })
3041
3379
  }
3042
3380
 
3043
- // Keep the first constructor in the class, remove the rest
3044
- // Build new class body with only the first constructor
3045
- let newBody = body.slice(0, ctors[0].fullEnd)
3046
- // Skip subsequent constructors
3047
- const afterLastCtor = ctors[ctors.length - 1].fullEnd
3048
- newBody += body.slice(afterLastCtor)
3049
-
3050
- // But we need to remove just the extra constructors, keeping other methods
3051
- // Better approach: remove constructors 2..N from the body
3381
+ // Remove the extra constructors, keeping the first constructor and other methods
3052
3382
  let cleanBody = body
3053
3383
  for (let i = ctors.length - 1; i >= 1; i--) {
3054
3384
  const ctor = ctors[i]
@@ -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
  /**
@@ -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: wasmBlocks.blocks,
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, { vmTarget })
473
+ ? preprocess(source, {
474
+ vmTarget,
475
+ moduleLoader: options.moduleLoader,
476
+ filename: options.filename,
477
+ })
437
478
  : {
438
479
  source,
439
480
  returnType: undefined,
@@ -60,8 +60,12 @@ describe('TJS Performance', () => {
60
60
  console.log(` Median: ${median.toFixed(0)}ms`)
61
61
  console.log(` Max: ${max.toFixed(0)}ms`)
62
62
 
63
- // Cold start should be under 200ms
64
- expect(median).toBeLessThan(200)
63
+ // Regression guardrail, not a micro-benchmark: this wall-clock is
64
+ // dominated by `bun` process spawn + module-graph load (the one-line
65
+ // transpile is negligible), so it's machine- and load-dependent. Keep the
66
+ // bar generous — it catches a gross regression (cold start ballooning to
67
+ // seconds) without flaking on a loaded box or slower CI.
68
+ expect(median).toBeLessThan(500)
65
69
  })
66
70
 
67
71
  it('should measure tjs emit time', async () => {
@@ -84,7 +88,8 @@ describe('TJS Performance', () => {
84
88
  console.log(`\n tjs emit cold start (5 runs):`)
85
89
  console.log(` Median: ${median.toFixed(0)}ms`)
86
90
 
87
- expect(median).toBeLessThan(200)
91
+ // Generous regression guardrail — see the tjsx cold-start note above.
92
+ expect(median).toBeLessThan(500)
88
93
  })
89
94
 
90
95
  it('should measure tjs check time', async () => {
@@ -107,7 +112,8 @@ describe('TJS Performance', () => {
107
112
  console.log(`\n tjs check cold start (5 runs):`)
108
113
  console.log(` Median: ${median.toFixed(0)}ms`)
109
114
 
110
- expect(median).toBeLessThan(200)
115
+ // Generous regression guardrail — see the tjsx cold-start note above.
116
+ expect(median).toBeLessThan(500)
111
117
  })
112
118
  })
113
119
 
@@ -73,7 +73,6 @@ export {
73
73
  }
74
74
 
75
75
  // Version from package.json - injected at build time or imported
76
- // eslint-disable-next-line @typescript-eslint/no-var-requires
77
76
  const pkg = require('../../package.json') as { version: string }
78
77
 
79
78
  export const TJS_VERSION: string = pkg.version