tjs-lang 0.7.7 → 0.8.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.
Files changed (70) hide show
  1. package/CLAUDE.md +99 -33
  2. package/bin/docs.js +4 -1
  3. package/demo/docs.json +104 -22
  4. package/demo/src/examples.test.ts +1 -0
  5. package/demo/src/imports.test.ts +16 -4
  6. package/demo/src/imports.ts +60 -15
  7. package/demo/src/playground-shared.ts +9 -8
  8. package/demo/src/tfs-worker.js +205 -147
  9. package/demo/src/tjs-playground.ts +34 -10
  10. package/demo/src/ts-examples.ts +8 -8
  11. package/demo/src/ts-playground.ts +24 -8
  12. package/dist/index.js +118 -101
  13. package/dist/index.js.map +4 -4
  14. package/dist/src/lang/bool-coercion.d.ts +50 -0
  15. package/dist/src/lang/docs.d.ts +31 -6
  16. package/dist/src/lang/linter.d.ts +8 -0
  17. package/dist/src/lang/parser-transforms.d.ts +18 -0
  18. package/dist/src/lang/parser-types.d.ts +2 -0
  19. package/dist/src/lang/parser.d.ts +3 -0
  20. package/dist/src/lang/runtime.d.ts +34 -0
  21. package/dist/src/lang/types.d.ts +9 -1
  22. package/dist/src/rbac/index.d.ts +1 -1
  23. package/dist/src/vm/runtime.d.ts +1 -1
  24. package/dist/tjs-eval.js +38 -36
  25. package/dist/tjs-eval.js.map +4 -4
  26. package/dist/tjs-from-ts.js +20 -20
  27. package/dist/tjs-from-ts.js.map +3 -3
  28. package/dist/tjs-lang.js +85 -83
  29. package/dist/tjs-lang.js.map +4 -4
  30. package/dist/tjs-vm.js +47 -45
  31. package/dist/tjs-vm.js.map +4 -4
  32. package/llms.txt +79 -0
  33. package/package.json +9 -4
  34. package/src/cli/commands/convert.test.ts +16 -21
  35. package/src/lang/bool-coercion.test.ts +203 -0
  36. package/src/lang/bool-coercion.ts +314 -0
  37. package/src/lang/codegen.test.ts +137 -0
  38. package/src/lang/docs.test.ts +476 -1
  39. package/src/lang/docs.ts +471 -37
  40. package/src/lang/emitters/ast.ts +11 -12
  41. package/src/lang/emitters/dts.test.ts +41 -0
  42. package/src/lang/emitters/dts.ts +9 -0
  43. package/src/lang/emitters/js-tests.ts +9 -4
  44. package/src/lang/emitters/js-wasm.ts +57 -65
  45. package/src/lang/emitters/js.ts +198 -3
  46. package/src/lang/features.test.ts +4 -3
  47. package/src/lang/index.ts +9 -0
  48. package/src/lang/inference.ts +54 -0
  49. package/src/lang/linter.test.ts +104 -1
  50. package/src/lang/linter.ts +124 -1
  51. package/src/lang/module-loader.test.ts +318 -0
  52. package/src/lang/module-loader.ts +419 -0
  53. package/src/lang/parser-params.ts +31 -0
  54. package/src/lang/parser-transforms.ts +640 -0
  55. package/src/lang/parser-types.ts +35 -0
  56. package/src/lang/parser.test.ts +73 -1
  57. package/src/lang/parser.ts +77 -3
  58. package/src/lang/runtime.ts +98 -0
  59. package/src/lang/types.ts +6 -0
  60. package/src/lang/wasm.test.ts +1293 -2
  61. package/src/lang/wasm.ts +470 -87
  62. package/src/linalg/index.tjs +119 -0
  63. package/src/linalg/linalg.test.ts +294 -0
  64. package/src/linalg/vector-search.bench.test.ts +395 -0
  65. package/src/rbac/index.ts +2 -2
  66. package/src/rbac/rules.tjs.d.ts +9 -0
  67. package/src/vm/atoms/batteries.ts +2 -2
  68. package/src/vm/runtime.ts +10 -3
  69. package/dist/src/rbac/rules.d.ts +0 -184
  70. package/src/rbac/rules.js +0 -338
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { describe, it, expect } from 'bun:test'
9
- import { generateDocs, generateDocsMarkdown } from './docs'
9
+ import { generateDocs, generateDocsMarkdown, prettifyTestBody } from './docs'
10
10
 
