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
@@ -15,6 +15,17 @@ export interface ParseOptions {
15
15
  * When true, skips == to Is() transformation since the VM handles == correctly.
16
16
  */
17
17
  vmTarget?: boolean
18
+ /**
19
+ * Optional ModuleLoader for cross-file `wasm function` composition (Phase 3).
20
+ * When provided, imports are resolved at transpile time and matching wasm
21
+ * functions are composed into the consumer's WebAssembly.Module. When
22
+ * omitted, imports are preserved verbatim (the default — runtime resolves
23
+ * them as before).
24
+ *
25
+ * Type is left as `any` here to avoid a circular import with module-loader.ts;
26
+ * callers should pass a `ModuleLoader` instance.
27
+ */
28
+ moduleLoader?: any
18
29
  }
19
30
 
20
31
  /**
@@ -37,6 +48,21 @@ export interface ParseOptions {
37
48
  export interface WasmBlock {
38
49
  /** Unique ID for this block */
39
50
  id: string
51
+ /**
52
+ * Declared function name (only set for top-level `wasm function NAME(...)`
53
+ * declarations — Phase 1+). Used by Phase 3 cross-file composition to
54
+ * match an imported symbol against a wasm function declaration. Inline
55
+ * `wasm {}` blocks have no name and don't participate in composition.
56
+ */
57
+ name?: string
58
+ /**
59
+ * Declared return-type annotation, e.g. `'f64'`. Only set for top-level
60
+ * `wasm function NAME(...): RetType` declarations; presence/absence is
61
+ * used to determine `hasReturn` BEFORE the body is compiled, so the
62
+ * function index map can be built up-front for wasm-to-wasm calls.
63
+ * Inline blocks have no declared return type.
64
+ */
65
+ returnType?: string
40
66
  /** The body (JS subset that compiles to WASM, also used as fallback) */
41
67
  body: string
42
68
  /** Explicit fallback body (only if different from body) */
@@ -83,6 +109,13 @@ export interface PreprocessOptions {
83
109
  * Default: false (transform == to Is() for TJS code running in regular JS)
84
110
  */
85
111
  vmTarget?: boolean
112
+ /**
113
+ * Optional ModuleLoader for cross-file `wasm function` composition (Phase 3).
114
+ * See ParseOptions.moduleLoader for details.
115
+ */
116
+ moduleLoader?: any
117
+ /** Path of the file being preprocessed (used as importer context). */
118
+ filename?: string
86
119
  }
87
120
 
88
121
  /**
@@ -144,6 +177,8 @@ export interface TjsModes {
144
177
  tjsSafeEval: boolean
145
178
  /** TjsNoVar: var declarations are syntax errors */
146
179
  tjsNoVar: boolean
180
+ /** TjsSafeAssign: let declarations need an initializer or `: example` annotation; literal undefined/null/void 0 assigned to typed lets is flagged */
181
+ tjsSafeAssign: boolean
147
182
  }
148
183
 
