tjs-lang 0.8.0 → 0.8.2

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 (86) hide show
  1. package/CLAUDE.md +15 -5
  2. package/CONTEXT.md +4 -0
  3. package/demo/docs.json +12 -690
  4. package/demo/examples.test.ts +6 -2
  5. package/demo/src/playground-shared.ts +48 -16
  6. package/demo/src/playground-test-results.test.ts +112 -0
  7. package/demo/src/tjs-playground.ts +11 -5
  8. package/dist/eslint.config.d.ts +2 -0
  9. package/dist/index.js +146 -141
  10. package/dist/index.js.map +4 -4
  11. package/dist/src/lang/core.d.ts +1 -1
  12. package/dist/src/lang/dialect.d.ts +35 -0
  13. package/dist/src/lang/emitters/ast.d.ts +1 -1
  14. package/dist/src/lang/emitters/js-tests.d.ts +7 -0
  15. package/dist/src/lang/emitters/js-wasm.d.ts +5 -1
  16. package/dist/src/lang/emitters/js.d.ts +21 -0
  17. package/dist/src/lang/index.d.ts +3 -1
  18. package/dist/src/lang/module-loader.d.ts +125 -0
  19. package/dist/src/lang/parser-transforms.d.ts +79 -0
  20. package/dist/src/lang/parser-types.d.ts +50 -0
  21. package/dist/src/lang/parser.d.ts +18 -0
  22. package/dist/src/lang/transpiler.d.ts +1 -0
  23. package/dist/src/lang/types.d.ts +18 -0
  24. package/dist/src/lang/wasm.d.ts +67 -1
  25. package/dist/src/vm/runtime.d.ts +18 -0
  26. package/dist/src/vm/vm.d.ts +15 -1
  27. package/dist/tjs-batteries.js +2 -2
  28. package/dist/tjs-batteries.js.map +2 -2
  29. package/dist/tjs-eval.js +43 -41
  30. package/dist/tjs-eval.js.map +3 -3
  31. package/dist/tjs-from-ts.js +2 -2
  32. package/dist/tjs-from-ts.js.map +2 -2
  33. package/dist/tjs-lang.js +107 -104
  34. package/dist/tjs-lang.js.map +4 -4
  35. package/dist/tjs-vm.js +53 -51
  36. package/dist/tjs-vm.js.map +3 -3
  37. package/docs/README.md +2 -0
  38. package/docs/lm-studio-setup.md +143 -0
  39. package/docs/universal-endpoint.md +122 -0
  40. package/llms.txt +9 -2
  41. package/package.json +24 -4
  42. package/src/batteries/audit.ts +6 -5
  43. package/src/batteries/llm.ts +8 -3
  44. package/src/builder.ts +0 -3
  45. package/src/cli/commands/check.ts +3 -2
  46. package/src/cli/commands/emit.ts +4 -2
  47. package/src/cli/commands/run.ts +6 -2
  48. package/src/cli/commands/test.ts +1 -1
  49. package/src/cli/commands/types.ts +2 -2
  50. package/src/lang/codegen.test.ts +4 -1
  51. package/src/lang/core.ts +6 -4
  52. package/src/lang/dialect.test.ts +63 -0
  53. package/src/lang/dialect.ts +50 -0
  54. package/src/lang/docs.test.ts +0 -1
  55. package/src/lang/emitters/ast.ts +145 -2
  56. package/src/lang/emitters/from-ts.ts +1 -1
  57. package/src/lang/emitters/js-tests.ts +46 -37
  58. package/src/lang/emitters/js.ts +19 -3
  59. package/src/lang/features.test.ts +6 -5
  60. package/src/lang/index.ts +18 -5
  61. package/src/lang/linter.ts +1 -1
  62. package/src/lang/module-loader.test.ts +9 -5
  63. package/src/lang/module-loader.ts +4 -5
  64. package/src/lang/parser-params.ts +1 -1
  65. package/src/lang/parser-transforms.ts +12 -18
  66. package/src/lang/parser-types.ts +17 -0
  67. package/src/lang/parser.test.ts +12 -6
  68. package/src/lang/parser.ts +113 -3
  69. package/src/lang/perf.test.ts +10 -4
  70. package/src/lang/runtime.ts +0 -1
  71. package/src/lang/subset-invariant.test.ts +90 -0
  72. package/src/lang/transpiler.ts +8 -0
  73. package/src/lang/types.ts +18 -0
  74. package/src/lang/wasm.test.ts +22 -16
  75. package/src/linalg/linalg.test.ts +16 -4
  76. package/src/linalg/vector-search.bench.test.ts +31 -10
  77. package/src/types/Type.ts +6 -6
  78. package/src/use-cases/asymmetric-client-server.test.ts +0 -2
  79. package/src/use-cases/batteries.test.ts +4 -0
  80. package/src/use-cases/client-server.test.ts +1 -1
  81. package/src/use-cases/local-helpers.test.ts +219 -0
  82. package/src/use-cases/timeout-overrides.test.ts +169 -0
  83. package/src/use-cases/unbundled-imports.test.ts +0 -1
  84. package/src/vm/atoms/batteries.ts +17 -3
  85. package/src/vm/runtime.ts +92 -7
  86. package/src/vm/vm.ts +50 -10
