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.
@@ -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
  *
@@ -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
 
@@ -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
+ }
@@ -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)