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
|
@@ -1188,6 +1188,109 @@ export function transformTypeDeclarations(source: string): string {
|
|
|
1188
1188
|
return result
|
|
1189
1189
|
}
|
|
1190
1190
|
|
|
1191
|
+
/**
|
|
1192
|
+
* Transform FunctionPredicate declarations
|
|
1193
|
+
*
|
|
1194
|
+
* Block form:
|
|
1195
|
+
* FunctionPredicate Callback {
|
|
1196
|
+
* params: { x: 0, y: '' }
|
|
1197
|
+
* returns: false
|
|
1198
|
+
* }
|
|
1199
|
+
* → const Callback = FunctionPredicate('Callback', { params: { x: 0, y: '' }, returns: false })
|
|
1200
|
+
*
|
|
1201
|
+
* Function form:
|
|
1202
|
+
* FunctionPredicate Handler(existingFn, 'description')
|
|
1203
|
+
* → const Handler = FunctionPredicate('description', existingFn)
|
|
1204
|
+
*/
|
|
1205
|
+
export function transformFunctionPredicateDeclarations(source: string): string {
|
|
1206
|
+
let result = ''
|
|
1207
|
+
let i = 0
|
|
1208
|
+
|
|
1209
|
+
while (i < source.length) {
|
|
1210
|
+
const fpMatch = source
|
|
1211
|
+
.slice(i)
|
|
1212
|
+
.match(/^\bFunctionPredicate\s+([A-Z_][a-zA-Z0-9_]*)\s*/)
|
|
1213
|
+
if (fpMatch) {
|
|
1214
|
+
const fpName = fpMatch[1]
|
|
1215
|
+
const j = i + fpMatch[0].length
|
|
1216
|
+
|
|
1217
|
+
// Check for block form: FunctionPredicate Name { ... }
|
|
1218
|
+
if (source[j] === '{') {
|
|
1219
|
+
// Find matching closing brace
|
|
1220
|
+
let depth = 1
|
|
1221
|
+
let k = j + 1
|
|
1222
|
+
while (k < source.length && depth > 0) {
|
|
1223
|
+
if (source[k] === '{') depth++
|
|
1224
|
+
else if (source[k] === '}') depth--
|
|
1225
|
+
k++
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (depth === 0) {
|
|
1229
|
+
const blockBody = source.slice(j + 1, k - 1).trim()
|
|
1230
|
+
|
|
1231
|
+
// Extract params: { ... }
|
|
1232
|
+
const paramsMatch = blockBody.match(/params\s*:\s*(\{[^}]*\})/)
|
|
1233
|
+
// Extract returns value
|
|
1234
|
+
const returnsMatch = blockBody.match(/returns\s*:\s*(.+?)(?:\n|$)/)
|
|
1235
|
+
// Extract returnContract
|
|
1236
|
+
const contractMatch = blockBody.match(
|
|
1237
|
+
/returnContract\s*:\s*['"](\w+)['"]/
|
|
1238
|
+
)
|
|
1239
|
+
// Extract description
|
|
1240
|
+
const descMatch = blockBody.match(/description\s*:\s*(['"])([^]*?)\1/)
|
|
1241
|
+
|
|
1242
|
+
const spec: string[] = []
|
|
1243
|
+
if (paramsMatch) spec.push(`params: ${paramsMatch[1]}`)
|
|
1244
|
+
if (returnsMatch) spec.push(`returns: ${returnsMatch[1].trim()}`)
|
|
1245
|
+
if (contractMatch) {
|
|
1246
|
+
spec.push(`returnContract: '${contractMatch[1]}'`)
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const desc = descMatch ? descMatch[2] : fpName
|
|
1250
|
+
result += `const ${fpName} = FunctionPredicate('${desc}', { ${spec.join(
|
|
1251
|
+
', '
|
|
1252
|
+
)} })`
|
|
1253
|
+
i = k
|
|
1254
|
+
continue
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Check for function form: FunctionPredicate Name(fn, 'desc')
|
|
1259
|
+
if (source[j] === '(') {
|
|
1260
|
+
// Find matching closing paren
|
|
1261
|
+
let depth = 1
|
|
1262
|
+
let k = j + 1
|
|
1263
|
+
while (k < source.length && depth > 0) {
|
|
1264
|
+
if (source[k] === '(') depth++
|
|
1265
|
+
else if (source[k] === ')') depth--
|
|
1266
|
+
k++
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (depth === 0) {
|
|
1270
|
+
const args = source.slice(j + 1, k - 1).trim()
|
|
1271
|
+
// Split on comma: fn, 'description'
|
|
1272
|
+
const commaIdx = args.indexOf(',')
|
|
1273
|
+
if (commaIdx !== -1) {
|
|
1274
|
+
const fnRef = args.slice(0, commaIdx).trim()
|
|
1275
|
+
const desc = args.slice(commaIdx + 1).trim()
|
|
1276
|
+
result += `const ${fpName} = FunctionPredicate(${desc}, ${fnRef})`
|
|
1277
|
+
} else {
|
|
1278
|
+
// Just a function reference, name as description
|
|
1279
|
+
result += `const ${fpName} = FunctionPredicate('${fpName}', ${args})`
|
|
1280
|
+
}
|
|
1281
|
+
i = k
|
|
1282
|
+
continue
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
result += source[i]
|
|
1288
|
+
i++
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
return result
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1191
1294
|
/**
|
|
1192
1295
|
* Transform Generic block declarations
|
|
1193
1296
|
*
|
package/src/lang/parser.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
transformEqualityToStructural,
|
|
37
37
|
transformTypeDeclarations,
|
|
38
38
|
transformGenericDeclarations,
|
|
39
|
+
transformFunctionPredicateDeclarations,
|
|
39
40
|
transformUnionDeclarations,
|
|
40
41
|
transformEnumDeclarations,
|
|
41
42
|
transformExtendDeclarations,
|
|
@@ -167,6 +168,7 @@ export function preprocess(
|
|
|
167
168
|
// Enum Status { Pending, Active, Done } -> const Status = Enum(...)
|
|
168
169
|
source = transformTypeDeclarations(source)
|
|
169
170
|
source = transformGenericDeclarations(source)
|
|
171
|
+
source = transformFunctionPredicateDeclarations(source)
|
|
170
172
|
source = transformUnionDeclarations(source)
|
|
171
173
|
source = transformEnumDeclarations(source)
|
|
172
174
|
|
package/src/lang/runtime.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
Union,
|
|
18
18
|
Generic,
|
|
19
19
|
Enum,
|
|
20
|
+
FunctionPredicate,
|
|
20
21
|
Nullable,
|
|
21
22
|
Optional,
|
|
22
23
|
TArray,
|
|
@@ -48,6 +49,7 @@ export {
|
|
|
48
49
|
Union,
|
|
49
50
|
Generic,
|
|
50
51
|
Enum,
|
|
52
|
+
FunctionPredicate,
|
|
51
53
|
Nullable,
|
|
52
54
|
Optional,
|
|
53
55
|
TArray,
|
|
@@ -1179,6 +1181,7 @@ export function createRuntime() {
|
|
|
1179
1181
|
Union,
|
|
1180
1182
|
Generic,
|
|
1181
1183
|
Enum,
|
|
1184
|
+
FunctionPredicate,
|
|
1182
1185
|
Nullable,
|
|
1183
1186
|
Optional,
|
|
1184
1187
|
TArray,
|
|
@@ -1251,6 +1254,7 @@ export const runtime = {
|
|
|
1251
1254
|
Union,
|
|
1252
1255
|
Generic,
|
|
1253
1256
|
Enum,
|
|
1257
|
+
FunctionPredicate,
|
|
1254
1258
|
Nullable,
|
|
1255
1259
|
Optional,
|
|
1256
1260
|
TArray,
|
|
@@ -1594,6 +1594,75 @@ describe('fromTS — tosijs conversion edge cases', () => {
|
|
|
1594
1594
|
})
|
|
1595
1595
|
})
|
|
1596
1596
|
|
|
1597
|
+
// =============================================================================
|
|
1598
|
+
// FUNCTION TYPES → FunctionPredicate
|
|
1599
|
+
// =============================================================================
|
|
1600
|
+
|
|
1601
|
+
describe('Function Types (FunctionPredicate)', () => {
|
|
1602
|
+
test('function type alias emits FunctionPredicate', () => {
|
|
1603
|
+
const result = fromTS(`type Callback = (x: number, y: string) => boolean`, {
|
|
1604
|
+
emitTJS: true,
|
|
1605
|
+
})
|
|
1606
|
+
expect(result.code).toContain('FunctionPredicate Callback')
|
|
1607
|
+
expect(result.code).toContain('params: { x: 0.0')
|
|
1608
|
+
expect(result.code).toContain("y: ''")
|
|
1609
|
+
expect(result.code).toContain('returns: false')
|
|
1610
|
+
})
|
|
1611
|
+
|
|
1612
|
+
test('void function type alias', () => {
|
|
1613
|
+
const result = fromTS(`type Logger = (msg: string) => void`, {
|
|
1614
|
+
emitTJS: true,
|
|
1615
|
+
})
|
|
1616
|
+
expect(result.code).toContain('FunctionPredicate Logger')
|
|
1617
|
+
expect(result.code).toContain("params: { msg: '' }")
|
|
1618
|
+
// void return should not emit a returns line
|
|
1619
|
+
expect(result.code).not.toMatch(/returns:/)
|
|
1620
|
+
})
|
|
1621
|
+
|
|
1622
|
+
test('exported function type alias preserves export', () => {
|
|
1623
|
+
const result = fromTS(`export type Handler = (event: Event) => boolean`, {
|
|
1624
|
+
emitTJS: true,
|
|
1625
|
+
})
|
|
1626
|
+
expect(result.code).toContain('export FunctionPredicate Handler')
|
|
1627
|
+
})
|
|
1628
|
+
|
|
1629
|
+
test('inline function param emits FunctionPredicate', () => {
|
|
1630
|
+
const result = fromTS(
|
|
1631
|
+
`function process(cb: (item: string) => number): void {}`,
|
|
1632
|
+
{ emitTJS: true }
|
|
1633
|
+
)
|
|
1634
|
+
expect(result.code).toContain("FunctionPredicate('function'")
|
|
1635
|
+
})
|
|
1636
|
+
|
|
1637
|
+
test('no-arg function type', () => {
|
|
1638
|
+
const result = fromTS(`type Thunk = () => number`, { emitTJS: true })
|
|
1639
|
+
expect(result.code).toContain('FunctionPredicate Thunk')
|
|
1640
|
+
expect(result.code).toContain('returns: 0.0')
|
|
1641
|
+
})
|
|
1642
|
+
|
|
1643
|
+
test('function type with multiple params', () => {
|
|
1644
|
+
const result = fromTS(
|
|
1645
|
+
`type Reducer = (acc: number, item: string, index: number) => number`,
|
|
1646
|
+
{ emitTJS: true }
|
|
1647
|
+
)
|
|
1648
|
+
expect(result.code).toContain('FunctionPredicate Reducer')
|
|
1649
|
+
expect(result.code).toContain('acc: 0.0')
|
|
1650
|
+
expect(result.code).toContain("item: ''")
|
|
1651
|
+
expect(result.code).toContain('index: 0.0')
|
|
1652
|
+
expect(result.code).toContain('returns: 0.0')
|
|
1653
|
+
})
|
|
1654
|
+
|
|
1655
|
+
test('function type transpiles through full TJS pipeline', () => {
|
|
1656
|
+
const tsResult = fromTS(
|
|
1657
|
+
`type Compare = (a: number, b: number) => boolean`,
|
|
1658
|
+
{ emitTJS: true }
|
|
1659
|
+
)
|
|
1660
|
+
const jsResult = tjs(tsResult.code, { runTests: false })
|
|
1661
|
+
expect(jsResult.code).toContain('FunctionPredicate')
|
|
1662
|
+
expect(jsResult.code).toContain('Compare')
|
|
1663
|
+
})
|
|
1664
|
+
})
|
|
1665
|
+
|
|
1597
1666
|
// =============================================================================
|
|
1598
1667
|
// SUMMARY: Features to implement (in priority order)
|
|
1599
1668
|
// =============================================================================
|
package/src/types/Type.ts
CHANGED
|
@@ -608,3 +608,151 @@ export function Enum<T extends Record<string, string | number>>(
|
|
|
608
608
|
|
|
609
609
|
return enumType
|
|
610
610
|
}
|
|
611
|
+
|
|
612
|
+
// =============================================================================
|
|
613
|
+
// FunctionPredicate - Runtime type for function signatures
|
|
614
|
+
// =============================================================================
|
|
615
|
+
|
|
616
|
+
/** Return contract levels in order of strictness */
|
|
617
|
+
export type ReturnContract = 'assertReturns' | 'returns' | 'checkedReturns'
|
|
618
|
+
|
|
619
|
+
/** Specification for a FunctionPredicate */
|
|
620
|
+
export interface FunctionPredicateSpec {
|
|
621
|
+
/** Parameter types as example values */
|
|
622
|
+
params?: Record<string, any>
|
|
623
|
+
/** Return type as example value */
|
|
624
|
+
returns?: any
|
|
625
|
+
/** Return contract level */
|
|
626
|
+
returnContract?: ReturnContract
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** A runtime type that validates function signatures */
|
|
630
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
631
|
+
export interface FunctionPredicateType extends RuntimeType<Function> {
|
|
632
|
+
/** Parameter specification */
|
|
633
|
+
readonly params: Record<string, any>
|
|
634
|
+
/** Return type specification */
|
|
635
|
+
readonly returns?: any
|
|
636
|
+
/** Return contract level */
|
|
637
|
+
readonly returnContract: ReturnContract
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** Infer a TypeDescriptor kind from an example value */
|
|
641
|
+
function kindOfExample(example: unknown): string | null {
|
|
642
|
+
if (example === null) return 'null'
|
|
643
|
+
if (example === undefined) return 'undefined'
|
|
644
|
+
switch (typeof example) {
|
|
645
|
+
case 'string':
|
|
646
|
+
return 'string'
|
|
647
|
+
case 'boolean':
|
|
648
|
+
return 'boolean'
|
|
649
|
+
case 'number':
|
|
650
|
+
return Number.isInteger(example) ? 'integer' : 'number'
|
|
651
|
+
case 'object':
|
|
652
|
+
return Array.isArray(example) ? 'array' : 'object'
|
|
653
|
+
default:
|
|
654
|
+
return null
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Create a runtime type for function signatures.
|
|
660
|
+
*
|
|
661
|
+
* Forms:
|
|
662
|
+
* FunctionPredicate(name, spec) - from a specification object
|
|
663
|
+
* FunctionPredicate(name, fn) - from an existing typed function
|
|
664
|
+
*
|
|
665
|
+
* @example
|
|
666
|
+
* const Callback = FunctionPredicate('Callback', {
|
|
667
|
+
* params: { x: 0, y: 0 },
|
|
668
|
+
* returns: 0,
|
|
669
|
+
* })
|
|
670
|
+
* Callback.check((a, b) => a + b) // true (typeof === 'function')
|
|
671
|
+
* Callback.check(42) // false
|
|
672
|
+
*
|
|
673
|
+
* @example
|
|
674
|
+
* function add(a: 0, b: 0) -> 0 { return a + b }
|
|
675
|
+
* const Adder = FunctionPredicate('Adder', add)
|
|
676
|
+
* // Extracts params/returns from add.__tjs
|
|
677
|
+
*/
|
|
678
|
+
export function FunctionPredicate(
|
|
679
|
+
name: string,
|
|
680
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
681
|
+
specOrFn: FunctionPredicateSpec | Function
|
|
682
|
+
): FunctionPredicateType {
|
|
683
|
+
let params: Record<string, any> = {}
|
|
684
|
+
let returns: any = undefined
|
|
685
|
+
let returnContract: ReturnContract = 'assertReturns'
|
|
686
|
+
|
|
687
|
+
if (typeof specOrFn === 'function') {
|
|
688
|
+
// Extract from function's __tjs metadata
|
|
689
|
+
const meta = (specOrFn as any).__tjs
|
|
690
|
+
if (meta) {
|
|
691
|
+
// Build params from __tjs.params
|
|
692
|
+
if (meta.params) {
|
|
693
|
+
for (const [key, info] of Object.entries(meta.params)) {
|
|
694
|
+
params[key] = (info as any)?.example ?? null
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// Extract return type
|
|
698
|
+
if (meta.returns) {
|
|
699
|
+
returns = (meta.returns as any)?.example ?? null
|
|
700
|
+
}
|
|
701
|
+
// Extract return contract from safety markers
|
|
702
|
+
if (meta.safeReturn) returnContract = 'checkedReturns'
|
|
703
|
+
else if (meta.unsafe) returnContract = 'assertReturns'
|
|
704
|
+
else returnContract = 'returns'
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
params = specOrFn.params ?? {}
|
|
708
|
+
returns = specOrFn.returns
|
|
709
|
+
returnContract = specOrFn.returnContract ?? 'assertReturns'
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const fpType: FunctionPredicateType = {
|
|
713
|
+
description: name,
|
|
714
|
+
params,
|
|
715
|
+
returns,
|
|
716
|
+
returnContract,
|
|
717
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
718
|
+
check: (value: unknown): value is Function => {
|
|
719
|
+
if (typeof value !== 'function') return false
|
|
720
|
+
|
|
721
|
+
// Structural validation: check arity and __tjs metadata
|
|
722
|
+
const expectedArity = Object.keys(params).length
|
|
723
|
+
if (expectedArity > 0) {
|
|
724
|
+
// Check function.length (number of params before first default)
|
|
725
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
726
|
+
const fn = value as Function
|
|
727
|
+
const meta = (fn as any).__tjs
|
|
728
|
+
if (meta?.params) {
|
|
729
|
+
// Has TJS metadata — check param count matches
|
|
730
|
+
const metaParamCount = Object.keys(meta.params).length
|
|
731
|
+
if (metaParamCount !== expectedArity) return false
|
|
732
|
+
|
|
733
|
+
// Check param type kinds match where both sides have type info
|
|
734
|
+
const expectedKeys = Object.keys(params)
|
|
735
|
+
const metaKeys = Object.keys(meta.params)
|
|
736
|
+
for (let i = 0; i < expectedKeys.length; i++) {
|
|
737
|
+
const metaInfo = meta.params[metaKeys[i]]
|
|
738
|
+
const expectedExample = params[expectedKeys[i]]
|
|
739
|
+
if (metaInfo?.type?.kind && expectedExample !== undefined) {
|
|
740
|
+
const expectedKind = kindOfExample(expectedExample)
|
|
741
|
+
if (
|
|
742
|
+
expectedKind &&
|
|
743
|
+
metaInfo.type.kind !== expectedKind &&
|
|
744
|
+
metaInfo.type.kind !== 'any'
|
|
745
|
+
)
|
|
746
|
+
return false
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return true
|
|
753
|
+
},
|
|
754
|
+
__runtimeType: true as const,
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return fpType
|
|
758
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -38,6 +38,11 @@ export {
|
|
|
38
38
|
TRecord,
|
|
39
39
|
type GenericType,
|
|
40
40
|
type TypeParam,
|
|
41
|
+
// Function predicates
|
|
42
|
+
FunctionPredicate,
|
|
43
|
+
type FunctionPredicateType,
|
|
44
|
+
type FunctionPredicateSpec,
|
|
45
|
+
type ReturnContract,
|
|
41
46
|
} from './Type'
|
|
42
47
|
|
|
43
48
|
// Timestamp and LegalDate utilities (pure functions)
|