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.
@@ -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 || (TJS style)', () => {
128
+ test('union with | (string or integer)', () => {
129
129
  const { metadata } = transpileToJS(`
130
- function flexible(id: '' || 0) {
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 || null', () => {
138
+ test('nullable with | null', () => {
139
139
  const { metadata } = transpileToJS(`
140
- function maybeString(s: '' || null) {
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 || (TJS style)', () => {
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
  // =============================================================================
@@ -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
- { op: 'return', value: { $expr: 'ident', name: 'res' } },
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
- { op: 'return', value: { $expr: 'ident', name: 'res' } },
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
- { op: 'return', value: { $expr: 'ident', name: 'res' } },
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
- { op: 'return', value: { $expr: 'ident', name: 'res' } },
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
- { op: 'return', value: { $expr: 'ident', name: 'res' } },
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
- { op: 'return', value: { $expr: 'ident', name: 'res' } },
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
  })
@@ -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
  }