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.
- package/CLAUDE.md +15 -5
- package/CONTEXT.md +4 -0
- package/demo/docs.json +12 -690
- package/demo/examples.test.ts +6 -2
- package/demo/src/playground-shared.ts +48 -16
- package/demo/src/playground-test-results.test.ts +112 -0
- package/demo/src/tjs-playground.ts +11 -5
- package/dist/eslint.config.d.ts +2 -0
- package/dist/index.js +146 -141
- package/dist/index.js.map +4 -4
- package/dist/src/lang/core.d.ts +1 -1
- package/dist/src/lang/dialect.d.ts +35 -0
- package/dist/src/lang/emitters/ast.d.ts +1 -1
- package/dist/src/lang/emitters/js-tests.d.ts +7 -0
- package/dist/src/lang/emitters/js-wasm.d.ts +5 -1
- package/dist/src/lang/emitters/js.d.ts +21 -0
- package/dist/src/lang/index.d.ts +3 -1
- package/dist/src/lang/module-loader.d.ts +125 -0
- package/dist/src/lang/parser-transforms.d.ts +79 -0
- package/dist/src/lang/parser-types.d.ts +50 -0
- package/dist/src/lang/parser.d.ts +18 -0
- package/dist/src/lang/transpiler.d.ts +1 -0
- package/dist/src/lang/types.d.ts +18 -0
- package/dist/src/lang/wasm.d.ts +67 -1
- package/dist/src/vm/runtime.d.ts +18 -0
- package/dist/src/vm/vm.d.ts +15 -1
- package/dist/tjs-batteries.js +2 -2
- package/dist/tjs-batteries.js.map +2 -2
- package/dist/tjs-eval.js +43 -41
- package/dist/tjs-eval.js.map +3 -3
- package/dist/tjs-from-ts.js +2 -2
- package/dist/tjs-from-ts.js.map +2 -2
- package/dist/tjs-lang.js +107 -104
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +53 -51
- package/dist/tjs-vm.js.map +3 -3
- package/docs/README.md +2 -0
- package/docs/lm-studio-setup.md +143 -0
- package/docs/universal-endpoint.md +122 -0
- package/llms.txt +9 -2
- package/package.json +24 -4
- package/src/batteries/audit.ts +6 -5
- package/src/batteries/llm.ts +8 -3
- package/src/builder.ts +0 -3
- package/src/cli/commands/check.ts +3 -2
- package/src/cli/commands/emit.ts +4 -2
- package/src/cli/commands/run.ts +6 -2
- package/src/cli/commands/test.ts +1 -1
- package/src/cli/commands/types.ts +2 -2
- package/src/lang/codegen.test.ts +4 -1
- package/src/lang/core.ts +6 -4
- package/src/lang/dialect.test.ts +63 -0
- package/src/lang/dialect.ts +50 -0
- package/src/lang/docs.test.ts +0 -1
- package/src/lang/emitters/ast.ts +145 -2
- package/src/lang/emitters/from-ts.ts +1 -1
- package/src/lang/emitters/js-tests.ts +46 -37
- package/src/lang/emitters/js.ts +19 -3
- package/src/lang/features.test.ts +6 -5
- package/src/lang/index.ts +18 -5
- package/src/lang/linter.ts +1 -1
- package/src/lang/module-loader.test.ts +9 -5
- package/src/lang/module-loader.ts +4 -5
- package/src/lang/parser-params.ts +1 -1
- package/src/lang/parser-transforms.ts +12 -18
- package/src/lang/parser-types.ts +17 -0
- package/src/lang/parser.test.ts +12 -6
- package/src/lang/parser.ts +113 -3
- package/src/lang/perf.test.ts +10 -4
- package/src/lang/runtime.ts +0 -1
- package/src/lang/subset-invariant.test.ts +90 -0
- package/src/lang/transpiler.ts +8 -0
- package/src/lang/types.ts +18 -0
- package/src/lang/wasm.test.ts +22 -16
- package/src/linalg/linalg.test.ts +16 -4
- package/src/linalg/vector-search.bench.test.ts +31 -10
- package/src/types/Type.ts +6 -6
- package/src/use-cases/asymmetric-client-server.test.ts +0 -2
- package/src/use-cases/batteries.test.ts +4 -0
- package/src/use-cases/client-server.test.ts +1 -1
- package/src/use-cases/local-helpers.test.ts +219 -0
- package/src/use-cases/timeout-overrides.test.ts +169 -0
- package/src/use-cases/unbundled-imports.test.ts +0 -1
- package/src/vm/atoms/batteries.ts +17 -3
- package/src/vm/runtime.ts +92 -7
- package/src/vm/vm.ts +50 -10
|
@@ -276,7 +276,7 @@ export function transformParenExpressions(
|
|
|
276
276
|
|
|
277
277
|
// Check for safety marker right after opening paren: (? or (!
|
|
278
278
|
const afterParen = source[i + matchLen]
|
|
279
|
-
let safetyMarker: '?' | '!' | null
|
|
279
|
+
let safetyMarker: '?' | '!' | null
|
|
280
280
|
let paramStart = i + matchLen
|
|
281
281
|
|
|
282
282
|
if (afterParen === '?' || afterParen === '!') {
|
|
@@ -296,7 +296,7 @@ export function extractWasmFunctions(source: string): {
|
|
|
296
296
|
i++
|
|
297
297
|
continue
|
|
298
298
|
}
|
|
299
|
-
|
|
299
|
+
const paramsSource = source.slice(parensStart, j - 1)
|
|
300
300
|
// j now points just past the closing `)`
|
|
301
301
|
|
|
302
302
|
// Phase 2: detect `(!` — reserved syntax for unsafe wasm functions
|
|
@@ -413,7 +413,10 @@ export function composeImportedWasmFunctions(
|
|
|
413
413
|
source: string,
|
|
414
414
|
options: {
|
|
415
415
|
loader?: {
|
|
416
|
-
load(
|
|
416
|
+
load(
|
|
417
|
+
specifier: string,
|
|
418
|
+
importerPath?: string
|
|
419
|
+
): {
|
|
417
420
|
parseResult: { wasmBlocks: WasmBlock[] }
|
|
418
421
|
} | null
|
|
419
422
|
}
|
|
@@ -466,7 +469,8 @@ export function composeImportedWasmFunctions(
|
|
|
466
469
|
// Default imports, namespace imports, and side-effect imports are left
|
|
467
470
|
// alone — only named-bindings are candidates for wasm composition.
|
|
468
471
|
// Multiline imports are supported via the [^}]*? non-greedy match.
|
|
469
|
-
const importRe =
|
|
472
|
+
const importRe =
|
|
473
|
+
/^(\s*)import\s*\{([^}]*?)\}\s*from\s*(['"])([^'"]+)\3\s*;?\s*$/gm
|
|
470
474
|
|
|
471
475
|
const replaced = source.replace(
|
|
472
476
|
importRe,
|
|
@@ -514,13 +518,11 @@ export function composeImportedWasmFunctions(
|
|
|
514
518
|
// `import { outer } from 'lib'` work when outer internally calls
|
|
515
519
|
// inner — both end up in the consumer's composed module.
|
|
516
520
|
pullInTransitively(wasmBlock, importedWasmFunctions)
|
|
517
|
-
const argNames = wasmBlock.captures.map((c) =>
|
|
518
|
-
c.split(':')[0].trim()
|
|
519
|
-
)
|
|
521
|
+
const argNames = wasmBlock.captures.map((c) => c.split(':')[0].trim())
|
|
520
522
|
wrappers.push(
|
|
521
|
-
`function ${local}(${argNames.join(
|
|
522
|
-
|
|
523
|
-
|
|
523
|
+
`function ${local}(${argNames.join(', ')}) { return globalThis.${
|
|
524
|
+
wasmBlock.id
|
|
525
|
+
}(${argNames.join(', ')}) }`
|
|
524
526
|
)
|
|
525
527
|
}
|
|
526
528
|
|
|
@@ -3376,15 +3378,7 @@ export function transformPolymorphicConstructors(
|
|
|
3376
3378
|
})
|
|
3377
3379
|
}
|
|
3378
3380
|
|
|
3379
|
-
//
|
|
3380
|
-
// Build new class body with only the first constructor
|
|
3381
|
-
let newBody = body.slice(0, ctors[0].fullEnd)
|
|
3382
|
-
// Skip subsequent constructors
|
|
3383
|
-
const afterLastCtor = ctors[ctors.length - 1].fullEnd
|
|
3384
|
-
newBody += body.slice(afterLastCtor)
|
|
3385
|
-
|
|
3386
|
-
// But we need to remove just the extra constructors, keeping other methods
|
|
3387
|
-
// Better approach: remove constructors 2..N from the body
|
|
3381
|
+
// Remove the extra constructors, keeping the first constructor and other methods
|
|
3388
3382
|
let cleanBody = body
|
|
3389
3383
|
for (let i = ctors.length - 1; i >= 1; i--) {
|
|
3390
3384
|
const ctor = ctors[i]
|
package/src/lang/parser-types.ts
CHANGED
|
@@ -15,6 +15,17 @@ export interface ParseOptions {
|
|
|
15
15
|
* When true, skips == to Is() transformation since the VM handles == correctly.
|
|
16
16
|
*/
|
|
17
17
|
vmTarget?: boolean
|
|
18
|
+
/**
|
|
19
|
+
* Source dialect — tells the transpiler what kind of source this string is:
|
|
20
|
+
* - `'tjs'`: native TJS, all footgun-removal modes ON (structural `==`,
|
|
21
|
+
* `TjsStandard`, etc.). This is the default for a bare string.
|
|
22
|
+
* - `'js'`: plain JavaScript — modes OFF and `safety: 'none'`, so the source's
|
|
23
|
+
* own semantics are preserved (no `==`→`Eq`, no truthiness rewrite, etc.).
|
|
24
|
+
*
|
|
25
|
+
* Authoritative when set; otherwise the dialect is inferred (the fromTS
|
|
26
|
+
* annotation and `vmTarget` ⇒ JS-compatible). See PRINCIPLES.md (TJS ⊇ JS).
|
|
27
|
+
*/
|
|
28
|
+
dialect?: 'js' | 'tjs'
|
|
18
29
|
/**
|
|
19
30
|
* Optional ModuleLoader for cross-file `wasm function` composition (Phase 3).
|
|
20
31
|
* When provided, imports are resolved at transpile time and matching wasm
|
|
@@ -109,6 +120,12 @@ export interface PreprocessOptions {
|
|
|
109
120
|
* Default: false (transform == to Is() for TJS code running in regular JS)
|
|
110
121
|
*/
|
|
111
122
|
vmTarget?: boolean
|
|
123
|
+
/**
|
|
124
|
+
* Source dialect: `'js'` ⇒ modes OFF / `safety: 'none'` (preserve plain-JS
|
|
125
|
+
* semantics); `'tjs'` ⇒ native modes ON. Authoritative when set; otherwise
|
|
126
|
+
* inferred from the fromTS annotation / `vmTarget`. See ParseOptions.dialect.
|
|
127
|
+
*/
|
|
128
|
+
dialect?: 'js' | 'tjs'
|
|
112
129
|
/**
|
|
113
130
|
* Optional ModuleLoader for cross-file `wasm function` composition (Phase 3).
|
|
114
131
|
* See ParseOptions.moduleLoader for details.
|
package/src/lang/parser.test.ts
CHANGED
|
@@ -814,13 +814,19 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
814
814
|
})
|
|
815
815
|
|
|
816
816
|
describe('Error handling', () => {
|
|
817
|
-
it('
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
817
|
+
it('accepts multiple functions as local helpers (last is the entry)', () => {
|
|
818
|
+
// Multiple top-level functions are now the local-helpers feature: the
|
|
819
|
+
// last declaration is the entry point, the rest are helpers called by
|
|
820
|
+
// name via callLocal. See use-cases/local-helpers.test.ts for semantics.
|
|
821
|
+
const { ast } = transpile(`
|
|
822
|
+
function helper(x: 0): 0 { return x }
|
|
823
|
+
function main(n: 0): 0 {
|
|
824
|
+
const r = helper(n)
|
|
825
|
+
return r
|
|
826
|
+
}
|
|
822
827
|
`)
|
|
823
|
-
|
|
828
|
+
// Unused/used helpers are collected onto the root node by name.
|
|
829
|
+
expect((ast as any).helpers?.helper).toBeDefined()
|
|
824
830
|
})
|
|
825
831
|
|
|
826
832
|
it('should require a function when only a class is provided', () => {
|
package/src/lang/parser.ts
CHANGED
|
@@ -135,9 +135,17 @@ export function preprocess(
|
|
|
135
135
|
// The /* tjs <- filename */ annotation is the signal
|
|
136
136
|
const isFromTS = /\/\*\s*tjs\s*<-\s*\S+\s*\*\//.test(source)
|
|
137
137
|
|
|
138
|
-
// Native TJS: all modes ON by default (TJS is its own language)
|
|
139
|
-
// TS-originated or VM target
|
|
140
|
-
|
|
138
|
+
// Native TJS: all modes ON by default (TJS is its own language).
|
|
139
|
+
// Plain JS (dialect: 'js'), TS-originated, or VM target: all modes OFF +
|
|
140
|
+
// safety none, so the source's own semantics are preserved (JS-compatible).
|
|
141
|
+
// An explicit `dialect` is authoritative; otherwise infer from the fromTS
|
|
142
|
+
// annotation / vmTarget. See PRINCIPLES.md (TJS ⊇ JS).
|
|
143
|
+
const isCompat =
|
|
144
|
+
options.dialect === 'js'
|
|
145
|
+
? true
|
|
146
|
+
: options.dialect === 'tjs'
|
|
147
|
+
? false
|
|
148
|
+
: isFromTS || options.vmTarget
|
|
141
149
|
const tjsModes: TjsModes = isCompat
|
|
142
150
|
? {
|
|
143
151
|
tjsEquals: false,
|
|
@@ -452,6 +460,7 @@ export function parse(
|
|
|
452
460
|
filename = '<source>',
|
|
453
461
|
colonShorthand = true,
|
|
454
462
|
vmTarget = false,
|
|
463
|
+
dialect,
|
|
455
464
|
} = options
|
|
456
465
|
|
|
457
466
|
// Preprocess for custom syntax
|
|
@@ -472,6 +481,7 @@ export function parse(
|
|
|
472
481
|
} = colonShorthand
|
|
473
482
|
? preprocess(source, {
|
|
474
483
|
vmTarget,
|
|
484
|
+
dialect,
|
|
475
485
|
moduleLoader: options.moduleLoader,
|
|
476
486
|
filename: options.filename,
|
|
477
487
|
})
|
|
@@ -602,6 +612,106 @@ export function validateSingleFunction(
|
|
|
602
612
|
return functions[0]
|
|
603
613
|
}
|
|
604
614
|
|
|
615
|
+
/**
|
|
616
|
+
* Extract top-level function declarations from the parsed program.
|
|
617
|
+
*
|
|
618
|
+
* Returns `{ entry, helpers }` where:
|
|
619
|
+
* - `entry` is the last function declaration (the agent's entry point)
|
|
620
|
+
* - `helpers` are all preceding function declarations, looked up by name
|
|
621
|
+
*
|
|
622
|
+
* This matches the natural "helpers first, agent last" pattern, including
|
|
623
|
+
* the TOOL_LIBRARY use case where helper async wrappers are prepended
|
|
624
|
+
* before the user-supplied agent function.
|
|
625
|
+
*
|
|
626
|
+
* Same top-level construct restrictions as `validateSingleFunction`:
|
|
627
|
+
* imports, exports, and classes are rejected.
|
|
628
|
+
*/
|
|
629
|
+
export function extractFunctions(
|
|
630
|
+
ast: Program,
|
|
631
|
+
filename?: string
|
|
632
|
+
): { entry: FunctionDeclaration; helpers: Map<string, FunctionDeclaration> } {
|
|
633
|
+
// Top-level construct checks (same as validateSingleFunction)
|
|
634
|
+
for (const node of ast.body) {
|
|
635
|
+
if (node.type === 'ImportDeclaration') {
|
|
636
|
+
throw new SyntaxError(
|
|
637
|
+
'Imports are not supported. All atoms must be registered with the VM.',
|
|
638
|
+
node.loc?.start || { line: 1, column: 0 },
|
|
639
|
+
undefined,
|
|
640
|
+
filename
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (
|
|
645
|
+
node.type === 'ExportNamedDeclaration' ||
|
|
646
|
+
node.type === 'ExportDefaultDeclaration'
|
|
647
|
+
) {
|
|
648
|
+
throw new SyntaxError(
|
|
649
|
+
'Exports are not supported. The function is automatically exported.',
|
|
650
|
+
node.loc?.start || { line: 1, column: 0 },
|
|
651
|
+
undefined,
|
|
652
|
+
filename
|
|
653
|
+
)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (node.type === 'ClassDeclaration') {
|
|
657
|
+
throw new SyntaxError(
|
|
658
|
+
'Classes are not supported. Agent99 uses functional composition.',
|
|
659
|
+
node.loc?.start || { line: 1, column: 0 },
|
|
660
|
+
undefined,
|
|
661
|
+
filename
|
|
662
|
+
)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const functions = ast.body.filter(
|
|
667
|
+
(node): node is FunctionDeclaration => node.type === 'FunctionDeclaration'
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
if (functions.length === 0) {
|
|
671
|
+
throw new SyntaxError(
|
|
672
|
+
'Source must contain a function declaration',
|
|
673
|
+
{ line: 1, column: 0 },
|
|
674
|
+
undefined,
|
|
675
|
+
filename
|
|
676
|
+
)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const entry = functions[functions.length - 1]
|
|
680
|
+
const helpers = new Map<string, FunctionDeclaration>()
|
|
681
|
+
|
|
682
|
+
for (let i = 0; i < functions.length - 1; i++) {
|
|
683
|
+
const fn = functions[i]
|
|
684
|
+
const name = fn.id?.name
|
|
685
|
+
if (!name) {
|
|
686
|
+
throw new SyntaxError(
|
|
687
|
+
'Helper function must have a name',
|
|
688
|
+
fn.loc?.start || { line: 1, column: 0 },
|
|
689
|
+
undefined,
|
|
690
|
+
filename
|
|
691
|
+
)
|
|
692
|
+
}
|
|
693
|
+
if (helpers.has(name)) {
|
|
694
|
+
throw new SyntaxError(
|
|
695
|
+
`Duplicate helper function name: ${name}`,
|
|
696
|
+
fn.loc?.start || { line: 1, column: 0 },
|
|
697
|
+
undefined,
|
|
698
|
+
filename
|
|
699
|
+
)
|
|
700
|
+
}
|
|
701
|
+
if (name === entry.id?.name) {
|
|
702
|
+
throw new SyntaxError(
|
|
703
|
+
`Helper function cannot share a name with the entry function: ${name}`,
|
|
704
|
+
fn.loc?.start || { line: 1, column: 0 },
|
|
705
|
+
undefined,
|
|
706
|
+
filename
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
helpers.set(name, fn)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return { entry, helpers }
|
|
713
|
+
}
|
|
714
|
+
|
|
605
715
|
/**
|
|
606
716
|
* Extract TDoc comment from before a function
|
|
607
717
|
*
|
package/src/lang/perf.test.ts
CHANGED
|
@@ -60,8 +60,12 @@ describe('TJS Performance', () => {
|
|
|
60
60
|
console.log(` Median: ${median.toFixed(0)}ms`)
|
|
61
61
|
console.log(` Max: ${max.toFixed(0)}ms`)
|
|
62
62
|
|
|
63
|
-
//
|
|
64
|
-
|
|
63
|
+
// Regression guardrail, not a micro-benchmark: this wall-clock is
|
|
64
|
+
// dominated by `bun` process spawn + module-graph load (the one-line
|
|
65
|
+
// transpile is negligible), so it's machine- and load-dependent. Keep the
|
|
66
|
+
// bar generous — it catches a gross regression (cold start ballooning to
|
|
67
|
+
// seconds) without flaking on a loaded box or slower CI.
|
|
68
|
+
expect(median).toBeLessThan(500)
|
|
65
69
|
})
|
|
66
70
|
|
|
67
71
|
it('should measure tjs emit time', async () => {
|
|
@@ -84,7 +88,8 @@ describe('TJS Performance', () => {
|
|
|
84
88
|
console.log(`\n tjs emit cold start (5 runs):`)
|
|
85
89
|
console.log(` Median: ${median.toFixed(0)}ms`)
|
|
86
90
|
|
|
87
|
-
|
|
91
|
+
// Generous regression guardrail — see the tjsx cold-start note above.
|
|
92
|
+
expect(median).toBeLessThan(500)
|
|
88
93
|
})
|
|
89
94
|
|
|
90
95
|
it('should measure tjs check time', async () => {
|
|
@@ -107,7 +112,8 @@ describe('TJS Performance', () => {
|
|
|
107
112
|
console.log(`\n tjs check cold start (5 runs):`)
|
|
108
113
|
console.log(` Median: ${median.toFixed(0)}ms`)
|
|
109
114
|
|
|
110
|
-
|
|
115
|
+
// Generous regression guardrail — see the tjsx cold-start note above.
|
|
116
|
+
expect(median).toBeLessThan(500)
|
|
111
117
|
})
|
|
112
118
|
})
|
|
113
119
|
|
package/src/lang/runtime.ts
CHANGED
|
@@ -73,7 +73,6 @@ export {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// Version from package.json - injected at build time or imported
|
|
76
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
77
76
|
const pkg = require('../../package.json') as { version: string }
|
|
78
77
|
|
|
79
78
|
export const TJS_VERSION: string = pkg.version
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { tjs, transpile } from './index'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Guards the language subset invariants engraved in PRINCIPLES.md:
|
|
6
|
+
*
|
|
7
|
+
* AJS ⊆ TJS — every legal AJS source is legal TJS source
|
|
8
|
+
* JS ⊆ TJS (no modes) — every legal JS program is legal TJS
|
|
9
|
+
*
|
|
10
|
+
* TJS may do MORE with the same source (enforce contracts, run signature
|
|
11
|
+
* tests) but must never REJECT source the subset accepts. The classic way this
|
|
12
|
+
* breaks: a build-time signature test that can't *run* (it calls an AJS atom
|
|
13
|
+
* that doesn't exist at build time, etc.) gets escalated into a transpile
|
|
14
|
+
* error. Such tests must be *inconclusive*, never failing.
|
|
15
|
+
*/
|
|
16
|
+
describe('Language subset invariants (PRINCIPLES.md)', () => {
|
|
17
|
+
// Representative AJS-shaped sources. Each must be valid AJS (transpile to a
|
|
18
|
+
// VM AST) AND valid TJS (tjs() must not throw). Several carry return types
|
|
19
|
+
// and call atoms — the exact shape that used to be illegal TJS.
|
|
20
|
+
const ajsSnippets: Array<[string, string]> = [
|
|
21
|
+
[
|
|
22
|
+
'agent returning an object (no types)',
|
|
23
|
+
`function main(n: 0) {\n return { doubled: n * 2 }\n}`,
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
'atom call + return type',
|
|
27
|
+
`function main(url: ''): { x: '' } {\n const x = httpFetch({ url })\n return { x }\n}`,
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
'helper with a typed signature',
|
|
31
|
+
`function double(x: 0): 0 {\n return x * 2\n}\nfunction main(n: 0) {\n const d = double(n)\n return { d }\n}`,
|
|
32
|
+
],
|
|
33
|
+
[
|
|
34
|
+
'helper that calls an atom + return type',
|
|
35
|
+
`function fetchIt(u: ''): '' {\n const r = httpFetch({ url: u })\n return r\n}\nfunction main(url: '') {\n const x = fetchIt(url)\n return { x }\n}`,
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
'consistent signature example still validates',
|
|
39
|
+
`function add(a: 2, b: 3): 5 {\n return a + b\n}\nfunction main(x: 0, y: 0) {\n const s = add(x, y)\n return { s }\n}`,
|
|
40
|
+
],
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
describe('TJS ⊇ AJS', () => {
|
|
44
|
+
for (const [label, src] of ajsSnippets) {
|
|
45
|
+
it(`valid as both AJS and TJS: ${label}`, () => {
|
|
46
|
+
// Valid AJS (produces a VM AST)…
|
|
47
|
+
expect(() => transpile(src, { vmTarget: true })).not.toThrow()
|
|
48
|
+
// …and therefore must be valid TJS (never rejected).
|
|
49
|
+
expect(() => tjs(src)).not.toThrow()
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('un-runnable signature tests are inconclusive, not failures', () => {
|
|
55
|
+
const r = tjs(
|
|
56
|
+
`function main(url: ''): { x: '' } {\n const x = httpFetch({ url })\n return { x }\n}`
|
|
57
|
+
)
|
|
58
|
+
const sig = r.testResults?.find((t: any) => t.isSignatureTest)
|
|
59
|
+
expect(sig).toBeDefined()
|
|
60
|
+
expect(sig?.passed).toBe(false)
|
|
61
|
+
expect(sig?.inconclusive).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('still REJECTS a genuinely inconsistent signature example (validation intact)', () => {
|
|
65
|
+
// 2 + 3 = 5, not 99 — the test runs cleanly and mismatches → hard failure.
|
|
66
|
+
expect(() =>
|
|
67
|
+
tjs(`function add(a: 2, b: 3): 99 {\n return a + b\n}`)
|
|
68
|
+
).toThrow(/inconsistent/)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('TJS (no modes) ⊇ JS', () => {
|
|
72
|
+
// Plain JavaScript under options-off TJS (TjsCompat disables all modes).
|
|
73
|
+
const jsSnippets: Array<[string, string]> = [
|
|
74
|
+
['arithmetic fn', `function f(x) { return x + 1 }`],
|
|
75
|
+
[
|
|
76
|
+
'control flow + array methods',
|
|
77
|
+
`function f(xs) {\n let total = 0\n for (const x of xs) { total += x }\n return xs.map(v => v * 2).filter(v => v > total)\n}`,
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
'object + destructuring',
|
|
81
|
+
`function f(o) {\n const { a, b } = o\n return { ...o, sum: a + b }\n}`,
|
|
82
|
+
],
|
|
83
|
+
]
|
|
84
|
+
for (const [label, src] of jsSnippets) {
|
|
85
|
+
it(`accepts plain JS: ${label}`, () => {
|
|
86
|
+
expect(() => tjs(`TjsCompat\n${src}`)).not.toThrow()
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
})
|
package/src/lang/transpiler.ts
CHANGED
|
@@ -16,6 +16,14 @@ export { transpile, ajs, tjs, createAgent, getToolDefinitions } from './core'
|
|
|
16
16
|
// Parser
|
|
17
17
|
export { parse, preprocess, extractTDoc } from './parser'
|
|
18
18
|
|
|
19
|
+
// Dialect resolution for file-based tooling (extension → dialect)
|
|
20
|
+
export {
|
|
21
|
+
dialectForFilename,
|
|
22
|
+
sourceKindForFilename,
|
|
23
|
+
type Dialect,
|
|
24
|
+
type SourceKind,
|
|
25
|
+
} from './dialect'
|
|
26
|
+
|
|
19
27
|
// AST emitter
|
|
20
28
|
export { transformFunction } from './emitters/ast'
|
|
21
29
|
|
package/src/lang/types.ts
CHANGED
|
@@ -225,6 +225,21 @@ export interface TransformContext {
|
|
|
225
225
|
filename: string
|
|
226
226
|
/** Options */
|
|
227
227
|
options: TranspileOptions
|
|
228
|
+
/**
|
|
229
|
+
* Helper functions (top-level functions declared before the entry function).
|
|
230
|
+
* Calls to these names emit `callLocal` instead of treating them as atom calls.
|
|
231
|
+
*/
|
|
232
|
+
helpers?: Map<string, any /* FunctionDeclaration */>
|
|
233
|
+
/**
|
|
234
|
+
* Cache of transformed helper bodies. Lazily populated as helpers are
|
|
235
|
+
* referenced from the entry function (or from other helpers).
|
|
236
|
+
*/
|
|
237
|
+
helperSteps?: Map<string, { steps: any[]; paramNames: string[] }>
|
|
238
|
+
/**
|
|
239
|
+
* Names of helpers currently being transformed. Used to detect direct or
|
|
240
|
+
* transitive recursion.
|
|
241
|
+
*/
|
|
242
|
+
helperTransforming?: Set<string>
|
|
228
243
|
}
|
|
229
244
|
|
|
230
245
|
/** Create a child context for nested scopes */
|
|
@@ -239,6 +254,9 @@ export function createChildContext(parent: TransformContext): TransformContext {
|
|
|
239
254
|
source: parent.source,
|
|
240
255
|
filename: parent.filename,
|
|
241
256
|
options: parent.options,
|
|
257
|
+
helpers: parent.helpers,
|
|
258
|
+
helperSteps: parent.helperSteps,
|
|
259
|
+
helperTransforming: parent.helperTransforming,
|
|
242
260
|
}
|
|
243
261
|
}
|
|
244
262
|
|
package/src/lang/wasm.test.ts
CHANGED
|
@@ -1901,8 +1901,14 @@ describe('module consolidation (Phase 0.5)', () => {
|
|
|
1901
1901
|
|
|
1902
1902
|
const result = compileBlocksToModule(blocks)
|
|
1903
1903
|
expect(result.exports).toHaveLength(2)
|
|
1904
|
-
expect(result.exports[0]).toMatchObject({
|
|
1905
|
-
|
|
1904
|
+
expect(result.exports[0]).toMatchObject({
|
|
1905
|
+
id: 'b0',
|
|
1906
|
+
exportName: 'compute_0',
|
|
1907
|
+
})
|
|
1908
|
+
expect(result.exports[1]).toMatchObject({
|
|
1909
|
+
id: 'b1',
|
|
1910
|
+
exportName: 'compute_1',
|
|
1911
|
+
})
|
|
1906
1912
|
|
|
1907
1913
|
// Instantiate and confirm both exports work
|
|
1908
1914
|
const instance = await instantiateWasm(result.bytes)
|
|
@@ -2117,10 +2123,8 @@ function dbl(arr: Float32Array, len: 0) {
|
|
|
2117
2123
|
buf[1] = 2
|
|
2118
2124
|
buf[2] = 3
|
|
2119
2125
|
buf[3] = 4
|
|
2120
|
-
|
|
2121
2126
|
;(globalThis as any).__test_inc(buf, 4) // [2, 3, 4, 5]
|
|
2122
2127
|
expect(Array.from(buf)).toEqual([2, 3, 4, 5])
|
|
2123
|
-
|
|
2124
2128
|
;(globalThis as any).__test_dbl(buf, 4) // [4, 6, 8, 10]
|
|
2125
2129
|
expect(Array.from(buf)).toEqual([4, 6, 8, 10])
|
|
2126
2130
|
} finally {
|
|
@@ -2313,14 +2317,20 @@ describe('boundary distribution form (Phase 4)', () => {
|
|
|
2313
2317
|
async function dynamicImportLibrary(transpiled: string): Promise<any> {
|
|
2314
2318
|
const { tmpdir } = await import('node:os')
|
|
2315
2319
|
const { join } = await import('node:path')
|
|
2316
|
-
const { writeFileSync, unlinkSync } = await import('node:fs')
|
|
2320
|
+
const { writeFileSync, unlinkSync, realpathSync } = await import('node:fs')
|
|
2321
|
+
const { pathToFileURL } = await import('node:url')
|
|
2317
2322
|
const path = join(
|
|
2318
2323
|
tmpdir(),
|
|
2319
2324
|
`tjs-lib-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mjs`
|
|
2320
2325
|
)
|
|
2321
2326
|
writeFileSync(path, transpiled)
|
|
2327
|
+
// Resolve macOS /var → /private/var symlink and use file:// URL — bun's
|
|
2328
|
+
// resolver in subsequent test cases (after a `new Function(emittedCode)`
|
|
2329
|
+
// execution earlier in the same file) fails on bare absolute paths with
|
|
2330
|
+
// "Cannot find module ... from ''". This form is robust to that state.
|
|
2331
|
+
const url = pathToFileURL(realpathSync(path)).href
|
|
2322
2332
|
try {
|
|
2323
|
-
const mod = await import(
|
|
2333
|
+
const mod = await import(url)
|
|
2324
2334
|
// Wait for the async wasm bootstrap inside the module to finish.
|
|
2325
2335
|
// The bootstrap runs as a top-level IIFE; instantiation is async.
|
|
2326
2336
|
await new Promise((r) => setTimeout(r, 100))
|
|
@@ -2384,9 +2394,7 @@ export wasm function mul(a: f64, b: f64): f64 { return a * b }
|
|
|
2384
2394
|
// Both should produce the same numeric results.
|
|
2385
2395
|
const { tjs } = await import('./index')
|
|
2386
2396
|
const { createRuntime } = await import('./runtime')
|
|
2387
|
-
const { ModuleLoader, inMemoryFileSystem } = await import(
|
|
2388
|
-
'./module-loader'
|
|
2389
|
-
)
|
|
2397
|
+
const { ModuleLoader, inMemoryFileSystem } = await import('./module-loader')
|
|
2390
2398
|
|
|
2391
2399
|
const librarySource = `
|
|
2392
2400
|
export wasm function dot3(
|
|
@@ -2568,9 +2576,7 @@ function compute(x: 0.0): 0.0 {
|
|
|
2568
2576
|
expect(result.code).toContain('globalThis.__tjs_wasm_fast(a)')
|
|
2569
2577
|
// The remaining import keeps `slow` only
|
|
2570
2578
|
expect(result.code).toMatch(/import\s*\{\s*slow\s*\}\s*from/)
|
|
2571
|
-
expect(result.code).not.toMatch(
|
|
2572
|
-
/import\s*\{\s*fast\s*,\s*slow\s*\}\s*from/
|
|
2573
|
-
)
|
|
2579
|
+
expect(result.code).not.toMatch(/import\s*\{\s*fast\s*,\s*slow\s*\}\s*from/)
|
|
2574
2580
|
})
|
|
2575
2581
|
|
|
2576
2582
|
it('composes multiple wasm functions from one library', async () => {
|
|
@@ -3046,9 +3052,7 @@ wasm function isOdd(n: i32): f64 {
|
|
|
3046
3052
|
// inner loop.
|
|
3047
3053
|
const { tjs } = await import('./index')
|
|
3048
3054
|
const { createRuntime } = await import('./runtime')
|
|
3049
|
-
const { ModuleLoader, inMemoryFileSystem } = await import(
|
|
3050
|
-
'./module-loader'
|
|
3051
|
-
)
|
|
3055
|
+
const { ModuleLoader, inMemoryFileSystem } = await import('./module-loader')
|
|
3052
3056
|
|
|
3053
3057
|
const loader = new ModuleLoader({
|
|
3054
3058
|
fs: inMemoryFileSystem({
|
|
@@ -3162,7 +3166,9 @@ wasm function caller(): f64 {
|
|
|
3162
3166
|
`
|
|
3163
3167
|
const result = tjs(source, { runTests: false })
|
|
3164
3168
|
// caller fails because takesTwo gets one arg instead of two
|
|
3165
|
-
const callerResult = result.wasmCompiled!.find(
|
|
3169
|
+
const callerResult = result.wasmCompiled!.find(
|
|
3170
|
+
(b) => b.id === '__tjs_wasm_caller'
|
|
3171
|
+
)
|
|
3166
3172
|
expect(callerResult).toBeDefined()
|
|
3167
3173
|
expect(callerResult!.success).toBe(false)
|
|
3168
3174
|
expect(callerResult!.error).toMatch(/takesTwo expects 2 arguments, got 1/)
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { describe, it, expect } from 'bun:test'
|
|
17
|
-
import { readFileSync } from 'node:fs'
|
|
17
|
+
import { readFileSync, realpathSync } from 'node:fs'
|
|
18
18
|
import { join } from 'node:path'
|
|
19
19
|
import { tmpdir } from 'node:os'
|
|
20
|
+
import { pathToFileURL } from 'node:url'
|
|
20
21
|
import { writeFileSync, unlinkSync } from 'node:fs'
|
|
21
22
|
|
|
22
23
|
const LINALG_PATH = join(import.meta.dir, 'index.tjs')
|
|
@@ -41,8 +42,13 @@ async function dynamicImportLibrary(transpiled: string): Promise<any> {
|
|
|
41
42
|
`linalg-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mjs`
|
|
42
43
|
)
|
|
43
44
|
writeFileSync(path, transpiled)
|
|
45
|
+
// Resolve macOS /var → /private/var symlink and use file:// URL — bun's
|
|
46
|
+
// resolver in subsequent test cases (after a `new Function(emittedCode)`
|
|
47
|
+
// execution earlier in the same file) fails on bare absolute paths with
|
|
48
|
+
// "Cannot find module ... from ''". This form is robust to that state.
|
|
49
|
+
const url = pathToFileURL(realpathSync(path)).href
|
|
44
50
|
try {
|
|
45
|
-
const mod = await import(
|
|
51
|
+
const mod = await import(url)
|
|
46
52
|
// Wait for the async wasm bootstrap inside the module to finish
|
|
47
53
|
await new Promise((r) => setTimeout(r, 100))
|
|
48
54
|
return mod
|
|
@@ -203,8 +209,14 @@ function cosine(a, b, n) {
|
|
|
203
209
|
// Orthogonal vectors → cosine 0
|
|
204
210
|
const ox = wasmBuffer(Float32Array, 4)
|
|
205
211
|
const oy = wasmBuffer(Float32Array, 4)
|
|
206
|
-
ox[0] = 1
|
|
207
|
-
|
|
212
|
+
ox[0] = 1
|
|
213
|
+
ox[1] = 0
|
|
214
|
+
ox[2] = 0
|
|
215
|
+
ox[3] = 0
|
|
216
|
+
oy[0] = 0
|
|
217
|
+
oy[1] = 1
|
|
218
|
+
oy[2] = 0
|
|
219
|
+
oy[3] = 0
|
|
208
220
|
const ortho = (globalThis as any).__test_cosine(ox, oy, 4)
|
|
209
221
|
expect(ortho).toBeCloseTo(0, 4)
|
|
210
222
|
} finally {
|
|
@@ -30,10 +30,7 @@ import { describe, it, expect } from 'bun:test'
|
|
|
30
30
|
import { readFileSync } from 'node:fs'
|
|
31
31
|
import { join } from 'node:path'
|
|
32
32
|
|
|
33
|
-
const LINALG_SOURCE = readFileSync(
|
|
34
|
-
join(import.meta.dir, 'index.tjs'),
|
|
35
|
-
'utf8'
|
|
36
|
-
)
|
|
33
|
+
const LINALG_SOURCE = readFileSync(join(import.meta.dir, 'index.tjs'), 'utf8')
|
|
37
34
|
|
|
38
35
|
// The inline baseline — single wasm{} block computing dot, magA, magB
|
|
39
36
|
// together. Mirrors what guides/examples/tjs/wasm-vector-search.md does.
|
|
@@ -161,7 +158,12 @@ async function loadVariant(
|
|
|
161
158
|
fnName: string,
|
|
162
159
|
varName: string
|
|
163
160
|
): Promise<{
|
|
164
|
-
search: (
|
|
161
|
+
search: (
|
|
162
|
+
corpus: Float32Array,
|
|
163
|
+
query: Float32Array,
|
|
164
|
+
count: number,
|
|
165
|
+
dim: number
|
|
166
|
+
) => number
|
|
165
167
|
wasmBuffer: (Ctor: any, len: number) => any
|
|
166
168
|
}> {
|
|
167
169
|
await new Function(
|
|
@@ -296,13 +298,28 @@ describe('Canonical demo: vector-search across three forms', () => {
|
|
|
296
298
|
const warmCount = Math.min(100, cfg.count)
|
|
297
299
|
for (let w = 0; w < 3; w++) {
|
|
298
300
|
inline.search(inlineCorpus, inlineQuery, warmCount, cfg.dim)
|
|
299
|
-
composedJs.search(
|
|
300
|
-
|
|
301
|
+
composedJs.search(
|
|
302
|
+
composedJsCorpus,
|
|
303
|
+
composedJsQuery,
|
|
304
|
+
warmCount,
|
|
305
|
+
cfg.dim
|
|
306
|
+
)
|
|
307
|
+
composedWasm.search(
|
|
308
|
+
composedWasmCorpus,
|
|
309
|
+
composedWasmQuery,
|
|
310
|
+
warmCount,
|
|
311
|
+
cfg.dim
|
|
312
|
+
)
|
|
301
313
|
}
|
|
302
314
|
|
|
303
315
|
// Time inline
|
|
304
316
|
const inlineStart = performance.now()
|
|
305
|
-
const inlineIdx = inline.search(
|
|
317
|
+
const inlineIdx = inline.search(
|
|
318
|
+
inlineCorpus,
|
|
319
|
+
inlineQuery,
|
|
320
|
+
cfg.count,
|
|
321
|
+
cfg.dim
|
|
322
|
+
)
|
|
306
323
|
const inlineMs = performance.now() - inlineStart
|
|
307
324
|
|
|
308
325
|
// Time composed JS-outer-loop
|
|
@@ -352,9 +369,13 @@ describe('Canonical demo: vector-search across three forms', () => {
|
|
|
352
369
|
const jsRatio = t.composedJsMs / t.inlineMs
|
|
353
370
|
const wasmRatio = t.composedWasmMs / t.inlineMs
|
|
354
371
|
console.log(
|
|
355
|
-
` ${t.label.padEnd(12)} | ${t.inlineMs
|
|
372
|
+
` ${t.label.padEnd(12)} | ${t.inlineMs
|
|
373
|
+
.toFixed(2)
|
|
374
|
+
.padStart(8)} | ${t.composedJsMs
|
|
375
|
+
.toFixed(2)
|
|
376
|
+
.padStart(11)} | ${jsRatio
|
|
356
377
|
.toFixed(2)
|
|
357
|
-
.padStart(
|
|
378
|
+
.padStart(6)}x | ${t.composedWasmMs
|
|
358
379
|
.toFixed(2)
|
|
359
380
|
.padStart(13)} | ${wasmRatio.toFixed(2).padStart(5)}x`
|
|
360
381
|
)
|