tjs-lang 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CLAUDE.md +99 -33
  2. package/bin/docs.js +4 -1
  3. package/demo/docs.json +104 -22
  4. package/demo/src/examples.test.ts +1 -0
  5. package/demo/src/imports.test.ts +16 -4
  6. package/demo/src/imports.ts +60 -15
  7. package/demo/src/playground-shared.ts +9 -8
  8. package/demo/src/tfs-worker.js +205 -147
  9. package/demo/src/tjs-playground.ts +34 -10
  10. package/demo/src/ts-examples.ts +8 -8
  11. package/demo/src/ts-playground.ts +24 -8
  12. package/dist/index.js +118 -101
  13. package/dist/index.js.map +4 -4
  14. package/dist/src/lang/bool-coercion.d.ts +50 -0
  15. package/dist/src/lang/docs.d.ts +31 -6
  16. package/dist/src/lang/linter.d.ts +8 -0
  17. package/dist/src/lang/parser-transforms.d.ts +18 -0
  18. package/dist/src/lang/parser-types.d.ts +2 -0
  19. package/dist/src/lang/parser.d.ts +3 -0
  20. package/dist/src/lang/runtime.d.ts +34 -0
  21. package/dist/src/lang/types.d.ts +9 -1
  22. package/dist/src/rbac/index.d.ts +1 -1
  23. package/dist/src/vm/runtime.d.ts +1 -1
  24. package/dist/tjs-eval.js +38 -36
  25. package/dist/tjs-eval.js.map +4 -4
  26. package/dist/tjs-from-ts.js +20 -20
  27. package/dist/tjs-from-ts.js.map +3 -3
  28. package/dist/tjs-lang.js +85 -83
  29. package/dist/tjs-lang.js.map +4 -4
  30. package/dist/tjs-vm.js +47 -45
  31. package/dist/tjs-vm.js.map +4 -4
  32. package/llms.txt +79 -0
  33. package/package.json +9 -4
  34. package/src/cli/commands/convert.test.ts +16 -21
  35. package/src/lang/bool-coercion.test.ts +203 -0
  36. package/src/lang/bool-coercion.ts +314 -0
  37. package/src/lang/codegen.test.ts +137 -0
  38. package/src/lang/docs.test.ts +476 -1
  39. package/src/lang/docs.ts +471 -37
  40. package/src/lang/emitters/ast.ts +11 -12
  41. package/src/lang/emitters/dts.test.ts +41 -0
  42. package/src/lang/emitters/dts.ts +9 -0
  43. package/src/lang/emitters/js-tests.ts +9 -4
  44. package/src/lang/emitters/js-wasm.ts +57 -65
  45. package/src/lang/emitters/js.ts +198 -3
  46. package/src/lang/features.test.ts +4 -3
  47. package/src/lang/index.ts +9 -0
  48. package/src/lang/inference.ts +54 -0
  49. package/src/lang/linter.test.ts +104 -1
  50. package/src/lang/linter.ts +124 -1
  51. package/src/lang/module-loader.test.ts +318 -0
  52. package/src/lang/module-loader.ts +419 -0
  53. package/src/lang/parser-params.ts +31 -0
  54. package/src/lang/parser-transforms.ts +640 -0
  55. package/src/lang/parser-types.ts +35 -0
  56. package/src/lang/parser.test.ts +73 -1
  57. package/src/lang/parser.ts +77 -3
  58. package/src/lang/runtime.ts +98 -0
  59. package/src/lang/types.ts +6 -0
  60. package/src/lang/wasm.test.ts +1293 -2
  61. package/src/lang/wasm.ts +470 -87
  62. package/src/linalg/index.tjs +119 -0
  63. package/src/linalg/linalg.test.ts +294 -0
  64. package/src/linalg/vector-search.bench.test.ts +395 -0
  65. package/src/rbac/index.ts +2 -2
  66. package/src/rbac/rules.tjs.d.ts +9 -0
  67. package/src/vm/atoms/batteries.ts +2 -2
  68. package/src/vm/runtime.ts +10 -3
  69. package/dist/src/rbac/rules.d.ts +0 -184
  70. package/src/rbac/rules.js +0 -338