@@ -448,9 +448,13 @@ describe('Playground Examples', () => {
448
448
  args.imageUrl = '/test-shapes.jpg'
449
449
  }
450
450
 
451
- // Use the SAME capabilities as the playground
451
+ // Use the SAME capabilities as the playground.
452
+ // Vision inference on a local model can take a while; give it an
453
+ // explicit budget well above the slow-atom timeout so the test is
454
+ // deterministic regardless of the run-level default.
452
455
  const runResult = await vm.run(result.ast, args, {
453
456
  fuel: 100000,
457
+ timeoutMs: 180000,
454
458
  capabilities: {
455
459
  fetch: httpFetch,
456
460
  llm: llmCapability || mockLLM,
@@ -466,7 +470,7 @@ describe('Playground Examples', () => {
466
470
  } finally {
467
471
  trackTestEnd()
468
472
  }
469
- }, 120000)
473
+ }, 240000) // bun-test timeout above the vm timeoutMs (180s) + overhead
470
474
  } else if (needsRetry) {
471
475
  // Examples that use runCode need retry due to LLM variability
472
476
  it(`${example.name} - runs successfully`, async () => {
@@ -255,28 +255,36 @@ export function renderConsoleMessages(
255
255
 
256
256
  /**
257
257
  * Render test results HTML with clickable error links and editor gutter markers.
258
- * Returns pass/fail counts so callers can update tab indicators.
258
+ * Returns pass/fail/inconclusive counts so callers can update tab indicators.
259
259
  */
260
260
  export function renderTestResults(
261
261
  tests: any[],
262
262
  outputEl: HTMLElement,
263
263
  editor: CodeMirror,
264
264
  goToLine: (line: number) => void
265
- ): { passed: number; failed: number } {
265
+ ): { passed: number; failed: number; inconclusive: number } {
266
266
  if (!tests || tests.length === 0) {
267
267
  outputEl.textContent = 'No tests defined'
268
268
  editor.clearMarkers()
269
- return { passed: 0, failed: 0 }
269
+ return { passed: 0, failed: 0, inconclusive: 0 }
270
270
  }
271
271
 
272
272
  const passed = tests.filter((t: any) => t.passed).length
273
- const failed = tests.filter((t: any) => !t.passed).length
274
-
275
- // Mark only FAILED tests in the editor. Passing tests appear in the
276
- // results panel and don't need an underline `severity: 'info'` was
277
- // rendering as a grey squiggle on every passing test's line, which is
278
- // visual noise.
279
- const failedWithLines = tests.filter((t: any) => t.line && !t.passed)
273
+ // Inconclusive = the test couldn't *run* (e.g. it calls an AJS atom that
274
+ // doesn't exist at build time). It is neither pass nor fail — surface it as
275
+ // a distinct warning state and keep it out of the failure count, so legal
276
+ // AJS doesn't read as broken in the playground. See PRINCIPLES.md.
277
+ const inconclusive = tests.filter(
278
+ (t: any) => !t.passed && t.inconclusive
279
+ ).length
280
+ const failed = tests.filter((t: any) => !t.passed && !t.inconclusive).length
281
+
282
+ // Mark only genuine FAILURES in the editor. Passing tests don't need an
283
+ // underline (it reads as visual noise), and an inconclusive test isn't a
284
+ // failure — a red squiggle would wrongly imply the code is broken.
285
+ const failedWithLines = tests.filter(
286
+ (t: any) => t.line && !t.passed && !t.inconclusive
287
+ )
280
288
  if (failedWithLines.length > 0) {
281
289
  editor.setMarkers(
282
290
  failedWithLines.map((t: any) => ({
@@ -294,19 +302,29 @@ export function renderTestResults(
294
302
  if (failed > 0) {
295
303
  html += `, <strong class="test-failed">${failed} failed</strong>`
296
304
  }
305
+ if (inconclusive > 0) {
306
+ html += `, <strong class="test-inconclusive">${inconclusive} inconclusive</strong>`
307
+ }
297
308
  html += `</div><ul class="test-list">`
298
309
 
299
310
  for (const test of tests) {
300
- const icon = test.passed ? '✓' : '✗'
301
- const cls = test.passed ? 'test-pass' : 'test-fail'
311
+ const state = test.passed
312
+ ? 'pass'
313
+ : test.inconclusive
314
+ ? 'inconclusive'
315
+ : 'fail'
316
+ const icon = test.passed ? '✓' : test.inconclusive ? '—' : '✗'
302
317
  const sigBadge = test.isSignatureTest
303
318
  ? ' <span class="sig-badge">signature</span>'
304
319
  : ''
305
320
  const dataLine = test.line ? ` data-line="${test.line}"` : ''
306
321
  const clickable = test.line ? ' clickable-test' : ''
307
- html += `<li class="${cls}${clickable}"${dataLine}>${icon} ${test.description}${sigBadge}`
308
- if (!test.passed && test.error) {
309
- html += `<div class="test-error">${test.error}</div>`
322
+ html += `<li class="test-${state}${clickable}"${dataLine}>${icon} ${test.description}${sigBadge}`
323
+ if (test.error) {
324
+ // Inconclusive notes are warnings (couldn't run), not failures.
325
+ const noteCls = test.inconclusive ? 'test-note' : 'test-error'
326
+ const prefix = test.inconclusive ? 'could not run — ' : ''
327
+ html += `<div class="${noteCls}">${prefix}${test.error}</div>`
310
328
  }
311
329
  html += `</li>`
312
330
  }
@@ -327,7 +345,7 @@ export function renderTestResults(
327
345
  })
328
346
  })
329
347
 
330
- return { passed, failed }
348
+ return { passed, failed, inconclusive }
331
349
  }
332
350
 
333
351
  // ---------------------------------------------------------------------------
@@ -589,6 +607,20 @@ export const sharedPlaygroundStyles: Record<string, Record<string, string>> = {
589
607
  fontFamily: 'var(--font-mono, monospace)',
590
608
  },
591
609
 
610
+ ':host .test-inconclusive': {
611
+ color: '#d97706',
612
+ },
613
+
614
+ ':host .test-note': {
615
+ marginLeft: '20px',
616
+ marginTop: '4px',
617
+ padding: '8px',
618
+ background: 'rgba(217, 119, 6, 0.1)',
619
+ borderRadius: '4px',
620
+ fontSize: '13px',
621
+ fontFamily: 'var(--font-mono, monospace)',
622
+ },
623
+
592
624
  ':host .clickable-test': {
593
625
  cursor: 'pointer',
594
626
  },
@@ -0,0 +1,112 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
2
+ import { GlobalRegistrator } from '@happy-dom/global-registrator'
3
+
4
+ beforeAll(() => GlobalRegistrator.register())
5
+ afterAll(() => GlobalRegistrator.unregister())
6
+
7
+ // Imported after DOM globals exist.
8
+ const { renderTestResults } = await import('./playground-shared')
9
+
10
+ function fakeEditor() {
11
+ const calls: any[] = []
12
+ return {
13
+ setMarkers: (m: any) => calls.push({ setMarkers: m }),
14
+ clearMarkers: () => calls.push({ clearMarkers: true }),
15
+ _calls: calls,
16
+ } as any
17
+ }
18
+
19
+ describe('renderTestResults — inconclusive affordance', () => {
20
+ it('separates inconclusive from passed and failed', () => {
21
+ const out = document.createElement('div')
22
+ const editor = fakeEditor()
23
+ const counts = renderTestResults(
24
+ [
25
+ { description: 'a', passed: true },
26
+ { description: 'b', passed: false, error: 'Expected 5 got 6' },
27
+ {
28
+ description: 'c signature example',
29
+ passed: false,
30
+ inconclusive: true,
31
+ isSignatureTest: true,
32
+ error: 'httpFetch is not defined',
33
+ },
34
+ ],
35
+ out,
36
+ editor,
37
+ () => {}
38
+ )
39
+
40
+ expect(counts).toEqual({ passed: 1, failed: 1, inconclusive: 1 })
41
+
42
+ // Summary shows all three states
43
+ expect(out.querySelector('.test-summary')?.textContent).toContain(
44
+ '1 passed'
45
+ )
46
+ expect(out.querySelector('.test-summary')?.textContent).toContain(
47
+ '1 failed'
48
+ )
49
+ expect(out.querySelector('.test-summary')?.textContent).toContain(
50
+ '1 inconclusive'
51
+ )
52
+
53
+ // The inconclusive test renders with its own class + a warning note (not
54
+ // a red error), with the "could not run" framing.
55
+ const incLi = out.querySelector('li.test-inconclusive')
56
+ expect(incLi).not.toBeNull()
57
+ expect(incLi?.textContent).toContain('—') // not the ✗ failure icon
58
+ expect(incLi?.querySelector('.test-note')).not.toBeNull()
59
+ expect(incLi?.querySelector('.test-error')).toBeNull()
60
+ expect(incLi?.querySelector('.test-note')?.textContent).toContain(
61
+ 'could not run'
62
+ )
63
+
64
+ // The genuine failure keeps the red error box + ✗ icon.
65
+ const failLi = out.querySelector('li.test-fail')
66
+ expect(failLi?.textContent).toContain('✗')
67
+ expect(failLi?.querySelector('.test-error')).not.toBeNull()
68
+ })
69
+
70
+ it('renders REAL transpiler output: atom-calling agent → inconclusive', async () => {
71
+ // End-to-end: the transpiler emits inconclusive for an un-runnable
72
+ // signature test; the playground must render it as such (not a failure).
73
+ const { tjs } = await import('../../src/lang')
74
+ const { testResults } = tjs(
75
+ `function main(url: ''): { x: '' } {\n const x = httpFetch({ url })\n return { x }\n}`
76
+ )
77
+ const out = document.createElement('div')
78
+ const counts = renderTestResults(
79
+ testResults as any,
80
+ out,
81
+ fakeEditor(),
82
+ () => {}
83
+ )
84
+ expect(counts.failed).toBe(0)
85
+ expect(counts.inconclusive).toBe(1)
86
+ expect(out.querySelector('li.test-inconclusive')).not.toBeNull()
87
+ expect(out.querySelector('li.test-fail')).toBeNull()
88
+ })
89
+
90
+ it('does NOT put an error marker on an inconclusive test line', () => {
91
+ const out = document.createElement('div')
92
+ const editor = fakeEditor()
93
+ renderTestResults(
94
+ [
95
+ {
96
+ description: 'sig',
97
+ passed: false,
98
+ inconclusive: true,
99
+ line: 3,
100
+ error: 'atom not defined',
101
+ },
102
+ ],
103
+ out,
104
+ editor,
105
+ () => {}
106
+ )
107
+ // No setMarkers with an error for the inconclusive line; clearMarkers instead.
108
+ const markerCalls = editor._calls.filter((c: any) => c.setMarkers)
109
+ expect(markerCalls).toHaveLength(0)
110
+ expect(editor._calls.some((c: any) => c.clearMarkers)).toBe(true)
111
+ })
112
+ })
@@ -149,7 +149,7 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
149
149
  // Split mode
150
150
  private _splitMode: null | 'code' | 'output' = null
151
151
  private _splitChannel: BroadcastChannel | null = null
152
- private _splitSessionId: string = ''
152
+ private _splitSessionId = ''
153
153
 
154
154
  // Build flags state
155
155
  private buildFlags = {
@@ -860,21 +860,27 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
860
860
 
861
861
  private updateTestResults(result: any) {
862
862
  const tests = result.testResults
863
- const { passed, failed } = renderTestResults(
863
+ const { passed, failed, inconclusive } = renderTestResults(
864
864
  tests,
865
865
  this.parts.testsOutput,
866
866
  this.parts.tjsEditor,
867
867
  (line) => this.goToSourceLine(line)
868
868
  )
869
- this.updateTestsTabLabel(passed, failed)
869
+ this.updateTestsTabLabel(passed, failed, inconclusive)
870
870
  }
871
871
 
872
- private updateTestsTabLabel(passed: number, failed: number) {
872
+ private updateTestsTabLabel(
873
+ passed: number,
874
+ failed: number,
875
+ inconclusive = 0
876
+ ) {
873
877
  const tabs = this.parts.outputTabs
874
878
  if (!tabs) return
875
879
 
876
880
  if (failed > 0) {
877
881
  tabs.style.setProperty('--test-indicator-color', '#dc2626')
882
+ } else if (inconclusive > 0) {
883
+ tabs.style.setProperty('--test-indicator-color', '#d97706')
878
884
  } else if (passed > 0) {
879
885
  tabs.style.setProperty('--test-indicator-color', '#16a34a')
880
886
  } else {
@@ -1394,7 +1400,7 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
1394
1400
  }
1395
1401
 
1396
1402
  // Navigate to a specific line in the source editor
1397
- goToSourceLine(line: number, column: number = 1) {
1403
+ goToSourceLine(line: number, column = 1) {
1398
1404
  this.parts.inputTabs.value = 0 // Switch to TJS tab (first tab)
1399
1405
  // Wait for tab switch and editor resize before scrolling
1400
1406
  setTimeout(() => {
@@ -0,0 +1,2 @@
1
+ declare const _default: import("typescript-eslint").FlatConfig.ConfigArray;
2
+ export default _default;