11
11
  describe('generateDocs', () => {
12
12
  describe('basic output', () => {
@@ -157,6 +157,154 @@ function second(x: 0): 0 { return x }
157
157
  })
158
158
  })
159
159
 
160
+ describe('JSDoc-style doc blocks', () => {
161
+ it('extracts /** */ blocks and strips leading asterisks', () => {
162
+ const source = `
163
+ /**
164
+ * # Title
165
+ *
166
+ * Body line 1
167
+ * Body line 2
168
+ */
169
+ `
170
+ const result = generateDocs(source)
171
+
172
+ expect(result.items).toHaveLength(1)
173
+ const doc = result.items[0] as any
174
+ expect(doc.type).toBe('doc')
175
+ expect(doc.content).toBe('# Title\n\nBody line 1\nBody line 2')
176
+ })
177
+
178
+ it('handles single-line JSDoc', () => {
179
+ const source = `/** A short note. */`
180
+ const result = generateDocs(source)
181
+
182
+ const doc = result.items[0] as any
183
+ expect(doc.type).toBe('doc')
184
+ expect(doc.content).toBe('A short note.')
185
+ })
186
+
187
+ it('preserves markdown lists and tables', () => {
188
+ const source = `
189
+ /**
190
+ * ## Options
191
+ *
192
+ * | Flag | Meaning |
193
+ * |------|---------|
194
+ * | \`-v\` | verbose |
195
+ *
196
+ * - first
197
+ * - second
198
+ */
199
+ `
200
+ const result = generateDocs(source)
201
+
202
+ const doc = result.items[0] as any
203
+ expect(doc.content).toContain('## Options')
204
+ expect(doc.content).toContain('| Flag | Meaning |')
205
+ expect(doc.content).toContain('- first')
206
+ })
207
+
208
+ it('leaves @param / @returns as plain markdown', () => {
209
+ const source = `
210
+ /**
211
+ * Square the input.
212
+ *
213
+ * @param x - the input
214
+ * @returns the squared value
215
+ */
216
+ function square(x: 0): 0 { return x * x }
217
+ `
218
+ const result = generateDocs(source)
219
+
220
+ const doc = result.items[0] as any
221
+ expect(doc.content).toContain('@param x - the input')
222
+ expect(doc.content).toContain('@returns the squared value')
223
+ })
224
+
225
+ it('skips JSDoc inside function bodies', () => {
226
+ const source = `
227
+ function outer() {
228
+ /**
229
+ * Should not be extracted — inside a body.
230
+ */
231
+ return 1
232
+ }
233
+ `
234
+ const result = generateDocs(source)
235
+
236
+ // Only the function itself, no doc item
237
+ const docs = result.items.filter((i) => i.type === 'doc')
238
+ expect(docs).toHaveLength(0)
239
+ })
240
+
241
+ it('skips empty JSDoc blocks', () => {
242
+ const source = `
243
+ /**
244
+ *
245
+ */
246
+ function f() {}
247
+ `
248
+ const result = generateDocs(source)
249
+
250
+ const docs = result.items.filter((i) => i.type === 'doc')
251
+ expect(docs).toHaveLength(0)
252
+ })
253
+
254
+ it('does not treat /* ... */ as a doc comment', () => {
255
+ const source = `
256
+ /* just a regular block comment */
257
+ function f() {}
258
+ `
259
+ const result = generateDocs(source)
260
+
261
+ const docs = result.items.filter((i) => i.type === 'doc')
262
+ expect(docs).toHaveLength(0)
263
+ })
264
+
265
+ it('interleaves JSDoc with functions in document order', () => {
266
+ const source = `
267
+ /**
268
+ * # First
269
+ */
270
+ function first(x: 0): 0 { return x }
271
+
272
+ /**
273
+ * # Second
274
+ */
275
+ function second(x: 0): 0 { return x }
276
+ `
277
+ const result = generateDocs(source)
278
+
279
+ expect(result.items).toHaveLength(4)
280
+ expect(result.items[0].type).toBe('doc')
281
+ expect((result.items[0] as any).content).toContain('# First')
282
+ expect(result.items[1].type).toBe('function')
283
+ expect(result.items[2].type).toBe('doc')
284
+ expect((result.items[2] as any).content).toContain('# Second')
285
+ expect(result.items[3].type).toBe('function')
286
+ })
287
+
288
+ it('coexists with /*# blocks in the same file', () => {
289
+ const source = `
290
+ /*#
291
+ ## TJS-native
292
+ */
293
+
294
+ /**
295
+ * ## JSDoc-native
296
+ */
297
+ function f(x: 0): 0 { return x }
298
+ `
299
+ const result = generateDocs(source)
300
+
301
+ const docs = result.items.filter((i) => i.type === 'doc') as any[]
302
+ expect(docs).toHaveLength(2)
303
+ expect(docs[0].content).toContain('TJS-native')
304
+ expect(docs[1].content).toContain('JSDoc-native')
305
+ })
306
+ })
307
+
160
308
  describe('markdown output', () => {
161
309
  it('renders doc blocks as plain markdown', () => {
162
310
  const source = `
@@ -452,3 +600,330 @@ function second(y: ''): '' {
452
600
  expect(secondPos).toBeLessThan(afterPos)
453
601
  })
454
602
  })
603
+
604
+ describe('prettifyTestBody', () => {
605
+ it('translates toBe', () => {
606
+ expect(prettifyTestBody('expect(x).toBe(y)')).toBe('x // → y')
607
+ })
608
+
609
+ it('translates toEqual to ≡', () => {
610
+ expect(prettifyTestBody('expect(a).toEqual(b)')).toBe('a // ≡ b')
611
+ })
612
+
613
+ it('handles balanced parens in expression', () => {
614
+ expect(
615
+ prettifyTestBody('expect(Boolean(new Boolean(false))).toBe(false)')
616
+ ).toBe('Boolean(new Boolean(false)) // → false')
617
+ })
618
+
619
+ it('handles balanced parens in expected value', () => {
620
+ expect(prettifyTestBody('expect(x).toBe(f(1, 2))')).toBe('x // → f(1, 2)')
621
+ })
622
+
623
+ it('handles toBeTruthy / toBeFalsy / toBeNull / toBeUndefined', () => {
624
+ expect(prettifyTestBody('expect(x).toBeTruthy()')).toBe('x // → truthy')
625
+ expect(prettifyTestBody('expect(x).toBeFalsy()')).toBe('x // → falsy')
626
+ expect(prettifyTestBody('expect(x).toBeNull()')).toBe('x // → null')
627
+ expect(prettifyTestBody('expect(x).toBeUndefined()')).toBe(
628
+ 'x // → undefined'
629
+ )
630
+ })
631
+
632
+ it('translates toContain / toThrow / toBeNaN', () => {
633
+ expect(prettifyTestBody('expect(arr).toContain(3)')).toBe(
634
+ 'arr // → contains 3'
635
+ )
636
+ expect(prettifyTestBody('expect(fn).toThrow()')).toBe('fn // → throws')
637
+ expect(prettifyTestBody('expect(x).toBeNaN()')).toBe('x // → NaN')
638
+ })
639
+
640
+ it('translates toBeGreaterThan / toBeLessThan', () => {
641
+ expect(prettifyTestBody('expect(x).toBeGreaterThan(5)')).toBe('x // → > 5')
642
+ expect(prettifyTestBody('expect(y).toBeLessThan(10)')).toBe('y // → < 10')
643
+ })
644
+
645
+ it('preserves non-expect lines', () => {
646
+ expect(prettifyTestBody('console.log("hi")')).toBe('console.log("hi")')
647
+ expect(prettifyTestBody('const x = 5')).toBe('const x = 5')
648
+ })
649
+
650
+ it('handles multiple expects on separate lines', () => {
651
+ const input = ' expect(x).toBe(1)\n expect(y).toBeTruthy()'
652
+ const expected = ' x // → 1\n y // → truthy'
653
+ expect(prettifyTestBody(input)).toBe(expected)
654
+ })
655
+
656
+ it('does not touch parens inside string literals', () => {
657
+ // The `expect(...)` inside the string literal should NOT be transformed
658
+ expect(prettifyTestBody(`const s = "expect(fake).toBe(impossible)"`)).toBe(
659
+ `const s = "expect(fake).toBe(impossible)"`
660
+ )
661
+ })
662
+
663
+ it('falls back gracefully on unknown matchers', () => {
664
+ expect(prettifyTestBody('expect(x).somethingWeird(y)')).toBe(
665
+ 'x // .somethingWeird(y)'
666
+ )
667
+ })
668
+ })
669
+
670
+ describe('function extraction', () => {
671
+ it('captures simple function signatures', () => {
672
+ const md = generateDocsMarkdown(
673
+ `function add(a: 0, b: 0): 0 { return a + b }`
674
+ )
675
+ expect(md).toContain('## add')
676
+ expect(md).toContain('function add(a: 0, b: 0): 0')
677
+ })
678
+
679
+ it('handles arrow-function defaults (params with nested parens)', () => {
680
+ // Regression: the old funcPattern used [^)]* and broke on `(x) => x`
681
+ const source = `
682
+ function mapStrings(arr: [''], fn = (x) => x): [''] {
683
+ return arr.map(fn)
684
+ }
685
+ function compose(f = (x) => x, g = (x) => x): 0 {
686
+ return f(g(5))
687
+ }
688
+ `
689
+ const md = generateDocsMarkdown(source)
690
+ expect(md).toContain('## mapStrings')
691
+ expect(md).toContain("function mapStrings(arr: [''], fn = (x) => x): ['']")
692
+ expect(md).toContain('## compose')
693
+ expect(md).toContain('function compose(f = (x) => x, g = (x) => x): 0')
694
+ })
695
+
696
+ it('handles object/array example values in params', () => {
697
+ const md = generateDocsMarkdown(
698
+ `function f(p: { a: 0, b: '' }, q: [0]): {} { return p }`
699
+ )
700
+ expect(md).toContain("function f(p: { a: 0, b: '' }, q: [0]): {}")
701
+ })
702
+
703
+ it('handles return-type annotation with quoted string', () => {
704
+ const md = generateDocsMarkdown(
705
+ `function greet(name: 'World'): 'Hello, World!' { return \`Hello, \${name}!\` }`
706
+ )
707
+ expect(md).toContain("function greet(name: 'World'): 'Hello, World!'")
708
+ })
709
+
710
+ it('does not extract nested function declarations', () => {
711
+ const source = `
712
+ function outer() {
713
+ function nested() { return 1 }
714
+ return nested
715
+ }
716
+ `
717
+ const result = generateDocs(source)
718
+ const fns = result.items.filter((i) => i.type === 'function')
719
+ expect(fns.length).toBe(1)
720
+ expect(fns[0].name).toBe('outer')
721
+ })
722
+
723
+ describe('function param rendering in docs', () => {
724
+ // We need transpiled type metadata for the param-table renderer.
725
+ // Use the lang index's `tjs()` to get types.
726
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
727
+ const { tjs } = require('./index')
728
+
729
+ it('renders an arrow-default param as `(x: any) => any` (not `function`)', () => {
730
+ const r = tjs(`function f(fn = (x) => x): 0 { return 0 }`)
731
+ const md = generateDocsMarkdown(r.code, r.types)
732
+ expect(md).toContain('`fn`: (x: any) => any')
733
+ })
734
+
735
+ it('infers return type from concise arrow body', () => {
736
+ const r = tjs(`function f(make = () => 5): 0 { return 0 }`)
737
+ const md = generateDocsMarkdown(r.code, r.types)
738
+ expect(md).toContain('`make`: () => number')
739
+ })
740
+
741
+ it('infers param types from arrow defaults', () => {
742
+ const r = tjs(
743
+ `function f(reduce = (acc = 0, x = 0) => 0): 0 { return 0 }`
744
+ )
745
+ const md = generateDocsMarkdown(r.code, r.types)
746
+ expect(md).toContain('`reduce`: (acc: number, x: number) => number')
747
+ })
748
+
749
+ it('does not show `e.g. undefined` for function example values', () => {
750
+ const r = tjs(`function f(fn = (x) => x): 0 { return 0 }`)
751
+ const md = generateDocsMarkdown(r.code, r.types)
752
+ expect(md).not.toContain('undefined')
753
+ })
754
+ })
755
+ })
756
+
757
+ describe('class extraction', () => {
758
+ it('extracts a class with constructor and methods', () => {
759
+ const source = `
760
+ class Point {
761
+ constructor(x: 0, y: 0) {
762
+ this.x = x
763
+ this.y = y
764
+ }
765
+
766
+ magnitude() {
767
+ return Math.sqrt(this.x * this.x + this.y * this.y)
768
+ }
769
+
770
+ toString() {
771
+ return \`(\${this.x}, \${this.y})\`
772
+ }
773
+ }
774
+ `
775
+ const md = generateDocsMarkdown(source)
776
+ expect(md).toContain('## Point')
777
+ expect(md).toContain('class Point {')
778
+ expect(md).toContain('constructor(x: 0, y: 0)')
779
+ expect(md).toContain('magnitude()')
780
+ expect(md).toContain('toString()')
781
+ // Method bodies should NOT appear
782
+ expect(md).not.toContain('this.x = x')
783
+ expect(md).not.toContain('Math.sqrt')
784
+ })
785
+
786
+ it('extracts multiple constructors', () => {
787
+ const source = `
788
+ class Color {
789
+ constructor(r: +0, g: +0, b: +0) { this.r = r; this.g = g; this.b = b }
790
+ constructor(hex: '#000000') { /* parse */ }
791
+ toString() { return 'rgb(...)' }
792
+ }
793
+ `
794
+ const md = generateDocsMarkdown(source)
795
+ expect(md).toContain('constructor(r: +0, g: +0, b: +0)')
796
+ expect(md).toContain("constructor(hex: '#000000')")
797
+ expect(md).toContain('toString()')
798
+ })
799
+
800
+ it('renders extends clause', () => {
801
+ const source = `
802
+ class ColorWithAlpha extends Color {
803
+ constructor(r: 0, g: 0, b: 0, a: 1.0) { super(r, g, b); this.a = a }
804
+ }
805
+ `
806
+ const md = generateDocsMarkdown(source)
807
+ expect(md).toContain('class ColorWithAlpha extends Color {')
808
+ })
809
+
810
+ it('handles static / async / get / set modifiers', () => {
811
+ const source = `
812
+ class Thing {
813
+ static load(path: '') { return path }
814
+ async fetch() { return null }
815
+ get name() { return 'x' }
816
+ set name(v: '') { this._name = v }
817
+ }
818
+ `
819
+ const md = generateDocsMarkdown(source)
820
+ expect(md).toContain("static load(path: '')")
821
+ expect(md).toContain('async fetch()')
822
+ expect(md).toContain('get name()')
823
+ expect(md).toContain("set name(v: '')")
824
+ })
825
+
826
+ it('captures return type annotations', () => {
827
+ const source = `
828
+ class Math2 {
829
+ add(a: 0, b: 0): 0 { return a + b }
830
+ }
831
+ `
832
+ const md = generateDocsMarkdown(source)
833
+ expect(md).toContain('add(a: 0, b: 0): 0')
834
+ })
835
+
836
+ it('does NOT extract `class Foo` text inside /*# */ doc blocks', () => {
837
+ // The doc-block prose is rendered as-is, but we should NOT emit a
838
+ // "## FakeClass" heading from the prose's `class FakeClass` mention.
839
+ const source = `
840
+ /*#
841
+ ## Example
842
+ Don't write this:
843
+
844
+ class FakeClass { constructor(x) {} }
845
+ */
846
+
847
+ class RealClass {
848
+ constructor() {}
849
+ }
850
+ `
851
+ const result = generateDocs(source)
852
+ const classItems = result.items.filter((i) => i.type === 'class')
853
+ expect(classItems.length).toBe(1)
854
+ expect(classItems[0].name).toBe('RealClass')
855
+ })
856
+
857
+ it('does NOT extract `function` text inside /*# */ doc blocks', () => {
858
+ const source = `
859
+ /*#
860
+ Don't write this:
861
+
862
+ function fakeFn() {}
863
+ */
864
+
865
+ function realFn() { return 1 }
866
+ `
867
+ const result = generateDocs(source)
868
+ const fnItems = result.items.filter((i) => i.type === 'function')
869
+ expect(fnItems.length).toBe(1)
870
+ expect(fnItems[0].name).toBe('realFn')
871
+ })
872
+
873
+ it('handles a class with no members', () => {
874
+ const md = generateDocsMarkdown('class Empty {}')
875
+ expect(md).toContain('## Empty')
876
+ expect(md).toContain('class Empty {')
877
+ expect(md).toContain('}')
878
+ })
879
+ })
880
+
881
+ describe('generateDocsMarkdown — test cases section', () => {
882
+ it('renders each named test as a "### <name> (test cases)" heading with prettified body', () => {
883
+ const source = `
884
+ test 'x is 5' {
885
+ expect(x).toBe(5)
886
+ }
887
+ test 'y is truthy' {
888
+ expect(y).toBeTruthy()
889
+ }
890
+ `
891
+ const md = generateDocsMarkdown(source)
892
+ expect(md).toContain('### x is 5 (test cases)')
893
+ expect(md).toContain('x // → 5')
894
+ expect(md).toContain('### y is truthy (test cases)')
895
+ expect(md).toContain('y // → truthy')
896
+ })
897
+
898
+ it('skips anonymous tests (no description)', () => {
899
+ const source = `
900
+ test {
901
+ expect(x).toBe(5)
902
+ }
903
+ `
904
+ const md = generateDocsMarkdown(source)
905
+ expect(md).not.toContain('(test cases)')
906
+ })
907
+
908
+ it('integrates with doc blocks and functions', () => {
909
+ const source = `
910
+ /*#
911
+ # Module
912
+ Some intro.
913
+ */
914
+
915
+ function add(a: 0, b: 0): 0 {
916
+ return a + b
917
+ }
918
+
919
+ test 'add(2, 3) is 5' {
920
+ expect(add(2, 3)).toBe(5)
921
+ }
922
+ `
923
+ const md = generateDocsMarkdown(source)
924
+ expect(md).toContain('# Module')
925
+ expect(md).toContain('## add')
926
+ expect(md).toContain('### add(2, 3) is 5 (test cases)')
927
+ expect(md).toContain('add(2, 3) // → 5')
928
+ })
929
+ })