tjs-lang 0.2.7 → 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 +32 -26
- package/demo/src/examples.ts +23 -83
- package/demo/src/playground-shared.ts +666 -0
- package/demo/src/tjs-playground.ts +65 -550
- package/demo/src/ts-examples.ts +5 -4
- package/demo/src/ts-playground.ts +50 -414
- package/dist/index.js +143 -160
- package/dist/index.js.map +12 -12
- package/dist/src/lang/emitters/js.d.ts +34 -2
- package/dist/src/lang/index.d.ts +1 -1
- package/dist/src/lang/types.d.ts +1 -1
- package/dist/src/types/Type.d.ts +3 -1
- package/dist/tjs-full.js +143 -160
- package/dist/tjs-full.js.map +12 -12
- package/dist/tjs-transpiler.js +122 -55
- package/dist/tjs-transpiler.js.map +9 -8
- package/dist/tjs-vm.js +14 -14
- package/dist/tjs-vm.js.map +5 -5
- package/docs/docs.json +792 -0
- package/docs/index.js +2652 -2835
- package/docs/index.js.map +11 -10
- 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 +228 -4
- package/src/lang/index.ts +0 -3
- package/src/lang/inference.ts +40 -8
- package/src/lang/lang.test.ts +192 -35
- package/src/lang/roundtrip.test.ts +155 -0
- 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/lang/wasm.test.ts +20 -0
- package/src/lang/wasm.ts +143 -0
- 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
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', () => {
|
|
@@ -2816,7 +2954,10 @@ function process(a: [], b: []) {
|
|
|
2816
2954
|
expect(result.source).toContain('globalThis.__tjs_wasm_1')
|
|
2817
2955
|
})
|
|
2818
2956
|
|
|
2819
|
-
it('should
|
|
2957
|
+
it('should compile WASM at transpile time and embed in output', async () => {
|
|
2958
|
+
const { installRuntime } = require('./runtime')
|
|
2959
|
+
installRuntime()
|
|
2960
|
+
|
|
2820
2961
|
const result = tjs(`
|
|
2821
2962
|
function double(x: 0, y: 0) {
|
|
2822
2963
|
return wasm {
|
|
@@ -2824,12 +2965,27 @@ function double(x: 0, y: 0) {
|
|
|
2824
2965
|
}
|
|
2825
2966
|
}`)
|
|
2826
2967
|
|
|
2827
|
-
//
|
|
2828
|
-
|
|
2829
|
-
expect(
|
|
2968
|
+
// WASM should be compiled at transpile time
|
|
2969
|
+
expect(result.wasmCompiled).toBeDefined()
|
|
2970
|
+
expect(result.wasmCompiled?.length).toBe(1)
|
|
2971
|
+
expect(result.wasmCompiled?.[0].success).toBe(true)
|
|
2972
|
+
expect(result.wasmCompiled?.[0].byteLength).toBeGreaterThan(0)
|
|
2973
|
+
|
|
2974
|
+
// Output should contain base64-encoded WASM
|
|
2975
|
+
expect(result.code).toContain('__wasmBlocks')
|
|
2976
|
+
expect(result.code).toContain('b64:')
|
|
2977
|
+
|
|
2978
|
+
// Execute with async function to allow WASM instantiation
|
|
2979
|
+
const fn = new Function(
|
|
2980
|
+
'return (async () => {' + result.code + '; return double(3, 4); })()'
|
|
2981
|
+
)
|
|
2982
|
+
expect(await fn()).toBe(15) // 3 * 4 + 3 = 15
|
|
2830
2983
|
})
|
|
2831
2984
|
|
|
2832
|
-
it('should use WASM when
|
|
2985
|
+
it('should use WASM compute function when instantiated', async () => {
|
|
2986
|
+
const { installRuntime } = require('./runtime')
|
|
2987
|
+
installRuntime()
|
|
2988
|
+
|
|
2833
2989
|
const result = tjs(`
|
|
2834
2990
|
function compute(a: 0, b: 0) {
|
|
2835
2991
|
return wasm {
|
|
@@ -2837,30 +2993,31 @@ function compute(a: 0, b: 0) {
|
|
|
2837
2993
|
}
|
|
2838
2994
|
}`)
|
|
2839
2995
|
|
|
2840
|
-
//
|
|
2841
|
-
const
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
return result;
|
|
2847
|
-
`
|
|
2848
|
-
const fn = new Function(code)
|
|
2849
|
-
// Should use the "WASM" version (our mock)
|
|
2850
|
-
expect(fn()).toBe(1200) // 3 * 4 * 100
|
|
2996
|
+
// Execute async to allow WASM instantiation
|
|
2997
|
+
const fn = new Function(
|
|
2998
|
+
'return (async () => {' + result.code + '; return compute(3, 4); })()'
|
|
2999
|
+
)
|
|
3000
|
+
// Should use the actual WASM version
|
|
3001
|
+
expect(await fn()).toBe(7) // 3 + 4 = 7
|
|
2851
3002
|
})
|
|
2852
3003
|
|
|
2853
|
-
it('should use explicit fallback when
|
|
3004
|
+
it('should use explicit fallback when WASM compilation fails', () => {
|
|
3005
|
+
// Test with code that can't compile to WASM (array.map)
|
|
2854
3006
|
const result = tjs(`
|
|
2855
3007
|
function transform(arr: []) {
|
|
2856
3008
|
return wasm {
|
|
2857
|
-
return arr
|
|
3009
|
+
return arr.map(x => x * 2)
|
|
2858
3010
|
} fallback {
|
|
2859
3011
|
return arr.map(x => x * 2)
|
|
2860
3012
|
}
|
|
2861
3013
|
}`)
|
|
2862
3014
|
|
|
2863
|
-
//
|
|
3015
|
+
// WASM compilation should fail (array.map not supported)
|
|
3016
|
+
expect(result.wasmCompiled?.[0].success).toBe(false)
|
|
3017
|
+
|
|
3018
|
+
// But code should still work using fallback
|
|
3019
|
+
const { installRuntime } = require('./runtime')
|
|
3020
|
+
installRuntime()
|
|
2864
3021
|
const fn = new Function(`${result.code}; return transform([1, 2, 3]);`)
|
|
2865
3022
|
expect(fn()).toEqual([2, 4, 6])
|
|
2866
3023
|
})
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TJS Roundtrip Tests
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that TJS code "just works" through the full pipeline:
|
|
5
|
+
* 1. Parse TJS source
|
|
6
|
+
* 2. Transpile to JS
|
|
7
|
+
* 3. Execute the result
|
|
8
|
+
*
|
|
9
|
+
* NOTE: Currently transpiled code requires globalThis.__tjs runtime to be set up.
|
|
10
|
+
* This is a known limitation - see TODO.md for "self-contained transpiler output".
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, test, expect, beforeAll } from 'bun:test'
|
|
14
|
+
import { tjs } from './index'
|
|
15
|
+
import { Is, IsNot } from './runtime'
|
|
16
|
+
|
|
17
|
+
// Set up the minimal runtime needed for transpiled code
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
;(globalThis as any).__tjs = {
|
|
20
|
+
Is,
|
|
21
|
+
IsNot,
|
|
22
|
+
pushStack: () => {},
|
|
23
|
+
popStack: () => {},
|
|
24
|
+
typeError: (path: string, expected: string, got: any) => {
|
|
25
|
+
const err = new Error(
|
|
26
|
+
`Type error at ${path}: expected ${expected}, got ${typeof got}`
|
|
27
|
+
)
|
|
28
|
+
;(err as any).$error = true
|
|
29
|
+
return err
|
|
30
|
+
},
|
|
31
|
+
createRuntime: () => (globalThis as any).__tjs,
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/** Helper to execute transpiled code and capture console output */
|
|
36
|
+
function execCode(code: string): any[] {
|
|
37
|
+
const logs: any[] = []
|
|
38
|
+
const mockConsole = { log: (...args: any[]) => logs.push(args) }
|
|
39
|
+
const fn = new Function('console', code)
|
|
40
|
+
fn(mockConsole)
|
|
41
|
+
return logs
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('TJS roundtrip - code should just work', () => {
|
|
45
|
+
test('basic function with type annotations', () => {
|
|
46
|
+
const source = `
|
|
47
|
+
function add(a: 0, b: 0) -> 0 {
|
|
48
|
+
return a + b
|
|
49
|
+
}
|
|
50
|
+
console.log(add(2, 3))
|
|
51
|
+
`
|
|
52
|
+
const result = tjs(source, { runTests: false })
|
|
53
|
+
expect(result.code).toBeDefined()
|
|
54
|
+
|
|
55
|
+
const logs = execCode(result.code)
|
|
56
|
+
expect(logs[0]).toEqual([5])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('template literals (backticks)', () => {
|
|
60
|
+
const source = `
|
|
61
|
+
function greet(name: 'World') -> '' {
|
|
62
|
+
return \`Hello, \${name}!\`
|
|
63
|
+
}
|
|
64
|
+
console.log(greet('TJS'))
|
|
65
|
+
`
|
|
66
|
+
const result = tjs(source, { runTests: false })
|
|
67
|
+
expect(result.code).toContain('Hello')
|
|
68
|
+
|
|
69
|
+
const logs = execCode(result.code)
|
|
70
|
+
expect(logs[0]).toEqual(['Hello, TJS!'])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('inline tests execute at transpile time', () => {
|
|
74
|
+
const source = `
|
|
75
|
+
function double(x: 0) -> 0 {
|
|
76
|
+
return x * 2
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
test 'double works' {
|
|
80
|
+
expect(double(5)).toBe(10)
|
|
81
|
+
}
|
|
82
|
+
`
|
|
83
|
+
// Tests run during transpilation - check the testResults array
|
|
84
|
+
const result = tjs(source, { runTests: 'report' })
|
|
85
|
+
|
|
86
|
+
// testResults contains test outcomes
|
|
87
|
+
expect(result.testResults).toBeDefined()
|
|
88
|
+
expect(result.testResults!.length).toBeGreaterThan(0)
|
|
89
|
+
const allPassed = result.testResults!.every((r: any) => r.passed)
|
|
90
|
+
expect(allPassed).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('apostrophes in strings', () => {
|
|
94
|
+
const source = `
|
|
95
|
+
const msg1 = "You can't do this in Jest"
|
|
96
|
+
const msg2 = "You'd need to export everything"
|
|
97
|
+
console.log(msg1, msg2)
|
|
98
|
+
`
|
|
99
|
+
const result = tjs(source, { runTests: false })
|
|
100
|
+
expect(result.code).toContain("can't")
|
|
101
|
+
expect(result.code).toContain("You'd")
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('escaped newlines in strings', () => {
|
|
105
|
+
const source = `
|
|
106
|
+
console.log('Line 1\\nLine 2')
|
|
107
|
+
`
|
|
108
|
+
const result = tjs(source, { runTests: false })
|
|
109
|
+
const logs = execCode(result.code)
|
|
110
|
+
expect(logs[0][0]).toContain('\n')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('regex patterns with escapes', () => {
|
|
114
|
+
const source = `
|
|
115
|
+
const EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/
|
|
116
|
+
console.log(EMAIL_REGEX.test('test@example.com'))
|
|
117
|
+
`
|
|
118
|
+
const result = tjs(source, { runTests: false })
|
|
119
|
+
const logs = execCode(result.code)
|
|
120
|
+
expect(logs[0]).toEqual([true])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('multiline JSDoc comments with backticks', () => {
|
|
124
|
+
const source = `
|
|
125
|
+
/**
|
|
126
|
+
* Returns \`hello\` to the caller
|
|
127
|
+
*/
|
|
128
|
+
function myFunc() {
|
|
129
|
+
return 42
|
|
130
|
+
}
|
|
131
|
+
console.log(myFunc())
|
|
132
|
+
`
|
|
133
|
+
const result = tjs(source, { runTests: false })
|
|
134
|
+
const logs = execCode(result.code)
|
|
135
|
+
expect(logs[0]).toEqual([42])
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('TJS imports', () => {
|
|
140
|
+
test('imports pass through unchanged (current behavior)', () => {
|
|
141
|
+
const source = `
|
|
142
|
+
import { AgentVM, ajs } from 'tjs-lang'
|
|
143
|
+
|
|
144
|
+
async function run() {
|
|
145
|
+
const vm = new AgentVM()
|
|
146
|
+
return vm
|
|
147
|
+
}
|
|
148
|
+
`
|
|
149
|
+
const result = tjs(source, { runTests: false })
|
|
150
|
+
|
|
151
|
+
// Currently imports pass through - this means code won't work
|
|
152
|
+
// standalone in browser without import map resolution
|
|
153
|
+
expect(result.code).toContain("import { AgentVM, ajs } from 'tjs-lang'")
|
|
154
|
+
})
|
|
155
|
+
})
|
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
|
package/src/lang/types.ts
CHANGED
|
@@ -42,7 +42,7 @@ describe('Basic Types', () => {
|
|
|
42
42
|
return x * 2
|
|
43
43
|
}
|
|
44
44
|
`)
|
|
45
|
-
expect(getFirstFunc(metadata).params.x.type.kind).toBe('
|
|
45
|
+
expect(getFirstFunc(metadata).params.x.type.kind).toBe('integer')
|
|
46
46
|
})
|
|
47
47
|
|
|
48
48
|
test('boolean parameter', () => {
|
|
@@ -76,7 +76,7 @@ describe('Basic Types', () => {
|
|
|
76
76
|
'string'
|
|
77
77
|
)
|
|
78
78
|
expect(getFirstFunc(metadata).params.user.type.shape?.age.kind).toBe(
|
|
79
|
-
'
|
|
79
|
+
'integer'
|
|
80
80
|
)
|
|
81
81
|
})
|
|
82
82
|
|
|
@@ -101,7 +101,9 @@ describe('Basic Types', () => {
|
|
|
101
101
|
}
|
|
102
102
|
`)
|
|
103
103
|
expect(getFirstFunc(metadata).params.nums.type.kind).toBe('array')
|
|
104
|
-
expect(getFirstFunc(metadata).params.nums.type.items?.kind).toBe(
|
|
104
|
+
expect(getFirstFunc(metadata).params.nums.type.items?.kind).toBe(
|
|
105
|
+
'integer'
|
|
106
|
+
)
|
|
105
107
|
})
|
|
106
108
|
|
|
107
109
|
test('array of objects', () => {
|
|
@@ -729,7 +731,7 @@ describe('Literal Types', () => {
|
|
|
729
731
|
return n
|
|
730
732
|
}
|
|
731
733
|
`)
|
|
732
|
-
expect(getFirstFunc(metadata).params.n.type.kind).toBe('
|
|
734
|
+
expect(getFirstFunc(metadata).params.n.type.kind).toBe('integer')
|
|
733
735
|
})
|
|
734
736
|
|
|
735
737
|
test('literal union type alias emits TJS Union', () => {
|
package/src/lang/wasm.test.ts
CHANGED
|
@@ -45,6 +45,26 @@ describe('WASM Compiler', () => {
|
|
|
45
45
|
expect(result.success).toBe(true)
|
|
46
46
|
})
|
|
47
47
|
|
|
48
|
+
it('should include WAT disassembly in result', () => {
|
|
49
|
+
const block: WasmBlock = {
|
|
50
|
+
id: '__tjs_wasm_test_wat',
|
|
51
|
+
body: 'return a + b',
|
|
52
|
+
captures: ['a', 'b'],
|
|
53
|
+
start: 0,
|
|
54
|
+
end: 0,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const result = compileToWasm(block)
|
|
58
|
+
expect(result.success).toBe(true)
|
|
59
|
+
expect(result.wat).toBeDefined()
|
|
60
|
+
expect(result.wat).toContain('(func (export "compute")')
|
|
61
|
+
expect(result.wat).toContain('(param $a f64)')
|
|
62
|
+
expect(result.wat).toContain('(param $b f64)')
|
|
63
|
+
expect(result.wat).toContain('f64.add')
|
|
64
|
+
expect(result.wat).toContain('local.get $a')
|
|
65
|
+
expect(result.wat).toContain('local.get $b')
|
|
66
|
+
})
|
|
67
|
+
|
|
48
68
|
it('should compile multiplication', () => {
|
|
49
69
|
const block: WasmBlock = {
|
|
50
70
|
id: '__tjs_wasm_test_3',
|