tjs-lang 0.8.1 → 0.8.3
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 +13 -4
- package/demo/autocomplete.test.ts +37 -0
- package/demo/docs.json +698 -8
- package/demo/examples.test.ts +6 -2
- package/demo/src/autocomplete.ts +40 -2
- package/demo/src/introspection-bridge.ts +140 -0
- package/demo/src/introspection-doc.test.ts +63 -0
- package/demo/src/playground-shared.ts +101 -16
- package/demo/src/playground-test-results.test.ts +112 -0
- package/demo/src/tjs-playground.ts +32 -5
- package/dist/examples/modules/dist/main.d.ts +34 -0
- package/dist/examples/modules/dist/math.d.ts +120 -0
- package/dist/index.js +115 -112
- 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.d.ts +12 -0
- package/dist/src/lang/index.d.ts +2 -1
- package/dist/src/lang/parser-types.d.ts +17 -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/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 -43
- package/dist/tjs-eval.js.map +3 -3
- package/dist/tjs-from-ts.js +1 -1
- package/dist/tjs-from-ts.js.map +2 -2
- package/dist/tjs-lang.js +76 -73
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +49 -49
- package/dist/tjs-vm.js.map +3 -3
- package/editors/codemirror/ajs-language.ts +130 -44
- package/editors/codemirror/completion-source.test.ts +114 -0
- package/editors/introspect-value.test.ts +61 -0
- package/editors/introspect-value.ts +86 -0
- package/editors/scope-symbols.test.ts +113 -0
- package/editors/scope-symbols.ts +173 -0
- package/llms.txt +1 -0
- package/package.json +21 -1
- package/src/batteries/audit.ts +3 -2
- 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/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/emitters/ast.ts +145 -2
- package/src/lang/emitters/js-tests.ts +46 -37
- package/src/lang/emitters/js.ts +19 -2
- package/src/lang/features.test.ts +6 -5
- package/src/lang/index.ts +40 -5
- 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/predicate-schema.test.ts +97 -0
- package/src/lang/predicate-schema.ts +168 -0
- package/src/lang/predicate.test.ts +184 -0
- package/src/lang/predicate.ts +550 -0
- package/src/lang/subset-invariant.test.ts +90 -0
- package/src/lang/suggest.test.ts +84 -0
- package/src/lang/transpiler.ts +34 -0
- package/src/lang/types.ts +18 -0
- package/src/lang/wasm.test.ts +8 -2
- package/src/linalg/linalg.test.ts +8 -2
- package/src/use-cases/batteries.test.ts +4 -0
- package/src/use-cases/local-helpers.test.ts +219 -0
- package/src/use-cases/timeout-overrides.test.ts +169 -0
- package/src/vm/atom-effects.test.ts +72 -0
- package/src/vm/atoms/batteries.ts +29 -3
- package/src/vm/runtime.ts +140 -4
- package/src/vm/vm.ts +47 -9
|
@@ -18,6 +18,13 @@ export interface TestResult {
|
|
|
18
18
|
error?: string
|
|
19
19
|
/** Whether this was an implicit signature test */
|
|
20
20
|
isSignatureTest?: boolean
|
|
21
|
+
/**
|
|
22
|
+
* The test could not be *run* (e.g. it references a name TJS can't resolve at
|
|
23
|
+
* build time — like an AJS atom — or the harness couldn't execute the module).
|
|
24
|
+
* Inconclusive ≠ failed: it never blocks transpilation. Surfaced as a warning
|
|
25
|
+
* (e.g. in the playground). See PRINCIPLES.md ("more, never illegal").
|
|
26
|
+
*/
|
|
27
|
+
inconclusive?: boolean
|
|
21
28
|
/** Source line number (1-indexed) where the test or error occurred */
|
|
22
29
|
line?: number
|
|
23
30
|
/** Source column number (1-indexed) */
|
|
@@ -678,7 +685,7 @@ export function runAllTests(
|
|
|
678
685
|
${body}
|
|
679
686
|
__testResults.push({ idx: ${i}, passed: true });
|
|
680
687
|
} catch (e) {
|
|
681
|
-
__testResults.push({ idx: ${i}, passed: false, error: e.message || String(e) });
|
|
688
|
+
__testResults.push({ idx: ${i}, passed: false, isRef: e instanceof ReferenceError, error: e.message || String(e) });
|
|
682
689
|
}
|
|
683
690
|
`
|
|
684
691
|
})
|
|
@@ -721,7 +728,10 @@ export function runAllTests(
|
|
|
721
728
|
__sigTestResults.push({ idx: ${i}, passed: false, error: 'Expected ' + __format(__expected) + ' at \\'${testLabel}\\', got ' + __format(__actual) });
|
|
722
729
|
}
|
|
723
730
|
} catch (e) {
|
|
724
|
-
|
|
731
|
+
// The function threw on its example inputs — the example couldn't be
|
|
732
|
+
// *evaluated* (e.g. it calls an atom that doesn't exist at build time).
|
|
733
|
+
// That's inconclusive, not a mismatch. Never block transpilation on it.
|
|
734
|
+
__sigTestResults.push({ idx: ${i}, passed: false, threw: true, error: e.message || String(e) });
|
|
725
735
|
}
|
|
726
736
|
`
|
|
727
737
|
})
|
|
@@ -834,62 +844,62 @@ export function runAllTests(
|
|
|
834
844
|
typeMatches
|
|
835
845
|
)
|
|
836
846
|
|
|
837
|
-
// Map block test results
|
|
847
|
+
// Map block test results. A block test that threw a ReferenceError
|
|
848
|
+
// couldn't *run* (it names something TJS can't resolve at build time, e.g.
|
|
849
|
+
// an AJS atom) → inconclusive, not failed. An assertion failure (plain
|
|
850
|
+
// Error from expect()) is a genuine failure.
|
|
838
851
|
for (const r of blockResults) {
|
|
839
852
|
const test = tests[r.idx]
|
|
840
|
-
|
|
841
|
-
const isImportError =
|
|
842
|
-
hasUnresolvedImports &&
|
|
853
|
+
const inconclusive =
|
|
843
854
|
!r.passed &&
|
|
844
|
-
r.
|
|
845
|
-
|
|
855
|
+
(r.isRef ||
|
|
856
|
+
(hasUnresolvedImports && r.error && /is not defined$/.test(r.error)))
|
|
846
857
|
results.push({
|
|
847
858
|
description: test.description,
|
|
848
|
-
passed:
|
|
849
|
-
|
|
859
|
+
passed: r.passed,
|
|
860
|
+
inconclusive: inconclusive || undefined,
|
|
861
|
+
error: r.error,
|
|
850
862
|
line: test.line,
|
|
851
863
|
})
|
|
852
864
|
}
|
|
853
865
|
|
|
854
|
-
// Map signature test results
|
|
866
|
+
// Map signature test results. A test that *threw* (couldn't evaluate the
|
|
867
|
+
// example) is inconclusive; only a clean value-mismatch is a failure.
|
|
855
868
|
for (const r of sigTestResults) {
|
|
856
869
|
const info = syncSigTestInfos[r.idx]
|
|
857
|
-
|
|
858
|
-
const isImportError =
|
|
859
|
-
hasUnresolvedImports &&
|
|
870
|
+
const inconclusive =
|
|
860
871
|
!r.passed &&
|
|
861
|
-
r.
|
|
862
|
-
|
|
872
|
+
(r.threw ||
|
|
873
|
+
(hasUnresolvedImports && r.error && /is not defined$/.test(r.error)))
|
|
863
874
|
const label = info.className
|
|
864
875
|
? `${info.className}.${info.funcName}`
|
|
865
876
|
: info.funcName
|
|
866
877
|
results.push({
|
|
867
878
|
description: `${label} signature example`,
|
|
868
|
-
passed:
|
|
869
|
-
|
|
879
|
+
passed: r.passed,
|
|
880
|
+
inconclusive: inconclusive || undefined,
|
|
881
|
+
error: r.error,
|
|
870
882
|
isSignatureTest: true,
|
|
871
883
|
line: info.line,
|
|
872
884
|
})
|
|
873
885
|
}
|
|
874
886
|
} catch (e: any) {
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
// through the runtime console.
|
|
887
|
+
// The whole module failed to execute, so NO test could run — every result
|
|
888
|
+
// here is inconclusive, never a failure. Making transpilation fail because
|
|
889
|
+
// an auto-generated test harness couldn't execute the module would turn
|
|
890
|
+
// legal code illegal (e.g. a module referencing AJS atoms, or — an edge —
|
|
891
|
+
// two top-level functions on one line). See PRINCIPLES.md.
|
|
892
|
+
//
|
|
893
|
+
// No line is attributed: the error came from module-level execution, not a
|
|
894
|
+
// specific function, and mis-attributing it would point the editor at the
|
|
895
|
+
// wrong site. The message is preserved as a warning on each result.
|
|
896
|
+
const note = `Module could not be executed for testing: ${e.message}`
|
|
886
897
|
for (const test of tests) {
|
|
887
898
|
results.push({
|
|
888
899
|
description: test.description,
|
|
889
|
-
passed:
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
: `Module execution failed: ${e.message}`,
|
|
900
|
+
passed: false,
|
|
901
|
+
inconclusive: true,
|
|
902
|
+
error: note,
|
|
893
903
|
})
|
|
894
904
|
}
|
|
895
905
|
for (const info of syncSigTestInfos) {
|
|
@@ -898,10 +908,9 @@ export function runAllTests(
|
|
|
898
908
|
: info.funcName
|
|
899
909
|
results.push({
|
|
900
910
|
description: `${label} signature example`,
|
|
901
|
-
passed:
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
: `Module execution failed: ${e.message}`,
|
|
911
|
+
passed: false,
|
|
912
|
+
inconclusive: true,
|
|
913
|
+
error: note,
|
|
905
914
|
isSignatureTest: true,
|
|
906
915
|
})
|
|
907
916
|
}
|
package/src/lang/emitters/js.ts
CHANGED
|
@@ -81,6 +81,18 @@ export interface TJSTranspileOptions {
|
|
|
81
81
|
sourceMap?: boolean
|
|
82
82
|
/** Mode: 'dev' | 'strict' | 'production' */
|
|
83
83
|
mode?: 'dev' | 'strict' | 'production'
|
|
84
|
+
/**
|
|
85
|
+
* Source dialect — what kind of source this string is:
|
|
86
|
+
* - `'tjs'` (default for a bare string): native TJS, footgun-removal modes ON
|
|
87
|
+
* (structural `==`, `TjsStandard`, etc.).
|
|
88
|
+
* - `'js'`: plain JavaScript — modes OFF, `safety: 'none'`; the source's own
|
|
89
|
+
* semantics are preserved (no `==`→`Eq`, no truthiness rewrite, no input
|
|
90
|
+
* validation). Use this when feeding tjs() a vanilla `.js` string so it
|
|
91
|
+
* transpiles without changing meaning. See PRINCIPLES.md (TJS ⊇ JS).
|
|
92
|
+
*
|
|
93
|
+
* For TypeScript, use the `fromTS` entry point (TS → TJS → JS).
|
|
94
|
+
*/
|
|
95
|
+
dialect?: 'js' | 'tjs'
|
|
84
96
|
/**
|
|
85
97
|
* Test execution mode:
|
|
86
98
|
* - true (default): run tests at transpile time, throw on failure
|
|
@@ -640,6 +652,7 @@ export function transpileToJS(
|
|
|
640
652
|
} = parse(cleanSource, {
|
|
641
653
|
filename,
|
|
642
654
|
colonShorthand: true,
|
|
655
|
+
dialect: options.dialect,
|
|
643
656
|
moduleLoader: options.moduleLoader,
|
|
644
657
|
})
|
|
645
658
|
|
|
@@ -650,6 +663,7 @@ export function transpileToJS(
|
|
|
650
663
|
// Pass through the moduleLoader so Phase 3 cross-file wasm composition
|
|
651
664
|
// sees imported `wasm function` declarations.
|
|
652
665
|
const preprocessed = preprocess(cleanSource, {
|
|
666
|
+
dialect: options.dialect,
|
|
653
667
|
moduleLoader: options.moduleLoader,
|
|
654
668
|
filename,
|
|
655
669
|
})
|
|
@@ -1119,8 +1133,11 @@ export function transpileToJS(
|
|
|
1119
1133
|
)
|
|
1120
1134
|
|
|
1121
1135
|
// Check for failures and throw only if runTests === true (strict mode)
|
|
1122
|
-
// 'only' and 'report' modes return results without throwing
|
|
1123
|
-
|
|
1136
|
+
// 'only' and 'report' modes return results without throwing.
|
|
1137
|
+
// Inconclusive results (a test that couldn't *run* — undefined refs like AJS
|
|
1138
|
+
// atoms, or a module the harness can't execute) never block transpilation:
|
|
1139
|
+
// that would turn subset-legal code illegal. See PRINCIPLES.md.
|
|
1140
|
+
const failures = testResults.filter((r) => !r.passed && !r.inconclusive)
|
|
1124
1141
|
if (failures.length > 0 && runTests === true) {
|
|
1125
1142
|
const errorLines = failures.map((f) => {
|
|
1126
1143
|
if (f.isSignatureTest) {
|
|
@@ -1097,14 +1097,15 @@ describe('signature tests (transpile-time)', () => {
|
|
|
1097
1097
|
`,
|
|
1098
1098
|
{ runTests: 'report' }
|
|
1099
1099
|
)
|
|
1100
|
-
//
|
|
1100
|
+
// The test can't run (unresolved import) → inconclusive, not failed.
|
|
1101
|
+
// Inconclusive never blocks transpilation (see PRINCIPLES.md).
|
|
1101
1102
|
expect(result.testResults).toBeDefined()
|
|
1102
1103
|
const sigTest = result.testResults!.find((t: any) =>
|
|
1103
1104
|
t.description.includes('validateUser')
|
|
1104
1105
|
)
|
|
1105
1106
|
expect(sigTest).toBeDefined()
|
|
1106
|
-
expect(sigTest?.passed).toBe(
|
|
1107
|
-
expect(sigTest?.
|
|
1107
|
+
expect(sigTest?.passed).toBe(false)
|
|
1108
|
+
expect(sigTest?.inconclusive).toBe(true)
|
|
1108
1109
|
})
|
|
1109
1110
|
|
|
1110
1111
|
it('should skip tests gracefully when imports are unresolved (function-level)', () => {
|
|
@@ -1124,8 +1125,8 @@ describe('signature tests (transpile-time)', () => {
|
|
|
1124
1125
|
t.description.includes('formatDate')
|
|
1125
1126
|
)
|
|
1126
1127
|
expect(sigTest).toBeDefined()
|
|
1127
|
-
expect(sigTest?.passed).toBe(
|
|
1128
|
-
expect(sigTest?.
|
|
1128
|
+
expect(sigTest?.passed).toBe(false)
|
|
1129
|
+
expect(sigTest?.inconclusive).toBe(true)
|
|
1129
1130
|
})
|
|
1130
1131
|
|
|
1131
1132
|
it('should test sync functions alongside async functions', () => {
|
package/src/lang/index.ts
CHANGED
|
@@ -27,11 +27,45 @@ import type {
|
|
|
27
27
|
TranspileResult,
|
|
28
28
|
FunctionSignature,
|
|
29
29
|
} from './types'
|
|
30
|
-
import { parse,
|
|
30
|
+
import { parse, extractFunctions } from './parser'
|
|
31
31
|
import { transformFunction } from './emitters/ast'
|
|
32
32
|
|
|
33
33
|
export * from './types'
|
|
34
|
-
export {
|
|
34
|
+
export {
|
|
35
|
+
parse,
|
|
36
|
+
preprocess,
|
|
37
|
+
extractTDoc,
|
|
38
|
+
validateSingleFunction,
|
|
39
|
+
extractFunctions,
|
|
40
|
+
} from './parser'
|
|
41
|
+
export {
|
|
42
|
+
dialectForFilename,
|
|
43
|
+
sourceKindForFilename,
|
|
44
|
+
type Dialect,
|
|
45
|
+
type SourceKind,
|
|
46
|
+
} from './dialect'
|
|
47
|
+
export {
|
|
48
|
+
verifyPredicate,
|
|
49
|
+
compilePredicate,
|
|
50
|
+
suggest,
|
|
51
|
+
effectfulFromAtoms,
|
|
52
|
+
formatPredicateDiagnostics,
|
|
53
|
+
PredicateFuelExhausted,
|
|
54
|
+
type PredicateDiagnostic,
|
|
55
|
+
type PredicateVerifyResult,
|
|
56
|
+
type VerifyPredicateOptions,
|
|
57
|
+
type CompilePredicateOptions,
|
|
58
|
+
type Suggestion,
|
|
59
|
+
type SuggestOptions,
|
|
60
|
+
} from './predicate'
|
|
61
|
+
export {
|
|
62
|
+
compilePredicateSchema,
|
|
63
|
+
validatePredicateSchema,
|
|
64
|
+
type PredicateSchema,
|
|
65
|
+
type SchemaError,
|
|
66
|
+
type SchemaValidationResult,
|
|
67
|
+
type PredicateSchemaOptions,
|
|
68
|
+
} from './predicate-schema'
|
|
35
69
|
export { transformFunction } from './emitters/ast'
|
|
36
70
|
export {
|
|
37
71
|
transpileToJS,
|
|
@@ -177,15 +211,16 @@ export function transpile(
|
|
|
177
211
|
})
|
|
178
212
|
|
|
179
213
|
// Validate structure
|
|
180
|
-
const
|
|
214
|
+
const { entry, helpers } = extractFunctions(program, options.filename)
|
|
181
215
|
|
|
182
216
|
// Transform to Agent99 AST
|
|
183
217
|
const { ast, signature, warnings } = transformFunction(
|
|
184
|
-
|
|
218
|
+
entry,
|
|
185
219
|
originalSource,
|
|
186
220
|
returnType,
|
|
187
221
|
options,
|
|
188
|
-
requiredParams
|
|
222
|
+
requiredParams,
|
|
223
|
+
helpers.size > 0 ? helpers : undefined
|
|
189
224
|
)
|
|
190
225
|
|
|
191
226
|
return {
|
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
|
*
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
compilePredicateSchema,
|
|
4
|
+
validatePredicateSchema,
|
|
5
|
+
type PredicateSchema,
|
|
6
|
+
} from './predicate-schema'
|
|
7
|
+
|
|
8
|
+
// A composable color predicate cluster (entry = last function, takes the value).
|
|
9
|
+
const COLOR = `
|
|
10
|
+
function isHex(v){ return typeof v == 'string' && /^#[0-9a-f]{3,8}$/i.test(v) }
|
|
11
|
+
function isVar(v){ return typeof v == 'string' && v.startsWith('var(--') && v.endsWith(')') }
|
|
12
|
+
function isCalc(v){ return typeof v == 'string' && v.startsWith('calc(') && v.endsWith(')') }
|
|
13
|
+
function isColor(v){ return isHex(v) || isVar(v) || isCalc(v) }
|
|
14
|
+
`
|
|
15
|
+
|
|
16
|
+
// A recursive predicate cluster — validates a tree of string/number leaves.
|
|
17
|
+
// Note the `!Array.isArray(o)` guard: arrays are objects in JS, so a node check
|
|
18
|
+
// must exclude them explicitly (a real predicate-authoring nuance).
|
|
19
|
+
const TREE = `
|
|
20
|
+
function isLeaf(v){ return typeof v == 'string' || typeof v == 'number' }
|
|
21
|
+
function isEntry(pair){ var val = pair[1]; return isLeaf(val) || isNode(val) }
|
|
22
|
+
function isNode(o){ return typeof o == 'object' && o != null && !Array.isArray(o) && Object.entries(o).every(isEntry) }
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
const colorSchema: PredicateSchema = {
|
|
26
|
+
type: 'object',
|
|
27
|
+
required: ['color'],
|
|
28
|
+
properties: {
|
|
29
|
+
color: { type: 'string', description: 'a CSS color', $predicate: COLOR },
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('predicate-schema — the computational half of JSON-Schema', () => {
|
|
34
|
+
it('a predicate-aware validator validates the value grammar', () => {
|
|
35
|
+
const validate = compilePredicateSchema(colorSchema)
|
|
36
|
+
expect(validate({ color: '#3a3' }).valid).toBe(true)
|
|
37
|
+
expect(validate({ color: 'var(--brand)' }).valid).toBe(true) // TS/JSON-Schema can't
|
|
38
|
+
expect(validate({ color: 'calc(1px + 1em)' }).valid).toBe(true)
|
|
39
|
+
|
|
40
|
+
const bad = validate({ color: 'notacolor' })
|
|
41
|
+
expect(bad.valid).toBe(false)
|
|
42
|
+
expect(bad.errors[0].path).toBe('/color')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('still does structural validation (type / required)', () => {
|
|
46
|
+
const validate = compilePredicateSchema(colorSchema)
|
|
47
|
+
expect(validate({ color: 123 }).errors[0].message).toMatch(
|
|
48
|
+
/expected string/
|
|
49
|
+
)
|
|
50
|
+
expect(validate({}).errors[0].message).toMatch(/missing required 'color'/)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('progressive enhancement: a naive validator ignores `$predicate`', () => {
|
|
54
|
+
// ignorePredicates models any standard JSON-Schema validator that doesn't
|
|
55
|
+
// know the keyword — it sees only `type: string`.
|
|
56
|
+
const naive = compilePredicateSchema(colorSchema, {
|
|
57
|
+
ignorePredicates: true,
|
|
58
|
+
})
|
|
59
|
+
expect(naive({ color: 'notacolor' }).valid).toBe(true) // string → passes
|
|
60
|
+
// …while the aware validator catches it:
|
|
61
|
+
expect(
|
|
62
|
+
compilePredicateSchema(colorSchema)({ color: 'notacolor' }).valid
|
|
63
|
+
).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('the schema is plain serializable JSON — code travels as data', () => {
|
|
67
|
+
const roundTripped: PredicateSchema = JSON.parse(
|
|
68
|
+
JSON.stringify(colorSchema)
|
|
69
|
+
)
|
|
70
|
+
expect(roundTripped).toEqual(colorSchema)
|
|
71
|
+
// and it still validates after a serialize/deserialize round-trip
|
|
72
|
+
const validate = compilePredicateSchema(roundTripped)
|
|
73
|
+
expect(validate({ color: '#abc' }).valid).toBe(true)
|
|
74
|
+
expect(validate({ color: 'nope' }).valid).toBe(false)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('a `$predicate` can recurse into open nested structure', () => {
|
|
78
|
+
const schema: PredicateSchema = { type: 'object', $predicate: TREE }
|
|
79
|
+
const validate = compilePredicateSchema(schema)
|
|
80
|
+
expect(validate({ a: 1, b: { c: 'x', d: { e: 2 } } }).valid).toBe(true)
|
|
81
|
+
expect(validate({ a: 1, bad: { nope: [] } }).valid).toBe(false) // array leaf
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('rejects an unsafe predicate at compile time (IO never embeds)', () => {
|
|
85
|
+
const evil: PredicateSchema = {
|
|
86
|
+
type: 'string',
|
|
87
|
+
$predicate: `function check(v){ return fetch(v) }`,
|
|
88
|
+
}
|
|
89
|
+
expect(() => compilePredicateSchema(evil)).toThrow(/Not predicate-safe/)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('one-shot helper works too', () => {
|
|
93
|
+
expect(validatePredicateSchema(colorSchema, { color: '#fff' }).valid).toBe(
|
|
94
|
+
true
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
})
|