tjs-lang 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CLAUDE.md +99 -33
  2. package/bin/docs.js +4 -1
  3. package/demo/docs.json +104 -22
  4. package/demo/src/examples.test.ts +1 -0
  5. package/demo/src/imports.test.ts +16 -4
  6. package/demo/src/imports.ts +60 -15
  7. package/demo/src/playground-shared.ts +9 -8
  8. package/demo/src/tfs-worker.js +205 -147
  9. package/demo/src/tjs-playground.ts +34 -10
  10. package/demo/src/ts-examples.ts +8 -8
  11. package/demo/src/ts-playground.ts +24 -8
  12. package/dist/index.js +118 -101
  13. package/dist/index.js.map +4 -4
  14. package/dist/src/lang/bool-coercion.d.ts +50 -0
  15. package/dist/src/lang/docs.d.ts +31 -6
  16. package/dist/src/lang/linter.d.ts +8 -0
  17. package/dist/src/lang/parser-transforms.d.ts +18 -0
  18. package/dist/src/lang/parser-types.d.ts +2 -0
  19. package/dist/src/lang/parser.d.ts +3 -0
  20. package/dist/src/lang/runtime.d.ts +34 -0
  21. package/dist/src/lang/types.d.ts +9 -1
  22. package/dist/src/rbac/index.d.ts +1 -1
  23. package/dist/src/vm/runtime.d.ts +1 -1
  24. package/dist/tjs-eval.js +38 -36
  25. package/dist/tjs-eval.js.map +4 -4
  26. package/dist/tjs-from-ts.js +20 -20
  27. package/dist/tjs-from-ts.js.map +3 -3
  28. package/dist/tjs-lang.js +85 -83
  29. package/dist/tjs-lang.js.map +4 -4
  30. package/dist/tjs-vm.js +47 -45
  31. package/dist/tjs-vm.js.map +4 -4
  32. package/llms.txt +79 -0
  33. package/package.json +9 -4
  34. package/src/cli/commands/convert.test.ts +16 -21
  35. package/src/lang/bool-coercion.test.ts +203 -0
  36. package/src/lang/bool-coercion.ts +314 -0
  37. package/src/lang/codegen.test.ts +137 -0
  38. package/src/lang/docs.test.ts +476 -1
  39. package/src/lang/docs.ts +471 -37
  40. package/src/lang/emitters/ast.ts +11 -12
  41. package/src/lang/emitters/dts.test.ts +41 -0
  42. package/src/lang/emitters/dts.ts +9 -0
  43. package/src/lang/emitters/js-tests.ts +9 -4
  44. package/src/lang/emitters/js-wasm.ts +57 -65
  45. package/src/lang/emitters/js.ts +198 -3
  46. package/src/lang/features.test.ts +4 -3
  47. package/src/lang/index.ts +9 -0
  48. package/src/lang/inference.ts +54 -0
  49. package/src/lang/linter.test.ts +104 -1
  50. package/src/lang/linter.ts +124 -1
  51. package/src/lang/module-loader.test.ts +318 -0
  52. package/src/lang/module-loader.ts +419 -0
  53. package/src/lang/parser-params.ts +31 -0
  54. package/src/lang/parser-transforms.ts +640 -0
  55. package/src/lang/parser-types.ts +35 -0
  56. package/src/lang/parser.test.ts +73 -1
  57. package/src/lang/parser.ts +77 -3
  58. package/src/lang/runtime.ts +98 -0
  59. package/src/lang/types.ts +6 -0
  60. package/src/lang/wasm.test.ts +1293 -2
  61. package/src/lang/wasm.ts +470 -87
  62. package/src/linalg/index.tjs +119 -0
  63. package/src/linalg/linalg.test.ts +294 -0
  64. package/src/linalg/vector-search.bench.test.ts +395 -0
  65. package/src/rbac/index.ts +2 -2
  66. package/src/rbac/rules.tjs.d.ts +9 -0
  67. package/src/vm/atoms/batteries.ts +2 -2
  68. package/src/vm/runtime.ts +10 -3
  69. package/dist/src/rbac/rules.d.ts +0 -184
  70. package/src/rbac/rules.js +0 -338
@@ -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 inline WASM blocks and generates JavaScript bootstrap code.
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 { compileToWasm } from '../wasm'
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 results: {
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
- for (const block of blocks) {
34
- const result = compileToWasm(block)
35
- if (result.success) {
36
- // Convert bytes to base64 for embedding
37
- const base64 = btoa(String.fromCharCode(...result.bytes))
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 (compiledBlocks.length === 0) {
42
+ if (compiled.exports.length === 0) {
60
43
  return { code: '', results }
61
44
  }
62
45
 
63
- // Generate WAT comments for each block
64
- const watComments = compiledBlocks
65
- .map((b) => {
66
- const watLines = b.wat.split('\n').map((line) => ` * ${line}`)
67
- return `/**\n * WASM: ${b.id}\n${watLines.join('\n')}\n */`
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
- // Generate self-contained bootstrap code
72
- // This runs immediately and sets up globalThis.__tjs_wasm_N functions
73
- const blockData = compiledBlocks
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
- (b) =>
76
- `{id:${JSON.stringify(b.id)},b64:${JSON.stringify(
77
- b.base64
78
- )},c:${JSON.stringify(b.captures)},m:${b.needsMemory}}`
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
- // Check if any block needs memory (shared across all blocks)
83
- const anyNeedsMemory = compiledBlocks.some((b) => b.needsMemory)
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 __wasmBlocks=[${blockData}];
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
- for(const{id,b64,c,m}of __wasmBlocks){
98
- const bytes=__b64ToBytes(b64);
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(mem.buffer);let off=__woff;const ptrs=[];
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===mem.buffer){ptrs.push(a.byteOffset)}
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===mem.buffer) continue;
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
- return { code, results }
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
  }
@@ -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
- const preprocessed = preprocess(cleanSource)
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
- check = `!Array.isArray(${fieldPath})`
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
- expect(result.code).toContain('__wasmBlocks')
1529
- expect(result.code).toContain('b64:')
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,
@@ -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
  *