tjs-lang 0.6.13 → 0.6.14

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.
@@ -0,0 +1,180 @@
1
+ # FunctionPredicate: Design Notes
2
+
3
+ _First-class function types in TJS, using the same pattern as Type/Generic._
4
+
5
+ ---
6
+
7
+ ## The Problem
8
+
9
+ TJS has no way to express "this parameter must be a function with this
10
+ signature." Currently:
11
+
12
+ - `() => void` in TypeScript becomes `undefined` in fromTS output
13
+ - There's no TJS syntax for function-typed parameters
14
+ - Callbacks, event handlers, and higher-order functions lose their
15
+ type information at the boundary
16
+
17
+ ## Design Principles
18
+
19
+ 1. **Functions are values** — a function should be usable as a type example,
20
+ just like `0` means "integer" and `''` means "string"
21
+ 2. **FunctionPredicate should work like Type/Generic** — same pattern of
22
+ predicate-based checking, introspection via metadata
23
+ 3. **The return contract is part of the type** — `->`, `-?`, and `-!` are
24
+ meaningful distinctions in the function's contract
25
+
26
+ ## The Three Return Contracts
27
+
28
+ | Marker | Name | Meaning |
29
+ |--------|------|---------|
30
+ | `->` | `returns` | Verified at transpile time (signature test) |
31
+ | `-?` | `checkedReturns` | Verified at transpile time AND runtime |
32
+ | `-!` | `assertReturns` | Declared but not verified (metadata only) |
33
+
34
+ These are not just build options — they describe the **trust level** of
35
+ the function's return type. A function with `-?` makes a stronger promise
36
+ than one with `-!`.
37
+
38
+ ## Syntax: Function as Type Example
39
+
40
+ The most TJS-idiomatic approach — a function IS its own type:
41
+
42
+ ```tjs
43
+ // This function's signature IS a type
44
+ function formatter(input: '', options: { locale: 'en' }) -? '' {
45
+ return input
46
+ }
47
+
48
+ // fn must match formatter's contract
49
+ function process(fn: formatter) {
50
+ const result = fn('hello', { locale: 'fr' })
51
+ }
52
+ ```
53
+
54
+ The runtime check for `fn: formatter`:
55
+ 1. `typeof fn === 'function'`
56
+ 2. `fn.__tjs` exists (it's a TJS-typed function)
57
+ 3. `fn.__tjs.params` shape-matches `formatter.__tjs.params`
58
+ 4. `fn.__tjs.returns` matches `formatter.__tjs.returns`
59
+
60
+ Untyped functions (no `__tjs`) would fail the check — they don't have
61
+ the metadata to verify against. Use `!` (unsafe) to skip the check for
62
+ interop with plain JS callbacks.
63
+
64
+ ## Syntax: Explicit FunctionPredicate
65
+
66
+ For cases where you want to declare a function type without writing an
67
+ example function:
68
+
69
+ ```tjs
70
+ FunctionPredicate Formatter {
71
+ description: 'formats a string with locale options'
72
+ params: { input: '', options: { locale: 'en' } }
73
+ returns: ''
74
+ }
75
+
76
+ // Or with checked returns:
77
+ FunctionPredicate Validator {
78
+ params: { value: null }
79
+ checkedReturns: false
80
+ }
81
+
82
+ // Or declared-only returns:
83
+ FunctionPredicate Callback {
84
+ params: { event: { type: '', target: null } }
85
+ assertReturns: undefined
86
+ }
87
+ ```
88
+
89
+ ## Syntax: FunctionPredicate from Function
90
+
91
+ Create a type from an existing function's metadata:
92
+
93
+ ```tjs
94
+ function myFormatter(input: '', options: { locale: 'en' }) -? '' {
95
+ return input
96
+ }
97
+
98
+ // Extract the type from the function
99
+ FunctionPredicate Formatter(myFormatter, 'string formatter with locale')
100
+ ```
101
+
102
+ This is analogous to `Type Name 'example'` — the function itself is the
103
+ example value, and its `__tjs` metadata defines the type.
104
+
105
+ ## Runtime Representation
106
+
107
+ A FunctionPredicate at runtime would be an object with:
108
+
109
+ ```javascript
110
+ {
111
+ check(fn) { ... }, // returns boolean
112
+ params: { ... }, // param descriptors
113
+ returns: { ... }, // return type descriptor
114
+ returnContract: 'checked' | 'returns' | 'assert',
115
+ description: '...',
116
+ default: exampleFn, // the example function, if provided
117
+ }
118
+ ```
119
+
120
+ This matches the shape of `Type()` — `check`, `default`, `description`.
121
+
122
+ ## Validation Levels
123
+
124
+ When checking `fn: SomeType` where SomeType is a FunctionPredicate:
125
+
126
+ | Check | What it verifies |
127
+ |-------|------------------|
128
+ | `typeof fn === 'function'` | It's callable |
129
+ | `fn.__tjs` exists | It's a TJS-typed function |
130
+ | Param count matches | Same arity (or compatible) |
131
+ | Param types match | Each param's type descriptor matches |
132
+ | Return type matches | Return type descriptor matches |
133
+ | Return contract | At least as strict as required |
134
+
135
+ Return contract strictness: `checkedReturns` (-?) > `returns` (->) > `assertReturns` (-!).
136
+ A `checkedReturns` function satisfies any requirement.
137
+ A `returns` function satisfies `returns` or `assertReturns`.
138
+ An `assertReturns` function only satisfies `assertReturns`.
139
+
140
+ ## Compatibility with Untyped Functions
141
+
142
+ Plain JS functions have no `__tjs` metadata. Options:
143
+
144
+ 1. **Strict**: Reject untyped functions (safe but hostile to JS interop)
145
+ 2. **Lenient**: Accept any function, only validate if `__tjs` exists
146
+ 3. **Unsafe marker**: Use `!` to skip the check for known-untyped callbacks
147
+
148
+ Option 3 is most consistent with TJS's existing patterns:
149
+
150
+ ```tjs
151
+ // Strict — fn must have matching __tjs metadata
152
+ function process(fn: formatter) { ... }
153
+
154
+ // Lenient — fn just needs to be callable
155
+ function process(! fn: formatter) { ... }
156
+ ```
157
+
158
+ ## Relationship to Existing Features
159
+
160
+ - **Type**: FunctionPredicate IS a Type — just one that checks function
161
+ signatures specifically. Could be implemented as a special case of Type
162
+ with a built-in predicate that introspects `__tjs`.
163
+ - **Generic**: FunctionPredicate could be generic too —
164
+ `FunctionPredicate Mapper<T, U> { params: { value: T }, returns: U }`
165
+ - **declaration block**: FunctionPredicates would benefit from declaration
166
+ blocks for `.d.ts` emission, same as Generic.
167
+
168
+ ## Implementation Path
169
+
170
+ 1. **Runtime**: Add `FunctionPredicate()` to the TJS runtime alongside
171
+ `Type()` and `Generic()`. Returns a type guard that checks `__tjs`
172
+ metadata on functions.
173
+ 2. **Parser**: Recognize `FunctionPredicate` as a declaration keyword
174
+ (same as `Type`, `Generic`). Parse the block or function-argument form.
175
+ 3. **Metadata**: The `__tjs` metadata for the return type already includes
176
+ `type` — add a `contract` field for the marker.
177
+ 4. **fromTS**: When converting `(x: number) => string` types, emit a
178
+ FunctionPredicate instead of `undefined`.
179
+ 5. **Inference**: When a function is used as a `:` param type, check if
180
+ it has `__tjs` metadata and validate the caller's function against it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tjs-lang",
3
- "version": "0.6.13",
3
+ "version": "0.6.14",
4
4
  "description": "Type-safe JavaScript dialect with runtime validation, sandboxed VM execution, and AI agent orchestration. Transpiles TypeScript to validated JS with fuel-metered execution for untrusted code.",
5
5
  "keywords": [
6
6
  "typescript",
package/src/cli/tjs.ts CHANGED
@@ -20,7 +20,7 @@ import { emit } from './commands/emit'
20
20
  import { convert } from './commands/convert'
21
21
  import { test } from './commands/test'
22
22
 
23
- const VERSION = '0.6.13'
23
+ const VERSION = '0.6.14'
24
24
 
25
25
  const HELP = `
26
26
  tjs - Typed JavaScript CLI
@@ -437,6 +437,64 @@ export Generic Box<T> {
437
437
  })
438
438
  })
439
439
 
440
+ describe('generateDTS — FunctionPredicate declarations', () => {
441
+ it('should emit FunctionPredicate as TS function type', () => {
442
+ const source = `
443
+ export FunctionPredicate Callback {
444
+ params: { x: 0, y: '' }
445
+ returns: false
446
+ }
447
+ `
448
+ const result = transpileToJS(source, { runTests: false })
449
+ const dts = generateDTS(result, source)
450
+
451
+ expect(dts).toContain(
452
+ 'export type Callback = (x: number, y: string) => boolean;'
453
+ )
454
+ })
455
+
456
+ it('should emit FunctionPredicate with no params as zero-arg function', () => {
457
+ const source = `
458
+ export FunctionPredicate Thunk {
459
+ returns: 0
460
+ }
461
+ `
462
+ const result = transpileToJS(source, { runTests: false })
463
+ const dts = generateDTS(result, source)
464
+
465
+ expect(dts).toContain('export type Thunk = () => number;')
466
+ })
467
+
468
+ it('should emit FunctionPredicate with no return as void', () => {
469
+ const source = `
470
+ export FunctionPredicate SideEffect {
471
+ params: { msg: '' }
472
+ }
473
+ `
474
+ const result = transpileToJS(source, { runTests: false })
475
+ const dts = generateDTS(result, source)
476
+
477
+ expect(dts).toContain('export type SideEffect = (msg: string) => void;')
478
+ })
479
+
480
+ it('should not emit non-exported FunctionPredicate when exports exist', () => {
481
+ const source = `
482
+ FunctionPredicate Internal {
483
+ params: { x: 0 }
484
+ }
485
+
486
+ export function use(fn: Internal) {
487
+ return fn(1)
488
+ }
489
+ `
490
+ const result = transpileToJS(source, { runTests: false })
491
+ const dts = generateDTS(result, source)
492
+
493
+ expect(dts).not.toContain('type Internal')
494
+ expect(dts).toContain('export declare function use(')
495
+ })
496
+ })
497
+
440
498
  describe('generateDTS — mixed declarations', () => {
441
499
  it('should handle file with functions, classes, types, and generics', () => {
442
500
  const source = `
@@ -169,6 +169,12 @@ function detectExports(source: string): Map<string, ExportInfo> {
169
169
  result.set(m[1], { exported: true, isDefault: false })
170
170
  }
171
171
 
172
+ // export FunctionPredicate Name
173
+ const fpRe = /^[ \t]*export\s+FunctionPredicate\s+(\w+)/gm
174
+ while ((m = fpRe.exec(source)) !== null) {
175
+ result.set(m[1], { exported: true, isDefault: false })
176
+ }
177
+
172
178
  // export { Name, Name2, ... } — re-export form
173
179
  const reExportRe = /^[ \t]*export\s*\{([^}]+)\}/gm
174
180
  while ((m = reExportRe.exec(source)) !== null) {
@@ -184,6 +190,62 @@ function detectExports(source: string): Map<string, ExportInfo> {
184
190
  return result
185
191
  }
186
192
 
193
+ /** Info about a FunctionPredicate declaration */
194
+ interface FunctionPredicateInfo {
195
+ params: { name: string; example: string }[]
196
+ returns?: string
197
+ }
198
+
199
+ /** Detect FunctionPredicate declarations and extract their param/return specs */
200
+ function detectFunctionPredicates(
201
+ source: string
202
+ ): Map<string, FunctionPredicateInfo> {
203
+ const result = new Map<string, FunctionPredicateInfo>()
204
+
205
+ // Block form: FunctionPredicate Name { params: { ... } returns: ... }
206
+ const blockRe = /^[ \t]*(?:export\s+)?FunctionPredicate\s+(\w+)\s*\{/gm
207
+ let m
208
+ while ((m = blockRe.exec(source)) !== null) {
209
+ const name = m[1]
210
+ const blockStart = m.index + m[0].length - 1
211
+
212
+ // Find matching closing brace
213
+ let depth = 1
214
+ let i = blockStart + 1
215
+ while (i < source.length && depth > 0) {
216
+ if (source[i] === '{') depth++
217
+ else if (source[i] === '}') depth--
218
+ i++
219
+ }
220
+ const body = source.slice(blockStart + 1, i - 1)
221
+
222
+ // Extract params object: params: { key: value, ... }
223
+ const params: FunctionPredicateInfo['params'] = []
224
+ const paramsMatch = body.match(/params\s*:\s*\{([^}]*)\}/)
225
+ if (paramsMatch) {
226
+ const paramsStr = paramsMatch[1]
227
+ const paramEntries = splitParams(paramsStr)
228
+ for (const entry of paramEntries) {
229
+ const kv = entry.match(/^(\w+)\s*:\s*(.+)$/)
230
+ if (kv) {
231
+ params.push({ name: kv[1], example: kv[2].trim() })
232
+ }
233
+ }
234
+ }
235
+
236
+ // Extract returns value
237
+ let returns: string | undefined
238
+ const returnsMatch = body.match(/returns\s*:\s*(.+?)(?:\n|$)/)
239
+ if (returnsMatch) {
240
+ returns = returnsMatch[1].trim()
241
+ }
242
+
243
+ result.set(name, { params, returns })
244
+ }
245
+
246
+ return result
247
+ }
248
+
187
249
  /** Info about a class extracted from source */
188
250
  interface ClassInfo {
189
251
  name: string
@@ -535,6 +597,28 @@ export function generateDTS(
535
597
  emitted.add(name)
536
598
  }
537
599
 
600
+ // Emit FunctionPredicate declarations as TS function types.
601
+ // FunctionPredicate Callback { params: { x: 0 } returns: '' }
602
+ // → export type Callback = (x: number) => string;
603
+ const funcPreds = detectFunctionPredicates(source)
604
+ for (const [name, fpInfo] of funcPreds) {
605
+ if (emitted.has(name)) continue
606
+
607
+ const exportInfo = exports.get(name)
608
+ const isExported = hasAnyExport ? !!exportInfo?.exported : true
609
+ if (!isExported) continue
610
+
611
+ const tsParams = fpInfo.params
612
+ .map((p) => `${p.name}: ${inferTSTypeFromExample(p.example)}`)
613
+ .join(', ')
614
+ const tsReturn =
615
+ fpInfo.returns !== undefined
616
+ ? inferTSTypeFromExample(fpInfo.returns)
617
+ : 'void'
618
+ lines.push(`export type ${name} = (${tsParams}) => ${tsReturn};`)
619
+ emitted.add(name)
620
+ }
621
+
538
622
  if (options.moduleName) {
539
623
  const indented = lines.map((l) => ` ${l}`).join('\n')
540
624
  return `declare module '${options.moduleName}' {\n${indented}\n}\n`
@@ -420,9 +420,24 @@ function typeToExample(
420
420
  return typeToExample(parenType.type, checker)
421
421
  }
422
422
 
423
- case ts.SyntaxKind.FunctionType:
424
- // Functions become undefined (can't really express as example)
425
- return 'undefined'
423
+ case ts.SyntaxKind.FunctionType: {
424
+ // Convert to inline FunctionPredicate expression
425
+ const funcType = type as ts.FunctionTypeNode
426
+ const fpParams: string[] = []
427
+ for (const param of funcType.parameters) {
428
+ const name = param.name?.getText() || '_'
429
+ if (name === 'this') continue
430
+ let paramExample = typeToExample(param.type, checker, warnings, ctx)
431
+ if (paramExample === 'any') paramExample = 'null'
432
+ fpParams.push(`${name}: ${paramExample}`)
433
+ }
434
+ let fpReturn = typeToExample(funcType.type, checker, warnings, ctx)
435
+ if (fpReturn === 'any') fpReturn = 'null'
436
+ const spec: string[] = []
437
+ if (fpParams.length > 0) spec.push(`params: { ${fpParams.join(', ')} }`)
438
+ if (fpReturn !== 'undefined') spec.push(`returns: ${fpReturn}`)
439
+ return `FunctionPredicate('function', { ${spec.join(', ')} })`
440
+ }
426
441
 
427
442
  case ts.SyntaxKind.TupleType: {
428
443
  const tupleType = type as ts.TupleTypeNode
@@ -978,6 +993,25 @@ function transformTypeAliasToType(
978
993
  return `Union ${typeName} '${typeName}' ${literalValues.join(' | ')}`
979
994
  }
980
995
 
996
+ // Function types → FunctionPredicate declaration
997
+ if (node.type.kind === ts.SyntaxKind.FunctionType) {
998
+ const funcType = node.type as ts.FunctionTypeNode
999
+ const fpParams: string[] = []
1000
+ for (const param of funcType.parameters) {
1001
+ const name = param.name?.getText(sourceFile) || '_'
1002
+ if (name === 'this') continue
1003
+ let paramExample = typeToExample(param.type, undefined, warnings)
1004
+ if (paramExample === 'any') paramExample = 'null'
1005
+ fpParams.push(`${name}: ${paramExample}`)
1006
+ }
1007
+ let fpReturn = typeToExample(funcType.type, undefined, warnings)
1008
+ if (fpReturn === 'any') fpReturn = 'null'
1009
+ const spec: string[] = []
1010
+ if (fpParams.length > 0) spec.push(`params: { ${fpParams.join(', ')} }`)
1011
+ if (fpReturn !== 'undefined') spec.push(`returns: ${fpReturn}`)
1012
+ return `FunctionPredicate ${typeName} {\n ${spec.join('\n ')}\n}`
1013
+ }
1014
+
981
1015
  const example = typeToExample(node.type, undefined, warnings)
982
1016
 
983
1017
  // 'any' and 'undefined' — skip declaration (undeclared = any in TJS)
@@ -2054,7 +2088,11 @@ export function fromTS(
2054
2088
  const isExported = statement.modifiers?.some(
2055
2089
  (m) => m.kind === ts.SyntaxKind.ExportKeyword
2056
2090
  )
2057
- tjsFunctions.push(isExported ? `export ${typeDecl}` : typeDecl)
2091
+ tjsFunctions.push(
2092
+ isExported
2093
+ ? typeDecl.replace(/^(\/\*[\s\S]*?\*\/\s*)?/, '$1export ')
2094
+ : typeDecl
2095
+ )
2058
2096
  }
2059
2097
  }
2060
2098
  }
@@ -2076,7 +2114,11 @@ export function fromTS(
2076
2114
  const isExported = statement.modifiers?.some(
2077
2115
  (m) => m.kind === ts.SyntaxKind.ExportKeyword
2078
2116
  )
2079
- tjsFunctions.push(isExported ? `export ${typeDecl}` : typeDecl)
2117
+ tjsFunctions.push(
2118
+ isExported
2119
+ ? typeDecl.replace(/^(\/\*[\s\S]*?\*\/\s*)?/, '$1export ')
2120
+ : typeDecl
2121
+ )
2080
2122
  }
2081
2123
  }
2082
2124
  }
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { FunctionPredicate } from '../types/Type'
3
+ import { preprocess } from './parser'
4
+ import { tjs } from './index'
5
+ import { fromTS } from './emitters/from-ts'
6
+
7
+ describe('FunctionPredicate runtime', () => {
8
+ it('should create a type that accepts functions', () => {
9
+ const Callback = FunctionPredicate('Callback', {
10
+ params: { x: 0 },
11
+ returns: '',
12
+ })
13
+ expect(Callback.check(() => {})).toBe(true)
14
+ expect(Callback.check((x: number) => String(x))).toBe(true)
15
+ expect(Callback.check(42)).toBe(false)
16
+ expect(Callback.check('not a function')).toBe(false)
17
+ expect(Callback.check(null)).toBe(false)
18
+ })
19
+
20
+ it('should create from existing typed function', () => {
21
+ const fn = (a: number, b: number) => a + b
22
+ ;(fn as any).__tjs = {
23
+ params: {
24
+ a: { type: { kind: 'integer' }, example: 0 },
25
+ b: { type: { kind: 'integer' }, example: 0 },
26
+ },
27
+ returns: { type: { kind: 'integer' }, example: 0 },
28
+ }
29
+ const Adder = FunctionPredicate('Adder', fn)
30
+ expect(Adder.params).toHaveProperty('a')
31
+ expect(Adder.params).toHaveProperty('b')
32
+ expect(Adder.description).toBe('Adder')
33
+ expect(Adder.__runtimeType).toBe(true)
34
+ })
35
+
36
+ it('should have correct return contract', () => {
37
+ const assert = FunctionPredicate('f', {
38
+ returnContract: 'assertReturns',
39
+ })
40
+ const returns = FunctionPredicate('f', { returnContract: 'returns' })
41
+ const checked = FunctionPredicate('f', {
42
+ returnContract: 'checkedReturns',
43
+ })
44
+ expect(assert.returnContract).toBe('assertReturns')
45
+ expect(returns.returnContract).toBe('returns')
46
+ expect(checked.returnContract).toBe('checkedReturns')
47
+ })
48
+
49
+ it('should default to assertReturns contract', () => {
50
+ const fp = FunctionPredicate('f', {})
51
+ expect(fp.returnContract).toBe('assertReturns')
52
+ })
53
+
54
+ it('should reject function with wrong arity via __tjs metadata', () => {
55
+ const Binop = FunctionPredicate('Binop', {
56
+ params: { a: 0, b: 0 },
57
+ returns: 0,
58
+ })
59
+ // Function with matching arity + types → pass
60
+ const goodFn = (a: number, b: number) => a + b
61
+ ;(goodFn as any).__tjs = {
62
+ params: {
63
+ a: { type: { kind: 'integer' }, example: 0 },
64
+ b: { type: { kind: 'integer' }, example: 0 },
65
+ },
66
+ }
67
+ expect(Binop.check(goodFn)).toBe(true)
68
+
69
+ // Function with wrong arity → fail
70
+ const wrongArity = (x: number) => x
71
+ ;(wrongArity as any).__tjs = {
72
+ params: {
73
+ x: { type: { kind: 'integer' }, example: 0 },
74
+ },
75
+ }
76
+ expect(Binop.check(wrongArity)).toBe(false)
77
+ })
78
+
79
+ it('should reject function with wrong param types via __tjs metadata', () => {
80
+ const StrFn = FunctionPredicate('StrFn', {
81
+ params: { name: '' },
82
+ returns: '',
83
+ })
84
+ // Function with string param → pass
85
+ const goodFn = (s: string) => s
86
+ ;(goodFn as any).__tjs = {
87
+ params: { s: { type: { kind: 'string' }, example: '' } },
88
+ }
89
+ expect(StrFn.check(goodFn)).toBe(true)
90
+
91
+ // Function with number param → fail
92
+ const badFn = (n: number) => String(n)
93
+ ;(badFn as any).__tjs = {
94
+ params: { n: { type: { kind: 'integer' }, example: 0 } },
95
+ }
96
+ expect(StrFn.check(badFn)).toBe(false)
97
+ })
98
+
99
+ it('should accept function with any-typed params', () => {
100
+ const Binop = FunctionPredicate('Binop', {
101
+ params: { a: 0, b: 0 },
102
+ })
103
+ const fn = (a: any, b: any) => a + b
104
+ ;(fn as any).__tjs = {
105
+ params: {
106
+ a: { type: { kind: 'any' }, example: null },
107
+ b: { type: { kind: 'any' }, example: null },
108
+ },
109
+ }
110
+ expect(Binop.check(fn)).toBe(true)
111
+ })
112
+
113
+ it('should accept plain functions without __tjs metadata', () => {
114
+ const Callback = FunctionPredicate('Callback', {
115
+ params: { x: 0 },
116
+ })
117
+ // Plain function with no metadata — should still pass (can't validate)
118
+ expect(Callback.check(() => {})).toBe(true)
119
+ expect(Callback.check(Math.abs)).toBe(true)
120
+ })
121
+ })
122
+
123
+ describe('FunctionPredicate parser transform', () => {
124
+ it('should transform block form', () => {
125
+ const result = preprocess(
126
+ "FunctionPredicate Callback {\n params: { x: 0 }\n returns: ''\n}"
127
+ )
128
+ expect(result.source).toContain("FunctionPredicate('Callback'")
129
+ expect(result.source).toContain('params: { x: 0 }')
130
+ expect(result.source).toContain("returns: ''")
131
+ })
132
+
133
+ it('should transform function form', () => {
134
+ const result = preprocess(
135
+ "FunctionPredicate Handler(myFn, 'event handler')"
136
+ )
137
+ expect(result.source).toContain("FunctionPredicate('event handler'")
138
+ expect(result.source).toContain('myFn')
139
+ })
140
+
141
+ it('should transpile block form through full pipeline', () => {
142
+ const result = tjs(
143
+ 'FunctionPredicate Callback {\n params: { x: 0 }\n returns: false\n}',
144
+ { runTests: false }
145
+ )
146
+ expect(result.code).toContain('FunctionPredicate')
147
+ expect(result.code).toContain('Callback')
148
+ })
149
+ })
150
+
151
+ describe('FunctionPredicate in fromTS', () => {
152
+ it('should convert function type alias to FunctionPredicate', () => {
153
+ const result = fromTS('type Callback = (x: number) => void', {
154
+ emitTJS: true,
155
+ })
156
+ expect(result.code).toContain('FunctionPredicate Callback')
157
+ expect(result.code).toContain('params: { x: 0.0 }')
158
+ })
159
+
160
+ it('should convert function type with return to FunctionPredicate', () => {
161
+ const result = fromTS('type Mapper = (value: string) => number', {
162
+ emitTJS: true,
163
+ })
164
+ expect(result.code).toContain('FunctionPredicate Mapper')
165
+ expect(result.code).toContain("params: { value: '' }")
166
+ expect(result.code).toContain('returns: 0.0')
167
+ })
168
+
169
+ it('should handle inline function params', () => {
170
+ const result = fromTS(
171
+ 'function process(cb: (x: number) => string): void {}',
172
+ { emitTJS: true }
173
+ )
174
+ expect(result.code).toContain("FunctionPredicate('function'")
175
+ })
176
+
177
+ it('should handle void function type', () => {
178
+ const result = fromTS('type VoidFn = () => void', { emitTJS: true })
179
+ expect(result.code).toContain('FunctionPredicate VoidFn')
180
+ })
181
+
182
+ it('should preserve export on function type alias', () => {
183
+ const result = fromTS('export type Handler = (event: Event) => boolean', {
184
+ emitTJS: true,
185
+ })
186
+ expect(result.code).toContain('export FunctionPredicate Handler')
187
+ })
188
+ })