package/src/lang/wasm.ts CHANGED
@@ -596,6 +596,21 @@ interface TypedParam {
596
596
  // Compilation Context
597
597
  // ============================================================================
598
598
 
599
+ /**
600
+ * Signature of a wasm function in the current module. Used to resolve
601
+ * cross-function calls (wasm-to-wasm `call <index>` instructions) without
602
+ * routing through JS. Populated by compileBlocksToModule before compiling
603
+ * any individual body, so forward references and mutual recursion work.
604
+ */
605
+ export interface ModuleFunctionSig {
606
+ /** Function index in the composed module */
607
+ index: number
608
+ /** Parameter types (used for arg-type checking + auto-conversion) */
609
+ params: TypedParam[]
610
+ /** Whether this function returns a value (f64) or is void */
611
+ hasReturn: boolean
612
+ }
613
+
599
614
  interface CompileContext {
600
615
  /** Parameter definitions */
601
616
  params: TypedParam[]
@@ -621,9 +636,19 @@ interface CompileContext {
621
636
  wat: string[]
622
637
  /** Current indentation level for WAT */
623
638
  watIndent: number
639
+ /**
640
+ * Other wasm functions in the same composed module that this body can
641
+ * call directly via `call <index>` instructions (no JS↔wasm boundary).
642
+ * Populated when compiling via compileBlocksToModule; empty for the
643
+ * single-block compileToWasm path.
644
+ */
645
+ moduleFunctions: Map<string, ModuleFunctionSig>
624
646
  }
625
647
 
626
- function createContext(params: TypedParam[]): CompileContext {
648
+ function createContext(
649
+ params: TypedParam[],
650
+ moduleFunctions: Map<string, ModuleFunctionSig> = new Map()
651
+ ): CompileContext {
627
652
  const ctx: CompileContext = {
628
653
  params,
629
654
  locals: new Map(),
@@ -637,6 +662,7 @@ function createContext(params: TypedParam[]): CompileContext {
637
662
  hasReturn: false,
638
663
  wat: [],
639
664
  watIndent: 1,
665
+ moduleFunctions,
640
666
  }
641
667
 
642
668
  // Add params to locals map
@@ -939,6 +965,10 @@ function inferExprType(
939
965
  const name = (call.callee as acorn.Identifier).name
940
966
  if (name === 'f32x4_extract_lane') return 'f32'
941
967
  if (name.startsWith('f32x4_') || name === 'v128_load') return 'v128'
968
+ // Wasm-to-wasm call: returns f64 if the called function has a return,
969
+ // or i32 (the dummy 0 pushed for void calls — see compileWasmFunctionCall)
970
+ const fn = ctx.moduleFunctions.get(name)
971
+ if (fn) return fn.hasReturn ? 'f64' : 'i32'
942
972
  }
943
973
  return 'f64'
944
974
  }
@@ -1579,9 +1609,27 @@ function compileCall(
1579
1609
 
1580
1610
  // Handle SIMD intrinsics: f32x4_xxx(...), v128_load(...)
1581
1611
  if (node.callee.type === 'Identifier') {
1582
- const name = (node.callee as acorn.Identifier).name
1583
- if (name.startsWith('f32x4_') || name.startsWith('v128_')) {
1584
- return compileSIMDCall(name, node.arguments as acorn.Expression[], ctx)
1612
+ const calleeName = (node.callee as acorn.Identifier).name
1613
+ if (calleeName.startsWith('f32x4_') || calleeName.startsWith('v128_')) {
1614
+ return compileSIMDCall(
1615
+ calleeName,
1616
+ node.arguments as acorn.Expression[],
1617
+ ctx
1618
+ )
1619
+ }
1620
+
1621
+ // Wasm-to-wasm call: the callee is another `wasm function` in the same
1622
+ // composed module. This is the cross-function `call <index>` path —
1623
+ // the consumer's wasm body calls a library kernel directly, with no
1624
+ // JS↔wasm boundary crossing.
1625
+ const fn = ctx.moduleFunctions.get(calleeName)
1626
+ if (fn) {
1627
+ return compileWasmFunctionCall(
1628
+ fn,
1629
+ calleeName,
1630
+ node.arguments as acorn.Expression[],
1631
+ ctx
1632
+ )
1585
1633
  }
1586
1634
  }
1587
1635
 
@@ -1589,6 +1637,92 @@ function compileCall(
1589
1637
  return [Op.f64_const, ...encodeF64(0)]
1590
1638
  }
1591
1639
 
1640
+ /**
1641
+ * Emit a `call <index>` to another wasm function in the same module.
1642
+ * Each argument is compiled and, if its inferred type doesn't match the
1643
+ * called function's parameter type, an explicit conversion instruction is
1644
+ * inserted (truncate for f→i, promote/demote for f32↔f64, etc.).
1645
+ * Void-returning calls push a dummy `i32.const 0` so an enclosing
1646
+ * ExpressionStatement's automatic drop has something to discard.
1647
+ */
1648
+ function compileWasmFunctionCall(
1649
+ fn: ModuleFunctionSig,
1650
+ name: string,
1651
+ args: acorn.Expression[],
1652
+ ctx: CompileContext
1653
+ ): number[] {
1654
+ if (args.length !== fn.params.length) {
1655
+ ctx.errors.push(
1656
+ `wasm function ${name} expects ${fn.params.length} arguments, got ${args.length}`
1657
+ )
1658
+ return [Op.f64_const, ...encodeF64(0)]
1659
+ }
1660
+
1661
+ const code: number[] = []
1662
+ for (let i = 0; i < args.length; i++) {
1663
+ const arg = args[i]
1664
+ const paramType = fn.params[i].type
1665
+ const argType = inferExprType(arg, ctx)
1666
+ code.push(...compileExpression(arg, ctx))
1667
+ // Insert conversion when arg type doesn't match param type.
1668
+ // Source target: argType → paramType.
1669
+ if (argType !== paramType) {
1670
+ const conv = convertOp(argType, paramType)
1671
+ if (conv === undefined) {
1672
+ ctx.errors.push(
1673
+ `wasm function ${name} param ${i} expects ${paramType}, got ${argType} (no conversion available)`
1674
+ )
1675
+ } else if (conv !== null) {
1676
+ code.push(conv)
1677
+ }
1678
+ }
1679
+ }
1680
+
1681
+ code.push(Op.call, ...encodeULEB128(fn.index))
1682
+
1683
+ // Void calls don't leave a value on the stack — push a dummy i32 0 so
1684
+ // ExpressionStatement's drop has something to remove. Same pattern used
1685
+ // by f32x4_store (line ~1632) for stores that have no return value.
1686
+ if (!fn.hasReturn) {
1687
+ code.push(Op.i32_const, 0)
1688
+ }
1689
+
1690
+ return code
1691
+ }
1692
+
1693
+ /**
1694
+ * Return the wasm opcode that converts `from` to `to`, or:
1695
+ * - `null` when no conversion is needed (types match — but caller should
1696
+ * have already checked this; included for safety)
1697
+ * - `undefined` when no conversion is available (e.g. anything ↔ v128)
1698
+ */
1699
+ function convertOp(
1700
+ from: WasmValueType,
1701
+ to: WasmValueType
1702
+ ): number | null | undefined {
1703
+ if (from === to) return null
1704
+ // v128 isn't convertible to/from scalar types
1705
+ if (from === 'v128' || to === 'v128') return undefined
1706
+ // i64 conversions aren't in the current opcode table — skip for now
1707
+ if (from === 'i64' || to === 'i64') return undefined
1708
+ switch (`${from}->${to}`) {
1709
+ case 'f64->i32':
1710
+ return Op.i32_trunc_f64_s
1711
+ case 'f32->i32':
1712
+ return Op.i32_trunc_f32_s
1713
+ case 'i32->f64':
1714
+ return Op.f64_convert_i32_s
1715
+ case 'i32->f32':
1716
+ return Op.f32_convert_i32_s
1717
+ case 'f32->f64':
1718
+ return Op.f64_promote_f32
1719
+ case 'f64->f32':
1720
+ return Op.f32_demote_f64
1721
+ default:
1722
+ return undefined
1723
+ }
1724
+ }
1725
+
1592
1726
  /** Compile SIMD intrinsic calls */
1593
1727
  function compileSIMDCall(
1594
1728
  name: string,
@@ -1802,63 +1936,35 @@ function parseTypeAnnotation(capture: string): TypedParam {
1802
1936
  return { name, type: typeMap[typeStr] ?? 'f64' }
1803
1937
  }
1804
1938
 
1805
- /** Build a complete WASM module */
1806
- function buildModule(
1807
- params: TypedParam[],
1808
- bodyCode: number[],
1809
- localTypes: WasmValueType[],
1810
- needsMemory: boolean,
1939
+ /**
1940
+ * Per-function intermediate: everything needed to embed one function inside
1941
+ * a (single- or multi-function) WASM module.
1942
+ */
1943
+ interface CompiledFunction {
1944
+ params: TypedParam[]
1945
+ bodyCode: number[]
1946
+ localTypes: WasmValueType[]
1947
+ needsMemory: boolean
1811
1948
  hasReturn: boolean
1812
- ): number[] {
1813
- // Magic number and version
1814
- const header = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]
1949
+ }
1815
1950
 
1816
- // Type section: function signature
1951
+ /** Encode a single function signature as a Type-section entry body */
1952
+ function encodeFuncType(params: TypedParam[], hasReturn: boolean): number[] {
1817
1953
  const paramWasmTypes = params.map((p) => Type[p.type])
1818
1954
  const returnSpec = hasReturn ? [0x01, Type.f64] : [0x00] // one f64 return OR void
1819
- const typeSection = encodeSection(Section.type, [
1820
- 0x01, // one type
1955
+ return [
1821
1956
  0x60, // func type
1822
1957
  ...encodeULEB128(params.length),
1823
1958
  ...paramWasmTypes,
1824
1959
  ...returnSpec,
1825
- ])
1826
-
1827
- // Memory section (if needed)
1828
- const memorySection: number[] = []
1829
- if (needsMemory) {
1830
- // Import memory from JS instead of declaring it
1831
- // This lets us share memory with typed arrays
1832
- }
1833
-
1834
- // Import section for memory
1835
- let importSection: number[] = []
1836
- if (needsMemory) {
1837
- importSection = encodeSection(Section.import, [
1838
- 0x01, // one import
1839
- ...encodeString('env'),
1840
- ...encodeString('memory'),
1841
- 0x02, // memory
1842
- 0x00, // flags: no max
1843
- 0x01, // initial: 1 page (64KB)
1844
- ])
1845
- }
1846
-
1847
- // Function section: function 0 has type 0
1848
- const funcSection = encodeSection(Section.function, [
1849
- 0x01, // one function
1850
- 0x00, // type index 0
1851
- ])
1852
-
1853
- // Export section: export function as "compute"
1854
- const exportSection = encodeSection(Section.export, [
1855
- 0x01, // one export
1856
- ...encodeString('compute'),
1857
- 0x00, // export kind: function
1858
- 0x00, // function index 0
1859
- ])
1960
+ ]
1961
+ }
1860
1962
 
1861
- // Code section
1963
+ /** Encode the locals + body bytes for one function as a Code-section entry */
1964
+ function encodeFuncBody(
1965
+ bodyCode: number[],
1966
+ localTypes: WasmValueType[]
1967
+ ): number[] {
1862
1968
  // Encode locals: group by type
1863
1969
  const localGroups: number[][] = []
1864
1970
  if (localTypes.length > 0) {
@@ -1883,24 +1989,98 @@ function buildModule(
1883
1989
 
1884
1990
  const funcBody = [...localsEncoded, ...bodyCode, Op.end]
1885
1991
 
1992
+ // The Code-section entry is a length-prefixed sequence of (locals + body)
1993
+ return [...encodeULEB128(funcBody.length), ...funcBody]
1994
+ }
1995
+
1996
+ /** Encode the memory-import section (used when any function needs memory) */
1997
+ function encodeMemoryImport(): number[] {
1998
+ return encodeSection(Section.import, [
1999
+ 0x01, // one import
2000
+ ...encodeString('env'),
2001
+ ...encodeString('memory'),
2002
+ 0x02, // memory
2003
+ 0x00, // flags: no max
2004
+ 0x01, // initial: 1 page (64KB)
2005
+ ])
2006
+ }
2007
+
2008
+ /** Build a complete WASM module containing N functions. */
2009
+ function buildMultiFunctionModule(
2010
+ functions: CompiledFunction[],
2011
+ exportNames: string[]
2012
+ ): number[] {
2013
+ if (functions.length !== exportNames.length) {
2014
+ throw new Error('functions and exportNames length mismatch')
2015
+ }
2016
+
2017
+ // Magic number and version
2018
+ const header = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]
2019
+
2020
+ // Type section: one entry per function (no dedup; modules are tiny)
2021
+ const typeEntries = functions.map((f) =>
2022
+ encodeFuncType(f.params, f.hasReturn)
2023
+ )
2024
+ const typeSection = encodeSection(Section.type, [
2025
+ ...encodeULEB128(typeEntries.length),
2026
+ ...typeEntries.flat(),
2027
+ ])
2028
+
2029
+ // Import section: shared memory if any function needs it
2030
+ const anyNeedsMemory = functions.some((f) => f.needsMemory)
2031
+ const importSection = anyNeedsMemory ? encodeMemoryImport() : []
2032
+
2033
+ // Function section: each function references its own type by index
2034
+ const funcSection = encodeSection(Section.function, [
2035
+ ...encodeULEB128(functions.length),
2036
+ ...functions.map((_, i) => encodeULEB128(i)).flat(),
2037
+ ])
2038
+
2039
+ // Export section: each function exported under its given name
2040
+ const exportSection = encodeSection(Section.export, [
2041
+ ...encodeULEB128(functions.length),
2042
+ ...exportNames
2043
+ .map((name, i) => [
2044
+ ...encodeString(name),
2045
+ 0x00, // export kind: function
2046
+ ...encodeULEB128(i), // function index
2047
+ ])
2048
+ .flat(),
2049
+ ])
2050
+
2051
+ // Code section: one body per function
2052
+ const codeBodies = functions.map((f) =>
2053
+ encodeFuncBody(f.bodyCode, f.localTypes)
2054
+ )
1886
2055
  const codeSection = encodeSection(Section.code, [
1887
- 0x01, // one function
1888
- ...encodeULEB128(funcBody.length),
1889
- ...funcBody,
2056
+ ...encodeULEB128(codeBodies.length),
2057
+ ...codeBodies.flat(),
1890
2058
  ])
1891
2059
 
1892
2060
  // Assemble module
1893
2061
  const sections = [...header, ...typeSection]
1894
-
1895
- if (importSection.length > 0) {
1896
- sections.push(...importSection)
1897
- }
1898
-
2062
+ if (importSection.length > 0) sections.push(...importSection)
1899
2063
  sections.push(...funcSection, ...exportSection, ...codeSection)
1900
-
1901
2064
  return sections
1902
2065
  }
1903
2066
 
2067
+ /**
2068
+ * Build a single-function module. Kept for the per-block path (legacy
2069
+ * `compileToWasm`, `createWasmFunction`). Always exports `compute`.
2070
+ */
2071
+ function buildModule(
2072
+ params: TypedParam[],
2073
+ bodyCode: number[],
2074
+ localTypes: WasmValueType[],
2075
+ needsMemory: boolean,
2076
+ hasReturn: boolean
2077
+ ): number[] {
2078
+ return buildMultiFunctionModule(
2079
+ [{ params, bodyCode, localTypes, needsMemory, hasReturn }],
2080
+ ['compute']
2081
+ )
2082
+ }
2083
+
1904
2084
  // ============================================================================
1905
2085
  // Public API
1906
2086
  // ============================================================================
@@ -1922,9 +2102,36 @@ export interface WasmCompileResult {
1922
2102
  }
1923
2103
 
1924
2104
  /**
1925
- * Compile a WASM block to WebAssembly
2105
+ * Per-block compile result: the intermediate before module wrapping.
2106
+ * Used to compose multiple blocks into a single multi-function module.
1926
2107
  */
1927
- export function compileToWasm(block: WasmBlock): WasmCompileResult {
2108
+ interface BlockCompileResult {
2109
+ success: boolean
2110
+ /** Compiled function pieces (when success === true) */
2111
+ fn?: CompiledFunction
2112
+ /** WAT disassembly (when success === true) */
2113
+ wat?: string
2114
+ /** Warnings (always present) */
2115
+ warnings: string[]
2116
+ /** Error message (when success === false) */
2117
+ error?: string
2118
+ }
2119
+
2120
+ /**
2121
+ * Compile a WASM block to its function-level intermediate (params, bytecode,
2122
+ * locals, etc.) WITHOUT wrapping in a module. Used by both the single-block
2123
+ * path (`compileToWasm`) and the multi-block path (`compileBlocksToModule`).
2124
+ *
2125
+ * @param moduleFunctions Map of other wasm functions in the same composed
2126
+ * module. When the body calls one of these by name, the compiler emits a
2127
+ * `call <index>` instruction instead of treating it as an unknown
2128
+ * identifier. Empty by default (single-block compilation has nothing to
2129
+ * call into).
2130
+ */
2131
+ function compileBlockToFunction(
2132
+ block: WasmBlock,
2133
+ moduleFunctions: Map<string, ModuleFunctionSig> = new Map()
2134
+ ): BlockCompileResult {
1928
2135
  try {
1929
2136
  // Parse type annotations from captures
1930
2137
  const params = block.captures.map(parseTypeAnnotation)
@@ -1939,9 +2146,8 @@ export function compileToWasm(block: WasmBlock): WasmCompileResult {
1939
2146
  ast = acorn.parse(wrapped, { ecmaVersion: 2022 }) as acorn.Program
1940
2147
  } catch (e: any) {
1941
2148
  return {
1942
- bytes: new Uint8Array(),
1943
- warnings: [],
1944
2149
  success: false,
2150
+ warnings: [],
1945
2151
  error: `Parse error: ${e.message}`,
1946
2152
  }
1947
2153
  }
@@ -1951,7 +2157,7 @@ export function compileToWasm(block: WasmBlock): WasmCompileResult {
1951
2157
  const body = funcDecl.body.body
1952
2158
 
1953
2159
  // Create compilation context
1954
- const ctx = createContext(params)
2160
+ const ctx = createContext(params, moduleFunctions)
1955
2161
 
1956
2162
  // Compile statements
1957
2163
  const code: number[] = []
@@ -1959,45 +2165,222 @@ export function compileToWasm(block: WasmBlock): WasmCompileResult {
1959
2165
  code.push(...compileStatement(stmt, ctx))
1960
2166
  }
1961
2167
 
1962
- // Check for errors
1963
2168
  if (ctx.errors.length > 0) {
1964
2169
  return {
1965
- bytes: new Uint8Array(),
1966
- warnings: ctx.warnings,
1967
2170
  success: false,
2171
+ warnings: ctx.warnings,
1968
2172
  error: ctx.errors.join('; '),
1969
2173
  }
1970
2174
  }
1971
2175
 
1972
- // Build the module
1973
- const moduleBytes = buildModule(
1974
- params,
1975
- code,
1976
- ctx.localTypes,
1977
- ctx.needsMemory,
1978
- ctx.hasReturn
1979
- )
1980
-
1981
- // Generate WAT disassembly for debugging
1982
- const watText = disassemble(code, params, ctx.localTypes)
1983
-
1984
2176
  return {
1985
- bytes: new Uint8Array(moduleBytes),
1986
- warnings: ctx.warnings,
1987
2177
  success: true,
1988
- needsMemory: ctx.needsMemory,
1989
- wat: watText,
2178
+ fn: {
2179
+ params,
2180
+ bodyCode: code,
2181
+ localTypes: ctx.localTypes,
2182
+ needsMemory: ctx.needsMemory,
2183
+ hasReturn: ctx.hasReturn,
2184
+ },
2185
+ wat: disassemble(code, params, ctx.localTypes),
2186
+ warnings: ctx.warnings,
1990
2187
  }
1991
2188
  } catch (e: any) {
1992
2189
  return {
1993
- bytes: new Uint8Array(),
1994
- warnings: [],
1995
2190
  success: false,
2191
+ warnings: [],
1996
2192
  error: e.message,
1997
2193
  }
1998
2194
  }
1999
2195
  }
2000
2196
 
2197
+ /**
2198
+ * Compile a single WASM block to a complete WebAssembly module.
2199
+ * The module exports a single function named `compute`.
2200
+ */
2201
+ export function compileToWasm(block: WasmBlock): WasmCompileResult {
2202
+ const r = compileBlockToFunction(block)
2203
+ if (!r.success || !r.fn) {
2204
+ return {
2205
+ bytes: new Uint8Array(),
2206
+ warnings: r.warnings,
2207
+ success: false,
2208
+ error: r.error,
2209
+ }
2210
+ }
2211
+ const moduleBytes = buildModule(
2212
+ r.fn.params,
2213
+ r.fn.bodyCode,
2214
+ r.fn.localTypes,
2215
+ r.fn.needsMemory,
2216
+ r.fn.hasReturn
2217
+ )
2218
+ return {
2219
+ bytes: new Uint8Array(moduleBytes),
2220
+ warnings: r.warnings,
2221
+ success: true,
2222
+ needsMemory: r.fn.needsMemory,
2223
+ wat: r.wat,
2224
+ }
2225
+ }
2226
+
2227
+ // ============================================================================
2228
+ // Multi-block module composition (one module, N exports)
2229
+ // ============================================================================
2230
+
2231
+ /** Per-export metadata produced by compileBlocksToModule */
2232
+ export interface BlockExport {
2233
+ /** Original block ID (assigned by the parser) */
2234
+ id: string
2235
+ /** Export name in the composed module (e.g. 'compute_0') */
2236
+ exportName: string
2237
+ /** Capture annotations (preserved for runtime wrapper) */
2238
+ captures: string[]
2239
+ /** Whether this function reads/writes memory */
2240
+ needsMemory: boolean
2241
+ /** WAT disassembly */
2242
+ wat: string
2243
+ }
2244
+
2245
+ /** Result of composing multiple blocks into one module */
2246
+ export interface MultiBlockCompileResult {
2247
+ /** The composed module bytes, or empty if all blocks failed */
2248
+ bytes: Uint8Array
2249
+ /** Per-block compile status (preserves input order) */
2250
+ results: {
2251
+ id: string
2252
+ success: boolean
2253
+ error?: string
2254
+ /** Index into `exports` (only when success === true) */
2255
+ exportIndex?: number
2256
+ }[]
2257
+ /** Successfully-compiled exports (in module-index order) */
2258
+ exports: BlockExport[]
2259
+ /** True if any included function needs memory */
2260
+ needsMemory: boolean
2261
+ /** Aggregated warnings from all blocks */
2262
+ warnings: string[]
2263
+ }
2264
+
2265
+ /**
2266
+ * Compile N WASM blocks into a single WebAssembly module with N exports.
2267
+ * Failed blocks are skipped (their slot in `results` records the error)
2268
+ * but do not abort compilation of the rest.
2269
+ *
2270
+ * Exports are named `compute_0`, `compute_1`, ... in input order, skipping
2271
+ * indices that correspond to failed blocks.
2272
+ */
2273
+ export function compileBlocksToModule(
2274
+ blocks: WasmBlock[]
2275
+ ): MultiBlockCompileResult {
2276
+ // Pre-pass: assign a function index to every block and build the
2277
+ // moduleFunctions map for NAMED wasm functions. Done before compiling
2278
+ // any body, so wasm-to-wasm `call <index>` instructions resolve
2279
+ // regardless of declaration order (forward references and mutual
2280
+ // recursion both work).
2281
+ //
2282
+ // To keep call-instruction indices valid even when individual blocks
2283
+ // fail compilation, indices are assigned densely from the input list
2284
+ // and any failed block is replaced by a stub function with the same
2285
+ // signature (returns 0 for value-returning, no-op for void). Callers
2286
+ // never observe failed indices being skipped or renumbered.
2287
+ const moduleFunctions = new Map<string, ModuleFunctionSig>()
2288
+ const preSignatures: { params: TypedParam[]; hasReturn: boolean }[] = []
2289
+ for (let i = 0; i < blocks.length; i++) {
2290
+ const b = blocks[i]
2291
+ const params = b.captures.map(parseTypeAnnotation)
2292
+ const hasReturn = b.returnType !== undefined
2293
+ preSignatures.push({ params, hasReturn })
2294
+ if (b.name) {
2295
+ moduleFunctions.set(b.name, { index: i, params, hasReturn })
2296
+ }
2297
+ }
2298
+
2299
+ // Pass 2: compile each block with the full map in scope. Failed
2300
+ // blocks are replaced by stubs (same signature, trivial body).
2301
+ const results: MultiBlockCompileResult['results'] = []
2302
+ const compiledFns: CompiledFunction[] = []
2303
+ const exports: BlockExport[] = []
2304
+ const warnings: string[] = []
2305
+
2306
+ for (let i = 0; i < blocks.length; i++) {
2307
+ const block = blocks[i]
2308
+ const r = compileBlockToFunction(block, moduleFunctions)
2309
+ warnings.push(...r.warnings)
2310
+ if (!r.success || !r.fn) {
2311
+ // Emit a stub so the function index stays valid (callers' encoded
2312
+ // `call <i>` instructions still target a real wasm function — it
2313
+ // just returns a default value or does nothing).
2314
+ results.push({ id: block.id, success: false, error: r.error })
2315
+ compiledFns.push(stubFunction(preSignatures[i]))
2316
+ exports.push({
2317
+ id: block.id,
2318
+ exportName: `compute_${i}`,
2319
+ captures: block.captures,
2320
+ needsMemory: false,
2321
+ wat: `(failed: ${r.error ?? 'unknown error'})`,
2322
+ })
2323
+ continue
2324
+ }
2325
+ compiledFns.push(r.fn)
2326
+ exports.push({
2327
+ id: block.id,
2328
+ exportName: `compute_${i}`,
2329
+ captures: block.captures,
2330
+ needsMemory: r.fn.needsMemory,
2331
+ wat: r.wat ?? '',
2332
+ })
2333
+ results.push({ id: block.id, success: true, exportIndex: i })
2334
+ }
2335
+
2336
+ if (compiledFns.length === 0) {
2337
+ return {
2338
+ bytes: new Uint8Array(),
2339
+ results,
2340
+ exports: [],
2341
+ needsMemory: false,
2342
+ warnings,
2343
+ }
2344
+ }
2345
+
2346
+ const moduleBytes = buildMultiFunctionModule(
2347
+ compiledFns,
2348
+ exports.map((e) => e.exportName)
2349
+ )
2350
+
2351
+ return {
2352
+ bytes: new Uint8Array(moduleBytes),
2353
+ results,
2354
+ exports,
2355
+ needsMemory: exports.some((e) => e.needsMemory),
2356
+ warnings,
2357
+ }
2358
+ }
2359
+
2360
+ /**
2361
+ * Build a stub `CompiledFunction` matching the given signature. Used in
2362
+ * place of failed-compilation results so function indices remain valid
2363
+ * for any wasm-to-wasm calls that target this slot. The stub's behavior
2364
+ * is intentionally bland: return 0.0 for f64-returning, do nothing for
2365
+ * void. (Callers shouldn't end up here if their own compilation
2366
+ * succeeded — failed targets should be reported via the results array.)
2367
+ */
2368
+ function stubFunction(sig: {
2369
+ params: TypedParam[]
2370
+ hasReturn: boolean
2371
+ }): CompiledFunction {
2372
+ const bodyCode: number[] = sig.hasReturn
2373
+ ? [Op.f64_const, ...encodeF64(0)]
2374
+ : []
2375
+ return {
2376
+ params: sig.params,
2377
+ bodyCode,
2378
+ localTypes: [],
2379
+ needsMemory: false,
2380
+ hasReturn: sig.hasReturn,
2381
+ }
2382
+ }
2383
+
2001
2384
  /**
2002
2385
  * Instantiate a compiled WASM module
2003
2386
  */