tjs-lang 0.5.2 → 0.5.4
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 +1 -1
- package/demo/docs.json +9 -9
- package/demo/src/demo-nav.ts +51 -136
- package/demo/src/index.ts +35 -68
- package/demo/src/ts-playground.ts +18 -8
- package/dist/index.js +126 -112
- package/dist/index.js.map +11 -11
- package/dist/src/lang/emitters/js.d.ts +2 -2
- package/dist/src/lang/inference.d.ts +2 -2
- package/dist/src/test-examples.d.ts +41 -0
- package/dist/tjs-full.js +126 -112
- package/dist/tjs-full.js.map +11 -11
- package/dist/tjs-transpiler.js +102 -88
- package/dist/tjs-transpiler.js.map +8 -8
- package/dist/tjs-vm.js +18 -18
- package/dist/tjs-vm.js.map +5 -5
- package/package.json +1 -1
- package/src/lang/codegen.test.ts +76 -16
- package/src/lang/emitters/from-ts.ts +8 -28
- package/src/lang/emitters/js.ts +97 -7
- package/src/lang/eval.ts +41 -2
- package/src/lang/from-ts.test.ts +3 -3
- package/src/lang/inference.ts +34 -20
- package/src/lang/parser.test.ts +4 -4
- package/src/lang/transpiler.test.ts +5 -3
- package/src/lang/typescript-syntax.test.ts +20 -17
- package/src/runtime.test.ts +144 -13
- package/src/test-examples.test.ts +4 -12
- package/src/test-examples.ts +3 -1
- package/src/vm/runtime.ts +19 -0
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { describe, test, expect } from 'bun:test'
|
|
16
|
-
import { transpileToJS, fromTS } from './index'
|
|
16
|
+
import { transpileToJS, fromTS, tjs } from './index'
|
|
17
17
|
|
|
18
18
|
// Helper to get the first function's metadata from the Record
|
|
19
19
|
function getFirstFunc(metadata: Record<string, any>) {
|
|
@@ -125,9 +125,9 @@ describe('Basic Types', () => {
|
|
|
125
125
|
// =============================================================================
|
|
126
126
|
|
|
127
127
|
describe('Union Types', () => {
|
|
128
|
-
test('union with
|
|
128
|
+
test('union with | (string or integer)', () => {
|
|
129
129
|
const { metadata } = transpileToJS(`
|
|
130
|
-
function flexible(id: ''
|
|
130
|
+
function flexible(id: '' | 0) {
|
|
131
131
|
return String(id)
|
|
132
132
|
}
|
|
133
133
|
`)
|
|
@@ -135,9 +135,9 @@ describe('Union Types', () => {
|
|
|
135
135
|
expect(getFirstFunc(metadata).params.id.type.members?.length).toBe(2)
|
|
136
136
|
})
|
|
137
137
|
|
|
138
|
-
test('nullable with
|
|
138
|
+
test('nullable with | null', () => {
|
|
139
139
|
const { metadata } = transpileToJS(`
|
|
140
|
-
function maybeString(s: ''
|
|
140
|
+
function maybeString(s: '' | null) {
|
|
141
141
|
return s ?? 'default'
|
|
142
142
|
}
|
|
143
143
|
`)
|
|
@@ -153,18 +153,7 @@ describe('Union Types', () => {
|
|
|
153
153
|
expect(types?.flexible.params.id.type.kind).toBe('union')
|
|
154
154
|
})
|
|
155
155
|
|
|
156
|
-
test('union return type with
|
|
157
|
-
const { metadata } = transpileToJS(`
|
|
158
|
-
function find(id: 0) -! { name: '' } || null {
|
|
159
|
-
return null
|
|
160
|
-
}
|
|
161
|
-
`)
|
|
162
|
-
expect(getFirstFunc(metadata).returns?.kind).toBe('object')
|
|
163
|
-
expect(getFirstFunc(metadata).returns?.nullable).toBe(true)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
// TODO: Union return types like `{ obj } | null` not yet supported in parser
|
|
167
|
-
test.skip('union return type with | (TS style)', () => {
|
|
156
|
+
test('union return type with | (nullable object)', () => {
|
|
168
157
|
const { metadata } = transpileToJS(`
|
|
169
158
|
function find(id: 0) -! { name: '' } | null {
|
|
170
159
|
return null
|
|
@@ -858,6 +847,20 @@ describe('Real-World Patterns', () => {
|
|
|
858
847
|
// Generic T becomes any, but structure is preserved
|
|
859
848
|
expect(types?.chunk.params.array.type.kind).toBe('array')
|
|
860
849
|
})
|
|
850
|
+
|
|
851
|
+
test('?: boolean transpiles to required union param with no JS default', () => {
|
|
852
|
+
const tjsCode = fromTS('function f(excited?: boolean) { return excited }', {
|
|
853
|
+
emitTJS: true,
|
|
854
|
+
}).code
|
|
855
|
+
// TJS should have union annotation
|
|
856
|
+
expect(tjsCode).toContain('excited: false | undefined')
|
|
857
|
+
const jsResult = tjs(tjsCode)
|
|
858
|
+
// JS should not have default or bitwise OR — `:` means required
|
|
859
|
+
expect(jsResult.code).not.toMatch(/excited = false/)
|
|
860
|
+
expect(jsResult.code).not.toMatch(/excited = false \| undefined/)
|
|
861
|
+
// Should have union type check
|
|
862
|
+
expect(jsResult.code).toContain("typeof excited !== 'boolean'")
|
|
863
|
+
})
|
|
861
864
|
})
|
|
862
865
|
|
|
863
866
|
// =============================================================================
|
package/src/runtime.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'bun:test'
|
|
2
|
-
import { defineAtom } from './runtime'
|
|
2
|
+
import { defineAtom, isAgentError } from './runtime'
|
|
3
3
|
import { AgentVM } from './vm'
|
|
4
4
|
import { s } from 'tosijs-schema'
|
|
5
5
|
import { ajs } from './transpiler'
|
|
@@ -611,11 +611,17 @@ describe('Edge Cases', () => {
|
|
|
611
611
|
right: { $expr: 'literal', value: [1, 2, 3] },
|
|
612
612
|
},
|
|
613
613
|
},
|
|
614
|
-
{
|
|
614
|
+
{
|
|
615
|
+
op: 'return',
|
|
616
|
+
schema: {
|
|
617
|
+
type: 'object',
|
|
618
|
+
properties: { res: { type: 'boolean' } },
|
|
619
|
+
},
|
|
620
|
+
},
|
|
615
621
|
],
|
|
616
622
|
} as any
|
|
617
623
|
const result = await vm.run(ast, {})
|
|
618
|
-
expect(result.result).toBe(true)
|
|
624
|
+
expect(result.result.res).toBe(true)
|
|
619
625
|
})
|
|
620
626
|
|
|
621
627
|
it('== compares objects structurally', async () => {
|
|
@@ -632,11 +638,17 @@ describe('Edge Cases', () => {
|
|
|
632
638
|
right: { $expr: 'literal', value: { a: 1, b: 2 } },
|
|
633
639
|
},
|
|
634
640
|
},
|
|
635
|
-
{
|
|
641
|
+
{
|
|
642
|
+
op: 'return',
|
|
643
|
+
schema: {
|
|
644
|
+
type: 'object',
|
|
645
|
+
properties: { res: { type: 'boolean' } },
|
|
646
|
+
},
|
|
647
|
+
},
|
|
636
648
|
],
|
|
637
649
|
} as any
|
|
638
650
|
const result = await vm.run(ast, {})
|
|
639
|
-
expect(result.result).toBe(true)
|
|
651
|
+
expect(result.result.res).toBe(true)
|
|
640
652
|
})
|
|
641
653
|
|
|
642
654
|
it('== does not coerce types', async () => {
|
|
@@ -653,11 +665,17 @@ describe('Edge Cases', () => {
|
|
|
653
665
|
right: { $expr: 'literal', value: 1 },
|
|
654
666
|
},
|
|
655
667
|
},
|
|
656
|
-
{
|
|
668
|
+
{
|
|
669
|
+
op: 'return',
|
|
670
|
+
schema: {
|
|
671
|
+
type: 'object',
|
|
672
|
+
properties: { res: { type: 'boolean' } },
|
|
673
|
+
},
|
|
674
|
+
},
|
|
657
675
|
],
|
|
658
676
|
} as any
|
|
659
677
|
const result = await vm.run(ast, {})
|
|
660
|
-
expect(result.result).toBe(false) // no coercion
|
|
678
|
+
expect(result.result.res).toBe(false) // no coercion
|
|
661
679
|
})
|
|
662
680
|
|
|
663
681
|
it('!= returns true for structurally different objects', async () => {
|
|
@@ -674,11 +692,17 @@ describe('Edge Cases', () => {
|
|
|
674
692
|
right: { $expr: 'literal', value: { a: 2 } },
|
|
675
693
|
},
|
|
676
694
|
},
|
|
677
|
-
{
|
|
695
|
+
{
|
|
696
|
+
op: 'return',
|
|
697
|
+
schema: {
|
|
698
|
+
type: 'object',
|
|
699
|
+
properties: { res: { type: 'boolean' } },
|
|
700
|
+
},
|
|
701
|
+
},
|
|
678
702
|
],
|
|
679
703
|
} as any
|
|
680
704
|
const result = await vm.run(ast, {})
|
|
681
|
-
expect(result.result).toBe(true)
|
|
705
|
+
expect(result.result.res).toBe(true)
|
|
682
706
|
})
|
|
683
707
|
|
|
684
708
|
it('null == undefined is true (nullish equality)', async () => {
|
|
@@ -695,11 +719,17 @@ describe('Edge Cases', () => {
|
|
|
695
719
|
right: { $expr: 'ident', name: 'missing' },
|
|
696
720
|
},
|
|
697
721
|
},
|
|
698
|
-
{
|
|
722
|
+
{
|
|
723
|
+
op: 'return',
|
|
724
|
+
schema: {
|
|
725
|
+
type: 'object',
|
|
726
|
+
properties: { res: { type: 'boolean' } },
|
|
727
|
+
},
|
|
728
|
+
},
|
|
699
729
|
],
|
|
700
730
|
} as any
|
|
701
731
|
const result = await vm.run(ast, {})
|
|
702
|
-
expect(result.result).toBe(true) // null == undefined
|
|
732
|
+
expect(result.result.res).toBe(true) // null == undefined
|
|
703
733
|
})
|
|
704
734
|
|
|
705
735
|
it('=== still uses identity comparison', async () => {
|
|
@@ -716,11 +746,112 @@ describe('Edge Cases', () => {
|
|
|
716
746
|
right: { $expr: 'literal', value: [1, 2] },
|
|
717
747
|
},
|
|
718
748
|
},
|
|
719
|
-
{
|
|
749
|
+
{
|
|
750
|
+
op: 'return',
|
|
751
|
+
schema: {
|
|
752
|
+
type: 'object',
|
|
753
|
+
properties: { res: { type: 'boolean' } },
|
|
754
|
+
},
|
|
755
|
+
},
|
|
720
756
|
],
|
|
721
757
|
} as any
|
|
722
758
|
const result = await vm.run(ast, {})
|
|
723
|
-
expect(result.result).toBe(false) // different references
|
|
759
|
+
expect(result.result.res).toBe(false) // different references
|
|
760
|
+
})
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
describe('strict object returns', () => {
|
|
764
|
+
it('should allow object returns', async () => {
|
|
765
|
+
const ast = ajs(`function test() { return { value: 42 } }`)
|
|
766
|
+
const result = await vm.run(ast, {})
|
|
767
|
+
expect(result.result).toEqual({ value: 42 })
|
|
768
|
+
expect(result.error).toBeUndefined()
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
it('should allow empty object returns', async () => {
|
|
772
|
+
const ast = ajs(`function test() { return {} }`)
|
|
773
|
+
const result = await vm.run(ast, {})
|
|
774
|
+
expect(result.result).toEqual({})
|
|
775
|
+
expect(result.error).toBeUndefined()
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
it('should allow bare return (no value)', async () => {
|
|
779
|
+
const ast = {
|
|
780
|
+
op: 'seq',
|
|
781
|
+
steps: [{ op: 'return', value: undefined }],
|
|
782
|
+
} as any
|
|
783
|
+
const result = await vm.run(ast, {})
|
|
784
|
+
expect(result.error).toBeUndefined()
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
it('should allow null return', async () => {
|
|
788
|
+
const ast = {
|
|
789
|
+
op: 'seq',
|
|
790
|
+
steps: [{ op: 'return', value: null }],
|
|
791
|
+
} as any
|
|
792
|
+
const result = await vm.run(ast, {})
|
|
793
|
+
expect(result.error).toBeUndefined()
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
it('should reject number return', async () => {
|
|
797
|
+
const ast = {
|
|
798
|
+
op: 'seq',
|
|
799
|
+
steps: [{ op: 'return', value: { $expr: 'literal', value: 42 } }],
|
|
800
|
+
} as any
|
|
801
|
+
const result = await vm.run(ast, {})
|
|
802
|
+
expect(isAgentError(result.result)).toBe(true)
|
|
803
|
+
expect(result.error?.message).toContain('must return an object')
|
|
804
|
+
expect(result.error?.message).toContain('number')
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
it('should reject string return', async () => {
|
|
808
|
+
const ast = {
|
|
809
|
+
op: 'seq',
|
|
810
|
+
steps: [{ op: 'return', value: { $expr: 'literal', value: 'hello' } }],
|
|
811
|
+
} as any
|
|
812
|
+
const result = await vm.run(ast, {})
|
|
813
|
+
expect(isAgentError(result.result)).toBe(true)
|
|
814
|
+
expect(result.error?.message).toContain('must return an object')
|
|
815
|
+
expect(result.error?.message).toContain('string')
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
it('should reject array return', async () => {
|
|
819
|
+
const ast = {
|
|
820
|
+
op: 'seq',
|
|
821
|
+
steps: [
|
|
822
|
+
{ op: 'return', value: { $expr: 'literal', value: [1, 2, 3] } },
|
|
823
|
+
],
|
|
824
|
+
} as any
|
|
825
|
+
const result = await vm.run(ast, {})
|
|
826
|
+
expect(isAgentError(result.result)).toBe(true)
|
|
827
|
+
expect(result.error?.message).toContain('must return an object')
|
|
828
|
+
expect(result.error?.message).toContain('array')
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
it('should reject boolean return', async () => {
|
|
832
|
+
const ast = {
|
|
833
|
+
op: 'seq',
|
|
834
|
+
steps: [{ op: 'return', value: { $expr: 'literal', value: true } }],
|
|
835
|
+
} as any
|
|
836
|
+
const result = await vm.run(ast, {})
|
|
837
|
+
expect(isAgentError(result.result)).toBe(true)
|
|
838
|
+
expect(result.error?.message).toContain('must return an object')
|
|
839
|
+
expect(result.error?.message).toContain('boolean')
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
it('should allow AgentError to propagate through return', async () => {
|
|
843
|
+
// Errors are objects but need special handling — must not be blocked
|
|
844
|
+
const ast = ajs(`
|
|
845
|
+
function test({ x }) {
|
|
846
|
+
if (x < 0) {
|
|
847
|
+
return { error: 'negative' }
|
|
848
|
+
}
|
|
849
|
+
return { value: x }
|
|
850
|
+
}
|
|
851
|
+
`)
|
|
852
|
+
const result = await vm.run(ast, { x: -1 })
|
|
853
|
+
expect(result.result).toEqual({ error: 'negative' })
|
|
854
|
+
expect(result.error).toBeUndefined()
|
|
724
855
|
})
|
|
725
856
|
})
|
|
726
857
|
})
|
|
@@ -20,9 +20,7 @@ const ajsExamples = loadExamples(join(ROOT, 'guides/examples/ajs'))
|
|
|
20
20
|
|
|
21
21
|
describe('loadExample helper', () => {
|
|
22
22
|
test('extracts metadata, title, language, and code', () => {
|
|
23
|
-
const ex = loadExample(
|
|
24
|
-
join(ROOT, 'guides/examples/tjs/hello-tjs.md')
|
|
25
|
-
)
|
|
23
|
+
const ex = loadExample(join(ROOT, 'guides/examples/tjs/hello-tjs.md'))
|
|
26
24
|
expect(ex.title).toBeTruthy()
|
|
27
25
|
expect(ex.code).toBeTruthy()
|
|
28
26
|
expect(ex.language).toBe('tjs')
|
|
@@ -31,9 +29,7 @@ describe('loadExample helper', () => {
|
|
|
31
29
|
})
|
|
32
30
|
|
|
33
31
|
test('extracts description', () => {
|
|
34
|
-
const ex = loadExample(
|
|
35
|
-
join(ROOT, 'guides/examples/ajs/hello-world.md')
|
|
36
|
-
)
|
|
32
|
+
const ex = loadExample(join(ROOT, 'guides/examples/ajs/hello-world.md'))
|
|
37
33
|
expect(ex.description).toContain('greeting')
|
|
38
34
|
})
|
|
39
35
|
})
|
|
@@ -71,13 +67,9 @@ describe('TJS examples with inline tests', () => {
|
|
|
71
67
|
|
|
72
68
|
test(`${ex.title} tests pass`, () => {
|
|
73
69
|
const result = tjs(ex.code, { runTests: 'report' })
|
|
74
|
-
const failures = (result.testResults || []).filter(
|
|
75
|
-
(t: any) => !t.passed
|
|
76
|
-
)
|
|
70
|
+
const failures = (result.testResults || []).filter((t: any) => !t.passed)
|
|
77
71
|
if (failures.length > 0) {
|
|
78
|
-
const msgs = failures.map(
|
|
79
|
-
(f: any) => `${f.description}: ${f.error}`
|
|
80
|
-
)
|
|
72
|
+
const msgs = failures.map((f: any) => `${f.description}: ${f.error}`)
|
|
81
73
|
throw new Error(`Test failures:\n${msgs.join('\n')}`)
|
|
82
74
|
}
|
|
83
75
|
})
|
package/src/test-examples.ts
CHANGED
|
@@ -60,7 +60,9 @@ export function parseExample(content: string, filePath = ''): ExampleFile {
|
|
|
60
60
|
if (metaMatch) {
|
|
61
61
|
try {
|
|
62
62
|
metadata = JSON.parse(metaMatch[1])
|
|
63
|
-
} catch {
|
|
63
|
+
} catch {
|
|
64
|
+
/* invalid JSON metadata, ignore */
|
|
65
|
+
}
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
// Extract title from first # heading
|
package/src/vm/runtime.ts
CHANGED
|
@@ -1554,6 +1554,25 @@ export const ret = defineAtom(
|
|
|
1554
1554
|
// New style: return has explicit value
|
|
1555
1555
|
if ('value' in step) {
|
|
1556
1556
|
const res = resolveValue(step.value, ctx)
|
|
1557
|
+
|
|
1558
|
+
// Enforce object returns — agents must return objects for composability
|
|
1559
|
+
if (
|
|
1560
|
+
res !== undefined &&
|
|
1561
|
+
res !== null &&
|
|
1562
|
+
!isAgentError(res) &&
|
|
1563
|
+
(typeof res !== 'object' || Array.isArray(res))
|
|
1564
|
+
) {
|
|
1565
|
+
const err = new AgentError(
|
|
1566
|
+
`Agent must return an object, got ${
|
|
1567
|
+
Array.isArray(res) ? 'array' : typeof res
|
|
1568
|
+
}`,
|
|
1569
|
+
'return'
|
|
1570
|
+
)
|
|
1571
|
+
ctx.error = err
|
|
1572
|
+
ctx.output = err
|
|
1573
|
+
return err
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1557
1576
|
ctx.output = res
|
|
1558
1577
|
return res
|
|
1559
1578
|
}
|