tjs-lang 0.2.8 → 0.3.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.
- package/demo/docs.json +20 -14
- package/demo/src/examples.ts +23 -83
- package/demo/src/playground-shared.ts +666 -0
- package/demo/src/tjs-playground.ts +52 -528
- package/demo/src/ts-examples.ts +5 -4
- package/demo/src/ts-playground.ts +50 -414
- package/dist/index.js +58 -23
- package/dist/index.js.map +9 -9
- package/dist/src/lang/types.d.ts +1 -1
- package/dist/src/types/Type.d.ts +3 -1
- package/dist/tjs-full.js +58 -23
- package/dist/tjs-full.js.map +9 -9
- package/dist/tjs-transpiler.js +55 -20
- package/dist/tjs-transpiler.js.map +7 -7
- package/dist/tjs-vm.js +14 -14
- package/dist/tjs-vm.js.map +5 -5
- package/docs/index.js +740 -1010
- package/docs/index.js.map +9 -8
- package/editors/codemirror/ajs-language.ts +27 -1
- package/editors/codemirror/autocomplete.test.ts +3 -3
- package/package.json +1 -1
- package/src/lang/codegen.test.ts +11 -11
- package/src/lang/emitters/from-ts.ts +1 -1
- package/src/lang/emitters/js.ts +74 -0
- package/src/lang/inference.ts +40 -8
- package/src/lang/lang.test.ts +154 -16
- package/src/lang/runtime.ts +7 -0
- package/src/lang/types.ts +2 -0
- package/src/lang/typescript-syntax.test.ts +6 -4
- package/src/types/Type.test.ts +64 -0
- package/src/types/Type.ts +22 -1
- package/src/use-cases/transpiler-integration.test.ts +10 -10
- package/src/vm/atoms/batteries.ts +2 -0
|
@@ -1353,6 +1353,17 @@ function getPlaceholderForParam(name: string, info: any): string {
|
|
|
1353
1353
|
return String(ex)
|
|
1354
1354
|
}
|
|
1355
1355
|
|
|
1356
|
+
// Then check for examples from schema metadata (use first as placeholder)
|
|
1357
|
+
const examples = info.type?.examples || info.examples
|
|
1358
|
+
if (Array.isArray(examples) && examples.length > 0) {
|
|
1359
|
+
const ex = examples[0]
|
|
1360
|
+
if (typeof ex === 'string') return `'${ex}'`
|
|
1361
|
+
if (typeof ex === 'number' || typeof ex === 'boolean') return String(ex)
|
|
1362
|
+
if (Array.isArray(ex)) return JSON.stringify(ex)
|
|
1363
|
+
if (typeof ex === 'object') return JSON.stringify(ex)
|
|
1364
|
+
return String(ex)
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1356
1367
|
// Then check for explicit default value
|
|
1357
1368
|
if (info.default !== undefined && info.default !== null) {
|
|
1358
1369
|
const def = info.default
|
|
@@ -1481,12 +1492,27 @@ function tjsCompletionSource(config: AutocompleteConfig = {}) {
|
|
|
1481
1492
|
// Handle both { type: 'string' } and { kind: 'string' } formats
|
|
1482
1493
|
const returnType =
|
|
1483
1494
|
meta.returns?.type || meta.returns?.kind || 'void'
|
|
1495
|
+
|
|
1496
|
+
// Build info string with description and parameter examples
|
|
1497
|
+
let infoText = meta.description || ''
|
|
1498
|
+
for (const [pName, pInfo] of paramEntries as [string, any][]) {
|
|
1499
|
+
const pExamples = pInfo.type?.examples || pInfo.examples
|
|
1500
|
+
if (Array.isArray(pExamples) && pExamples.length > 0) {
|
|
1501
|
+
const formatted = pExamples
|
|
1502
|
+
.map((ex: any) =>
|
|
1503
|
+
typeof ex === 'string' ? `'${ex}'` : String(ex)
|
|
1504
|
+
)
|
|
1505
|
+
.join(', ')
|
|
1506
|
+
infoText += `${infoText ? '\n' : ''}${pName}: e.g. ${formatted}`
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1484
1510
|
options.push(
|
|
1485
1511
|
snippetCompletion(`${name}(${snippetParams})`, {
|
|
1486
1512
|
label: name,
|
|
1487
1513
|
type: 'function',
|
|
1488
1514
|
detail: `(${paramList}) -> ${returnType}`,
|
|
1489
|
-
info:
|
|
1515
|
+
info: infoText || undefined,
|
|
1490
1516
|
boost: 2, // Boost user-defined functions above globals
|
|
1491
1517
|
})
|
|
1492
1518
|
)
|
|
@@ -432,8 +432,8 @@ const after = 2
|
|
|
432
432
|
|
|
433
433
|
expect(metadata).toBeDefined()
|
|
434
434
|
// metadata is now keyed by function name
|
|
435
|
-
expect(metadata?.add?.params?.a?.type?.kind).toBe('
|
|
436
|
-
expect(metadata?.add?.params?.b?.type?.kind).toBe('
|
|
435
|
+
expect(metadata?.add?.params?.a?.type?.kind).toBe('integer')
|
|
436
|
+
expect(metadata?.add?.params?.b?.type?.kind).toBe('integer')
|
|
437
437
|
})
|
|
438
438
|
|
|
439
439
|
it('extracts example-based types', () => {
|
|
@@ -445,7 +445,7 @@ const after = 2
|
|
|
445
445
|
expect(metadata).toBeDefined()
|
|
446
446
|
// metadata is now keyed by function name
|
|
447
447
|
expect(metadata?.greet?.params?.name?.type?.kind).toBe('string')
|
|
448
|
-
expect(metadata?.greet?.params?.times?.type?.kind).toBe('
|
|
448
|
+
expect(metadata?.greet?.params?.times?.type?.kind).toBe('integer')
|
|
449
449
|
expect(metadata?.greet?.params?.times?.default).toBe(1)
|
|
450
450
|
})
|
|
451
451
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tjs-lang",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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/lang/codegen.test.ts
CHANGED
|
@@ -54,7 +54,7 @@ describe('TS → TJS conversion quality', () => {
|
|
|
54
54
|
const ts = `function sum(nums: number[]): number { return nums.reduce((a, b) => a + b, 0) }`
|
|
55
55
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
56
56
|
|
|
57
|
-
expect(code).toContain('nums: [0]')
|
|
57
|
+
expect(code).toContain('nums: [0.0]')
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
it('converts object param correctly', () => {
|
|
@@ -62,7 +62,7 @@ describe('TS → TJS conversion quality', () => {
|
|
|
62
62
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
63
63
|
|
|
64
64
|
expect(code).toContain("name: ''")
|
|
65
|
-
expect(code).toContain('age: 0')
|
|
65
|
+
expect(code).toContain('age: 0.0')
|
|
66
66
|
})
|
|
67
67
|
|
|
68
68
|
it('handles multiple params in order', () => {
|
|
@@ -70,7 +70,7 @@ describe('TS → TJS conversion quality', () => {
|
|
|
70
70
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
71
71
|
|
|
72
72
|
// Should have both params with colon syntax
|
|
73
|
-
expect(code).toMatch(/add\(a: 0, b: 0\)/)
|
|
73
|
+
expect(code).toMatch(/add\(a: 0\.0, b: 0\.0\)/)
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
it('handles mixed required and optional params', () => {
|
|
@@ -228,7 +228,7 @@ class Calculator {
|
|
|
228
228
|
`
|
|
229
229
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
230
230
|
|
|
231
|
-
expect(code).toContain('add(a: 0, b: 0) -! 0')
|
|
231
|
+
expect(code).toContain('add(a: 0.0, b: 0.0) -! 0.0')
|
|
232
232
|
})
|
|
233
233
|
|
|
234
234
|
it('converts getters and setters', () => {
|
|
@@ -269,7 +269,7 @@ class MathUtils {
|
|
|
269
269
|
`
|
|
270
270
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
271
271
|
|
|
272
|
-
expect(code).toContain('static double(x: 0) -! 0')
|
|
272
|
+
expect(code).toContain('static double(x: 0.0) -! 0.0')
|
|
273
273
|
})
|
|
274
274
|
|
|
275
275
|
it('converts async methods', () => {
|
|
@@ -396,7 +396,7 @@ describe('TJS → JS transpilation quality', () => {
|
|
|
396
396
|
|
|
397
397
|
expect(code).toContain('__tjs')
|
|
398
398
|
// types is now keyed by function name
|
|
399
|
-
expect(types?.double?.returns?.kind).toBe('
|
|
399
|
+
expect(types?.double?.returns?.kind).toBe('integer')
|
|
400
400
|
})
|
|
401
401
|
|
|
402
402
|
it('marks required params correctly', () => {
|
|
@@ -561,8 +561,8 @@ function greet(name: string): string {
|
|
|
561
561
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
562
562
|
|
|
563
563
|
// All functions should be present (TS transpiler uses -! to skip signature tests)
|
|
564
|
-
expect(code).toContain('function add(a: 0, b: 0) -! 0')
|
|
565
|
-
expect(code).toContain('function multiply(a: 0, b: 0) -! 0')
|
|
564
|
+
expect(code).toContain('function add(a: 0.0, b: 0.0) -! 0.0')
|
|
565
|
+
expect(code).toContain('function multiply(a: 0.0, b: 0.0) -! 0.0')
|
|
566
566
|
expect(code).toContain("function greet(name: '') -! ''")
|
|
567
567
|
|
|
568
568
|
// Should be valid TJS (no TypeScript syntax remaining)
|
|
@@ -738,7 +738,7 @@ function greet(name: '', excited = false) -! '' {
|
|
|
738
738
|
// add function
|
|
739
739
|
expect(types?.add?.params?.a?.required).toBe(true)
|
|
740
740
|
expect(types?.add?.params?.b?.required).toBe(true)
|
|
741
|
-
expect(types?.add?.returns?.kind).toBe('
|
|
741
|
+
expect(types?.add?.returns?.kind).toBe('integer')
|
|
742
742
|
|
|
743
743
|
// greet function
|
|
744
744
|
expect(types?.greet?.params?.name?.required).toBe(true)
|
|
@@ -1146,7 +1146,7 @@ function processUser(user: { name: string; age: number }): string {
|
|
|
1146
1146
|
// TS → TJS
|
|
1147
1147
|
const { code: tjsCode } = fromTS(ts, { emitTJS: true })
|
|
1148
1148
|
expect(tjsCode).toContain("name: ''")
|
|
1149
|
-
expect(tjsCode).toContain('age: 0')
|
|
1149
|
+
expect(tjsCode).toContain('age: 0.0')
|
|
1150
1150
|
|
|
1151
1151
|
// TJS → JS (already has -! from TS transpiler)
|
|
1152
1152
|
const { code: jsCode, types } = tjs(tjsCode)
|
|
@@ -1386,7 +1386,7 @@ function getData(id: 0) -! { value: 0 } {
|
|
|
1386
1386
|
expect(result.value).toBeUndefined()
|
|
1387
1387
|
|
|
1388
1388
|
// But error properties are accessible
|
|
1389
|
-
expect(result.message).toContain('Expected
|
|
1389
|
+
expect(result.message).toContain('Expected integer')
|
|
1390
1390
|
})
|
|
1391
1391
|
})
|
|
1392
1392
|
|
package/src/lang/emitters/js.ts
CHANGED
|
@@ -868,6 +868,10 @@ function generateTypeCheckExpr(
|
|
|
868
868
|
return `typeof ${fieldPath} !== 'string'`
|
|
869
869
|
case 'number':
|
|
870
870
|
return `typeof ${fieldPath} !== 'number'`
|
|
871
|
+
case 'integer':
|
|
872
|
+
return `(typeof ${fieldPath} !== 'number' || !Number.isInteger(${fieldPath}))`
|
|
873
|
+
case 'non-negative-integer':
|
|
874
|
+
return `(typeof ${fieldPath} !== 'number' || !Number.isInteger(${fieldPath}) || ${fieldPath} < 0)`
|
|
871
875
|
case 'boolean':
|
|
872
876
|
return `typeof ${fieldPath} !== 'boolean'`
|
|
873
877
|
case 'null':
|
|
@@ -1493,6 +1497,41 @@ function runAllTests(
|
|
|
1493
1497
|
if (!__deepEqual(actual, expected)) {
|
|
1494
1498
|
throw new Error('Expected ' + __format(expected) + ' but got ' + __format(actual))
|
|
1495
1499
|
}
|
|
1500
|
+
},
|
|
1501
|
+
toContain(item) {
|
|
1502
|
+
if (!Array.isArray(actual) || !actual.some(function(v) { return __deepEqual(v, item) })) {
|
|
1503
|
+
throw new Error('Expected ' + __format(actual) + ' to contain ' + __format(item))
|
|
1504
|
+
}
|
|
1505
|
+
},
|
|
1506
|
+
toBeTruthy() {
|
|
1507
|
+
if (!actual) {
|
|
1508
|
+
throw new Error('Expected ' + __format(actual) + ' to be truthy')
|
|
1509
|
+
}
|
|
1510
|
+
},
|
|
1511
|
+
toBeFalsy() {
|
|
1512
|
+
if (actual) {
|
|
1513
|
+
throw new Error('Expected ' + __format(actual) + ' to be falsy')
|
|
1514
|
+
}
|
|
1515
|
+
},
|
|
1516
|
+
toBeNull() {
|
|
1517
|
+
if (actual !== null) {
|
|
1518
|
+
throw new Error('Expected null but got ' + __format(actual))
|
|
1519
|
+
}
|
|
1520
|
+
},
|
|
1521
|
+
toBeUndefined() {
|
|
1522
|
+
if (actual !== undefined) {
|
|
1523
|
+
throw new Error('Expected undefined but got ' + __format(actual))
|
|
1524
|
+
}
|
|
1525
|
+
},
|
|
1526
|
+
toBeGreaterThan(n) {
|
|
1527
|
+
if (!(actual > n)) {
|
|
1528
|
+
throw new Error('Expected ' + __format(actual) + ' to be greater than ' + n)
|
|
1529
|
+
}
|
|
1530
|
+
},
|
|
1531
|
+
toBeLessThan(n) {
|
|
1532
|
+
if (!(actual < n)) {
|
|
1533
|
+
throw new Error('Expected ' + __format(actual) + ' to be less than ' + n)
|
|
1534
|
+
}
|
|
1496
1535
|
}
|
|
1497
1536
|
}
|
|
1498
1537
|
}
|
|
@@ -1634,6 +1673,41 @@ function runTestBlocks(
|
|
|
1634
1673
|
if (!__deepEqual(actual, expected)) {
|
|
1635
1674
|
throw new Error('Expected ' + __format(expected) + ' but got ' + __format(actual))
|
|
1636
1675
|
}
|
|
1676
|
+
},
|
|
1677
|
+
toContain(item) {
|
|
1678
|
+
if (!Array.isArray(actual) || !actual.some(function(v) { return __deepEqual(v, item) })) {
|
|
1679
|
+
throw new Error('Expected ' + __format(actual) + ' to contain ' + __format(item))
|
|
1680
|
+
}
|
|
1681
|
+
},
|
|
1682
|
+
toBeTruthy() {
|
|
1683
|
+
if (!actual) {
|
|
1684
|
+
throw new Error('Expected ' + __format(actual) + ' to be truthy')
|
|
1685
|
+
}
|
|
1686
|
+
},
|
|
1687
|
+
toBeFalsy() {
|
|
1688
|
+
if (actual) {
|
|
1689
|
+
throw new Error('Expected ' + __format(actual) + ' to be falsy')
|
|
1690
|
+
}
|
|
1691
|
+
},
|
|
1692
|
+
toBeNull() {
|
|
1693
|
+
if (actual !== null) {
|
|
1694
|
+
throw new Error('Expected null but got ' + __format(actual))
|
|
1695
|
+
}
|
|
1696
|
+
},
|
|
1697
|
+
toBeUndefined() {
|
|
1698
|
+
if (actual !== undefined) {
|
|
1699
|
+
throw new Error('Expected undefined but got ' + __format(actual))
|
|
1700
|
+
}
|
|
1701
|
+
},
|
|
1702
|
+
toBeGreaterThan(n) {
|
|
1703
|
+
if (!(actual > n)) {
|
|
1704
|
+
throw new Error('Expected ' + __format(actual) + ' to be greater than ' + n)
|
|
1705
|
+
}
|
|
1706
|
+
},
|
|
1707
|
+
toBeLessThan(n) {
|
|
1708
|
+
if (!(actual < n)) {
|
|
1709
|
+
throw new Error('Expected ' + __format(actual) + ' to be less than ' + n)
|
|
1710
|
+
}
|
|
1637
1711
|
}
|
|
1638
1712
|
}
|
|
1639
1713
|
}
|
package/src/lang/inference.ts
CHANGED
|
@@ -29,7 +29,13 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
|
|
|
29
29
|
return { kind: 'string' }
|
|
30
30
|
}
|
|
31
31
|
if (typeof value === 'number') {
|
|
32
|
-
|
|
32
|
+
// Distinguish float vs integer by checking if source contains '.'
|
|
33
|
+
// 2.0 -> number (float), 42 -> integer
|
|
34
|
+
const raw = (node as any).raw as string | undefined
|
|
35
|
+
if (raw && raw.includes('.')) {
|
|
36
|
+
return { kind: 'number' }
|
|
37
|
+
}
|
|
38
|
+
return { kind: 'integer' }
|
|
33
39
|
}
|
|
34
40
|
if (typeof value === 'boolean') {
|
|
35
41
|
return { kind: 'boolean' }
|
|
@@ -110,14 +116,26 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
|
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
case 'UnaryExpression': {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
) {
|
|
118
|
-
const value =
|
|
119
|
+
const op = (node as any).operator
|
|
120
|
+
const arg = (node as any).argument
|
|
121
|
+
|
|
122
|
+
// +N means non-negative integer (e.g., +1, +3)
|
|
123
|
+
if (op === '+' && arg.type === 'Literal') {
|
|
124
|
+
const value = arg.value
|
|
119
125
|
if (typeof value === 'number') {
|
|
120
|
-
return { kind: '
|
|
126
|
+
return { kind: 'non-negative-integer' }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// -N means integer or float depending on source
|
|
131
|
+
if (op === '-' && arg.type === 'Literal') {
|
|
132
|
+
const value = arg.value
|
|
133
|
+
if (typeof value === 'number') {
|
|
134
|
+
const raw = arg.raw as string | undefined
|
|
135
|
+
if (raw && raw.includes('.')) {
|
|
136
|
+
return { kind: 'number' }
|
|
137
|
+
}
|
|
138
|
+
return { kind: 'integer' }
|
|
121
139
|
}
|
|
122
140
|
}
|
|
123
141
|
return { kind: 'any' }
|
|
@@ -258,6 +276,10 @@ export function extractLiteralValue(node: Expression): any {
|
|
|
258
276
|
const arg = extractLiteralValue((node as any).argument)
|
|
259
277
|
return typeof arg === 'number' ? -arg : undefined
|
|
260
278
|
}
|
|
279
|
+
if ((node as any).operator === '+') {
|
|
280
|
+
const arg = extractLiteralValue((node as any).argument)
|
|
281
|
+
return typeof arg === 'number' ? +arg : undefined
|
|
282
|
+
}
|
|
261
283
|
return undefined
|
|
262
284
|
|
|
263
285
|
case 'LogicalExpression': {
|
|
@@ -310,6 +332,12 @@ export function typeToString(type: TypeDescriptor): string {
|
|
|
310
332
|
return type.nullable ? 'string | null' : 'string'
|
|
311
333
|
case 'number':
|
|
312
334
|
return type.nullable ? 'number | null' : 'number'
|
|
335
|
+
case 'integer':
|
|
336
|
+
return type.nullable ? 'integer | null' : 'integer'
|
|
337
|
+
case 'non-negative-integer':
|
|
338
|
+
return type.nullable
|
|
339
|
+
? 'non-negative integer | null'
|
|
340
|
+
: 'non-negative integer'
|
|
313
341
|
case 'boolean':
|
|
314
342
|
return type.nullable ? 'boolean | null' : 'boolean'
|
|
315
343
|
case 'null':
|
|
@@ -354,6 +382,10 @@ export function checkType(value: any, type: TypeDescriptor): boolean {
|
|
|
354
382
|
return typeof value === 'string'
|
|
355
383
|
case 'number':
|
|
356
384
|
return typeof value === 'number'
|
|
385
|
+
case 'integer':
|
|
386
|
+
return typeof value === 'number' && Number.isInteger(value)
|
|
387
|
+
case 'non-negative-integer':
|
|
388
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0
|
|
357
389
|
case 'boolean':
|
|
358
390
|
return typeof value === 'boolean'
|
|
359
391
|
case 'array':
|
package/src/lang/lang.test.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
wrap,
|
|
16
16
|
} from './index'
|
|
17
17
|
import { preprocess } from './parser'
|
|
18
|
+
import { createRuntime, isMonadicError } from './runtime'
|
|
18
19
|
import { Schema } from './schema'
|
|
19
20
|
|
|
20
21
|
describe('Transpiler', () => {
|
|
@@ -293,7 +294,7 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
293
294
|
return { count }
|
|
294
295
|
}
|
|
295
296
|
`)
|
|
296
|
-
expect(signature.parameters.count.type.kind).toBe('
|
|
297
|
+
expect(signature.parameters.count.type.kind).toBe('integer')
|
|
297
298
|
expect(signature.parameters.count.required).toBe(true)
|
|
298
299
|
})
|
|
299
300
|
|
|
@@ -303,7 +304,7 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
303
304
|
return { limit }
|
|
304
305
|
}
|
|
305
306
|
`)
|
|
306
|
-
expect(signature.parameters.limit.type.kind).toBe('
|
|
307
|
+
expect(signature.parameters.limit.type.kind).toBe('integer')
|
|
307
308
|
expect(signature.parameters.limit.required).toBe(false)
|
|
308
309
|
expect(signature.parameters.limit.default).toBe(10)
|
|
309
310
|
})
|
|
@@ -336,7 +337,7 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
336
337
|
`)
|
|
337
338
|
expect(signature.parameters.user.type.kind).toBe('object')
|
|
338
339
|
expect(signature.parameters.user.type.shape?.name.kind).toBe('string')
|
|
339
|
-
expect(signature.parameters.user.type.shape?.age.kind).toBe('
|
|
340
|
+
expect(signature.parameters.user.type.shape?.age.kind).toBe('integer')
|
|
340
341
|
})
|
|
341
342
|
|
|
342
343
|
it('should handle array types with colon syntax', () => {
|
|
@@ -357,13 +358,150 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
357
358
|
`)
|
|
358
359
|
expect(signature.parameters.name.type.kind).toBe('string')
|
|
359
360
|
expect(signature.parameters.name.required).toBe(true)
|
|
360
|
-
expect(signature.parameters.count.type.kind).toBe('
|
|
361
|
+
expect(signature.parameters.count.type.kind).toBe('integer')
|
|
361
362
|
expect(signature.parameters.count.required).toBe(true)
|
|
362
|
-
expect(signature.parameters.limit.type.kind).toBe('
|
|
363
|
+
expect(signature.parameters.limit.type.kind).toBe('integer')
|
|
363
364
|
expect(signature.parameters.limit.required).toBe(false)
|
|
364
365
|
})
|
|
365
366
|
})
|
|
366
367
|
|
|
368
|
+
describe('Numeric type narrowing', () => {
|
|
369
|
+
it('should infer integer from whole number literal', () => {
|
|
370
|
+
const { signature } = transpile(`
|
|
371
|
+
function test(count: 42) { return { count } }
|
|
372
|
+
`)
|
|
373
|
+
expect(signature.parameters.count.type.kind).toBe('integer')
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('should infer float (number) from decimal literal', () => {
|
|
377
|
+
const { signature } = transpile(`
|
|
378
|
+
function test(rate: 3.14) { return { rate } }
|
|
379
|
+
`)
|
|
380
|
+
expect(signature.parameters.rate.type.kind).toBe('number')
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('should infer float from 0.0', () => {
|
|
384
|
+
const { signature } = transpile(`
|
|
385
|
+
function test(value: 0.0) { return { value } }
|
|
386
|
+
`)
|
|
387
|
+
expect(signature.parameters.value.type.kind).toBe('number')
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('should infer non-negative-integer from +N syntax', () => {
|
|
391
|
+
const { signature } = transpile(`
|
|
392
|
+
function test(age: +20) { return { age } }
|
|
393
|
+
`)
|
|
394
|
+
expect(signature.parameters.age.type.kind).toBe('non-negative-integer')
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
it('should infer non-negative-integer from +0', () => {
|
|
398
|
+
const { signature } = transpile(`
|
|
399
|
+
function test(index: +0) { return { index } }
|
|
400
|
+
`)
|
|
401
|
+
expect(signature.parameters.index.type.kind).toBe('non-negative-integer')
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('should infer integer from negative literal', () => {
|
|
405
|
+
const { signature } = transpile(`
|
|
406
|
+
function test(offset: -5) { return { offset } }
|
|
407
|
+
`)
|
|
408
|
+
expect(signature.parameters.offset.type.kind).toBe('integer')
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('should infer number from negative decimal', () => {
|
|
412
|
+
const { signature } = transpile(`
|
|
413
|
+
function test(temp: -3.5) { return { temp } }
|
|
414
|
+
`)
|
|
415
|
+
expect(signature.parameters.temp.type.kind).toBe('number')
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('should generate correct runtime validation for integer', () => {
|
|
419
|
+
const result = tjs(`function test(n: 1) -> 0 { return n }`)
|
|
420
|
+
// Should check Number.isInteger
|
|
421
|
+
expect(result.code).toContain('Number.isInteger')
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('should generate correct runtime validation for non-negative-integer', () => {
|
|
425
|
+
const result = tjs(`function test(n: +1) -> 0 { return n }`)
|
|
426
|
+
// Should check Number.isInteger AND >= 0
|
|
427
|
+
expect(result.code).toContain('Number.isInteger')
|
|
428
|
+
expect(result.code).toContain('< 0')
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('should validate integer at runtime', () => {
|
|
432
|
+
const result = tjs(`function check(n: 1) -> 0 { return n }`)
|
|
433
|
+
const savedTjs = globalThis.__tjs
|
|
434
|
+
globalThis.__tjs = createRuntime()
|
|
435
|
+
try {
|
|
436
|
+
const fn = new Function(result.code + '\nreturn check')()
|
|
437
|
+
// Valid integer
|
|
438
|
+
expect(fn(42)).toBe(42)
|
|
439
|
+
// Float should fail
|
|
440
|
+
const bad = fn(3.14)
|
|
441
|
+
expect(isMonadicError(bad)).toBe(true)
|
|
442
|
+
} finally {
|
|
443
|
+
globalThis.__tjs = savedTjs
|
|
444
|
+
}
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('should validate non-negative-integer at runtime', () => {
|
|
448
|
+
const result = tjs(`function check(n: +1) -> 0 { return n }`)
|
|
449
|
+
const savedTjs = globalThis.__tjs
|
|
450
|
+
globalThis.__tjs = createRuntime()
|
|
451
|
+
try {
|
|
452
|
+
const fn = new Function(result.code + '\nreturn check')()
|
|
453
|
+
// Valid non-negative integer
|
|
454
|
+
expect(fn(0)).toBe(0)
|
|
455
|
+
expect(fn(42)).toBe(42)
|
|
456
|
+
// Negative integer should fail
|
|
457
|
+
const negResult = fn(-1)
|
|
458
|
+
expect(isMonadicError(negResult)).toBe(true)
|
|
459
|
+
// Float should fail
|
|
460
|
+
const floatResult = fn(3.14)
|
|
461
|
+
expect(isMonadicError(floatResult)).toBe(true)
|
|
462
|
+
} finally {
|
|
463
|
+
globalThis.__tjs = savedTjs
|
|
464
|
+
}
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
it('should validate float (number) accepts all numbers at runtime', () => {
|
|
468
|
+
const result = tjs(`function check(n: 0.0) -> 0.0 { return n }`)
|
|
469
|
+
const savedTjs = globalThis.__tjs
|
|
470
|
+
globalThis.__tjs = createRuntime()
|
|
471
|
+
try {
|
|
472
|
+
const fn = new Function(result.code + '\nreturn check')()
|
|
473
|
+
// All numbers should pass for float
|
|
474
|
+
expect(fn(42)).toBe(42)
|
|
475
|
+
expect(fn(3.14)).toBe(3.14)
|
|
476
|
+
expect(fn(-5)).toBe(-5)
|
|
477
|
+
expect(fn(0)).toBe(0)
|
|
478
|
+
} finally {
|
|
479
|
+
globalThis.__tjs = savedTjs
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it('should handle numeric types in object shapes', () => {
|
|
484
|
+
const { signature } = transpile(`
|
|
485
|
+
function test(point: { x: 0.0, y: 0.0, index: 0 }) { return point }
|
|
486
|
+
`)
|
|
487
|
+
expect(signature.parameters.point.type.shape?.x.kind).toBe('number')
|
|
488
|
+
expect(signature.parameters.point.type.shape?.y.kind).toBe('number')
|
|
489
|
+
expect(signature.parameters.point.type.shape?.index.kind).toBe('integer')
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('should handle numeric types in array items', () => {
|
|
493
|
+
const { signature } = transpile(`
|
|
494
|
+
function test(counts: [0]) { return counts }
|
|
495
|
+
`)
|
|
496
|
+
expect(signature.parameters.counts.type.items?.kind).toBe('integer')
|
|
497
|
+
|
|
498
|
+
const { signature: sig2 } = transpile(`
|
|
499
|
+
function test(values: [0.0]) { return values }
|
|
500
|
+
`)
|
|
501
|
+
expect(sig2.parameters.values.type.items?.kind).toBe('number')
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
|
|
367
505
|
describe('Basic transpilation', () => {
|
|
368
506
|
it('should transpile a simple function', () => {
|
|
369
507
|
const { ast } = transpile(`
|
|
@@ -934,7 +1072,7 @@ describe('TJS Emitter', () => {
|
|
|
934
1072
|
`)
|
|
935
1073
|
expect(result.code).not.toContain('->')
|
|
936
1074
|
expect(result.types.add.returns).toBeDefined()
|
|
937
|
-
expect(result.types.add.returns?.kind).toBe('
|
|
1075
|
+
expect(result.types.add.returns?.kind).toBe('integer')
|
|
938
1076
|
})
|
|
939
1077
|
|
|
940
1078
|
it('should mark parameters as required when using colon syntax', () => {
|
|
@@ -968,7 +1106,7 @@ describe('TJS Emitter', () => {
|
|
|
968
1106
|
'string'
|
|
969
1107
|
)
|
|
970
1108
|
expect(result.types.process.params.user.type.shape?.age.kind).toBe(
|
|
971
|
-
'
|
|
1109
|
+
'integer'
|
|
972
1110
|
)
|
|
973
1111
|
})
|
|
974
1112
|
|
|
@@ -979,7 +1117,7 @@ describe('TJS Emitter', () => {
|
|
|
979
1117
|
}
|
|
980
1118
|
`)
|
|
981
1119
|
expect(result.types.sum.params.numbers.type.kind).toBe('array')
|
|
982
|
-
expect(result.types.sum.params.numbers.type.items?.kind).toBe('
|
|
1120
|
+
expect(result.types.sum.params.numbers.type.items?.kind).toBe('integer')
|
|
983
1121
|
})
|
|
984
1122
|
|
|
985
1123
|
it('should generate __tjs metadata object', () => {
|
|
@@ -1052,7 +1190,7 @@ function greet(name: 'world') {
|
|
|
1052
1190
|
}
|
|
1053
1191
|
`
|
|
1054
1192
|
expect(result.code).toContain('function double')
|
|
1055
|
-
expect(result.types.double.params.n.type.kind).toBe('
|
|
1193
|
+
expect(result.types.double.params.n.type.kind).toBe('integer')
|
|
1056
1194
|
})
|
|
1057
1195
|
|
|
1058
1196
|
it('should handle interpolation in tagged template', () => {
|
|
@@ -1106,7 +1244,7 @@ function greet(name: 'world') {
|
|
|
1106
1244
|
`)
|
|
1107
1245
|
expect(result.types.test.returns).toBeDefined()
|
|
1108
1246
|
expect(result.types.test.returns?.kind).toBe('object')
|
|
1109
|
-
expect(result.types.test.returns?.shape?.result.kind).toBe('
|
|
1247
|
+
expect(result.types.test.returns?.shape?.result.kind).toBe('integer')
|
|
1110
1248
|
})
|
|
1111
1249
|
})
|
|
1112
1250
|
|
|
@@ -1155,9 +1293,9 @@ function greet(name: 'world') {
|
|
|
1155
1293
|
return x
|
|
1156
1294
|
}
|
|
1157
1295
|
`)
|
|
1158
|
-
expect(result.types.compute.params.x.type.kind).toBe('
|
|
1296
|
+
expect(result.types.compute.params.x.type.kind).toBe('integer')
|
|
1159
1297
|
expect(result.types.compute.params.y.type.kind).toBe('string')
|
|
1160
|
-
expect(result.types.compute.returns?.kind).toBe('
|
|
1298
|
+
expect(result.types.compute.returns?.kind).toBe('integer')
|
|
1161
1299
|
})
|
|
1162
1300
|
|
|
1163
1301
|
// === NEW TESTS: Multi-function and no-function support ===
|
|
@@ -1350,8 +1488,8 @@ function greet(name: 'world') {
|
|
|
1350
1488
|
`,
|
|
1351
1489
|
{ runTests: false }
|
|
1352
1490
|
)
|
|
1353
|
-
// Should have inline validation
|
|
1354
|
-
expect(result.code).toContain(
|
|
1491
|
+
// Should have inline validation (integer check for integer examples)
|
|
1492
|
+
expect(result.code).toContain('Number.isInteger(a)')
|
|
1355
1493
|
expect(result.code).toContain('__tjs.typeError')
|
|
1356
1494
|
})
|
|
1357
1495
|
|
|
@@ -1482,7 +1620,7 @@ describe('TypeScript to TJS Transpiler', () => {
|
|
|
1482
1620
|
`function sum(nums: number[]): number { return 0 }`,
|
|
1483
1621
|
{ emitTJS: true }
|
|
1484
1622
|
)
|
|
1485
|
-
expect(result.code).toContain('nums: [0]')
|
|
1623
|
+
expect(result.code).toContain('nums: [0.0]')
|
|
1486
1624
|
})
|
|
1487
1625
|
|
|
1488
1626
|
it('should handle object literal types', () => {
|
|
@@ -1490,7 +1628,7 @@ describe('TypeScript to TJS Transpiler', () => {
|
|
|
1490
1628
|
`function getUser(): { name: string, age: number } { return { name: '', age: 0 } }`,
|
|
1491
1629
|
{ emitTJS: true }
|
|
1492
1630
|
)
|
|
1493
|
-
expect(result.code).toContain("-! { name: '', age: 0 }") // -! for TS-transpiled
|
|
1631
|
+
expect(result.code).toContain("-! { name: '', age: 0.0 }") // -! for TS-transpiled
|
|
1494
1632
|
})
|
|
1495
1633
|
|
|
1496
1634
|
it('should handle nullable types', () => {
|
package/src/lang/runtime.ts
CHANGED
|
@@ -563,6 +563,13 @@ export function checkType(
|
|
|
563
563
|
if (expected === 'number' && actual === 'number') return null
|
|
564
564
|
if (expected === 'integer' && actual === 'number' && Number.isInteger(value))
|
|
565
565
|
return null
|
|
566
|
+
if (
|
|
567
|
+
expected === 'non-negative-integer' &&
|
|
568
|
+
actual === 'number' &&
|
|
569
|
+
Number.isInteger(value) &&
|
|
570
|
+
(value as number) >= 0
|
|
571
|
+
)
|
|
572
|
+
return null
|
|
566
573
|
|
|
567
574
|
// Object matching (basic)
|
|
568
575
|
if (expected === 'object' && actual === 'object') return null
|