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.
- package/CLAUDE.md +99 -33
- package/bin/docs.js +4 -1
- package/demo/docs.json +104 -22
- package/demo/src/examples.test.ts +1 -0
- package/demo/src/imports.test.ts +16 -4
- package/demo/src/imports.ts +60 -15
- package/demo/src/playground-shared.ts +9 -8
- package/demo/src/tfs-worker.js +205 -147
- package/demo/src/tjs-playground.ts +34 -10
- package/demo/src/ts-examples.ts +8 -8
- package/demo/src/ts-playground.ts +24 -8
- package/dist/index.js +118 -101
- package/dist/index.js.map +4 -4
- package/dist/src/lang/bool-coercion.d.ts +50 -0
- package/dist/src/lang/docs.d.ts +31 -6
- package/dist/src/lang/linter.d.ts +8 -0
- package/dist/src/lang/parser-transforms.d.ts +18 -0
- package/dist/src/lang/parser-types.d.ts +2 -0
- package/dist/src/lang/parser.d.ts +3 -0
- package/dist/src/lang/runtime.d.ts +34 -0
- package/dist/src/lang/types.d.ts +9 -1
- package/dist/src/rbac/index.d.ts +1 -1
- package/dist/src/vm/runtime.d.ts +1 -1
- package/dist/tjs-eval.js +38 -36
- package/dist/tjs-eval.js.map +4 -4
- package/dist/tjs-from-ts.js +20 -20
- package/dist/tjs-from-ts.js.map +3 -3
- package/dist/tjs-lang.js +85 -83
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +47 -45
- package/dist/tjs-vm.js.map +4 -4
- package/llms.txt +79 -0
- package/package.json +9 -4
- package/src/cli/commands/convert.test.ts +16 -21
- package/src/lang/bool-coercion.test.ts +203 -0
- package/src/lang/bool-coercion.ts +314 -0
- package/src/lang/codegen.test.ts +137 -0
- package/src/lang/docs.test.ts +476 -1
- package/src/lang/docs.ts +471 -37
- package/src/lang/emitters/ast.ts +11 -12
- package/src/lang/emitters/dts.test.ts +41 -0
- package/src/lang/emitters/dts.ts +9 -0
- package/src/lang/emitters/js-tests.ts +9 -4
- package/src/lang/emitters/js-wasm.ts +57 -65
- package/src/lang/emitters/js.ts +198 -3
- package/src/lang/features.test.ts +4 -3
- package/src/lang/index.ts +9 -0
- package/src/lang/inference.ts +54 -0
- package/src/lang/linter.test.ts +104 -1
- package/src/lang/linter.ts +124 -1
- package/src/lang/module-loader.test.ts +318 -0
- package/src/lang/module-loader.ts +419 -0
- package/src/lang/parser-params.ts +31 -0
- package/src/lang/parser-transforms.ts +640 -0
- package/src/lang/parser-types.ts +35 -0
- package/src/lang/parser.test.ts +73 -1
- package/src/lang/parser.ts +77 -3
- package/src/lang/runtime.ts +98 -0
- package/src/lang/types.ts +6 -0
- 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/src/rbac/index.ts +2 -2
- package/src/rbac/rules.tjs.d.ts +9 -0
- package/src/vm/atoms/batteries.ts +2 -2
- package/src/vm/runtime.ts +10 -3
- package/dist/src/rbac/rules.d.ts +0 -184
- 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(
|
|
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
|
|
1583
|
-
if (
|
|
1584
|
-
return compileSIMDCall(
|
|
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
|
-
/**
|
|
1806
|
-
function
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
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
|
-
|
|
1813
|
-
// Magic number and version
|
|
1814
|
-
const header = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]
|
|
1949
|
+
}
|
|
1815
1950
|
|
|
1816
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1888
|
-
...
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
1989
|
-
|
|
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
|
*/
|