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/emitters/dts.ts
CHANGED
|
@@ -81,6 +81,15 @@ export function typeDescriptorToTS(td: TypeDescriptor): string {
|
|
|
81
81
|
}
|
|
82
82
|
base = 'any'
|
|
83
83
|
break
|
|
84
|
+
case 'function': {
|
|
85
|
+
const params = td.params ?? []
|
|
86
|
+
const returns = td.returns ? typeDescriptorToTS(td.returns) : 'any'
|
|
87
|
+
const args = params
|
|
88
|
+
.map((p) => `${p.name}: ${typeDescriptorToTS(p.type)}`)
|
|
89
|
+
.join(', ')
|
|
90
|
+
base = `(${args}) => ${returns}`
|
|
91
|
+
break
|
|
92
|
+
}
|
|
84
93
|
default:
|
|
85
94
|
base = 'any'
|
|
86
95
|
}
|
|
@@ -876,6 +876,13 @@ export function runAllTests(
|
|
|
876
876
|
// skip tests gracefully rather than marking them as failures
|
|
877
877
|
const isUnresolvedRef = hasUnresolvedImports && e instanceof ReferenceError
|
|
878
878
|
|
|
879
|
+
// The error came from module-level code (e.g. an undefined identifier
|
|
880
|
+
// in `console.log(... x ...)`), NOT from the function/test under test.
|
|
881
|
+
// Don't attribute a line — otherwise the editor would mark the function
|
|
882
|
+
// declaration's line as the error site, misleading the user about where
|
|
883
|
+
// the actual problem is. The test still appears as failed in the test
|
|
884
|
+
// list with the explanatory message; the user finds the real error
|
|
885
|
+
// through the runtime console.
|
|
879
886
|
for (const test of tests) {
|
|
880
887
|
results.push({
|
|
881
888
|
description: test.description,
|
|
@@ -883,7 +890,6 @@ export function runAllTests(
|
|
|
883
890
|
error: isUnresolvedRef
|
|
884
891
|
? undefined
|
|
885
892
|
: `Module execution failed: ${e.message}`,
|
|
886
|
-
line: test.line,
|
|
887
893
|
})
|
|
888
894
|
}
|
|
889
895
|
for (const info of syncSigTestInfos) {
|
|
@@ -897,7 +903,6 @@ export function runAllTests(
|
|
|
897
903
|
? undefined
|
|
898
904
|
: `Module execution failed: ${e.message}`,
|
|
899
905
|
isSignatureTest: true,
|
|
900
|
-
line: info.line,
|
|
901
906
|
})
|
|
902
907
|
}
|
|
903
908
|
}
|
|
@@ -949,7 +954,7 @@ function runTestBlocks(
|
|
|
949
954
|
const tjsStub = `
|
|
950
955
|
const __saved_tjs = globalThis.__tjs;
|
|
951
956
|
class __MonadicError extends Error { constructor(m,p,e,a,c){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c;} }
|
|
952
|
-
const __stub_tjs = { version: '0.0.0', MonadicError: __MonadicError, pushStack: () => {}, popStack: () => {}, getStack: () => [], typeError: (path, expected, value) => new __MonadicError(\`Type error at \${path}: expected \${expected}\`, path, expected, typeof value), createRuntime: function() { return this; } };
|
|
957
|
+
const __stub_tjs = { version: '0.0.0', MonadicError: __MonadicError, pushStack: () => {}, popStack: () => {}, getStack: () => [], typeError: (path, expected, value) => new __MonadicError(\`Type error at \${path}: expected \${expected}\`, path, expected, typeof value), toBool: (v) => (v instanceof Boolean || v instanceof Number || v instanceof String) ? Boolean(v.valueOf()) : Boolean(v), createRuntime: function() { return this; } };
|
|
953
958
|
globalThis.__tjs = __stub_tjs;
|
|
954
959
|
`
|
|
955
960
|
const tjsRestore = `globalThis.__tjs = __saved_tjs;`
|
|
@@ -1308,7 +1313,7 @@ function runSignatureTest(
|
|
|
1308
1313
|
const tjsStub = `
|
|
1309
1314
|
const __saved_tjs = globalThis.__tjs;
|
|
1310
1315
|
class __MonadicError extends Error { constructor(m,p,e,a,c){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c;} }
|
|
1311
|
-
const __stub_tjs = { version: '0.0.0', MonadicError: __MonadicError, pushStack: () => {}, popStack: () => {}, getStack: () => [], typeError: (path, expected, value) => new __MonadicError(\`Type error at \${path}: expected \${expected}\`, path, expected, typeof value), createRuntime: function() { return this; } };
|
|
1316
|
+
const __stub_tjs = { version: '0.0.0', MonadicError: __MonadicError, pushStack: () => {}, popStack: () => {}, getStack: () => [], typeError: (path, expected, value) => new __MonadicError(\`Type error at \${path}: expected \${expected}\`, path, expected, typeof value), toBool: (v) => (v instanceof Boolean || v instanceof Number || v instanceof String) ? Boolean(v.valueOf()) : Boolean(v), createRuntime: function() { return this; } };
|
|
1312
1317
|
globalThis.__tjs = __stub_tjs;
|
|
1313
1318
|
`
|
|
1314
1319
|
const tjsRestore = `globalThis.__tjs = __saved_tjs;`
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TJS WASM Bootstrap Generation
|
|
3
3
|
*
|
|
4
|
-
* Compiles
|
|
4
|
+
* Compiles the file's WASM blocks into a single WebAssembly.Module with
|
|
5
|
+
* one exported function per block, then emits JavaScript that compiles
|
|
6
|
+
* and instantiates the module once at startup. This is the foundation
|
|
7
|
+
* for cross-file wasm composition (see wasm-library-plan.md, Phase 3) —
|
|
8
|
+
* once everything's in one module, intra-module calls cost nothing.
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
|
-
import {
|
|
11
|
+
import { compileBlocksToModule } from '../wasm'
|
|
8
12
|
import type { WasmBlock } from '../parser'
|
|
9
13
|
|
|
10
14
|
export function generateWasmBootstrap(blocks: WasmBlock[]): {
|
|
@@ -16,75 +20,61 @@ export function generateWasmBootstrap(blocks: WasmBlock[]): {
|
|
|
16
20
|
byteLength?: number
|
|
17
21
|
}[]
|
|
18
22
|
} {
|
|
19
|
-
const
|
|
20
|
-
id: string
|
|
21
|
-
success: boolean
|
|
22
|
-
error?: string
|
|
23
|
-
byteLength?: number
|
|
24
|
-
}[] = []
|
|
25
|
-
const compiledBlocks: {
|
|
26
|
-
id: string
|
|
27
|
-
base64: string
|
|
28
|
-
captures: string[]
|
|
29
|
-
needsMemory: boolean
|
|
30
|
-
wat: string
|
|
31
|
-
}[] = []
|
|
23
|
+
const compiled = compileBlocksToModule(blocks)
|
|
32
24
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
compiledBlocks.push({
|
|
39
|
-
id: block.id,
|
|
40
|
-
base64,
|
|
41
|
-
captures: block.captures,
|
|
42
|
-
needsMemory: result.needsMemory ?? false,
|
|
43
|
-
wat: result.wat ?? '',
|
|
44
|
-
})
|
|
45
|
-
results.push({
|
|
46
|
-
id: block.id,
|
|
47
|
-
success: true,
|
|
48
|
-
byteLength: result.bytes.length,
|
|
49
|
-
})
|
|
50
|
-
} else {
|
|
51
|
-
results.push({
|
|
52
|
-
id: block.id,
|
|
53
|
-
success: false,
|
|
54
|
-
error: result.error,
|
|
55
|
-
})
|
|
25
|
+
// Map per-block status to the public result shape (preserves input order)
|
|
26
|
+
const exportById = new Map(compiled.exports.map((e) => [e.id, e]))
|
|
27
|
+
const results = compiled.results.map((r) => {
|
|
28
|
+
if (!r.success) {
|
|
29
|
+
return { id: r.id, success: false, error: r.error }
|
|
56
30
|
}
|
|
57
|
-
|
|
31
|
+
const exp = exportById.get(r.id)!
|
|
32
|
+
return {
|
|
33
|
+
id: r.id,
|
|
34
|
+
success: true,
|
|
35
|
+
byteLength: compiled.bytes.length,
|
|
36
|
+
// Per-export byte length isn't meaningful in the consolidated module;
|
|
37
|
+
// we report the total module size for every successful block.
|
|
38
|
+
_exportName: exp.exportName,
|
|
39
|
+
}
|
|
40
|
+
})
|
|
58
41
|
|
|
59
|
-
if (
|
|
42
|
+
if (compiled.exports.length === 0) {
|
|
60
43
|
return { code: '', results }
|
|
61
44
|
}
|
|
62
45
|
|
|
63
|
-
//
|
|
64
|
-
const watComments =
|
|
65
|
-
.map((
|
|
66
|
-
const watLines =
|
|
67
|
-
return `/**\n * WASM: ${
|
|
46
|
+
// WAT comment block — one section per included function
|
|
47
|
+
const watComments = compiled.exports
|
|
48
|
+
.map((e) => {
|
|
49
|
+
const watLines = e.wat.split('\n').map((line) => ` * ${line}`)
|
|
50
|
+
return `/**\n * WASM: ${e.id} (export: ${e.exportName})\n${watLines.join(
|
|
51
|
+
'\n'
|
|
52
|
+
)}\n */`
|
|
68
53
|
})
|
|
69
54
|
.join('\n')
|
|
70
55
|
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
56
|
+
// Per-export metadata embedded in the bootstrap.
|
|
57
|
+
// id = original block id (becomes globalThis[id])
|
|
58
|
+
// n = export name in the composed module (instance.exports[n])
|
|
59
|
+
// c = capture annotations for type-aware wrapping
|
|
60
|
+
// m = whether this export uses memory (must be true if any other
|
|
61
|
+
// export uses it, since memory is shared at module level)
|
|
62
|
+
const exportData = compiled.exports
|
|
74
63
|
.map(
|
|
75
|
-
(
|
|
76
|
-
`{id:${JSON.stringify(
|
|
77
|
-
|
|
78
|
-
)},c:${JSON.stringify(
|
|
64
|
+
(e) =>
|
|
65
|
+
`{id:${JSON.stringify(e.id)},n:${JSON.stringify(
|
|
66
|
+
e.exportName
|
|
67
|
+
)},c:${JSON.stringify(e.captures)},m:${e.needsMemory}}`
|
|
79
68
|
)
|
|
80
69
|
.join(',')
|
|
81
70
|
|
|
82
|
-
|
|
83
|
-
const anyNeedsMemory =
|
|
71
|
+
const moduleBase64 = btoa(String.fromCharCode(...compiled.bytes))
|
|
72
|
+
const anyNeedsMemory = compiled.needsMemory
|
|
84
73
|
|
|
85
74
|
const code = `${watComments}
|
|
86
75
|
;(async()=>{
|
|
87
|
-
const
|
|
76
|
+
const __wasmExports=[${exportData}];
|
|
77
|
+
const __wasmModuleB64=${JSON.stringify(moduleBase64)};
|
|
88
78
|
const __b64ToBytes=s=>{const b=atob(s),a=new Uint8Array(b.length);for(let i=0;i<b.length;i++)a[i]=b.charCodeAt(i);return a};
|
|
89
79
|
const __parseType=c=>{const m=c.match(/^(\\w+)\\s*:\\s*(\\w+)$/);if(!m)return{n:c,t:'f64',a:false};const[,n,ts]=m;const at={Float32Array:'f32',Float64Array:'f64',Int32Array:'i32',Uint8Array:'i32'};if(at[ts])return{n,t:'i32',a:true,at:ts};return{n,t:'f64',a:false}};
|
|
90
80
|
${
|
|
@@ -94,30 +84,32 @@ let __woff=0;
|
|
|
94
84
|
globalThis.wasmBuffer=function(Ctor,len){const bytes=len*Ctor.BYTES_PER_ELEMENT;const align=Math.max(Ctor.BYTES_PER_ELEMENT,16);__woff=(__woff+align-1)&~(align-1);const arr=new Ctor(__wasmMem.buffer,__woff,len);__woff+=bytes;return arr};`
|
|
95
85
|
: ''
|
|
96
86
|
}
|
|
97
|
-
|
|
98
|
-
|
|
87
|
+
const __wasmInst=await WebAssembly.instantiate(await WebAssembly.compile(__b64ToBytes(__wasmModuleB64)),${
|
|
88
|
+
anyNeedsMemory ? '{env:{memory:__wasmMem}}' : '{}'
|
|
89
|
+
});
|
|
90
|
+
for(const{id,n,c,m}of __wasmExports){
|
|
91
|
+
const compute=__wasmInst.exports[n];
|
|
99
92
|
const params=c.map(__parseType);
|
|
100
93
|
const hasArrays=params.some(p=>p.a);
|
|
101
|
-
const mem=m?__wasmMem:null;
|
|
102
|
-
const imp=mem?{env:{memory:mem}}:{};
|
|
103
|
-
const inst=await WebAssembly.instantiate(await WebAssembly.compile(bytes),imp);
|
|
104
|
-
const compute=inst.exports.compute;
|
|
105
94
|
if(!hasArrays){globalThis[id]=compute;continue}
|
|
106
95
|
globalThis[id]=function(...args){
|
|
107
|
-
const mv=new Uint8Array(
|
|
96
|
+
const mv=new Uint8Array(__wasmMem.buffer);let off=__woff;const ptrs=[];
|
|
108
97
|
for(let i=0;i<params.length;i++){const p=params[i],a=args[i];
|
|
109
98
|
if(p.a&&a?.buffer){
|
|
110
|
-
if(a.buffer===
|
|
99
|
+
if(a.buffer===__wasmMem.buffer){ptrs.push(a.byteOffset)}
|
|
111
100
|
else{const ab=new Uint8Array(a.buffer,a.byteOffset,a.byteLength);off=(off+15)&~15;mv.set(ab,off);ptrs.push(off);off+=ab.length}
|
|
112
101
|
} else ptrs.push(a)}
|
|
113
102
|
const r=compute(...ptrs);off=__woff;
|
|
114
103
|
for(let i=0;i<params.length;i++){const p=params[i],a=args[i];
|
|
115
104
|
if(p.a&&a?.buffer){
|
|
116
|
-
if(a.buffer===
|
|
105
|
+
if(a.buffer===__wasmMem.buffer) continue;
|
|
117
106
|
const ab=new Uint8Array(a.buffer,a.byteOffset,a.byteLength);off=(off+15)&~15;ab.set(mv.slice(off,off+ab.length));off+=ab.length}}
|
|
118
107
|
return r};
|
|
119
108
|
}})();
|
|
120
109
|
`.trim()
|
|
121
110
|
|
|
122
|
-
|
|
111
|
+
// Strip the temporary _exportName field before returning to caller.
|
|
112
|
+
const publicResults = results.map(({ _exportName: _, ...rest }) => rest)
|
|
113
|
+
|
|
114
|
+
return { code, results: publicResults }
|
|
123
115
|
}
|
package/src/lang/emitters/js.ts
CHANGED
|
@@ -69,6 +69,10 @@ import {
|
|
|
69
69
|
} from './js-tests'
|
|
70
70
|
export { stripModuleSyntax, stripTjsPreamble } from './js-tests'
|
|
71
71
|
import { generateWasmBootstrap } from './js-wasm'
|
|
72
|
+
import {
|
|
73
|
+
rewriteBoolCoercion,
|
|
74
|
+
rewriteBoolCoercionInSource,
|
|
75
|
+
} from '../bool-coercion'
|
|
72
76
|
|
|
73
77
|
export interface TJSTranspileOptions {
|
|
74
78
|
/** Filename for error messages */
|
|
@@ -97,6 +101,15 @@ export interface TJSTranspileOptions {
|
|
|
97
101
|
* Used when tests depend on imported modules.
|
|
98
102
|
*/
|
|
99
103
|
resolvedImports?: Record<string, string>
|
|
104
|
+
/**
|
|
105
|
+
* Optional ModuleLoader for cross-file `wasm function` composition (Phase 3
|
|
106
|
+
* of the wasm-library plan). When provided, `import { dot } from
|
|
107
|
+
* 'tjs-lang/linalg'`-style imports are resolved at transpile time and any
|
|
108
|
+
* matching `wasm function` declarations are composed into this file's
|
|
109
|
+
* consolidated WebAssembly.Module. When omitted, imports are preserved
|
|
110
|
+
* verbatim (default behavior — runtime resolves them).
|
|
111
|
+
*/
|
|
112
|
+
moduleLoader?: any
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
/** Result of running tests at transpile time */
|
|
@@ -419,6 +432,19 @@ function generateInlineValidationCode(
|
|
|
419
432
|
// 2. Type checks with proper error emission
|
|
420
433
|
for (const [paramName, param] of params) {
|
|
421
434
|
const path = `${pathPrefix}${funcName}.${paramName}`
|
|
435
|
+
|
|
436
|
+
// For array params: if the array contains a MonadicError, propagate
|
|
437
|
+
// the first one we find instead of failing the type check with
|
|
438
|
+
// "expected array, got X". This is the "errors propagate, not
|
|
439
|
+
// accumulate" rule — a function receiving an array of values where
|
|
440
|
+
// one is an error should surface that error, not say the array's
|
|
441
|
+
// shape is wrong.
|
|
442
|
+
if (param.type.kind === 'array') {
|
|
443
|
+
lines.push(
|
|
444
|
+
`if (Array.isArray(${paramName})) { for (const __i of ${paramName}) { if (__i instanceof Error && __i.path !== undefined) return __i } }`
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
|
|
422
448
|
const typeCheck = generateTypeCheckExpr(paramName, param.type)
|
|
423
449
|
|
|
424
450
|
if (typeCheck) {
|
|
@@ -436,6 +462,19 @@ function generateInlineValidationCode(
|
|
|
436
462
|
)
|
|
437
463
|
}
|
|
438
464
|
}
|
|
465
|
+
|
|
466
|
+
// If the param is a function with declared shape (e.g. `fn = (x: 0) => 0`),
|
|
467
|
+
// wrap it so its arguments and return value are validated on every call.
|
|
468
|
+
// Skipped when shape is unspecified or contains non-simple kinds.
|
|
469
|
+
if (param.type.kind === 'function') {
|
|
470
|
+
const shapeCheck = generateFunctionShapeCheck(paramName, param.type, path)
|
|
471
|
+
if (shapeCheck) {
|
|
472
|
+
lines.push(shapeCheck)
|
|
473
|
+
// checkFnShape returns either the function unchanged or a
|
|
474
|
+
// MonadicError. Re-check Error propagation after the assignment.
|
|
475
|
+
lines.push(`if (${paramName} instanceof Error) return ${paramName};`)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
439
478
|
}
|
|
440
479
|
|
|
441
480
|
if (lines.length === 0) return null
|
|
@@ -601,13 +640,19 @@ export function transpileToJS(
|
|
|
601
640
|
} = parse(cleanSource, {
|
|
602
641
|
filename,
|
|
603
642
|
colonShorthand: true,
|
|
643
|
+
moduleLoader: options.moduleLoader,
|
|
604
644
|
})
|
|
605
645
|
|
|
606
646
|
// Find ALL functions in the program
|
|
607
647
|
const functions = findAllFunctions(program)
|
|
608
648
|
|
|
609
649
|
// Preprocess source (handles TJS syntax transformations)
|
|
610
|
-
|
|
650
|
+
// Pass through the moduleLoader so Phase 3 cross-file wasm composition
|
|
651
|
+
// sees imported `wasm function` declarations.
|
|
652
|
+
const preprocessed = preprocess(cleanSource, {
|
|
653
|
+
moduleLoader: options.moduleLoader,
|
|
654
|
+
filename,
|
|
655
|
+
})
|
|
611
656
|
|
|
612
657
|
// Apply the same source-level equality transforms to extracted test/mock
|
|
613
658
|
// bodies so they observe the module's TJS semantics (e.g. structural ==).
|
|
@@ -618,12 +663,18 @@ export function transpileToJS(
|
|
|
618
663
|
if (preprocessed.tjsModes.tjsEquals) {
|
|
619
664
|
t.body = transformEqualityToStructural(t.body)
|
|
620
665
|
}
|
|
666
|
+
if (preprocessed.tjsModes.tjsStandard) {
|
|
667
|
+
t.body = rewriteBoolCoercionInSource(t.body)
|
|
668
|
+
}
|
|
621
669
|
}
|
|
622
670
|
for (const m of mocks) {
|
|
623
671
|
m.body = transformIsOperators(m.body)
|
|
624
672
|
if (preprocessed.tjsModes.tjsEquals) {
|
|
625
673
|
m.body = transformEqualityToStructural(m.body)
|
|
626
674
|
}
|
|
675
|
+
if (preprocessed.tjsModes.tjsStandard) {
|
|
676
|
+
m.body = rewriteBoolCoercionInSource(m.body)
|
|
677
|
+
}
|
|
627
678
|
}
|
|
628
679
|
|
|
629
680
|
// Build types map for all functions
|
|
@@ -671,6 +722,41 @@ export function transpileToJS(
|
|
|
671
722
|
warnings.push(...funcWarnings)
|
|
672
723
|
allTypes[funcName] = types
|
|
673
724
|
|
|
725
|
+
// Cross-reference inference: when a parameter default is a bare
|
|
726
|
+
// identifier referring to a previously-declared TJS function, use that
|
|
727
|
+
// function's signature as the parameter's type. So
|
|
728
|
+
//
|
|
729
|
+
// function strLength(s: ''): 0 { ... }
|
|
730
|
+
// function map(arr: [''], counter = strLength) { ... }
|
|
731
|
+
//
|
|
732
|
+
// makes `counter`'s type `(s: string) => integer` (instead of `any`),
|
|
733
|
+
// which means the checkFnShape pass-time check fires when a wrong-
|
|
734
|
+
// shape callback is passed at the call site.
|
|
735
|
+
for (const param of func.params) {
|
|
736
|
+
if (
|
|
737
|
+
param.type === 'AssignmentPattern' &&
|
|
738
|
+
param.left.type === 'Identifier' &&
|
|
739
|
+
param.right.type === 'Identifier'
|
|
740
|
+
) {
|
|
741
|
+
const localName = param.left.name
|
|
742
|
+
const refName = (param.right as any).name as string
|
|
743
|
+
const refInfo = allTypes[refName]
|
|
744
|
+
if (refInfo && types.params[localName]) {
|
|
745
|
+
const fnParams = Object.entries(refInfo.params).map(([n, p]) => ({
|
|
746
|
+
name: n,
|
|
747
|
+
type: p.type,
|
|
748
|
+
}))
|
|
749
|
+
const fnReturns =
|
|
750
|
+
(refInfo as any).returns ?? ({ kind: 'any' } as TypeDescriptor)
|
|
751
|
+
types.params[localName].type = {
|
|
752
|
+
kind: 'function',
|
|
753
|
+
params: fnParams,
|
|
754
|
+
returns: fnReturns,
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
674
760
|
// Clean up param defaults in the emitted JS.
|
|
675
761
|
// After colon→equals transform, `x: false | undefined` becomes
|
|
676
762
|
// `x = false | undefined` in the parsed source.
|
|
@@ -780,6 +866,18 @@ export function transpileToJS(
|
|
|
780
866
|
}
|
|
781
867
|
}
|
|
782
868
|
|
|
869
|
+
// Boolean coercion rewrite (TjsStandard). Rewrites every truthiness
|
|
870
|
+
// context (`if`, `while`, `for`, `do/while`, `!`, `&&`, `||`, `?:`,
|
|
871
|
+
// and `Boolean(x)` calls) to call `__tjs.toBool` so boxed primitives
|
|
872
|
+
// unwrap before coercion. See src/lang/bool-coercion.ts.
|
|
873
|
+
if (preprocessed.tjsModes.tjsStandard) {
|
|
874
|
+
const boolPatches = rewriteBoolCoercion(program, preprocessed.source)
|
|
875
|
+
for (const p of boolPatches) {
|
|
876
|
+
deletions.push({ start: p.start, end: p.end })
|
|
877
|
+
insertions.push({ position: p.start, text: p.newText })
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
783
881
|
// Apply deletions first (reverse order to maintain offsets), then insertions.
|
|
784
882
|
// Deletions strip | union suffixes from param defaults in the output JS.
|
|
785
883
|
deletions.sort((a, b) => b.start - a.start)
|
|
@@ -821,6 +919,8 @@ export function transpileToJS(
|
|
|
821
919
|
const needsEnum = /\bEnum\(/.test(code)
|
|
822
920
|
const needsUnion = /\bUnion\(/.test(code)
|
|
823
921
|
const needsBang = code.includes('__tjs.bang(')
|
|
922
|
+
const needsToBool = code.includes('__tjs.toBool(')
|
|
923
|
+
const needsCheckFnShape = code.includes('__tjs.checkFnShape(')
|
|
824
924
|
const needsSafeEval = preprocessed.tjsModes.tjsSafeEval
|
|
825
925
|
|
|
826
926
|
const needsRuntime =
|
|
@@ -837,6 +937,8 @@ export function transpileToJS(
|
|
|
837
937
|
needsEnum ||
|
|
838
938
|
needsUnion ||
|
|
839
939
|
needsBang ||
|
|
940
|
+
needsToBool ||
|
|
941
|
+
needsCheckFnShape ||
|
|
840
942
|
needsSafeEval
|
|
841
943
|
|
|
842
944
|
if (needsRuntime) {
|
|
@@ -913,6 +1015,28 @@ export function transpileToJS(
|
|
|
913
1015
|
`function Union(d,...v){const vals=v.flat();return{description:d,check:x=>vals.includes(x),values:vals,__runtimeType:true}}`
|
|
914
1016
|
)
|
|
915
1017
|
}
|
|
1018
|
+
// toBool — honest truthiness (unwraps boxed primitives)
|
|
1019
|
+
if (needsToBool) {
|
|
1020
|
+
inlineParts.push(
|
|
1021
|
+
`function toBool(v){if(v instanceof Boolean||v instanceof Number||v instanceof String)return Boolean(v.valueOf());return Boolean(v)}`
|
|
1022
|
+
)
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// checkFnShape — pass-time shape check for function-typed params
|
|
1026
|
+
if (needsCheckFnShape) {
|
|
1027
|
+
// checkFnShape depends on MonadicError; ensure it's inlined
|
|
1028
|
+
if (!needsTypeError) {
|
|
1029
|
+
inlineParts.push(
|
|
1030
|
+
`class MonadicError extends Error{constructor(m,p,e,a,c,r){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c;this.reason=r}}`,
|
|
1031
|
+
`function typeError(p,e,v,r){const a=v===null?'null':typeof v;const m=r?'Expected '+e+" for '"+p+"': "+r:'Expected '+e+" for '"+p+"', got "+a;const err=new MonadicError(m,p,e,a,undefined,r);const c=globalThis.__tjs?.getConfig?.();if(c?.logTypeErrors)console.error('[TJS TypeError] '+err.message);if(c?.throwTypeErrors)throw err;return err}`,
|
|
1032
|
+
`function isMonadicError(v){return v instanceof Error&&v.name==='MonadicError'&&'path' in v}`
|
|
1033
|
+
)
|
|
1034
|
+
}
|
|
1035
|
+
inlineParts.push(
|
|
1036
|
+
`function checkFnShape(fn,expectedParams,expectedReturn,path){if(typeof fn!=='function')return fn;const meta=fn.__tjs;if(!meta||!meta.params)return fn;const entries=Object.entries(meta.params);for(let i=0;i<expectedParams.length;i++){const e=expectedParams[i];if(e==='any')continue;const a=entries[i];if(!a)continue;const ak=a[1]&&a[1].type&&a[1].type.kind;if(!ak||ak==='any')continue;if(ak!==e)return new MonadicError("Expected (...arg"+i+": "+e+", ...) for '"+path+"', but callback declares arg"+i+" as "+ak,path+"(arg"+i+")",e,ak)}if(expectedReturn!=='any'&&meta.returns){const ar=(meta.returns.type&&meta.returns.type.kind)||meta.returns.kind;if(ar&&ar!=='any'&&ar!==expectedReturn)return new MonadicError("Expected callback returning "+expectedReturn+" for '"+path+"', but callback returns "+ar,path+"(return)",expectedReturn,ar)}return fn}`
|
|
1037
|
+
)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
916
1040
|
// Bang access (!.) — asserted non-null member access
|
|
917
1041
|
if (needsBang) {
|
|
918
1042
|
// bang depends on typeError and isMonadicError — ensure they're inlined
|
|
@@ -947,6 +1071,11 @@ export function transpileToJS(
|
|
|
947
1071
|
if (needsFunctionPredicate) fallbackEntries.push('FunctionPredicate')
|
|
948
1072
|
if (needsEnum) fallbackEntries.push('Enum')
|
|
949
1073
|
if (needsUnion) fallbackEntries.push('Union')
|
|
1074
|
+
if (needsToBool) fallbackEntries.push('toBool')
|
|
1075
|
+
if (needsCheckFnShape) {
|
|
1076
|
+
fallbackEntries.push('checkFnShape')
|
|
1077
|
+
if (!needsTypeError) fallbackEntries.push('typeError', 'isMonadicError')
|
|
1078
|
+
}
|
|
950
1079
|
if (needsBang) {
|
|
951
1080
|
fallbackEntries.push('bang')
|
|
952
1081
|
// Ensure typeError/isMonadicError are in fallback even if not otherwise needed
|
|
@@ -1289,13 +1418,33 @@ function generateTypeCheckExpr(
|
|
|
1289
1418
|
return `${fieldPath} !== null` // nullable doesn't apply to null itself
|
|
1290
1419
|
case 'undefined':
|
|
1291
1420
|
return `${fieldPath} !== undefined`
|
|
1292
|
-
case 'array':
|
|
1293
|
-
|
|
1421
|
+
case 'array': {
|
|
1422
|
+
// Always require an Array. If item type is known and non-trivial,
|
|
1423
|
+
// also validate every item — `arr: [0]` means "array of integers",
|
|
1424
|
+
// not "any array". Without this, a function returning
|
|
1425
|
+
// `[MonadicError, MonadicError]` would pass the `: [0]` return-
|
|
1426
|
+
// type check (it's an array) and surface a confusing array-of-
|
|
1427
|
+
// errors to the caller.
|
|
1428
|
+
const itemCheck =
|
|
1429
|
+
type.items && type.items.kind !== 'any'
|
|
1430
|
+
? generateTypeCheckExpr('__a', type.items)
|
|
1431
|
+
: null
|
|
1432
|
+
if (itemCheck) {
|
|
1433
|
+
check = `(!Array.isArray(${fieldPath}) || ${fieldPath}.some(__a => ${itemCheck}))`
|
|
1434
|
+
} else {
|
|
1435
|
+
check = `!Array.isArray(${fieldPath})`
|
|
1436
|
+
}
|
|
1294
1437
|
break
|
|
1438
|
+
}
|
|
1295
1439
|
case 'object':
|
|
1296
1440
|
// For nested objects, just check it's an object (deep validation is separate)
|
|
1297
1441
|
check = `(typeof ${fieldPath} !== 'object' || ${fieldPath} === null || Array.isArray(${fieldPath}))`
|
|
1298
1442
|
break
|
|
1443
|
+
case 'function':
|
|
1444
|
+
// Shape isn't validated at call time (we don't introspect arity or
|
|
1445
|
+
// call the function with probes) — just check it IS callable.
|
|
1446
|
+
check = `typeof ${fieldPath} !== 'function'`
|
|
1447
|
+
break
|
|
1299
1448
|
case 'union': {
|
|
1300
1449
|
const checks = (type as any).members
|
|
1301
1450
|
.map((m: TypeDescriptor) => generateTypeCheckExpr(fieldPath, m))
|
|
@@ -1321,6 +1470,52 @@ function generateTypeCheckExpr(
|
|
|
1321
1470
|
// Alias for backward compatibility with other functions that use this
|
|
1322
1471
|
const generateTypeCheck = generateTypeCheckExpr
|
|
1323
1472
|
|
|
1473
|
+
/** Kinds checkType can validate by string name (no RuntimeType needed). */
|
|
1474
|
+
const SIMPLE_KINDS = new Set([
|
|
1475
|
+
'string',
|
|
1476
|
+
'number',
|
|
1477
|
+
'integer',
|
|
1478
|
+
'non-negative-integer',
|
|
1479
|
+
'boolean',
|
|
1480
|
+
'function',
|
|
1481
|
+
'any',
|
|
1482
|
+
'undefined',
|
|
1483
|
+
'null',
|
|
1484
|
+
'object', // checkType handles this via typeof
|
|
1485
|
+
])
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Generate a `__tjs.checkFnShape(...)` call that validates a passed-in
|
|
1489
|
+
* function's declared shape against the expected shape ONCE at pass time.
|
|
1490
|
+
* On mismatch the param is reassigned to a MonadicError; the existing
|
|
1491
|
+
* `if (param instanceof Error) return param` check above handles
|
|
1492
|
+
* propagation. On match the param is unchanged. Untyped functions
|
|
1493
|
+
* (no `__tjs` metadata — anonymous arrows) pass through unchanged.
|
|
1494
|
+
*
|
|
1495
|
+
* Returns null when the expected shape can't be represented as simple
|
|
1496
|
+
* TypeSpec strings, or when there's nothing useful to check (all-`any`).
|
|
1497
|
+
*/
|
|
1498
|
+
function generateFunctionShapeCheck(
|
|
1499
|
+
paramName: string,
|
|
1500
|
+
type: TypeDescriptor,
|
|
1501
|
+
path: string
|
|
1502
|
+
): string | null {
|
|
1503
|
+
const fnParams = (type.params ?? []) as Array<{
|
|
1504
|
+
name: string
|
|
1505
|
+
type: TypeDescriptor
|
|
1506
|
+
}>
|
|
1507
|
+
const fnReturns = type.returns ?? { kind: 'any' as const }
|
|
1508
|
+
const paramKinds = fnParams.map((p) => p.type?.kind)
|
|
1509
|
+
const allSimple =
|
|
1510
|
+
paramKinds.every((k) => k && SIMPLE_KINDS.has(k)) &&
|
|
1511
|
+
SIMPLE_KINDS.has(fnReturns.kind)
|
|
1512
|
+
const hasUsefulCheck =
|
|
1513
|
+
paramKinds.some((k) => k !== 'any') || fnReturns.kind !== 'any'
|
|
1514
|
+
if (!allSimple || !hasUsefulCheck) return null
|
|
1515
|
+
const paramTypesJson = JSON.stringify(paramKinds)
|
|
1516
|
+
return `if (typeof ${paramName} === 'function') ${paramName} = __tjs.checkFnShape(${paramName}, ${paramTypesJson}, '${fnReturns.kind}', '${path}');`
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1324
1519
|
/**
|
|
1325
1520
|
* Generate the complete function wrapper with inline validation
|
|
1326
1521
|
*
|
|
@@ -1524,9 +1524,10 @@ function double(x: 0, y: 0) {
|
|
|
1524
1524
|
expect(result.wasmCompiled?.[0].success).toBe(true)
|
|
1525
1525
|
expect(result.wasmCompiled?.[0].byteLength).toBeGreaterThan(0)
|
|
1526
1526
|
|
|
1527
|
-
// Output should contain base64-encoded WASM
|
|
1528
|
-
|
|
1529
|
-
expect(result.code).toContain('
|
|
1527
|
+
// Output should contain base64-encoded WASM. After Phase 0.5 consolidation
|
|
1528
|
+
// the emitter ships one module per file and a per-export metadata table.
|
|
1529
|
+
expect(result.code).toContain('__wasmExports')
|
|
1530
|
+
expect(result.code).toContain('__wasmModuleB64')
|
|
1530
1531
|
|
|
1531
1532
|
// Execute with async function to allow WASM instantiation
|
|
1532
1533
|
const fn = new Function(
|
package/src/lang/index.ts
CHANGED
|
@@ -74,6 +74,15 @@ export {
|
|
|
74
74
|
type FunctionTypeInfo,
|
|
75
75
|
type ParamTypeInfo,
|
|
76
76
|
} from './docs'
|
|
77
|
+
export {
|
|
78
|
+
ModuleLoader,
|
|
79
|
+
inMemoryFileSystem,
|
|
80
|
+
type ModuleLoaderOptions,
|
|
81
|
+
type FileSystem,
|
|
82
|
+
type LoadedModule,
|
|
83
|
+
type ImportEntry,
|
|
84
|
+
type ExportEntry,
|
|
85
|
+
} from './module-loader'
|
|
77
86
|
export {
|
|
78
87
|
extractTests,
|
|
79
88
|
assertFunction,
|
package/src/lang/inference.ts
CHANGED
|
@@ -137,6 +137,31 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
|
|
|
137
137
|
return { kind: 'any' }
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
case 'ArrowFunctionExpression':
|
|
141
|
+
case 'FunctionExpression': {
|
|
142
|
+
// Function example value (e.g. `fn = (x) => x` or `cb = function() {}`).
|
|
143
|
+
// Capture parameter names + types and (for concise arrow bodies)
|
|
144
|
+
// infer the return type from the body expression.
|
|
145
|
+
const fn = node as any
|
|
146
|
+
const params: Array<{ name: string; type: TypeDescriptor }> =
|
|
147
|
+
fn.params.map((p: any) => paramShape(p))
|
|
148
|
+
|
|
149
|
+
// Concise arrow body: body IS the return expression, so we can
|
|
150
|
+
// infer its type. Block bodies (function expressions, multi-line
|
|
151
|
+
// arrows) stay `any` — scanning return statements is a separate
|
|
152
|
+
// can of worms.
|
|
153
|
+
let returns: TypeDescriptor = { kind: 'any' }
|
|
154
|
+
if (
|
|
155
|
+
fn.type === 'ArrowFunctionExpression' &&
|
|
156
|
+
fn.body &&
|
|
157
|
+
fn.body.type !== 'BlockStatement'
|
|
158
|
+
) {
|
|
159
|
+
returns = inferTypeFromValue(fn.body)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { kind: 'function', params, returns }
|
|
163
|
+
}
|
|
164
|
+
|
|
140
165
|
case 'UnaryExpression': {
|
|
141
166
|
const op = (node as any).operator
|
|
142
167
|
const arg = (node as any).argument
|
|
@@ -168,6 +193,35 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
|
|
|
168
193
|
}
|
|
169
194
|
}
|
|
170
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Extract a function-parameter shape from a Pattern AST node. Used when
|
|
198
|
+
* we encounter a function/arrow EXAMPLE value and want to record what
|
|
199
|
+
* its declared parameters look like for documentation and .d.ts emit.
|
|
200
|
+
*
|
|
201
|
+
* Plain identifier (`x`) → { name: 'x', type: any }
|
|
202
|
+
* Default value (`x = 0`) → { name: 'x', type: integer }
|
|
203
|
+
* Rest (`...args`) → { name: '...args', type: array }
|
|
204
|
+
* Destructuring (`{a}`, `[x]`) → name: '?', type: any (we'd need
|
|
205
|
+
* to mirror parseParameter to do
|
|
206
|
+
* this properly; not worth the
|
|
207
|
+
* complexity for example values)
|
|
208
|
+
*/
|
|
209
|
+
function paramShape(p: any): { name: string; type: TypeDescriptor } {
|
|
210
|
+
if (p.type === 'Identifier') {
|
|
211
|
+
return { name: p.name, type: { kind: 'any' } }
|
|
212
|
+
}
|
|
213
|
+
if (p.type === 'AssignmentPattern' && p.left?.type === 'Identifier') {
|
|
214
|
+
return { name: p.left.name, type: inferTypeFromValue(p.right) }
|
|
215
|
+
}
|
|
216
|
+
if (p.type === 'RestElement' && p.argument?.type === 'Identifier') {
|
|
217
|
+
return {
|
|
218
|
+
name: `...${p.argument.name}`,
|
|
219
|
+
type: { kind: 'array', items: { kind: 'any' } },
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { name: '?', type: { kind: 'any' } }
|
|
223
|
+
}
|
|
224
|
+
|
|
171
225
|
/**
|
|
172
226
|
* Parse a parameter and extract its type and default value
|
|
173
227
|
*
|