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.
- package/CLAUDE.md +40 -17
- package/demo/docs.json +1 -1
- package/dist/index.js +119 -116
- package/dist/index.js.map +8 -8
- package/dist/src/lang/parser-transforms.d.ts +15 -0
- package/dist/src/lang/runtime.d.ts +4 -2
- package/dist/src/types/Type.d.ts +41 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/tjs-full.js +119 -116
- package/dist/tjs-full.js.map +8 -8
- package/dist/tjs-vm.js +37 -37
- package/dist/tjs-vm.js.map +4 -4
- package/docs/function-predicate-design.md +180 -0
- package/package.json +1 -1
- package/src/cli/tjs.ts +1 -1
- package/src/lang/emitters/dts.test.ts +58 -0
- package/src/lang/emitters/dts.ts +84 -0
- package/src/lang/emitters/from-ts.ts +47 -5
- package/src/lang/function-predicate.test.ts +188 -0
- package/src/lang/parser-transforms.ts +103 -0
- package/src/lang/parser.ts +2 -0
- package/src/lang/runtime.ts +4 -0
- package/src/lang/typescript-syntax.test.ts +69 -0
- package/src/types/Type.ts +148 -0
- package/src/types/index.ts +5 -0
|
@@ -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.
|
|
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
|
@@ -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 = `
|
package/src/lang/emitters/dts.ts
CHANGED
|
@@ -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
|
-
//
|
|
425
|
-
|
|
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(
|
|
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(
|
|
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
|
+
})
|