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.test.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { describe, it, expect } from 'bun:test'
|
|
6
6
|
import {
|
|
7
7
|
compileToWasm,
|
|
8
|
+
compileBlocksToModule,
|
|
8
9
|
instantiateWasm,
|
|
9
10
|
registerWasmBlock,
|
|
10
11
|
createWasmFunction,
|
|
@@ -1617,8 +1618,10 @@ function inc(arr: Float32Array, len: 0) {
|
|
|
1617
1618
|
}
|
|
1618
1619
|
`
|
|
1619
1620
|
const result = tjs(source)
|
|
1620
|
-
// The wrapper should check buffer identity
|
|
1621
|
-
|
|
1621
|
+
// The wrapper should check buffer identity against the shared memory.
|
|
1622
|
+
// After Phase 0.5 consolidation there's one __wasmMem per file, so
|
|
1623
|
+
// wrappers reference it directly (no per-block `mem` alias).
|
|
1624
|
+
expect(result.code).toContain('a.buffer===__wasmMem.buffer')
|
|
1622
1625
|
})
|
|
1623
1626
|
})
|
|
1624
1627
|
|
|
@@ -1877,3 +1880,1291 @@ function simdScale(arr: Float32Array, len: 0, factor: 0.0) {
|
|
|
1877
1880
|
})
|
|
1878
1881
|
})
|
|
1879
1882
|
})
|
|
1883
|
+
|
|
1884
|
+
describe('module consolidation (Phase 0.5)', () => {
|
|
1885
|
+
describe('compileBlocksToModule', () => {
|
|
1886
|
+
it('emits one module with N exports for N blocks', async () => {
|
|
1887
|
+
const blocks: WasmBlock[] = [
|
|
1888
|
+
{
|
|
1889
|
+
id: 'b0',
|
|
1890
|
+
captures: ['a: f64', 'b: f64'],
|
|
1891
|
+
body: 'return a + b',
|
|
1892
|
+
hasReturn: true,
|
|
1893
|
+
} as WasmBlock,
|
|
1894
|
+
{
|
|
1895
|
+
id: 'b1',
|
|
1896
|
+
captures: ['x: f64', 'y: f64'],
|
|
1897
|
+
body: 'return x * y',
|
|
1898
|
+
hasReturn: true,
|
|
1899
|
+
} as WasmBlock,
|
|
1900
|
+
]
|
|
1901
|
+
|
|
1902
|
+
const result = compileBlocksToModule(blocks)
|
|
1903
|
+
expect(result.exports).toHaveLength(2)
|
|
1904
|
+
expect(result.exports[0]).toMatchObject({ id: 'b0', exportName: 'compute_0' })
|
|
1905
|
+
expect(result.exports[1]).toMatchObject({ id: 'b1', exportName: 'compute_1' })
|
|
1906
|
+
|
|
1907
|
+
// Instantiate and confirm both exports work
|
|
1908
|
+
const instance = await instantiateWasm(result.bytes)
|
|
1909
|
+
const add = instance.exports.compute_0 as (a: number, b: number) => number
|
|
1910
|
+
const mul = instance.exports.compute_1 as (a: number, b: number) => number
|
|
1911
|
+
expect(add(2, 3)).toBe(5)
|
|
1912
|
+
expect(mul(4, 5)).toBe(20)
|
|
1913
|
+
})
|
|
1914
|
+
|
|
1915
|
+
it('preserves input order in results, including failures', () => {
|
|
1916
|
+
const blocks: WasmBlock[] = [
|
|
1917
|
+
{
|
|
1918
|
+
id: 'ok0',
|
|
1919
|
+
captures: ['a: f64'],
|
|
1920
|
+
body: 'return a + 1',
|
|
1921
|
+
hasReturn: true,
|
|
1922
|
+
} as WasmBlock,
|
|
1923
|
+
{
|
|
1924
|
+
id: 'bad',
|
|
1925
|
+
captures: ['x: f64'],
|
|
1926
|
+
// Syntax error — fails compilation
|
|
1927
|
+
body: 'this is not valid js {{{',
|
|
1928
|
+
hasReturn: true,
|
|
1929
|
+
} as WasmBlock,
|
|
1930
|
+
{
|
|
1931
|
+
id: 'ok1',
|
|
1932
|
+
captures: ['b: f64'],
|
|
1933
|
+
body: 'return b * 2',
|
|
1934
|
+
hasReturn: true,
|
|
1935
|
+
} as WasmBlock,
|
|
1936
|
+
]
|
|
1937
|
+
|
|
1938
|
+
const result = compileBlocksToModule(blocks)
|
|
1939
|
+
expect(result.results).toHaveLength(3)
|
|
1940
|
+
expect(result.results[0]).toMatchObject({ id: 'ok0', success: true })
|
|
1941
|
+
expect(result.results[1]).toMatchObject({ id: 'bad', success: false })
|
|
1942
|
+
expect(result.results[2]).toMatchObject({ id: 'ok1', success: true })
|
|
1943
|
+
|
|
1944
|
+
// All three blocks appear in exports — failed blocks become stub
|
|
1945
|
+
// functions so that wasm-to-wasm `call <index>` instructions targeting
|
|
1946
|
+
// their slot stay valid. The `results` array distinguishes successes
|
|
1947
|
+
// from failures.
|
|
1948
|
+
expect(result.exports).toHaveLength(3)
|
|
1949
|
+
expect(result.exports.map((e) => e.id)).toEqual(['ok0', 'bad', 'ok1'])
|
|
1950
|
+
// Export indices stay dense and aligned with input order
|
|
1951
|
+
expect(result.exports.map((e) => e.exportName)).toEqual([
|
|
1952
|
+
'compute_0',
|
|
1953
|
+
'compute_1',
|
|
1954
|
+
'compute_2',
|
|
1955
|
+
])
|
|
1956
|
+
})
|
|
1957
|
+
|
|
1958
|
+
it('produces a valid module even when all blocks fail (stubs only)', () => {
|
|
1959
|
+
// All blocks fail compilation — module is still well-formed with
|
|
1960
|
+
// stub functions in each slot. This preserves index stability for
|
|
1961
|
+
// any callers that might reference these by index.
|
|
1962
|
+
const blocks: WasmBlock[] = [
|
|
1963
|
+
{
|
|
1964
|
+
id: 'bad',
|
|
1965
|
+
captures: ['x: f64'],
|
|
1966
|
+
body: 'this is not valid js {{{',
|
|
1967
|
+
hasReturn: true,
|
|
1968
|
+
} as WasmBlock,
|
|
1969
|
+
]
|
|
1970
|
+
|
|
1971
|
+
const result = compileBlocksToModule(blocks)
|
|
1972
|
+
expect(result.exports).toHaveLength(1)
|
|
1973
|
+
expect(result.results[0].success).toBe(false)
|
|
1974
|
+
// Module bytes are emitted; the stub is callable but returns 0
|
|
1975
|
+
expect(result.bytes.length).toBeGreaterThan(0)
|
|
1976
|
+
})
|
|
1977
|
+
|
|
1978
|
+
it('imports memory only when at least one function needs it', () => {
|
|
1979
|
+
// Pure-arithmetic block: no memory needed
|
|
1980
|
+
const noMemBlocks: WasmBlock[] = [
|
|
1981
|
+
{
|
|
1982
|
+
id: 'b0',
|
|
1983
|
+
captures: ['a: f64'],
|
|
1984
|
+
body: 'return a + 1',
|
|
1985
|
+
hasReturn: true,
|
|
1986
|
+
} as WasmBlock,
|
|
1987
|
+
]
|
|
1988
|
+
const noMemResult = compileBlocksToModule(noMemBlocks)
|
|
1989
|
+
expect(noMemResult.needsMemory).toBe(false)
|
|
1990
|
+
// Should instantiate with no imports
|
|
1991
|
+
expect(async () => {
|
|
1992
|
+
await instantiateWasm(noMemResult.bytes)
|
|
1993
|
+
}).not.toThrow()
|
|
1994
|
+
|
|
1995
|
+
// Mixed: one block uses Float32Array (needs memory), one doesn't.
|
|
1996
|
+
// The whole module imports memory; the pure block coexists fine.
|
|
1997
|
+
const mixedBlocks: WasmBlock[] = [
|
|
1998
|
+
{
|
|
1999
|
+
id: 'b0',
|
|
2000
|
+
captures: ['a: f64'],
|
|
2001
|
+
body: 'return a + 1',
|
|
2002
|
+
hasReturn: true,
|
|
2003
|
+
} as WasmBlock,
|
|
2004
|
+
{
|
|
2005
|
+
id: 'b1',
|
|
2006
|
+
captures: ['arr: Float32Array', 'len: f64'],
|
|
2007
|
+
body: 'for (let i = 0; i < len; i++) arr[i] = arr[i] + 1.0',
|
|
2008
|
+
hasReturn: false,
|
|
2009
|
+
} as WasmBlock,
|
|
2010
|
+
]
|
|
2011
|
+
const mixedResult = compileBlocksToModule(mixedBlocks)
|
|
2012
|
+
expect(mixedResult.needsMemory).toBe(true)
|
|
2013
|
+
expect(mixedResult.exports).toHaveLength(2)
|
|
2014
|
+
})
|
|
2015
|
+
|
|
2016
|
+
it('handles void and value-returning functions in the same module', async () => {
|
|
2017
|
+
const blocks: WasmBlock[] = [
|
|
2018
|
+
{
|
|
2019
|
+
id: 'sum',
|
|
2020
|
+
captures: ['a: f64', 'b: f64'],
|
|
2021
|
+
body: 'return a + b',
|
|
2022
|
+
hasReturn: true,
|
|
2023
|
+
} as WasmBlock,
|
|
2024
|
+
{
|
|
2025
|
+
id: 'noop',
|
|
2026
|
+
captures: ['x: f64'],
|
|
2027
|
+
body: 'let y = x', // No return — void function
|
|
2028
|
+
hasReturn: false,
|
|
2029
|
+
} as WasmBlock,
|
|
2030
|
+
]
|
|
2031
|
+
|
|
2032
|
+
const result = compileBlocksToModule(blocks)
|
|
2033
|
+
expect(result.exports).toHaveLength(2)
|
|
2034
|
+
|
|
2035
|
+
const instance = await instantiateWasm(result.bytes)
|
|
2036
|
+
const sum = instance.exports.compute_0 as (a: number, b: number) => number
|
|
2037
|
+
const noop = instance.exports.compute_1 as (x: number) => void
|
|
2038
|
+
expect(sum(7, 8)).toBe(15)
|
|
2039
|
+
expect(noop(42)).toBeUndefined()
|
|
2040
|
+
})
|
|
2041
|
+
})
|
|
2042
|
+
|
|
2043
|
+
describe('emitted bootstrap (single WebAssembly.compile per file)', () => {
|
|
2044
|
+
it('two wasm blocks in one source produce ONE compile call', async () => {
|
|
2045
|
+
const { tjs } = await import('./index')
|
|
2046
|
+
const source = `
|
|
2047
|
+
function inc(arr: Float32Array, len: 0) {
|
|
2048
|
+
wasm {
|
|
2049
|
+
for (let i = 0; i < len; i++) { arr[i] = arr[i] + 1.0 }
|
|
2050
|
+
} fallback {
|
|
2051
|
+
for (let i = 0; i < len; i++) arr[i] += 1
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
function dbl(arr: Float32Array, len: 0) {
|
|
2056
|
+
wasm {
|
|
2057
|
+
for (let i = 0; i < len; i++) { arr[i] = arr[i] * 2.0 }
|
|
2058
|
+
} fallback {
|
|
2059
|
+
for (let i = 0; i < len; i++) arr[i] *= 2
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
`
|
|
2063
|
+
const result = tjs(source)
|
|
2064
|
+
|
|
2065
|
+
// The hallmark of consolidation: exactly one compile call across all blocks
|
|
2066
|
+
const compileCalls = (result.code.match(/WebAssembly\.compile\(/g) || [])
|
|
2067
|
+
.length
|
|
2068
|
+
expect(compileCalls).toBe(1)
|
|
2069
|
+
|
|
2070
|
+
// Both functions appear under their own export names in the module
|
|
2071
|
+
expect(result.code).toContain('"compute_0"')
|
|
2072
|
+
expect(result.code).toContain('"compute_1"')
|
|
2073
|
+
})
|
|
2074
|
+
|
|
2075
|
+
it('two wasm functions actually run after consolidated bootstrap', async () => {
|
|
2076
|
+
const { tjs } = await import('./index')
|
|
2077
|
+
const { createRuntime } = await import('./runtime')
|
|
2078
|
+
const source = `
|
|
2079
|
+
function inc(arr: Float32Array, len: 0) {
|
|
2080
|
+
wasm {
|
|
2081
|
+
for (let i = 0; i < len; i++) { arr[i] = arr[i] + 1.0 }
|
|
2082
|
+
} fallback {
|
|
2083
|
+
for (let i = 0; i < len; i++) arr[i] += 1
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
function dbl(arr: Float32Array, len: 0) {
|
|
2088
|
+
wasm {
|
|
2089
|
+
for (let i = 0; i < len; i++) { arr[i] = arr[i] * 2.0 }
|
|
2090
|
+
} fallback {
|
|
2091
|
+
for (let i = 0; i < len; i++) arr[i] *= 2
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
`
|
|
2095
|
+
const result = tjs(source)
|
|
2096
|
+
const savedTjs = globalThis.__tjs
|
|
2097
|
+
try {
|
|
2098
|
+
globalThis.__tjs = createRuntime()
|
|
2099
|
+
// Wrap in IIFE so the emitted `const __tjs = ...` doesn't clash with
|
|
2100
|
+
// the outer parameter; expose the user functions via globalThis.
|
|
2101
|
+
await new Function(
|
|
2102
|
+
'__tjs',
|
|
2103
|
+
`return (async () => { ${result.code}\n` +
|
|
2104
|
+
`globalThis.__test_inc = inc;\n` +
|
|
2105
|
+
`globalThis.__test_dbl = dbl;\n` +
|
|
2106
|
+
`})();`
|
|
2107
|
+
)(globalThis.__tjs)
|
|
2108
|
+
|
|
2109
|
+
// Wait for the single async bootstrap (one instantiate) to complete
|
|
2110
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2111
|
+
|
|
2112
|
+
const wasmBuffer = (globalThis as any).wasmBuffer
|
|
2113
|
+
expect(typeof wasmBuffer).toBe('function')
|
|
2114
|
+
|
|
2115
|
+
const buf = wasmBuffer(Float32Array, 4)
|
|
2116
|
+
buf[0] = 1
|
|
2117
|
+
buf[1] = 2
|
|
2118
|
+
buf[2] = 3
|
|
2119
|
+
buf[3] = 4
|
|
2120
|
+
|
|
2121
|
+
;(globalThis as any).__test_inc(buf, 4) // [2, 3, 4, 5]
|
|
2122
|
+
expect(Array.from(buf)).toEqual([2, 3, 4, 5])
|
|
2123
|
+
|
|
2124
|
+
;(globalThis as any).__test_dbl(buf, 4) // [4, 6, 8, 10]
|
|
2125
|
+
expect(Array.from(buf)).toEqual([4, 6, 8, 10])
|
|
2126
|
+
} finally {
|
|
2127
|
+
globalThis.__tjs = savedTjs
|
|
2128
|
+
delete (globalThis as any).wasmBuffer
|
|
2129
|
+
delete (globalThis as any).__test_inc
|
|
2130
|
+
delete (globalThis as any).__test_dbl
|
|
2131
|
+
}
|
|
2132
|
+
})
|
|
2133
|
+
})
|
|
2134
|
+
})
|
|
2135
|
+
|
|
2136
|
+
describe('wasm function declarations (Phase 1)', () => {
|
|
2137
|
+
it('extracts a top-level wasm function as a WasmBlock', async () => {
|
|
2138
|
+
const { tjs } = await import('./index')
|
|
2139
|
+
const source = `
|
|
2140
|
+
wasm function add(a: f64, b: f64): f64 {
|
|
2141
|
+
return a + b
|
|
2142
|
+
}
|
|
2143
|
+
`
|
|
2144
|
+
const result = tjs(source)
|
|
2145
|
+
expect(result.wasmCompiled).toBeDefined()
|
|
2146
|
+
expect(result.wasmCompiled).toHaveLength(1)
|
|
2147
|
+
expect(result.wasmCompiled![0]).toMatchObject({
|
|
2148
|
+
id: '__tjs_wasm_add',
|
|
2149
|
+
success: true,
|
|
2150
|
+
})
|
|
2151
|
+
})
|
|
2152
|
+
|
|
2153
|
+
it('emits a regular JS wrapper that forwards to the wasm export', async () => {
|
|
2154
|
+
const { tjs } = await import('./index')
|
|
2155
|
+
const source = `
|
|
2156
|
+
wasm function add(a: f64, b: f64): f64 {
|
|
2157
|
+
return a + b
|
|
2158
|
+
}
|
|
2159
|
+
`
|
|
2160
|
+
const result = tjs(source)
|
|
2161
|
+
// The declaration is replaced with a wrapper function — that wrapper
|
|
2162
|
+
// forwards to globalThis.__tjs_wasm_add, which the bootstrap sets up.
|
|
2163
|
+
expect(result.code).toContain('function add(a, b)')
|
|
2164
|
+
expect(result.code).toContain('globalThis.__tjs_wasm_add(a, b)')
|
|
2165
|
+
})
|
|
2166
|
+
|
|
2167
|
+
it('preserves the export modifier on the wrapper', async () => {
|
|
2168
|
+
const { tjs } = await import('./index')
|
|
2169
|
+
const source = `
|
|
2170
|
+
export wasm function mul(a: f64, b: f64): f64 {
|
|
2171
|
+
return a * b
|
|
2172
|
+
}
|
|
2173
|
+
`
|
|
2174
|
+
const result = tjs(source)
|
|
2175
|
+
expect(result.code).toContain('export function mul(a, b)')
|
|
2176
|
+
expect(result.code).toContain('globalThis.__tjs_wasm_mul(a, b)')
|
|
2177
|
+
})
|
|
2178
|
+
|
|
2179
|
+
it('runs end-to-end: declare wasm function, call it, get correct result', async () => {
|
|
2180
|
+
const { tjs } = await import('./index')
|
|
2181
|
+
const { createRuntime } = await import('./runtime')
|
|
2182
|
+
const source = `
|
|
2183
|
+
wasm function add(a: f64, b: f64): f64 {
|
|
2184
|
+
return a + b
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
wasm function sub(a: f64, b: f64): f64 {
|
|
2188
|
+
return a - b
|
|
2189
|
+
}
|
|
2190
|
+
`
|
|
2191
|
+
const result = tjs(source)
|
|
2192
|
+
const savedTjs = globalThis.__tjs
|
|
2193
|
+
try {
|
|
2194
|
+
globalThis.__tjs = createRuntime()
|
|
2195
|
+
await new Function(
|
|
2196
|
+
'__tjs',
|
|
2197
|
+
`return (async () => { ${result.code}\n` +
|
|
2198
|
+
`globalThis.__test_add = add;\n` +
|
|
2199
|
+
`globalThis.__test_sub = sub;\n` +
|
|
2200
|
+
`})();`
|
|
2201
|
+
)(globalThis.__tjs)
|
|
2202
|
+
|
|
2203
|
+
// Wait for the async bootstrap to complete
|
|
2204
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2205
|
+
|
|
2206
|
+
expect((globalThis as any).__test_add(3, 4)).toBe(7)
|
|
2207
|
+
expect((globalThis as any).__test_sub(10, 7)).toBe(3)
|
|
2208
|
+
} finally {
|
|
2209
|
+
globalThis.__tjs = savedTjs
|
|
2210
|
+
delete (globalThis as any).__test_add
|
|
2211
|
+
delete (globalThis as any).__test_sub
|
|
2212
|
+
}
|
|
2213
|
+
})
|
|
2214
|
+
|
|
2215
|
+
it('works with Float32Array parameters (zero-copy via wasmBuffer)', async () => {
|
|
2216
|
+
const { tjs } = await import('./index')
|
|
2217
|
+
const { createRuntime } = await import('./runtime')
|
|
2218
|
+
const source = `
|
|
2219
|
+
wasm function scaleArray(arr: Float32Array, len: f64, factor: f64) {
|
|
2220
|
+
for (let i = 0; i < len; i++) {
|
|
2221
|
+
arr[i] = arr[i] * factor
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
`
|
|
2225
|
+
const result = tjs(source)
|
|
2226
|
+
const savedTjs = globalThis.__tjs
|
|
2227
|
+
try {
|
|
2228
|
+
globalThis.__tjs = createRuntime()
|
|
2229
|
+
await new Function(
|
|
2230
|
+
'__tjs',
|
|
2231
|
+
`return (async () => { ${result.code}\n` +
|
|
2232
|
+
`globalThis.__test_scale = scaleArray;\n` +
|
|
2233
|
+
`})();`
|
|
2234
|
+
)(globalThis.__tjs)
|
|
2235
|
+
|
|
2236
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2237
|
+
|
|
2238
|
+
const wasmBuffer = (globalThis as any).wasmBuffer
|
|
2239
|
+
expect(typeof wasmBuffer).toBe('function')
|
|
2240
|
+
const buf = wasmBuffer(Float32Array, 4)
|
|
2241
|
+
buf[0] = 1
|
|
2242
|
+
buf[1] = 2
|
|
2243
|
+
buf[2] = 3
|
|
2244
|
+
buf[3] = 4
|
|
2245
|
+
;(globalThis as any).__test_scale(buf, 4, 3.0)
|
|
2246
|
+
expect(Array.from(buf)).toEqual([3, 6, 9, 12])
|
|
2247
|
+
} finally {
|
|
2248
|
+
globalThis.__tjs = savedTjs
|
|
2249
|
+
delete (globalThis as any).wasmBuffer
|
|
2250
|
+
delete (globalThis as any).__test_scale
|
|
2251
|
+
}
|
|
2252
|
+
})
|
|
2253
|
+
|
|
2254
|
+
it('coexists with inline wasm {} blocks in the same file', async () => {
|
|
2255
|
+
const { tjs } = await import('./index')
|
|
2256
|
+
// No return-type annotation on `inline` — auto signature tests would
|
|
2257
|
+
// call the function at transpile time before wasm is instantiated and
|
|
2258
|
+
// fail because the dispatch returns undefined. The functional check is
|
|
2259
|
+
// about block extraction + module consolidation, not the auto-test.
|
|
2260
|
+
const source = `
|
|
2261
|
+
wasm function topLevel(a: f64, b: f64): f64 {
|
|
2262
|
+
return a * b
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
function inline(x: 0, y: 0) {
|
|
2266
|
+
return wasm {
|
|
2267
|
+
return x + y
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
`
|
|
2271
|
+
const result = tjs(source, { runTests: false })
|
|
2272
|
+
expect(result.wasmCompiled).toHaveLength(2)
|
|
2273
|
+
const ids = result.wasmCompiled!.map((b) => b.id).sort()
|
|
2274
|
+
expect(ids[0]).toBe('__tjs_wasm_0') // inline block
|
|
2275
|
+
expect(ids[1]).toBe('__tjs_wasm_topLevel') // named wasm function
|
|
2276
|
+
// Both compile into the same consolidated module — verify exactly one
|
|
2277
|
+
// WebAssembly.compile call in the output.
|
|
2278
|
+
const compileCalls = (result.code.match(/WebAssembly\.compile\(/g) || [])
|
|
2279
|
+
.length
|
|
2280
|
+
expect(compileCalls).toBe(1)
|
|
2281
|
+
})
|
|
2282
|
+
|
|
2283
|
+
it('handles wasm function with no params', async () => {
|
|
2284
|
+
const { tjs } = await import('./index')
|
|
2285
|
+
const source = `
|
|
2286
|
+
wasm function answer(): f64 {
|
|
2287
|
+
return 42
|
|
2288
|
+
}
|
|
2289
|
+
`
|
|
2290
|
+
const result = tjs(source)
|
|
2291
|
+
expect(result.wasmCompiled).toHaveLength(1)
|
|
2292
|
+
expect(result.wasmCompiled![0].success).toBe(true)
|
|
2293
|
+
expect(result.code).toContain('function answer()')
|
|
2294
|
+
expect(result.code).toContain('globalThis.__tjs_wasm_answer()')
|
|
2295
|
+
})
|
|
2296
|
+
|
|
2297
|
+
it('does not match identifiers that contain "wasm" (e.g. mywasm)', async () => {
|
|
2298
|
+
const { tjs } = await import('./index')
|
|
2299
|
+
const source = `
|
|
2300
|
+
function mywasm(x: 0): 0 { return x }
|
|
2301
|
+
`
|
|
2302
|
+
const result = tjs(source)
|
|
2303
|
+
// No wasm blocks should be extracted from this — the source contains no
|
|
2304
|
+
// actual `wasm function` declaration.
|
|
2305
|
+
expect(result.wasmCompiled ?? []).toHaveLength(0)
|
|
2306
|
+
})
|
|
2307
|
+
})
|
|
2308
|
+
|
|
2309
|
+
describe('boundary distribution form (Phase 4)', () => {
|
|
2310
|
+
// Write the transpiled library to a tmp .mjs file and dynamically import
|
|
2311
|
+
// it. This is the most authentic test of the boundary form: real ESM
|
|
2312
|
+
// resolution, real exports, real WebAssembly instantiation.
|
|
2313
|
+
async function dynamicImportLibrary(transpiled: string): Promise<any> {
|
|
2314
|
+
const { tmpdir } = await import('node:os')
|
|
2315
|
+
const { join } = await import('node:path')
|
|
2316
|
+
const { writeFileSync, unlinkSync } = await import('node:fs')
|
|
2317
|
+
const path = join(
|
|
2318
|
+
tmpdir(),
|
|
2319
|
+
`tjs-lib-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mjs`
|
|
2320
|
+
)
|
|
2321
|
+
writeFileSync(path, transpiled)
|
|
2322
|
+
try {
|
|
2323
|
+
const mod = await import(path)
|
|
2324
|
+
// Wait for the async wasm bootstrap inside the module to finish.
|
|
2325
|
+
// The bootstrap runs as a top-level IIFE; instantiation is async.
|
|
2326
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2327
|
+
return mod
|
|
2328
|
+
} finally {
|
|
2329
|
+
try {
|
|
2330
|
+
unlinkSync(path)
|
|
2331
|
+
} catch {
|
|
2332
|
+
/* ignore */
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
it('emits a self-contained ES module with exported wasm wrappers', async () => {
|
|
2338
|
+
const { tjs } = await import('./index')
|
|
2339
|
+
const librarySource = `
|
|
2340
|
+
export wasm function add(a: f64, b: f64): f64 { return a + b }
|
|
2341
|
+
export wasm function mul(a: f64, b: f64): f64 { return a * b }
|
|
2342
|
+
`
|
|
2343
|
+
const result = tjs(librarySource, { runTests: false })
|
|
2344
|
+
|
|
2345
|
+
// Both wrappers exported
|
|
2346
|
+
expect(result.code).toContain('export function add(a, b)')
|
|
2347
|
+
expect(result.code).toContain('export function mul(a, b)')
|
|
2348
|
+
|
|
2349
|
+
// Each wrapper forwards to its globalThis-registered wasm function
|
|
2350
|
+
expect(result.code).toContain('globalThis.__tjs_wasm_add(a, b)')
|
|
2351
|
+
expect(result.code).toContain('globalThis.__tjs_wasm_mul(a, b)')
|
|
2352
|
+
|
|
2353
|
+
// The wasm module is base64-embedded and instantiated at the top
|
|
2354
|
+
expect(result.code).toContain('__wasmModuleB64')
|
|
2355
|
+
expect(result.code).toContain('WebAssembly.compile')
|
|
2356
|
+
|
|
2357
|
+
// No external runtime setup required — the inline __tjs fallback
|
|
2358
|
+
// covers everything actually used. (Only the helpers this file needs
|
|
2359
|
+
// are inlined, so simple wasm-wrapper libraries get a small fallback;
|
|
2360
|
+
// libraries with type checks would also inline MonadicError etc.)
|
|
2361
|
+
expect(result.code).toContain('globalThis.__tjs?.createRuntime?.()')
|
|
2362
|
+
})
|
|
2363
|
+
|
|
2364
|
+
it('dynamic import of the boundary form gives a working module', async () => {
|
|
2365
|
+
const { tjs } = await import('./index')
|
|
2366
|
+
const librarySource = `
|
|
2367
|
+
export wasm function add(a: f64, b: f64): f64 { return a + b }
|
|
2368
|
+
export wasm function mul(a: f64, b: f64): f64 { return a * b }
|
|
2369
|
+
`
|
|
2370
|
+
const result = tjs(librarySource, { runTests: false })
|
|
2371
|
+
|
|
2372
|
+
const lib = await dynamicImportLibrary(result.code)
|
|
2373
|
+
expect(typeof lib.add).toBe('function')
|
|
2374
|
+
expect(typeof lib.mul).toBe('function')
|
|
2375
|
+
expect(lib.add(7, 5)).toBe(12)
|
|
2376
|
+
expect(lib.mul(6, 7)).toBe(42)
|
|
2377
|
+
})
|
|
2378
|
+
|
|
2379
|
+
it('boundary form and composed form return identical results', async () => {
|
|
2380
|
+
// Same library source, consumed two different ways:
|
|
2381
|
+
// - boundary: transpile → write to disk → dynamic import → call exports
|
|
2382
|
+
// - composed: Phase 3 — moduleLoader pulls the wasm body into the
|
|
2383
|
+
// consumer's own module
|
|
2384
|
+
// Both should produce the same numeric results.
|
|
2385
|
+
const { tjs } = await import('./index')
|
|
2386
|
+
const { createRuntime } = await import('./runtime')
|
|
2387
|
+
const { ModuleLoader, inMemoryFileSystem } = await import(
|
|
2388
|
+
'./module-loader'
|
|
2389
|
+
)
|
|
2390
|
+
|
|
2391
|
+
const librarySource = `
|
|
2392
|
+
export wasm function dot3(
|
|
2393
|
+
ax: f64, ay: f64, az: f64,
|
|
2394
|
+
bx: f64, by: f64, bz: f64
|
|
2395
|
+
): f64 {
|
|
2396
|
+
return ax * bx + ay * by + az * bz
|
|
2397
|
+
}
|
|
2398
|
+
`
|
|
2399
|
+
// Boundary form
|
|
2400
|
+
const libCompiled = tjs(librarySource, { runTests: false })
|
|
2401
|
+
const lib = await dynamicImportLibrary(libCompiled.code)
|
|
2402
|
+
const boundary = lib.dot3(1, 2, 3, 4, 5, 6)
|
|
2403
|
+
|
|
2404
|
+
// Composed form (Phase 3 path)
|
|
2405
|
+
const loader = new ModuleLoader({
|
|
2406
|
+
fs: inMemoryFileSystem({ '/proj/linalg.tjs': librarySource }),
|
|
2407
|
+
baseDir: '/proj',
|
|
2408
|
+
})
|
|
2409
|
+
const consumerSource = `
|
|
2410
|
+
import { dot3 } from './linalg.tjs'
|
|
2411
|
+
`
|
|
2412
|
+
const consumerCompiled = tjs(consumerSource, {
|
|
2413
|
+
moduleLoader: loader,
|
|
2414
|
+
filename: '/proj/app.tjs',
|
|
2415
|
+
runTests: false,
|
|
2416
|
+
})
|
|
2417
|
+
|
|
2418
|
+
let composed: number
|
|
2419
|
+
const savedTjs = globalThis.__tjs
|
|
2420
|
+
try {
|
|
2421
|
+
globalThis.__tjs = createRuntime()
|
|
2422
|
+
await new Function(
|
|
2423
|
+
'__tjs',
|
|
2424
|
+
`return (async () => { ${consumerCompiled.code}\n` +
|
|
2425
|
+
`globalThis.__test_dot3 = dot3;\n` +
|
|
2426
|
+
`})();`
|
|
2427
|
+
)(globalThis.__tjs)
|
|
2428
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2429
|
+
composed = (globalThis as any).__test_dot3(1, 2, 3, 4, 5, 6)
|
|
2430
|
+
} finally {
|
|
2431
|
+
globalThis.__tjs = savedTjs
|
|
2432
|
+
delete (globalThis as any).__test_dot3
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
expect(boundary).toBe(composed)
|
|
2436
|
+
expect(boundary).toBe(1 * 4 + 2 * 5 + 3 * 6) // 32 — pen-and-paper truth
|
|
2437
|
+
})
|
|
2438
|
+
|
|
2439
|
+
it('boundary form library works for a plain JS consumer (no tjs involvement)', async () => {
|
|
2440
|
+
// Build the library, write to disk, import it, then call from a
|
|
2441
|
+
// plain JS function (simulating a consumer with no tjs in the chain).
|
|
2442
|
+
// The library's wasm bootstrap should run, the wrapper should be
|
|
2443
|
+
// callable, and the wasm function should produce correct results.
|
|
2444
|
+
const { tjs } = await import('./index')
|
|
2445
|
+
const librarySource = `
|
|
2446
|
+
export wasm function square(x: f64): f64 { return x * x }
|
|
2447
|
+
`
|
|
2448
|
+
const result = tjs(librarySource, { runTests: false })
|
|
2449
|
+
const lib = await dynamicImportLibrary(result.code)
|
|
2450
|
+
|
|
2451
|
+
// Use the import from a plain JS function — no tjs runtime involved
|
|
2452
|
+
function jsConsumer(x: number): number {
|
|
2453
|
+
return lib.square(x) + 1
|
|
2454
|
+
}
|
|
2455
|
+
expect(jsConsumer(5)).toBe(26)
|
|
2456
|
+
})
|
|
2457
|
+
})
|
|
2458
|
+
|
|
2459
|
+
describe('cross-file wasm composition (Phase 3)', () => {
|
|
2460
|
+
// Build a minimal ModuleLoader backed by an in-memory FS for hermetic tests
|
|
2461
|
+
async function buildLoader(files: Record<string, string>) {
|
|
2462
|
+
const { ModuleLoader, inMemoryFileSystem } = await import('./module-loader')
|
|
2463
|
+
return new ModuleLoader({
|
|
2464
|
+
fs: inMemoryFileSystem(files),
|
|
2465
|
+
baseDir: '/proj',
|
|
2466
|
+
})
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
it('composes an imported wasm function into the consumer module', async () => {
|
|
2470
|
+
const { tjs } = await import('./index')
|
|
2471
|
+
const loader = await buildLoader({
|
|
2472
|
+
'/proj/linalg.tjs': `
|
|
2473
|
+
wasm function dot(a: f64, b: f64): f64 {
|
|
2474
|
+
return a * b
|
|
2475
|
+
}
|
|
2476
|
+
`,
|
|
2477
|
+
})
|
|
2478
|
+
const source = `
|
|
2479
|
+
import { dot } from './linalg.tjs'
|
|
2480
|
+
|
|
2481
|
+
function compute(a: 0.0, b: 0.0): 0.0 {
|
|
2482
|
+
return dot(a, b)
|
|
2483
|
+
}
|
|
2484
|
+
`
|
|
2485
|
+
const result = tjs(source, {
|
|
2486
|
+
moduleLoader: loader,
|
|
2487
|
+
filename: '/proj/app.tjs',
|
|
2488
|
+
runTests: false,
|
|
2489
|
+
})
|
|
2490
|
+
|
|
2491
|
+
// The imported wasm function got composed in
|
|
2492
|
+
expect(result.wasmCompiled).toBeDefined()
|
|
2493
|
+
expect(result.wasmCompiled).toHaveLength(1)
|
|
2494
|
+
expect(result.wasmCompiled![0]).toMatchObject({
|
|
2495
|
+
id: '__tjs_wasm_dot',
|
|
2496
|
+
success: true,
|
|
2497
|
+
})
|
|
2498
|
+
|
|
2499
|
+
// The import statement was rewritten to a local wrapper
|
|
2500
|
+
expect(result.code).not.toContain("import { dot } from './linalg.tjs'")
|
|
2501
|
+
expect(result.code).toContain('function dot(a, b)')
|
|
2502
|
+
expect(result.code).toContain('globalThis.__tjs_wasm_dot(a, b)')
|
|
2503
|
+
})
|
|
2504
|
+
|
|
2505
|
+
it('keeps imports unchanged when no loader is supplied', async () => {
|
|
2506
|
+
const { tjs } = await import('./index')
|
|
2507
|
+
const source = `
|
|
2508
|
+
import { dot } from './linalg.tjs'
|
|
2509
|
+
|
|
2510
|
+
function compute(a: 0.0, b: 0.0): 0.0 {
|
|
2511
|
+
return dot(a, b)
|
|
2512
|
+
}
|
|
2513
|
+
`
|
|
2514
|
+
// No moduleLoader option — default behavior preserved
|
|
2515
|
+
const result = tjs(source, { runTests: false })
|
|
2516
|
+
expect(result.code).toContain("import { dot } from './linalg.tjs'")
|
|
2517
|
+
expect(result.wasmCompiled ?? []).toHaveLength(0)
|
|
2518
|
+
})
|
|
2519
|
+
|
|
2520
|
+
it('preserves imports that do NOT resolve to wasm functions', async () => {
|
|
2521
|
+
const { tjs } = await import('./index')
|
|
2522
|
+
const loader = await buildLoader({
|
|
2523
|
+
'/proj/regular.tjs': `
|
|
2524
|
+
export function helper(x: 0.0): 0.0 { return x + 1.0 }
|
|
2525
|
+
`,
|
|
2526
|
+
})
|
|
2527
|
+
const source = `
|
|
2528
|
+
import { helper } from './regular.tjs'
|
|
2529
|
+
|
|
2530
|
+
function compute(x: 0.0): 0.0 {
|
|
2531
|
+
return helper(x)
|
|
2532
|
+
}
|
|
2533
|
+
`
|
|
2534
|
+
const result = tjs(source, {
|
|
2535
|
+
moduleLoader: loader,
|
|
2536
|
+
filename: '/proj/app.tjs',
|
|
2537
|
+
runTests: false,
|
|
2538
|
+
})
|
|
2539
|
+
// `helper` is a regular function, not a wasm function — import stays
|
|
2540
|
+
expect(result.code).toContain("import { helper } from './regular.tjs'")
|
|
2541
|
+
expect(result.wasmCompiled ?? []).toHaveLength(0)
|
|
2542
|
+
})
|
|
2543
|
+
|
|
2544
|
+
it('handles mixed imports (some wasm, some regular) in one statement', async () => {
|
|
2545
|
+
const { tjs } = await import('./index')
|
|
2546
|
+
const loader = await buildLoader({
|
|
2547
|
+
'/proj/lib.tjs': `
|
|
2548
|
+
wasm function fast(a: f64): f64 { return a * 2 }
|
|
2549
|
+
export function slow(x: 0.0): 0.0 { return x + 1.0 }
|
|
2550
|
+
`,
|
|
2551
|
+
})
|
|
2552
|
+
const source = `
|
|
2553
|
+
import { fast, slow } from './lib.tjs'
|
|
2554
|
+
|
|
2555
|
+
function compute(x: 0.0): 0.0 {
|
|
2556
|
+
return fast(x) + slow(x)
|
|
2557
|
+
}
|
|
2558
|
+
`
|
|
2559
|
+
const result = tjs(source, {
|
|
2560
|
+
moduleLoader: loader,
|
|
2561
|
+
filename: '/proj/app.tjs',
|
|
2562
|
+
runTests: false,
|
|
2563
|
+
})
|
|
2564
|
+
// fast was composed; slow remains imported
|
|
2565
|
+
expect(result.wasmCompiled).toHaveLength(1)
|
|
2566
|
+
expect(result.wasmCompiled![0].id).toBe('__tjs_wasm_fast')
|
|
2567
|
+
expect(result.code).toContain('function fast(a)')
|
|
2568
|
+
expect(result.code).toContain('globalThis.__tjs_wasm_fast(a)')
|
|
2569
|
+
// The remaining import keeps `slow` only
|
|
2570
|
+
expect(result.code).toMatch(/import\s*\{\s*slow\s*\}\s*from/)
|
|
2571
|
+
expect(result.code).not.toMatch(
|
|
2572
|
+
/import\s*\{\s*fast\s*,\s*slow\s*\}\s*from/
|
|
2573
|
+
)
|
|
2574
|
+
})
|
|
2575
|
+
|
|
2576
|
+
it('composes multiple wasm functions from one library', async () => {
|
|
2577
|
+
const { tjs } = await import('./index')
|
|
2578
|
+
const loader = await buildLoader({
|
|
2579
|
+
'/proj/linalg.tjs': `
|
|
2580
|
+
wasm function dot(a: f64, b: f64): f64 { return a * b }
|
|
2581
|
+
wasm function add(a: f64, b: f64): f64 { return a + b }
|
|
2582
|
+
wasm function unused(x: f64): f64 { return x }
|
|
2583
|
+
`,
|
|
2584
|
+
})
|
|
2585
|
+
const source = `
|
|
2586
|
+
import { dot, add } from './linalg.tjs'
|
|
2587
|
+
|
|
2588
|
+
function compute(a: 0.0, b: 0.0): 0.0 {
|
|
2589
|
+
return add(dot(a, b), b)
|
|
2590
|
+
}
|
|
2591
|
+
`
|
|
2592
|
+
const result = tjs(source, {
|
|
2593
|
+
moduleLoader: loader,
|
|
2594
|
+
filename: '/proj/app.tjs',
|
|
2595
|
+
runTests: false,
|
|
2596
|
+
})
|
|
2597
|
+
expect(result.wasmCompiled).toHaveLength(2)
|
|
2598
|
+
const ids = result.wasmCompiled!.map((b) => b.id).sort()
|
|
2599
|
+
expect(ids).toEqual(['__tjs_wasm_add', '__tjs_wasm_dot'])
|
|
2600
|
+
// One consolidated WebAssembly.Module per file (Phase 0.5 acceptance)
|
|
2601
|
+
const compileCalls = (result.code.match(/WebAssembly\.compile\(/g) || [])
|
|
2602
|
+
.length
|
|
2603
|
+
expect(compileCalls).toBe(1)
|
|
2604
|
+
})
|
|
2605
|
+
|
|
2606
|
+
it('module shape: composed functions are local (no extra imports beyond env.memory)', async () => {
|
|
2607
|
+
// This is the Phase 3 acceptance criterion: imported wasm functions
|
|
2608
|
+
// are LOCAL to the consumer's module, not imported at the wasm level.
|
|
2609
|
+
const { tjs } = await import('./index')
|
|
2610
|
+
const { compileBlocksToModule } = await import('./wasm')
|
|
2611
|
+
const loader = await buildLoader({
|
|
2612
|
+
'/proj/linalg.tjs': `
|
|
2613
|
+
wasm function dot(a: f64, b: f64): f64 { return a * b }
|
|
2614
|
+
`,
|
|
2615
|
+
})
|
|
2616
|
+
const source = `
|
|
2617
|
+
import { dot } from './linalg.tjs'
|
|
2618
|
+
function compute(a: 0.0, b: 0.0): 0.0 { return dot(a, b) }
|
|
2619
|
+
`
|
|
2620
|
+
tjs(source, {
|
|
2621
|
+
moduleLoader: loader,
|
|
2622
|
+
filename: '/proj/app.tjs',
|
|
2623
|
+
runTests: false,
|
|
2624
|
+
})
|
|
2625
|
+
|
|
2626
|
+
// Now compile the loaded module's wasm blocks and inspect the bytes.
|
|
2627
|
+
// The wasm binary format: after the magic + version, sections appear.
|
|
2628
|
+
// Section 2 is "import" — we want to verify ONLY env.memory is imported
|
|
2629
|
+
// (no host function imports).
|
|
2630
|
+
const linalgBlocks = (await loader.load('./linalg.tjs', '/proj/app.tjs'))!
|
|
2631
|
+
.parseResult.wasmBlocks
|
|
2632
|
+
const composed = compileBlocksToModule(linalgBlocks)
|
|
2633
|
+
expect(composed.exports).toHaveLength(1)
|
|
2634
|
+
expect(composed.exports[0].id).toBe('__tjs_wasm_dot')
|
|
2635
|
+
|
|
2636
|
+
// The composed module needs no memory for a pure-scalar `dot(a, b) = a*b`
|
|
2637
|
+
expect(composed.needsMemory).toBe(false)
|
|
2638
|
+
|
|
2639
|
+
// Confirm the bytecode parses as a valid WebAssembly.Module
|
|
2640
|
+
const mod = new WebAssembly.Module(composed.bytes)
|
|
2641
|
+
expect(mod).toBeInstanceOf(WebAssembly.Module)
|
|
2642
|
+
|
|
2643
|
+
// Inspect imports: should have NO function imports.
|
|
2644
|
+
// WebAssembly.Module.imports returns [{ module, name, kind }, ...]
|
|
2645
|
+
const imports = WebAssembly.Module.imports(mod)
|
|
2646
|
+
const functionImports = imports.filter((i) => i.kind === 'function')
|
|
2647
|
+
expect(functionImports).toHaveLength(0)
|
|
2648
|
+
})
|
|
2649
|
+
|
|
2650
|
+
it('end-to-end: imported wasm function runs and produces correct results', async () => {
|
|
2651
|
+
const { tjs } = await import('./index')
|
|
2652
|
+
const { createRuntime } = await import('./runtime')
|
|
2653
|
+
const loader = await buildLoader({
|
|
2654
|
+
'/proj/linalg.tjs': `
|
|
2655
|
+
wasm function add(a: f64, b: f64): f64 { return a + b }
|
|
2656
|
+
wasm function mul(a: f64, b: f64): f64 { return a * b }
|
|
2657
|
+
`,
|
|
2658
|
+
})
|
|
2659
|
+
const source = `
|
|
2660
|
+
import { add, mul } from './linalg.tjs'
|
|
2661
|
+
`
|
|
2662
|
+
const result = tjs(source, {
|
|
2663
|
+
moduleLoader: loader,
|
|
2664
|
+
filename: '/proj/app.tjs',
|
|
2665
|
+
runTests: false,
|
|
2666
|
+
})
|
|
2667
|
+
|
|
2668
|
+
const savedTjs = globalThis.__tjs
|
|
2669
|
+
try {
|
|
2670
|
+
globalThis.__tjs = createRuntime()
|
|
2671
|
+
await new Function(
|
|
2672
|
+
'__tjs',
|
|
2673
|
+
`return (async () => { ${result.code}\n` +
|
|
2674
|
+
`globalThis.__test_add = add;\n` +
|
|
2675
|
+
`globalThis.__test_mul = mul;\n` +
|
|
2676
|
+
`})();`
|
|
2677
|
+
)(globalThis.__tjs)
|
|
2678
|
+
|
|
2679
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2680
|
+
|
|
2681
|
+
expect((globalThis as any).__test_add(2, 3)).toBe(5)
|
|
2682
|
+
expect((globalThis as any).__test_mul(4, 5)).toBe(20)
|
|
2683
|
+
} finally {
|
|
2684
|
+
globalThis.__tjs = savedTjs
|
|
2685
|
+
delete (globalThis as any).__test_add
|
|
2686
|
+
delete (globalThis as any).__test_mul
|
|
2687
|
+
}
|
|
2688
|
+
})
|
|
2689
|
+
|
|
2690
|
+
describe('tree-shaking & transitive deps', () => {
|
|
2691
|
+
async function loaderWith(files: Record<string, string>) {
|
|
2692
|
+
const { ModuleLoader, inMemoryFileSystem } = await import(
|
|
2693
|
+
'./module-loader'
|
|
2694
|
+
)
|
|
2695
|
+
return new ModuleLoader({
|
|
2696
|
+
fs: inMemoryFileSystem(files),
|
|
2697
|
+
baseDir: '/proj',
|
|
2698
|
+
})
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
it('only pulls in explicitly-imported wasm functions (tree-shaking)', async () => {
|
|
2702
|
+
const { tjs } = await import('./index')
|
|
2703
|
+
const loader = await loaderWith({
|
|
2704
|
+
'/proj/lib.tjs': `
|
|
2705
|
+
wasm function wanted(x: f64): f64 { return x * 2 }
|
|
2706
|
+
wasm function unwanted(x: f64): f64 { return x + 999 }
|
|
2707
|
+
wasm function also_unwanted(x: f64): f64 { return x - 1 }
|
|
2708
|
+
`,
|
|
2709
|
+
})
|
|
2710
|
+
const source = `
|
|
2711
|
+
import { wanted } from './lib.tjs'
|
|
2712
|
+
function go() { return wanted(5) }
|
|
2713
|
+
`
|
|
2714
|
+
const result = tjs(source, {
|
|
2715
|
+
moduleLoader: loader,
|
|
2716
|
+
filename: '/proj/app.tjs',
|
|
2717
|
+
runTests: false,
|
|
2718
|
+
})
|
|
2719
|
+
// Only `wanted` should be in the composed module
|
|
2720
|
+
expect(result.wasmCompiled).toHaveLength(1)
|
|
2721
|
+
expect(result.wasmCompiled![0].id).toBe('__tjs_wasm_wanted')
|
|
2722
|
+
})
|
|
2723
|
+
|
|
2724
|
+
it('pulls in transitive wasm-to-wasm callees automatically', async () => {
|
|
2725
|
+
// `outer` calls `inner`; the consumer imports only `outer`. Without
|
|
2726
|
+
// transitive walking, outer's body's `call <inner-index>` would fail
|
|
2727
|
+
// (the inner function wouldn't be in the consumer's module). With
|
|
2728
|
+
// transitive walking, both end up in the composed module.
|
|
2729
|
+
const { tjs } = await import('./index')
|
|
2730
|
+
const { createRuntime } = await import('./runtime')
|
|
2731
|
+
const loader = await loaderWith({
|
|
2732
|
+
'/proj/lib.tjs': `
|
|
2733
|
+
wasm function inner(x: f64): f64 { return x * 2 }
|
|
2734
|
+
wasm function outer(x: f64): f64 { return inner(x) + 1 }
|
|
2735
|
+
`,
|
|
2736
|
+
})
|
|
2737
|
+
const source = `
|
|
2738
|
+
import { outer } from './lib.tjs'
|
|
2739
|
+
`
|
|
2740
|
+
const result = tjs(source, {
|
|
2741
|
+
moduleLoader: loader,
|
|
2742
|
+
filename: '/proj/app.tjs',
|
|
2743
|
+
runTests: false,
|
|
2744
|
+
})
|
|
2745
|
+
// Both pulled in
|
|
2746
|
+
expect(result.wasmCompiled).toHaveLength(2)
|
|
2747
|
+
expect(result.wasmCompiled!.every((b) => b.success)).toBe(true)
|
|
2748
|
+
const ids = result.wasmCompiled!.map((b) => b.id).sort()
|
|
2749
|
+
expect(ids).toEqual(['__tjs_wasm_inner', '__tjs_wasm_outer'])
|
|
2750
|
+
|
|
2751
|
+
// Run end-to-end to confirm the call actually works
|
|
2752
|
+
const savedTjs = globalThis.__tjs
|
|
2753
|
+
try {
|
|
2754
|
+
globalThis.__tjs = createRuntime()
|
|
2755
|
+
await new Function(
|
|
2756
|
+
'__tjs',
|
|
2757
|
+
`return (async () => { ${result.code}\n` +
|
|
2758
|
+
`globalThis.__test_outer = outer;\n` +
|
|
2759
|
+
`})();`
|
|
2760
|
+
)(globalThis.__tjs)
|
|
2761
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2762
|
+
// outer(5) = inner(5) + 1 = 10 + 1 = 11
|
|
2763
|
+
expect((globalThis as any).__test_outer(5)).toBe(11)
|
|
2764
|
+
} finally {
|
|
2765
|
+
globalThis.__tjs = savedTjs
|
|
2766
|
+
delete (globalThis as any).__test_outer
|
|
2767
|
+
for (const key of Object.keys(globalThis)) {
|
|
2768
|
+
if (key.startsWith('__tjs_wasm_')) {
|
|
2769
|
+
delete (globalThis as any)[key]
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
})
|
|
2774
|
+
|
|
2775
|
+
it('handles multi-step transitive chains', async () => {
|
|
2776
|
+
// a → b → c. Importing only `a` should pull in b and c.
|
|
2777
|
+
const { tjs } = await import('./index')
|
|
2778
|
+
const loader = await loaderWith({
|
|
2779
|
+
'/proj/lib.tjs': `
|
|
2780
|
+
wasm function c(x: f64): f64 { return x + 1 }
|
|
2781
|
+
wasm function b(x: f64): f64 { return c(x) * 2 }
|
|
2782
|
+
wasm function a(x: f64): f64 { return b(x) + 3 }
|
|
2783
|
+
wasm function unrelated(x: f64): f64 { return x }
|
|
2784
|
+
`,
|
|
2785
|
+
})
|
|
2786
|
+
const source = `
|
|
2787
|
+
import { a } from './lib.tjs'
|
|
2788
|
+
`
|
|
2789
|
+
const result = tjs(source, {
|
|
2790
|
+
moduleLoader: loader,
|
|
2791
|
+
filename: '/proj/app.tjs',
|
|
2792
|
+
runTests: false,
|
|
2793
|
+
})
|
|
2794
|
+
// a, b, c pulled in; unrelated is not
|
|
2795
|
+
expect(result.wasmCompiled).toHaveLength(3)
|
|
2796
|
+
const ids = result.wasmCompiled!.map((b) => b.id).sort()
|
|
2797
|
+
expect(ids).toEqual(['__tjs_wasm_a', '__tjs_wasm_b', '__tjs_wasm_c'])
|
|
2798
|
+
})
|
|
2799
|
+
|
|
2800
|
+
it('handles mutual recursion across the import boundary', async () => {
|
|
2801
|
+
// Two mutually-recursive library functions. Importing either one
|
|
2802
|
+
// should pull in both.
|
|
2803
|
+
const { tjs } = await import('./index')
|
|
2804
|
+
const loader = await loaderWith({
|
|
2805
|
+
'/proj/lib.tjs': `
|
|
2806
|
+
wasm function ping(n: i32): f64 {
|
|
2807
|
+
if (n == 0) return 0.0
|
|
2808
|
+
return pong(n - 1) + 1
|
|
2809
|
+
}
|
|
2810
|
+
wasm function pong(n: i32): f64 {
|
|
2811
|
+
if (n == 0) return 0.0
|
|
2812
|
+
return ping(n - 1) + 1
|
|
2813
|
+
}
|
|
2814
|
+
`,
|
|
2815
|
+
})
|
|
2816
|
+
const source = `
|
|
2817
|
+
import { ping } from './lib.tjs'
|
|
2818
|
+
`
|
|
2819
|
+
const result = tjs(source, {
|
|
2820
|
+
moduleLoader: loader,
|
|
2821
|
+
filename: '/proj/app.tjs',
|
|
2822
|
+
runTests: false,
|
|
2823
|
+
})
|
|
2824
|
+
expect(result.wasmCompiled).toHaveLength(2)
|
|
2825
|
+
expect(result.wasmCompiled!.every((b) => b.success)).toBe(true)
|
|
2826
|
+
})
|
|
2827
|
+
|
|
2828
|
+
it('does not over-pull when an identifier appears as a substring', async () => {
|
|
2829
|
+
// The body's regex scan uses word-boundary anchors. A function named
|
|
2830
|
+
// `add` shouldn't trigger if the body has `add2` or `_add` or similar.
|
|
2831
|
+
const { tjs } = await import('./index')
|
|
2832
|
+
const loader = await loaderWith({
|
|
2833
|
+
'/proj/lib.tjs': `
|
|
2834
|
+
wasm function add(a: f64, b: f64): f64 { return a + b }
|
|
2835
|
+
wasm function user(x: f64): f64 {
|
|
2836
|
+
// The local "_add" is just a variable name — not a call to add()
|
|
2837
|
+
let _add = x * 2
|
|
2838
|
+
return _add + 1
|
|
2839
|
+
}
|
|
2840
|
+
`,
|
|
2841
|
+
})
|
|
2842
|
+
const source = `
|
|
2843
|
+
import { user } from './lib.tjs'
|
|
2844
|
+
`
|
|
2845
|
+
const result = tjs(source, {
|
|
2846
|
+
moduleLoader: loader,
|
|
2847
|
+
filename: '/proj/app.tjs',
|
|
2848
|
+
runTests: false,
|
|
2849
|
+
})
|
|
2850
|
+
// user is pulled in; add is NOT (word-boundary regex doesn't match _add as add)
|
|
2851
|
+
expect(result.wasmCompiled).toHaveLength(1)
|
|
2852
|
+
expect(result.wasmCompiled![0].id).toBe('__tjs_wasm_user')
|
|
2853
|
+
})
|
|
2854
|
+
})
|
|
2855
|
+
})
|
|
2856
|
+
|
|
2857
|
+
describe('wasm function purity & unsafe marker (Phase 2)', () => {
|
|
2858
|
+
it('rejects `wasm function (! ...)` with a clear error', async () => {
|
|
2859
|
+
const { tjs } = await import('./index')
|
|
2860
|
+
const source = `
|
|
2861
|
+
wasm function dangerous(! aPtr: i32, n: i32): f64 {
|
|
2862
|
+
return 0
|
|
2863
|
+
}
|
|
2864
|
+
`
|
|
2865
|
+
expect(() => tjs(source)).toThrow(/Unsafe wasm functions/)
|
|
2866
|
+
expect(() => tjs(source)).toThrow(/dangerous/)
|
|
2867
|
+
expect(() => tjs(source)).toThrow(/reserved for a future phase/)
|
|
2868
|
+
})
|
|
2869
|
+
|
|
2870
|
+
it('accepts wasm function without the bang marker', async () => {
|
|
2871
|
+
const { tjs } = await import('./index')
|
|
2872
|
+
const source = `
|
|
2873
|
+
wasm function safe(a: f64, b: f64): f64 {
|
|
2874
|
+
return a + b
|
|
2875
|
+
}
|
|
2876
|
+
`
|
|
2877
|
+
expect(() => tjs(source)).not.toThrow()
|
|
2878
|
+
})
|
|
2879
|
+
|
|
2880
|
+
it('purity: host-import calls in a wasm function body fail with a clear error', async () => {
|
|
2881
|
+
// Math.sin requires a host import; the wasm bytecode builder doesn't
|
|
2882
|
+
// support that path today, so this fails at compile time. This test
|
|
2883
|
+
// documents the property: wasm function bodies are pure compute — any
|
|
2884
|
+
// host-import call is a compile error.
|
|
2885
|
+
const { tjs } = await import('./index')
|
|
2886
|
+
const source = `
|
|
2887
|
+
wasm function tryHostCall(x: f64): f64 {
|
|
2888
|
+
return Math.sin(x)
|
|
2889
|
+
}
|
|
2890
|
+
`
|
|
2891
|
+
const result = tjs(source)
|
|
2892
|
+
// Block extraction succeeds but compilation fails
|
|
2893
|
+
expect(result.wasmCompiled).toBeDefined()
|
|
2894
|
+
expect(result.wasmCompiled).toHaveLength(1)
|
|
2895
|
+
expect(result.wasmCompiled![0].success).toBe(false)
|
|
2896
|
+
expect(result.wasmCompiled![0].error).toMatch(/Math\.sin/)
|
|
2897
|
+
expect(result.wasmCompiled![0].error).toMatch(/import/i)
|
|
2898
|
+
})
|
|
2899
|
+
|
|
2900
|
+
it('purity: inline Math ops that DO compile (sqrt, abs, etc.) work fine', async () => {
|
|
2901
|
+
// sqrt, abs, floor, ceil, min, max compile to wasm intrinsics — no host
|
|
2902
|
+
// imports needed. Confirms the constraint is "no host imports", not
|
|
2903
|
+
// "no Math.* at all".
|
|
2904
|
+
const { tjs } = await import('./index')
|
|
2905
|
+
const source = `
|
|
2906
|
+
wasm function magnitude(x: f64, y: f64): f64 {
|
|
2907
|
+
return Math.sqrt(x * x + y * y)
|
|
2908
|
+
}
|
|
2909
|
+
`
|
|
2910
|
+
const result = tjs(source)
|
|
2911
|
+
expect(result.wasmCompiled).toBeDefined()
|
|
2912
|
+
expect(result.wasmCompiled![0].success).toBe(true)
|
|
2913
|
+
})
|
|
2914
|
+
})
|
|
2915
|
+
|
|
2916
|
+
describe('wasm-to-wasm calls (Phase 1.5)', () => {
|
|
2917
|
+
it('a wasm function can call another wasm function in the same file', async () => {
|
|
2918
|
+
const { tjs } = await import('./index')
|
|
2919
|
+
const { createRuntime } = await import('./runtime')
|
|
2920
|
+
const source = `
|
|
2921
|
+
wasm function square(x: f64): f64 {
|
|
2922
|
+
return x * x
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
wasm function sumOfSquares(a: f64, b: f64): f64 {
|
|
2926
|
+
return square(a) + square(b)
|
|
2927
|
+
}
|
|
2928
|
+
`
|
|
2929
|
+
const result = tjs(source, { runTests: false })
|
|
2930
|
+
expect(result.wasmCompiled).toHaveLength(2)
|
|
2931
|
+
expect(result.wasmCompiled!.every((b) => b.success)).toBe(true)
|
|
2932
|
+
|
|
2933
|
+
const savedTjs = globalThis.__tjs
|
|
2934
|
+
try {
|
|
2935
|
+
globalThis.__tjs = createRuntime()
|
|
2936
|
+
await new Function(
|
|
2937
|
+
'__tjs',
|
|
2938
|
+
`return (async () => { ${result.code}\n` +
|
|
2939
|
+
`globalThis.__test_sos = sumOfSquares;\n` +
|
|
2940
|
+
`})();`
|
|
2941
|
+
)(globalThis.__tjs)
|
|
2942
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2943
|
+
|
|
2944
|
+
// sumOfSquares(3, 4) = 9 + 16 = 25
|
|
2945
|
+
expect((globalThis as any).__test_sos(3, 4)).toBe(25)
|
|
2946
|
+
} finally {
|
|
2947
|
+
globalThis.__tjs = savedTjs
|
|
2948
|
+
delete (globalThis as any).__test_sos
|
|
2949
|
+
for (const key of Object.keys(globalThis)) {
|
|
2950
|
+
if (key.startsWith('__tjs_wasm_')) {
|
|
2951
|
+
delete (globalThis as any)[key]
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
})
|
|
2956
|
+
|
|
2957
|
+
it('forward references work (caller declared before callee)', async () => {
|
|
2958
|
+
// The pre-pass builds the function map before any body is compiled,
|
|
2959
|
+
// so a wasm function can call one declared LATER in the file.
|
|
2960
|
+
const { tjs } = await import('./index')
|
|
2961
|
+
const { createRuntime } = await import('./runtime')
|
|
2962
|
+
const source = `
|
|
2963
|
+
wasm function caller(x: f64): f64 {
|
|
2964
|
+
return callee(x) + 1
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
wasm function callee(x: f64): f64 {
|
|
2968
|
+
return x * 2
|
|
2969
|
+
}
|
|
2970
|
+
`
|
|
2971
|
+
const result = tjs(source, { runTests: false })
|
|
2972
|
+
expect(result.wasmCompiled!.every((b) => b.success)).toBe(true)
|
|
2973
|
+
|
|
2974
|
+
const savedTjs = globalThis.__tjs
|
|
2975
|
+
try {
|
|
2976
|
+
globalThis.__tjs = createRuntime()
|
|
2977
|
+
await new Function(
|
|
2978
|
+
'__tjs',
|
|
2979
|
+
`return (async () => { ${result.code}\n` +
|
|
2980
|
+
`globalThis.__test_caller = caller;\n` +
|
|
2981
|
+
`})();`
|
|
2982
|
+
)(globalThis.__tjs)
|
|
2983
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2984
|
+
expect((globalThis as any).__test_caller(5)).toBe(11) // 5*2 + 1
|
|
2985
|
+
} finally {
|
|
2986
|
+
globalThis.__tjs = savedTjs
|
|
2987
|
+
delete (globalThis as any).__test_caller
|
|
2988
|
+
for (const key of Object.keys(globalThis)) {
|
|
2989
|
+
if (key.startsWith('__tjs_wasm_')) {
|
|
2990
|
+
delete (globalThis as any)[key]
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
})
|
|
2995
|
+
|
|
2996
|
+
it('mutual recursion compiles and runs', async () => {
|
|
2997
|
+
const { tjs } = await import('./index')
|
|
2998
|
+
const { createRuntime } = await import('./runtime')
|
|
2999
|
+
// Classic mutual-recursion shape: isEven(n) calls isOdd(n-1); isOdd(n)
|
|
3000
|
+
// calls isEven(n-1). Returns 1.0 (true) or 0.0 (false).
|
|
3001
|
+
const source = `
|
|
3002
|
+
wasm function isEven(n: i32): f64 {
|
|
3003
|
+
if (n == 0) return 1.0
|
|
3004
|
+
return isOdd(n - 1)
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
wasm function isOdd(n: i32): f64 {
|
|
3008
|
+
if (n == 0) return 0.0
|
|
3009
|
+
return isEven(n - 1)
|
|
3010
|
+
}
|
|
3011
|
+
`
|
|
3012
|
+
const result = tjs(source, { runTests: false })
|
|
3013
|
+
expect(result.wasmCompiled!.every((b) => b.success)).toBe(true)
|
|
3014
|
+
|
|
3015
|
+
const savedTjs = globalThis.__tjs
|
|
3016
|
+
try {
|
|
3017
|
+
globalThis.__tjs = createRuntime()
|
|
3018
|
+
await new Function(
|
|
3019
|
+
'__tjs',
|
|
3020
|
+
`return (async () => { ${result.code}\n` +
|
|
3021
|
+
`globalThis.__test_isEven = isEven;\n` +
|
|
3022
|
+
`globalThis.__test_isOdd = isOdd;\n` +
|
|
3023
|
+
`})();`
|
|
3024
|
+
)(globalThis.__tjs)
|
|
3025
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3026
|
+
|
|
3027
|
+
expect((globalThis as any).__test_isEven(10)).toBe(1)
|
|
3028
|
+
expect((globalThis as any).__test_isEven(7)).toBe(0)
|
|
3029
|
+
expect((globalThis as any).__test_isOdd(7)).toBe(1)
|
|
3030
|
+
expect((globalThis as any).__test_isOdd(10)).toBe(0)
|
|
3031
|
+
} finally {
|
|
3032
|
+
globalThis.__tjs = savedTjs
|
|
3033
|
+
delete (globalThis as any).__test_isEven
|
|
3034
|
+
delete (globalThis as any).__test_isOdd
|
|
3035
|
+
for (const key of Object.keys(globalThis)) {
|
|
3036
|
+
if (key.startsWith('__tjs_wasm_')) {
|
|
3037
|
+
delete (globalThis as any)[key]
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
})
|
|
3042
|
+
|
|
3043
|
+
it('cross-file: composed imports can be called from a wasm function', async () => {
|
|
3044
|
+
// The big payoff: a consumer's wasm function calls imported library
|
|
3045
|
+
// kernels via wasm `call` instructions — no JS↔wasm boundary in the
|
|
3046
|
+
// inner loop.
|
|
3047
|
+
const { tjs } = await import('./index')
|
|
3048
|
+
const { createRuntime } = await import('./runtime')
|
|
3049
|
+
const { ModuleLoader, inMemoryFileSystem } = await import(
|
|
3050
|
+
'./module-loader'
|
|
3051
|
+
)
|
|
3052
|
+
|
|
3053
|
+
const loader = new ModuleLoader({
|
|
3054
|
+
fs: inMemoryFileSystem({
|
|
3055
|
+
'/proj/lib.tjs': `
|
|
3056
|
+
wasm function double(x: f64): f64 { return x * 2 }
|
|
3057
|
+
wasm function triple(x: f64): f64 { return x * 3 }
|
|
3058
|
+
`,
|
|
3059
|
+
}),
|
|
3060
|
+
baseDir: '/proj',
|
|
3061
|
+
})
|
|
3062
|
+
const consumerSource = `
|
|
3063
|
+
import { double, triple } from './lib.tjs'
|
|
3064
|
+
|
|
3065
|
+
wasm function fancy(x: f64): f64 {
|
|
3066
|
+
return double(x) + triple(x)
|
|
3067
|
+
}
|
|
3068
|
+
`
|
|
3069
|
+
const result = tjs(consumerSource, {
|
|
3070
|
+
moduleLoader: loader,
|
|
3071
|
+
filename: '/proj/app.tjs',
|
|
3072
|
+
runTests: false,
|
|
3073
|
+
})
|
|
3074
|
+
|
|
3075
|
+
// All three wasm functions (double, triple, fancy) live in one module
|
|
3076
|
+
expect(result.wasmCompiled).toHaveLength(3)
|
|
3077
|
+
expect(result.wasmCompiled!.every((b) => b.success)).toBe(true)
|
|
3078
|
+
const compileCalls = (result.code.match(/WebAssembly\.compile\(/g) || [])
|
|
3079
|
+
.length
|
|
3080
|
+
expect(compileCalls).toBe(1)
|
|
3081
|
+
|
|
3082
|
+
const savedTjs = globalThis.__tjs
|
|
3083
|
+
try {
|
|
3084
|
+
globalThis.__tjs = createRuntime()
|
|
3085
|
+
await new Function(
|
|
3086
|
+
'__tjs',
|
|
3087
|
+
`return (async () => { ${result.code}\n` +
|
|
3088
|
+
`globalThis.__test_fancy = fancy;\n` +
|
|
3089
|
+
`})();`
|
|
3090
|
+
)(globalThis.__tjs)
|
|
3091
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3092
|
+
|
|
3093
|
+
// fancy(5) = double(5) + triple(5) = 10 + 15 = 25
|
|
3094
|
+
expect((globalThis as any).__test_fancy(5)).toBe(25)
|
|
3095
|
+
} finally {
|
|
3096
|
+
globalThis.__tjs = savedTjs
|
|
3097
|
+
delete (globalThis as any).__test_fancy
|
|
3098
|
+
for (const key of Object.keys(globalThis)) {
|
|
3099
|
+
if (key.startsWith('__tjs_wasm_')) {
|
|
3100
|
+
delete (globalThis as any)[key]
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
})
|
|
3105
|
+
|
|
3106
|
+
it('arg type mismatch is detected with a clear error', async () => {
|
|
3107
|
+
// A wasm function expects i32 but is called with an f64 expression
|
|
3108
|
+
// that has no way to be converted safely without losing data. The
|
|
3109
|
+
// compiler should still accept it (lossy conversion is allowed) —
|
|
3110
|
+
// truncate is the standard wasm move for f64→i32.
|
|
3111
|
+
//
|
|
3112
|
+
// For an actually-incompatible case we'd need v128; testing that
|
|
3113
|
+
// would require setting up SIMD types. Instead we verify the
|
|
3114
|
+
// conversion path: pass an f64 expression to an i32 param and check
|
|
3115
|
+
// it works (the truncation happens automatically).
|
|
3116
|
+
const { tjs } = await import('./index')
|
|
3117
|
+
const { createRuntime } = await import('./runtime')
|
|
3118
|
+
const source = `
|
|
3119
|
+
wasm function takesInt(n: i32): f64 {
|
|
3120
|
+
return n + 0.5
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
wasm function caller(): f64 {
|
|
3124
|
+
// 3.7 is f64; takesInt expects i32 — the compiler inserts a truncate
|
|
3125
|
+
return takesInt(3.7)
|
|
3126
|
+
}
|
|
3127
|
+
`
|
|
3128
|
+
const result = tjs(source, { runTests: false })
|
|
3129
|
+
expect(result.wasmCompiled!.every((b) => b.success)).toBe(true)
|
|
3130
|
+
|
|
3131
|
+
const savedTjs = globalThis.__tjs
|
|
3132
|
+
try {
|
|
3133
|
+
globalThis.__tjs = createRuntime()
|
|
3134
|
+
await new Function(
|
|
3135
|
+
'__tjs',
|
|
3136
|
+
`return (async () => { ${result.code}\n` +
|
|
3137
|
+
`globalThis.__test_caller = caller;\n` +
|
|
3138
|
+
`})();`
|
|
3139
|
+
)(globalThis.__tjs)
|
|
3140
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3141
|
+
|
|
3142
|
+
// 3.7 truncates to 3 (i32), then 3 + 0.5 = 3.5
|
|
3143
|
+
expect((globalThis as any).__test_caller()).toBe(3.5)
|
|
3144
|
+
} finally {
|
|
3145
|
+
globalThis.__tjs = savedTjs
|
|
3146
|
+
delete (globalThis as any).__test_caller
|
|
3147
|
+
for (const key of Object.keys(globalThis)) {
|
|
3148
|
+
if (key.startsWith('__tjs_wasm_')) {
|
|
3149
|
+
delete (globalThis as any)[key]
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
})
|
|
3154
|
+
|
|
3155
|
+
it('argument count mismatch produces a clear compile error', async () => {
|
|
3156
|
+
const { tjs } = await import('./index')
|
|
3157
|
+
const source = `
|
|
3158
|
+
wasm function takesTwo(a: f64, b: f64): f64 { return a + b }
|
|
3159
|
+
wasm function caller(): f64 {
|
|
3160
|
+
return takesTwo(1.0)
|
|
3161
|
+
}
|
|
3162
|
+
`
|
|
3163
|
+
const result = tjs(source, { runTests: false })
|
|
3164
|
+
// caller fails because takesTwo gets one arg instead of two
|
|
3165
|
+
const callerResult = result.wasmCompiled!.find((b) => b.id === '__tjs_wasm_caller')
|
|
3166
|
+
expect(callerResult).toBeDefined()
|
|
3167
|
+
expect(callerResult!.success).toBe(false)
|
|
3168
|
+
expect(callerResult!.error).toMatch(/takesTwo expects 2 arguments, got 1/)
|
|
3169
|
+
})
|
|
3170
|
+
})
|