149
184
  /**
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from 'bun:test'
2
2
  import { transpile, ajs, tjs } from './index'
3
- import { preprocess } from './parser'
3
+ import { preprocess, parse } from './parser'
4
4
  import { createRuntime, isMonadicError } from './runtime'
5
5
 
6
6
  describe('Transpiler', () => {
@@ -1067,4 +1067,76 @@ test 'always fails' { throw new Error('intentional') }
1067
1067
  expect(varSetStep.value.op).toBe('??')
1068
1068
  })
1069
1069
  })
1070
+
1071
+ describe('stray `=>` on function declarations', () => {
1072
+ it('errors clearly on `function f(...): T => { body }`', () => {
1073
+ // A common mistake — writing arrow-function syntax on a regular
1074
+ // function declaration. Without the dedicated check, the `=>` falls
1075
+ // through to Acorn which complains at a misleading position.
1076
+ expect(() =>
1077
+ tjs(`function f(s: '', n = 0): 0 => {
1078
+ return s.length + n
1079
+ }`)
1080
+ ).toThrow(/Unexpected '=>' after function declaration/)
1081
+ })
1082
+
1083
+ it('errors with column pointing at the `=>` (not earlier in the line)', () => {
1084
+ let caught: any
1085
+ try {
1086
+ tjs(`function f(): 0 => { return 0 }`)
1087
+ } catch (e) {
1088
+ caught = e
1089
+ }
1090
+ expect(caught).toBeDefined()
1091
+ // The `=>` is at column 17 (1-indexed): `function f(): 0 ` is 16 chars
1092
+ expect(caught.column).toBe(16)
1093
+ })
1094
+
1095
+ it('does not error on regular function declarations', () => {
1096
+ expect(() =>
1097
+ tjs(`function f(s: ''): 0 { return s.length }`)
1098
+ ).not.toThrow()
1099
+ })
1100
+
1101
+ it('does not error on real arrow function expressions', () => {
1102
+ expect(() => tjs(`const f = (s = '') => s.length`)).not.toThrow()
1103
+ })
1104
+ })
1105
+
1106
+ describe('let type annotations (TjsSafeAssign)', () => {
1107
+ it("strips `: <example>` from `let x: ''` and records annotation", () => {
1108
+ const r = parse(`let x: ''`)
1109
+ expect(r.letAnnotations.get('x')).toBe("''")
1110
+ })
1111
+
1112
+ it('strips `: <example>` from `let x: 0 = 5` and keeps the initializer', () => {
1113
+ const r = parse(`let x: 0 = 5`)
1114
+ expect(r.letAnnotations.get('x')).toBe('0')
1115
+ const decl = r.ast.body[0] as any
1116
+ expect(decl.type).toBe('VariableDeclaration')
1117
+ expect(decl.declarations[0].init).not.toBeNull()
1118
+ })
1119
+
1120
+ it('handles object-literal example with nested braces', () => {
1121
+ const r = parse(
1122
+ `function f() { let result: { ok: false }; return result }`
1123
+ )
1124
+ expect(r.letAnnotations.get('result')).toBe('{ ok: false }')
1125
+ })
1126
+
1127
+ it('does not strip `:` inside string literals', () => {
1128
+ const r = parse(`let s = 'a:b:c'`)
1129
+ expect(r.letAnnotations.size).toBe(0)
1130
+ })
1131
+
1132
+ it('TjsSafeAssign mode is on by default in native TJS', () => {
1133
+ const r = parse(`let x = 0`)
1134
+ expect(r.tjsModes.tjsSafeAssign).toBe(true)
1135
+ })
1136
+
1137
+ it('TjsCompat directive disables TjsSafeAssign', () => {
1138
+ const r = parse(`TjsCompat\nlet x = 0`)
1139
+ expect(r.tjsModes.tjsSafeAssign).toBe(false)
1140
+ })
1141
+ })
1070
1142
  })
@@ -31,6 +31,8 @@ import { transformParenExpressions } from './parser-params'
31
31
  import {
32
32
  transformTryWithoutCatch,
33
33
  extractWasmBlocks,
34
+ extractWasmFunctions,
35
+ composeImportedWasmFunctions,
34
36
  transformIsOperators,
35
37
  insertAsiProtection,
36
38
  transformEqualityToStructural,
@@ -51,6 +53,7 @@ import {
51
53
  transformConstBang,
52
54
  transformBangAccess,
53
55
  transformExtensionCalls,
56
+ transformLetTypeAnnotations,
54
57
  } from './parser-transforms'
55
58
 
56
59
  // Re-export transformExtensionCalls for js.ts
@@ -120,6 +123,7 @@ export function preprocess(
120
123
  testErrors: string[]
121
124
  polymorphicNames: Set<string>
122
125
  extensions: Map<string, Set<string>>
126
+ letAnnotations: Map<string, string>
123
127
  } {
124
128
  const originalSource = source
125
129
  let moduleSafety: 'none' | 'inputs' | 'all' | undefined
@@ -143,6 +147,7 @@ export function preprocess(
143
147
  tjsStandard: false,
144
148
  tjsSafeEval: false,
145
149
  tjsNoVar: false,
150
+ tjsSafeAssign: false,
146
151
  }
147
152
  : {
148
153
  tjsEquals: true,
@@ -152,6 +157,7 @@ export function preprocess(
152
157
  tjsStandard: true,
153
158
  tjsSafeEval: false, // opt-in only (adds import)
154
159
  tjsNoVar: true,
160
+ tjsSafeAssign: true,
155
161
  }
156
162
 
157
163
  // Safety: native TJS defaults to 'inputs' (runtime default),
@@ -180,7 +186,7 @@ export function preprocess(
180
186
  // TjsCompat disables all TJS modes (useful for native TJS opting out)
181
187
  // Individual modes: TjsEquals, TjsClass, TjsDate, TjsNoeval, TjsStandard, TjsSafeEval
182
188
  const directivePattern =
183
- /^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)\s*(TjsStrict|TjsCompat|TjsEquals|TjsClass|TjsDate|TjsNoeval|TjsNoVar|TjsStandard|TjsSafeEval)\b/
189
+ /^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)\s*(TjsStrict|TjsCompat|TjsEquals|TjsClass|TjsDate|TjsNoeval|TjsNoVar|TjsStandard|TjsSafeEval|TjsSafeAssign)\b/
184
190
 
185
191
  let match
186
192
  while ((match = source.match(directivePattern))) {
@@ -194,6 +200,7 @@ export function preprocess(
194
200
  tjsModes.tjsNoeval = true
195
201
  tjsModes.tjsNoVar = true
196
202
  tjsModes.tjsStandard = true
203
+ tjsModes.tjsSafeAssign = true
197
204
  } else if (directive === 'TjsCompat') {
198
205
  // Disable all TJS modes (JS-compatible)
199
206
  tjsModes.tjsEquals = false
@@ -203,6 +210,7 @@ export function preprocess(
203
210
  tjsModes.tjsNoVar = false
204
211
  tjsModes.tjsStandard = false
205
212
  tjsModes.tjsSafeEval = false
213
+ tjsModes.tjsSafeAssign = false
206
214
  } else if (directive === 'TjsEquals') {
207
215
  tjsModes.tjsEquals = true
208
216
  } else if (directive === 'TjsClass') {
@@ -217,6 +225,8 @@ export function preprocess(
217
225
  tjsModes.tjsStandard = true
218
226
  } else if (directive === 'TjsSafeEval') {
219
227
  tjsModes.tjsSafeEval = true
228
+ } else if (directive === 'TjsSafeAssign') {
229
+ tjsModes.tjsSafeAssign = true
220
230
  }
221
231
 
222
232
  // Remove the directive from source
@@ -247,6 +257,21 @@ export function preprocess(
247
257
  // Must happen before acorn parsing since !. is not valid JS
248
258
  source = transformBangAccess(source)
249
259
 
260
+ // Transform `let x: <example>` declarations: strip annotation and record
261
+ // varName -> example. Must happen before paren transforms so the colon
262
+ // is not confused with TS-style annotations on params/returns.
263
+ const letAnnoResult = transformLetTypeAnnotations(source)
264
+ source = letAnnoResult.source
265
+ const letAnnotations = letAnnoResult.annotations
266
+
267
+ // Extract `wasm function NAME(...) { ... }` declarations EARLY, before
268
+ // any source-level transforms that would mangle wasm-body text. In
269
+ // particular, the equality transforms below rewrite `==` to `Eq()` and
270
+ // `Is`/`IsNot` to function calls — wasm bodies use literal operators
271
+ // and shouldn't be affected.
272
+ const wasmFunctions = extractWasmFunctions(source)
273
+ source = wasmFunctions.source
274
+
250
275
  // Transform Is/IsNot infix operators to function calls
251
276
  // a Is b -> Is(a, b)
252
277
  // a IsNot b -> IsNot(a, b)
@@ -275,6 +300,18 @@ export function preprocess(
275
300
  // Foo = ... -> const Foo = ...
276
301
  source = transformBareAssignments(source)
277
302
 
303
+ // Phase 3: cross-file wasm-function composition. When a ModuleLoader is
304
+ // supplied, resolve `import { ... } from '<spec>'` statements at transpile
305
+ // time. Any imported names that correspond to `wasm function` declarations
306
+ // in the source module get pulled into the consumer's wasm module, with
307
+ // the import statement rewritten to a local JS wrapper. No loader supplied
308
+ // = no behavior change (imports stay verbatim, runtime resolves them).
309
+ const importedWasm = composeImportedWasmFunctions(source, {
310
+ loader: options.moduleLoader,
311
+ importerPath: options.filename,
312
+ })
313
+ source = importedWasm.source
314
+
278
315
  // Unified paren expression transformer
279
316
  // Handles: function params, arrow params, return types, safe/unsafe markers
280
317
  // Model: open paren can be ( or (? or (!, close can be ) or )-> or )-? or )-!
@@ -309,9 +346,24 @@ export function preprocess(
309
346
  source = polyResult.source
310
347
 
311
348
  // Extract WASM blocks: wasm(args) { ... } fallback { ... }
349
+ // `wasm function` declarations are already extracted earlier in the pipeline
350
+ // (see above, before transformParenExpressions). This finds the remaining
351
+ // inline `wasm { ... }` blocks inside regular tjs functions.
312
352
  const wasmBlocks = extractWasmBlocks(source)
313
353
  source = wasmBlocks.source
314
354
 
355
+ // Combine all flavors of wasm blocks for the downstream emitter.
356
+ // They're indistinguishable from the compiler's perspective — all have
357
+ // an id, body, captures, and need the same module composition treatment.
358
+ // - wasmFunctions: top-level `wasm function NAME(...)` decls in this file
359
+ // - importedWasm: cross-file `wasm function`s pulled in via Phase 3
360
+ // - wasmBlocks: inline `wasm { ... }` blocks nested in tjs functions
361
+ const allWasmBlocks = [
362
+ ...wasmFunctions.blocks,
363
+ ...importedWasm.blocks,
364
+ ...wasmBlocks.blocks,
365
+ ]
366
+
315
367
  // Extract and run test blocks: test 'desc'? { body }
316
368
  // Tests run at transpile time and are stripped from output
317
369
  const testResult = extractAndRunTests(source, options.dangerouslySkipTests)
@@ -366,11 +418,12 @@ export function preprocess(
366
418
  requiredParams,
367
419
  unsafeFunctions,
368
420
  safeFunctions,
369
- wasmBlocks: wasmBlocks.blocks,
421
+ wasmBlocks: allWasmBlocks,
370
422
  tests: testResult.tests,
371
423
  testErrors: testResult.errors,
372
424
  polymorphicNames: polyResult.polymorphicNames,
373
425
  extensions: extResult.extensions,
426
+ letAnnotations,
374
427
  }
375
428
  }
376
429
 
@@ -392,6 +445,8 @@ export function parse(
392
445
  wasmBlocks: WasmBlock[]
393
446
  tests: TestBlock[]
394
447
  testErrors: string[]
448
+ letAnnotations: Map<string, string>
449
+ tjsModes: TjsModes
395
450
  } {
396
451
  const {
397
452
  filename = '<source>',
@@ -412,8 +467,14 @@ export function parse(
412
467
  wasmBlocks,
413
468
  tests,
414
469
  testErrors,
470
+ letAnnotations,
471
+ tjsModes,
415
472
  } = colonShorthand
416
- ? preprocess(source, { vmTarget })
473
+ ? preprocess(source, {
474
+ vmTarget,
475
+ moduleLoader: options.moduleLoader,
476
+ filename: options.filename,
477
+ })
417
478
  : {
418
479
  source,
419
480
  returnType: undefined,
@@ -426,6 +487,17 @@ export function parse(
426
487
  wasmBlocks: [] as WasmBlock[],
427
488
  tests: [] as TestBlock[],
428
489
  testErrors: [] as string[],
490
+ letAnnotations: new Map<string, string>(),
491
+ tjsModes: {
492
+ tjsEquals: false,
493
+ tjsClass: false,
494
+ tjsDate: false,
495
+ tjsNoeval: false,
496
+ tjsStandard: false,
497
+ tjsSafeEval: false,
498
+ tjsNoVar: false,
499
+ tjsSafeAssign: false,
500
+ } as TjsModes,
429
501
  }
430
502
 
431
503
  try {
@@ -448,6 +520,8 @@ export function parse(
448
520
  wasmBlocks,
449
521
  tests,
450
522
  testErrors,
523
+ letAnnotations,
524
+ tjsModes,
451
525
  }
452
526
  } catch (e: any) {
453
527
  // Convert Acorn error to our error type
@@ -605,6 +605,26 @@ export function TypeOf(value: unknown): string {
605
605
  return typeof value
606
606
  }
607
607
 
608
+ /**
609
+ * Honest boolean coercion. Like `Boolean(x)` but unwraps boxed primitives
610
+ * first, fixing the JS footgun `Boolean(new Boolean(false)) === true`.
611
+ *
612
+ * Under TjsStandard, the source rewriter wraps every truthiness context
613
+ * (if/while/for/do-while conditions, `!`, `&&`, `||`, ternary, and
614
+ * top-level `Boolean(x)` calls) with this function so a boxed `false`
615
+ * actually behaves as `false`.
616
+ */
617
+ export function toBool(value: unknown): boolean {
618
+ if (
619
+ value instanceof Boolean ||
620
+ value instanceof Number ||
621
+ value instanceof String
622
+ ) {
623
+ return Boolean((value as any).valueOf())
624
+ }
625
+ return Boolean(value)
626
+ }
627
+
608
628
  export function Eq(a: unknown, b: unknown): boolean {
609
629
  // Unwrap boxed primitives
610
630
  if (a instanceof String || a instanceof Number || a instanceof Boolean) {
@@ -844,6 +864,78 @@ type TypeSpec =
844
864
  | string
845
865
  | { check: (v: unknown) => boolean | string; description: string }
846
866
 
867
+ /**
868
+ * Check that a passed-in function's declared shape matches the expected
869
+ * shape. Returns the function unchanged on a match, or a MonadicError on
870
+ * mismatch. Untyped functions (no `__tjs` metadata — anonymous arrows
871
+ * like `x => false`) pass through unchanged on the assumption that the
872
+ * caller knows what they're doing; they accept any args and return
873
+ * whatever they return.
874
+ *
875
+ * This is a ONE-SHOT check at pass time, NOT a per-call wrapper. The TJS
876
+ * design call: a wrong-shape callback is ONE error at the boundary, not
877
+ * N errors when the receiving function invokes the callback N times.
878
+ *
879
+ * Compatibility rules (deliberately permissive — strict subtyping is a
880
+ * separate, larger feature):
881
+ * - For each expected param: the actual function may declare fewer
882
+ * params (extras simply not used). If both declare a kind, they
883
+ * must match exactly. Either side being `any` always matches.
884
+ * - For the return type: same exact-match rule when both are known.
885
+ */
886
+ export function checkFnShape(
887
+ fn: unknown,
888
+ expectedParamKinds: string[],
889
+ expectedReturnKind: string,
890
+ path: string
891
+ ): unknown {
892
+ if (typeof fn !== 'function') return fn // outer "is callable" check already ran
893
+ const meta = (fn as any).__tjs
894
+ if (!meta || !meta.params) return fn // untyped — let it run
895
+
896
+ const actualEntries = Object.entries(meta.params) as Array<
897
+ [string, { type?: { kind?: string } }]
898
+ >
899
+ for (let i = 0; i < expectedParamKinds.length; i++) {
900
+ const expectedKind = expectedParamKinds[i]
901
+ if (expectedKind === 'any') continue
902
+ const actual = actualEntries[i]
903
+ if (!actual) continue // function takes fewer params, OK
904
+ const actualKind = actual[1]?.type?.kind
905
+ if (!actualKind || actualKind === 'any') continue
906
+ if (actualKind !== expectedKind) {
907
+ return new MonadicError(
908
+ `Expected (...arg${i}: ${expectedKind}, ...) for '${path}', ` +
909
+ `but callback declares arg${i} as ${actualKind}`,
910
+ `${path}(arg${i})`,
911
+ expectedKind,
912
+ actualKind
913
+ )
914
+ }
915
+ }
916
+
917
+ if (expectedReturnKind !== 'any' && meta.returns) {
918
+ // Metadata's `returns` is `{ type: TypeDescriptor, defaults?: ... }`,
919
+ // but defensively also accept a bare TypeDescriptor.
920
+ const actualReturnKind = meta.returns.type?.kind ?? meta.returns.kind
921
+ if (
922
+ actualReturnKind &&
923
+ actualReturnKind !== 'any' &&
924
+ actualReturnKind !== expectedReturnKind
925
+ ) {
926
+ return new MonadicError(
927
+ `Expected callback returning ${expectedReturnKind} for '${path}', ` +
928
+ `but callback returns ${actualReturnKind}`,
929
+ `${path}(return)`,
930
+ expectedReturnKind,
931
+ actualReturnKind
932
+ )
933
+ }
934
+ }
935
+
936
+ return fn
937
+ }
938
+
847
939
  /** Parameter metadata with optional location */
848
940
  interface ParamMeta {
849
941
  type: TypeSpec
@@ -1478,6 +1570,7 @@ export function createRuntime() {
1478
1570
  checkType,
1479
1571
  validateArgs,
1480
1572
  wrap,
1573
+ checkFnShape,
1481
1574
  wrapClass,
1482
1575
  compareVersions,
1483
1576
  versionsCompatible,
@@ -1528,6 +1621,8 @@ export function createRuntime() {
1528
1621
  NotEq,
1529
1622
  // Honest typeof (typeof with TjsEquals)
1530
1623
  TypeOf,
1624
+ // Honest truthiness (unwraps boxed primitives)
1625
+ toBool,
1531
1626
  tjsEquals,
1532
1627
  // Extensions
1533
1628
  registerExtension: instanceRegisterExtension,
@@ -1559,6 +1654,7 @@ export const runtime = {
1559
1654
  checkType,
1560
1655
  validateArgs,
1561
1656
  wrap,
1657
+ checkFnShape,
1562
1658
  wrapClass,
1563
1659
  compareVersions,
1564
1660
  versionsCompatible,
@@ -1612,6 +1708,8 @@ export const runtime = {
1612
1708
  NotEq,
1613
1709
  // Honest typeof (used by typeof with TjsEquals)
1614
1710
  TypeOf,
1711
+ // Honest truthiness (used in TjsStandard for boxed-primitive coercion)
1712
+ toBool,
1615
1713
  }
1616
1714
 
1617
1715
  /**
package/src/lang/types.ts CHANGED
@@ -22,6 +22,7 @@ export interface TypeDescriptor {
22
22
  | 'array'
23
23
  | 'object'
24
24
  | 'union'
25
+ | 'function'
25
26
  | 'any'
26
27
  nullable?: boolean
27
28
  /** For arrays: the element type */
@@ -32,6 +33,11 @@ export interface TypeDescriptor {
32
33
  members?: TypeDescriptor[]
33
34
  /** For destructured parameters: full parameter descriptors */
34
35
  destructuredParams?: Record<string, ParameterDescriptor>
36
+ /** For functions: declared parameters with names and inferred types */
37
+ params?: Array<{ name: string; type: TypeDescriptor }>
38
+ /** For functions: inferred return type. Concise arrow bodies infer from
39
+ * the expression; block bodies and complex expressions stay `any`. */
40
+ returns?: TypeDescriptor
35
41
  }
36
42
 
37
43
  /** Describes a function parameter */