tjs-lang 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/demo/docs.json +9 -9
- package/demo/src/demo-nav.ts +51 -136
- package/demo/src/index.ts +35 -68
- package/demo/src/ts-playground.ts +18 -8
- package/dist/index.js +126 -112
- package/dist/index.js.map +11 -11
- package/dist/src/lang/emitters/js.d.ts +2 -2
- package/dist/src/lang/inference.d.ts +2 -2
- package/dist/src/test-examples.d.ts +41 -0
- package/dist/tjs-full.js +126 -112
- package/dist/tjs-full.js.map +11 -11
- package/dist/tjs-transpiler.js +102 -88
- package/dist/tjs-transpiler.js.map +8 -8
- package/dist/tjs-vm.js +18 -18
- package/dist/tjs-vm.js.map +5 -5
- package/package.json +1 -1
- package/src/lang/codegen.test.ts +76 -16
- package/src/lang/emitters/from-ts.ts +8 -28
- package/src/lang/emitters/js.ts +97 -7
- package/src/lang/eval.ts +41 -2
- package/src/lang/from-ts.test.ts +3 -3
- package/src/lang/inference.ts +34 -20
- package/src/lang/parser.test.ts +4 -4
- package/src/lang/transpiler.test.ts +5 -3
- package/src/lang/typescript-syntax.test.ts +20 -17
- package/src/runtime.test.ts +144 -13
- package/src/test-examples.test.ts +4 -12
- package/src/test-examples.ts +3 -1
- package/src/vm/runtime.ts +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tjs-lang",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Type-safe JavaScript dialect with runtime validation, sandboxed VM execution, and AI agent orchestration. Transpiles TypeScript to validated JS with fuel-metered execution for untrusted code.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
package/src/lang/codegen.test.ts
CHANGED
|
@@ -28,11 +28,11 @@ describe('TS → TJS conversion quality', () => {
|
|
|
28
28
|
expect(code).not.toContain('x: number')
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
it('converts optional param to
|
|
31
|
+
it('converts optional param to union with undefined', () => {
|
|
32
32
|
const ts = `function greet(name?: string): string { return name || 'World' }`
|
|
33
33
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
34
34
|
|
|
35
|
-
expect(code).toContain("name
|
|
35
|
+
expect(code).toContain("name: '' | undefined")
|
|
36
36
|
expect(code).not.toContain('name?')
|
|
37
37
|
})
|
|
38
38
|
|
|
@@ -47,7 +47,14 @@ describe('TS → TJS conversion quality', () => {
|
|
|
47
47
|
const ts = `function toggle(flag: boolean): boolean { return !flag }`
|
|
48
48
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
49
49
|
|
|
50
|
-
expect(code).toContain('flag:
|
|
50
|
+
expect(code).toContain('flag: false')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('converts optional boolean param to union with undefined', () => {
|
|
54
|
+
const ts = `function greet(name: string, excited?: boolean): string { return excited ? name + '!' : name }`
|
|
55
|
+
const { code } = fromTS(ts, { emitTJS: true })
|
|
56
|
+
|
|
57
|
+
expect(code).toContain('excited: false | undefined')
|
|
51
58
|
})
|
|
52
59
|
|
|
53
60
|
it('converts array param correctly', () => {
|
|
@@ -78,7 +85,7 @@ describe('TS → TJS conversion quality', () => {
|
|
|
78
85
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
79
86
|
|
|
80
87
|
expect(code).toContain("url: ''")
|
|
81
|
-
expect(code).toContain('timeout
|
|
88
|
+
expect(code).toContain('timeout: 0.0 | undefined')
|
|
82
89
|
})
|
|
83
90
|
})
|
|
84
91
|
|
|
@@ -101,7 +108,7 @@ describe('TS → TJS conversion quality', () => {
|
|
|
101
108
|
const ts = `function isValid(): boolean { return true }`
|
|
102
109
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
103
110
|
|
|
104
|
-
expect(code).toContain('-!
|
|
111
|
+
expect(code).toContain('-! false')
|
|
105
112
|
})
|
|
106
113
|
|
|
107
114
|
it('converts object return type to -! syntax', () => {
|
|
@@ -288,18 +295,18 @@ class Api {
|
|
|
288
295
|
})
|
|
289
296
|
|
|
290
297
|
describe('nullable types', () => {
|
|
291
|
-
it('converts T | null to T
|
|
298
|
+
it('converts T | null to T | null', () => {
|
|
292
299
|
const ts = `function maybe(x: string | null): string | null { return x }`
|
|
293
300
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
294
301
|
|
|
295
|
-
expect(code).toContain("''
|
|
302
|
+
expect(code).toContain("'' | null")
|
|
296
303
|
})
|
|
297
304
|
|
|
298
|
-
it('converts T | undefined to T
|
|
305
|
+
it('converts T | undefined to T | undefined', () => {
|
|
299
306
|
const ts = `function maybe(x: number | undefined): number | undefined { return x }`
|
|
300
307
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
301
308
|
|
|
302
|
-
expect(code).toContain('0
|
|
309
|
+
expect(code).toContain('0 | undefined')
|
|
303
310
|
})
|
|
304
311
|
})
|
|
305
312
|
|
|
@@ -371,12 +378,21 @@ class Api {
|
|
|
371
378
|
|
|
372
379
|
describe('TJS → JS transpilation quality', () => {
|
|
373
380
|
describe('colon syntax transformation', () => {
|
|
374
|
-
it('
|
|
381
|
+
it('strips colon type annotations from output (required params get no default)', () => {
|
|
375
382
|
const source = `function greet(name: 'World') { return name }`
|
|
376
383
|
const { code } = tjs(source)
|
|
377
384
|
|
|
378
|
-
|
|
385
|
+
// Required params (`:` syntax) should have no default in JS
|
|
386
|
+
expect(code).toContain('function greet(name)')
|
|
379
387
|
expect(code).not.toContain("name: 'World'")
|
|
388
|
+
expect(code).not.toContain('name = ')
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('preserves defaults for optional params (= syntax)', () => {
|
|
392
|
+
const source = `function greet(name = 'World') { return name }`
|
|
393
|
+
const { code } = tjs(source)
|
|
394
|
+
|
|
395
|
+
expect(code).toContain("name = 'World'")
|
|
380
396
|
})
|
|
381
397
|
})
|
|
382
398
|
|
|
@@ -624,12 +640,12 @@ console.log(second())
|
|
|
624
640
|
expect(code).not.toContain('y: string')
|
|
625
641
|
})
|
|
626
642
|
|
|
627
|
-
it('uses
|
|
643
|
+
it('uses union with undefined for optional params', () => {
|
|
628
644
|
const ts = `function test(x?: number, y?: string): void { }`
|
|
629
645
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
630
646
|
|
|
631
|
-
expect(code).toContain('x
|
|
632
|
-
expect(code).toContain("y
|
|
647
|
+
expect(code).toContain('x: 0.0 | undefined')
|
|
648
|
+
expect(code).toContain("y: '' | undefined")
|
|
633
649
|
})
|
|
634
650
|
|
|
635
651
|
it('uses -! syntax for return types (skip signature test)', () => {
|
|
@@ -1247,7 +1263,9 @@ function test(required: string, optional?: number): void { }
|
|
|
1247
1263
|
const { types } = tjs(tjsCode)
|
|
1248
1264
|
|
|
1249
1265
|
expect(types?.test?.params?.required?.required).toBe(true)
|
|
1250
|
-
|
|
1266
|
+
// TS optional becomes TJS union with undefined (required param that accepts undefined)
|
|
1267
|
+
expect(types?.test?.params?.optional?.required).toBe(true)
|
|
1268
|
+
expect(types?.test?.params?.optional?.type?.kind).toBe('union')
|
|
1251
1269
|
})
|
|
1252
1270
|
})
|
|
1253
1271
|
})
|
|
@@ -1780,7 +1798,7 @@ function format(x: '') -! '' { return x + '!' }
|
|
|
1780
1798
|
})
|
|
1781
1799
|
|
|
1782
1800
|
it('error short-circuits function body', () => {
|
|
1783
|
-
|
|
1801
|
+
const bodyExecuted = false
|
|
1784
1802
|
|
|
1785
1803
|
const { code } = tjs(`
|
|
1786
1804
|
function process(x: '') -! '' {
|
|
@@ -1927,4 +1945,46 @@ function divide(a: 10, b: 2) -? { value: 0, error = '' } {
|
|
|
1927
1945
|
expect(result.code).toContain('"error"')
|
|
1928
1946
|
})
|
|
1929
1947
|
})
|
|
1948
|
+
|
|
1949
|
+
describe('union param JS output', () => {
|
|
1950
|
+
it('required union param has no default in JS', () => {
|
|
1951
|
+
const result = tjs('function f(x: false | undefined) { return x }')
|
|
1952
|
+
expect(result.code).toContain('function f(x)')
|
|
1953
|
+
expect(result.code).not.toMatch(/x = false/)
|
|
1954
|
+
expect(result.code).not.toMatch(/x = false \| undefined/)
|
|
1955
|
+
})
|
|
1956
|
+
|
|
1957
|
+
it('optional union param keeps first value as default', () => {
|
|
1958
|
+
const result = tjs('function f(x = false | undefined) { return x }')
|
|
1959
|
+
expect(result.code).toContain('x = false')
|
|
1960
|
+
expect(result.code).not.toMatch(/x = false \| undefined/)
|
|
1961
|
+
})
|
|
1962
|
+
|
|
1963
|
+
it('generates union type check for required union param', () => {
|
|
1964
|
+
const result = tjs('function f(x: false | undefined) { return x }')
|
|
1965
|
+
expect(result.code).toContain("typeof x !== 'boolean'")
|
|
1966
|
+
expect(result.code).toContain('x !== undefined')
|
|
1967
|
+
})
|
|
1968
|
+
|
|
1969
|
+
it('generates nullable integer check for int|null', () => {
|
|
1970
|
+
const result = tjs('function f(x: 0 | null) { return x }')
|
|
1971
|
+
expect(result.code).toContain('function f(x)')
|
|
1972
|
+
// 0 | null is parsed as { kind: 'integer', nullable: true }, not a union
|
|
1973
|
+
expect(result.code).toContain("'integer'")
|
|
1974
|
+
// Nullable types skip the check when value is null
|
|
1975
|
+
expect(result.metadata?.f?.params?.x?.type?.nullable).toBe(true)
|
|
1976
|
+
})
|
|
1977
|
+
|
|
1978
|
+
it('preserves | in function body (bitwise OR)', () => {
|
|
1979
|
+
const result = tjs('function f(x: 0) { return x | 0xFF }')
|
|
1980
|
+
expect(result.code).toContain('x | 0xFF')
|
|
1981
|
+
})
|
|
1982
|
+
|
|
1983
|
+
it('preserves union metadata despite stripping from JS', () => {
|
|
1984
|
+
const result = tjs('function f(x: false | undefined) { return x }')
|
|
1985
|
+
const meta = result.metadata?.f
|
|
1986
|
+
expect(meta).toBeDefined()
|
|
1987
|
+
expect(meta.params.x.type.kind).toBe('union')
|
|
1988
|
+
})
|
|
1989
|
+
})
|
|
1930
1990
|
})
|
|
@@ -139,7 +139,7 @@ function typeToExample(
|
|
|
139
139
|
case ts.SyntaxKind.NumberKeyword:
|
|
140
140
|
return '0.0'
|
|
141
141
|
case ts.SyntaxKind.BooleanKeyword:
|
|
142
|
-
return '
|
|
142
|
+
return 'false'
|
|
143
143
|
case ts.SyntaxKind.NullKeyword:
|
|
144
144
|
return 'null'
|
|
145
145
|
case ts.SyntaxKind.UndefinedKeyword:
|
|
@@ -296,10 +296,10 @@ function typeToExample(
|
|
|
296
296
|
const hasUndefined = unionType.types.some(isUndefinedType)
|
|
297
297
|
|
|
298
298
|
if (nonNullTypes.length === 1 && (hasNull || hasUndefined)) {
|
|
299
|
-
// Nullable type: T | null -> T
|
|
299
|
+
// Nullable type: T | null -> T | null
|
|
300
300
|
const baseExample = typeToExample(nonNullTypes[0], checker)
|
|
301
|
-
if (hasNull) return `${baseExample}
|
|
302
|
-
if (hasUndefined) return `${baseExample}
|
|
301
|
+
if (hasNull) return `${baseExample} | null`
|
|
302
|
+
if (hasUndefined) return `${baseExample} | undefined`
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
// General union: use first type as example
|
|
@@ -916,7 +916,7 @@ function transformFunctionToTJS(
|
|
|
916
916
|
warnings?: string[],
|
|
917
917
|
includeLineNumber?: boolean
|
|
918
918
|
): string {
|
|
919
|
-
const params
|
|
919
|
+
const params = transformParams(node.parameters, sourceFile, warnings)
|
|
920
920
|
|
|
921
921
|
// Get line number (1-indexed) for source mapping
|
|
922
922
|
const { line } = sourceFile.getLineAndCharacterOfPosition(
|
|
@@ -924,27 +924,6 @@ function transformFunctionToTJS(
|
|
|
924
924
|
)
|
|
925
925
|
const lineComment = includeLineNumber ? `/* line ${line + 1} */\n` : ''
|
|
926
926
|
|
|
927
|
-
for (const param of node.parameters) {
|
|
928
|
-
const name = param.name.getText(sourceFile)
|
|
929
|
-
const isOptional = !!param.questionToken || !!param.initializer
|
|
930
|
-
const typeExample = typeToExample(param.type, undefined, warnings)
|
|
931
|
-
|
|
932
|
-
if (param.initializer) {
|
|
933
|
-
// Has default value - use it directly
|
|
934
|
-
const defaultText = param.initializer.getText(sourceFile)
|
|
935
|
-
params.push(`${name} = ${defaultText}`)
|
|
936
|
-
} else if (typeExample === 'any' || typeExample === 'undefined') {
|
|
937
|
-
// any/undefined type - no annotation in TJS (bare name means any)
|
|
938
|
-
params.push(name)
|
|
939
|
-
} else if (isOptional) {
|
|
940
|
-
// Optional without default - use = for optional
|
|
941
|
-
params.push(`${name} = ${typeExample}`)
|
|
942
|
-
} else {
|
|
943
|
-
// Required - use : for required
|
|
944
|
-
params.push(`${name}: ${typeExample}`)
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
927
|
const funcName =
|
|
949
928
|
explicitName ||
|
|
950
929
|
(ts.isFunctionDeclaration(node) && node.name
|
|
@@ -1203,8 +1182,9 @@ function transformParams(
|
|
|
1203
1182
|
// any/undefined type - no annotation in TJS (bare name means any)
|
|
1204
1183
|
params.push(name)
|
|
1205
1184
|
} else if (isOptional) {
|
|
1206
|
-
// Optional without default - use
|
|
1207
|
-
|
|
1185
|
+
// Optional without default - use union with undefined to preserve
|
|
1186
|
+
// three-state semantics (e.g. TS `flag?: boolean` can be true/false/undefined)
|
|
1187
|
+
params.push(`${name}: ${typeExample} | undefined`)
|
|
1208
1188
|
} else {
|
|
1209
1189
|
// Required - use : for required
|
|
1210
1190
|
params.push(`${name}: ${typeExample}`)
|
package/src/lang/emitters/js.ts
CHANGED
|
@@ -139,6 +139,26 @@ export interface TJSTypeInfo {
|
|
|
139
139
|
destructuredRequired?: Set<string>
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Check if a param used `:` (required) or `=` (optional) in the raw source.
|
|
144
|
+
* Finds the function's param list by name, then looks for `paramName:` vs `paramName =`.
|
|
145
|
+
*/
|
|
146
|
+
function isParamRequiredInSource(
|
|
147
|
+
source: string,
|
|
148
|
+
funcName: string,
|
|
149
|
+
paramName: string
|
|
150
|
+
): boolean {
|
|
151
|
+
if (!source || !funcName) return false
|
|
152
|
+
// Find the function declaration and its param list
|
|
153
|
+
const funcPattern = new RegExp(
|
|
154
|
+
`function\\s+${funcName}\\s*\\([^)]*?\\b${paramName}\\s*([=:])`,
|
|
155
|
+
's'
|
|
156
|
+
)
|
|
157
|
+
const match = source.match(funcPattern)
|
|
158
|
+
if (!match) return false
|
|
159
|
+
return match[1] === ':'
|
|
160
|
+
}
|
|
161
|
+
|
|
142
162
|
/**
|
|
143
163
|
* Extract type info for a single function declaration
|
|
144
164
|
*/
|
|
@@ -146,7 +166,8 @@ function extractFunctionTypeInfo(
|
|
|
146
166
|
func: FunctionDeclaration,
|
|
147
167
|
originalSource: string,
|
|
148
168
|
requiredParams: Set<string>,
|
|
149
|
-
returnTypeStr: string | null
|
|
169
|
+
returnTypeStr: string | null,
|
|
170
|
+
inputSource?: string
|
|
150
171
|
): { types: TJSTypeInfo; warnings: string[] } {
|
|
151
172
|
const warnings: string[] = []
|
|
152
173
|
|
|
@@ -205,9 +226,19 @@ function extractFunctionTypeInfo(
|
|
|
205
226
|
param.left.type === 'Identifier'
|
|
206
227
|
) {
|
|
207
228
|
const paramInfo = parseParameter(param, requiredParams)
|
|
229
|
+
// Determine if this param used `:` (required) or `=` (optional).
|
|
230
|
+
// The global requiredParams set is name-based, which fails when
|
|
231
|
+
// two functions share a param name with different syntax.
|
|
232
|
+
// Use the raw input source to check the actual syntax.
|
|
233
|
+
const isRequired = isParamRequiredInSource(
|
|
234
|
+
inputSource || '',
|
|
235
|
+
func.id?.name || '',
|
|
236
|
+
param.left.name
|
|
237
|
+
)
|
|
208
238
|
params[param.left.name] = {
|
|
209
239
|
...paramInfo,
|
|
210
|
-
required:
|
|
240
|
+
required: isRequired,
|
|
241
|
+
default: isRequired ? null : paramInfo.example ?? paramInfo.default,
|
|
211
242
|
description: tdoc.params[param.left.name],
|
|
212
243
|
}
|
|
213
244
|
} else if (param.type === 'ObjectPattern') {
|
|
@@ -346,7 +377,10 @@ function generateInlineValidationCode(
|
|
|
346
377
|
const typeCheck = generateTypeCheckExpr(paramName, param.type)
|
|
347
378
|
|
|
348
379
|
if (typeCheck) {
|
|
349
|
-
const expectedType =
|
|
380
|
+
const expectedType =
|
|
381
|
+
param.type.kind === 'union'
|
|
382
|
+
? (param.type as any).members.map((m: any) => m.kind).join(' | ')
|
|
383
|
+
: param.type.kind
|
|
350
384
|
if (param.required) {
|
|
351
385
|
lines.push(
|
|
352
386
|
`if (${typeCheck}) return __tjs.typeError('${path}', '${expectedType}', ${paramName});`
|
|
@@ -533,6 +567,9 @@ export function transpileToJS(
|
|
|
533
567
|
|
|
534
568
|
// Collect insertions: { position, text } to be applied in reverse order
|
|
535
569
|
const insertions: { position: number; text: string }[] = []
|
|
570
|
+
// Collect deletions for | union suffixes in param defaults
|
|
571
|
+
// e.g. `x = false | undefined` -> `x = false` (the `| undefined` is type-only)
|
|
572
|
+
const deletions: { start: number; end: number }[] = []
|
|
536
573
|
|
|
537
574
|
// Process each function
|
|
538
575
|
for (const func of functions) {
|
|
@@ -564,11 +601,40 @@ export function transpileToJS(
|
|
|
564
601
|
func,
|
|
565
602
|
originalSource,
|
|
566
603
|
requiredParams,
|
|
567
|
-
returnTypeStr
|
|
604
|
+
returnTypeStr,
|
|
605
|
+
cleanSource
|
|
568
606
|
)
|
|
569
607
|
warnings.push(...funcWarnings)
|
|
570
608
|
allTypes[funcName] = types
|
|
571
609
|
|
|
610
|
+
// Clean up param defaults in the emitted JS.
|
|
611
|
+
// After colon→equals transform, `x: false | undefined` becomes
|
|
612
|
+
// `x = false | undefined` in the parsed source.
|
|
613
|
+
// - For required params (`:` syntax), strip the entire `= value` — there's
|
|
614
|
+
// no JS default for required params, the value is a type annotation only.
|
|
615
|
+
// - For union defaults, strip just the `| suffix` to avoid bitwise OR.
|
|
616
|
+
for (const param of func.params) {
|
|
617
|
+
if (param.type === 'AssignmentPattern') {
|
|
618
|
+
const paramName =
|
|
619
|
+
(param as any).left?.name || (param as any).left?.value
|
|
620
|
+
const paramInfo = paramName ? types.params[paramName] : null
|
|
621
|
+
|
|
622
|
+
if (paramInfo?.required && paramInfo.default === null) {
|
|
623
|
+
// Required param — strip entire `= value` from JS output
|
|
624
|
+
deletions.push({
|
|
625
|
+
start: (param as any).left.end,
|
|
626
|
+
end: (param as any).right.end,
|
|
627
|
+
})
|
|
628
|
+
} else {
|
|
629
|
+
// Optional param with union — strip just the `| suffix`
|
|
630
|
+
const right = (param as any).right
|
|
631
|
+
if (right.type === 'BinaryExpression' && right.operator === '|') {
|
|
632
|
+
deletions.push({ start: right.left.end, end: right.end })
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
572
638
|
// Determine safety options
|
|
573
639
|
// Module-level "safety none" makes ALL functions unsafe (no validation)
|
|
574
640
|
const isUnsafe =
|
|
@@ -649,10 +715,27 @@ export function transpileToJS(
|
|
|
649
715
|
}
|
|
650
716
|
}
|
|
651
717
|
|
|
652
|
-
// Apply
|
|
653
|
-
|
|
654
|
-
|
|
718
|
+
// Apply deletions first (reverse order to maintain offsets), then insertions.
|
|
719
|
+
// Deletions strip | union suffixes from param defaults in the output JS.
|
|
720
|
+
deletions.sort((a, b) => b.start - a.start)
|
|
655
721
|
let code = preprocessed.source
|
|
722
|
+
for (const { start, end } of deletions) {
|
|
723
|
+
code = code.slice(0, start) + code.slice(end)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Adjust insertion positions for any deletions that shifted offsets
|
|
727
|
+
for (const ins of insertions) {
|
|
728
|
+
let shift = 0
|
|
729
|
+
for (const del of deletions) {
|
|
730
|
+
if (del.start < ins.position) {
|
|
731
|
+
shift += del.end - del.start
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
ins.position -= shift
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Apply insertions in reverse position order
|
|
738
|
+
insertions.sort((a, b) => b.position - a.position)
|
|
656
739
|
for (const { position, text } of insertions) {
|
|
657
740
|
code = code.slice(0, position) + text + code.slice(position)
|
|
658
741
|
}
|
|
@@ -1002,6 +1085,13 @@ function generateTypeCheckExpr(
|
|
|
1002
1085
|
case 'object':
|
|
1003
1086
|
// For nested objects, just check it's an object (deep validation is separate)
|
|
1004
1087
|
return `(typeof ${fieldPath} !== 'object' || ${fieldPath} === null || Array.isArray(${fieldPath}))`
|
|
1088
|
+
case 'union': {
|
|
1089
|
+
const checks = (type as any).members
|
|
1090
|
+
.map((m: TypeDescriptor) => generateTypeCheckExpr(fieldPath, m))
|
|
1091
|
+
.filter((c: string | null) => c !== null)
|
|
1092
|
+
if (checks.length === 0) return null
|
|
1093
|
+
return `(${checks.join(' && ')})`
|
|
1094
|
+
}
|
|
1005
1095
|
case 'any':
|
|
1006
1096
|
return null // No check needed
|
|
1007
1097
|
default:
|
package/src/lang/eval.ts
CHANGED
|
@@ -14,6 +14,27 @@ import { transpile } from './core'
|
|
|
14
14
|
let _vm: AgentVM<Record<string, never>> | null = null
|
|
15
15
|
const getVM = () => (_vm ??= new AgentVM())
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Walk an AST and wrap return values in { __result: value } objects.
|
|
19
|
+
* This lets Eval/SafeFunction return arbitrary values through the VM,
|
|
20
|
+
* which enforces strict object returns for agent composability.
|
|
21
|
+
*/
|
|
22
|
+
function wrapReturnValues(node: any): void {
|
|
23
|
+
if (!node || typeof node !== 'object') return
|
|
24
|
+
if (Array.isArray(node)) {
|
|
25
|
+
for (const child of node) wrapReturnValues(child)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
if (node.op === 'return' && 'value' in node) {
|
|
29
|
+
node.value = { __result: node.value }
|
|
30
|
+
}
|
|
31
|
+
// Recurse into steps (seq), branches (if/else), etc.
|
|
32
|
+
if (node.steps) wrapReturnValues(node.steps)
|
|
33
|
+
if (node.then) wrapReturnValues(node.then)
|
|
34
|
+
if (node.else) wrapReturnValues(node.else)
|
|
35
|
+
if (node.body) wrapReturnValues(node.body)
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
/** Capabilities that can be injected into SafeFunction/Eval */
|
|
18
39
|
export interface SafeCapabilities {
|
|
19
40
|
/** Fetch function for HTTP requests */
|
|
@@ -65,14 +86,24 @@ export async function Eval(options: EvalOptions): Promise<{
|
|
|
65
86
|
try {
|
|
66
87
|
const { ast } = transpile(wrappedCode)
|
|
67
88
|
|
|
89
|
+
// Box return values in objects for VM strict-return compliance.
|
|
90
|
+
// Walk AST and wrap each { op: 'return', value } into
|
|
91
|
+
// { op: 'return', value: { __result: originalValue } }
|
|
92
|
+
wrapReturnValues(ast)
|
|
93
|
+
|
|
68
94
|
const vmResult = await vm.run(ast, context, {
|
|
69
95
|
fuel,
|
|
70
96
|
timeoutMs,
|
|
71
97
|
capabilities,
|
|
72
98
|
})
|
|
73
99
|
|
|
100
|
+
// Unwrap the boxed result
|
|
101
|
+
const raw = vmResult.result
|
|
102
|
+
const result =
|
|
103
|
+
raw && typeof raw === 'object' && '__result' in raw ? raw.__result : raw
|
|
104
|
+
|
|
74
105
|
return {
|
|
75
|
-
result
|
|
106
|
+
result,
|
|
76
107
|
fuelUsed: vmResult.fuelUsed,
|
|
77
108
|
error: vmResult.error
|
|
78
109
|
? { message: vmResult.error.message || String(vmResult.error) }
|
|
@@ -128,6 +159,9 @@ export async function SafeFunction(options: SafeFunctionOptions): Promise<
|
|
|
128
159
|
// Pre-compile the AST (done once at creation time)
|
|
129
160
|
const { ast } = transpile(source)
|
|
130
161
|
|
|
162
|
+
// Box return values for VM strict-return compliance
|
|
163
|
+
wrapReturnValues(ast)
|
|
164
|
+
|
|
131
165
|
// Return a function that runs the pre-compiled AST
|
|
132
166
|
return async (...args: unknown[]) => {
|
|
133
167
|
const context: Record<string, unknown> = {}
|
|
@@ -142,8 +176,13 @@ export async function SafeFunction(options: SafeFunctionOptions): Promise<
|
|
|
142
176
|
capabilities,
|
|
143
177
|
})
|
|
144
178
|
|
|
179
|
+
// Unwrap the boxed result
|
|
180
|
+
const raw = vmResult.result
|
|
181
|
+
const result =
|
|
182
|
+
raw && typeof raw === 'object' && '__result' in raw ? raw.__result : raw
|
|
183
|
+
|
|
145
184
|
return {
|
|
146
|
-
result
|
|
185
|
+
result,
|
|
147
186
|
fuelUsed: vmResult.fuelUsed,
|
|
148
187
|
error: vmResult.error
|
|
149
188
|
? { message: vmResult.error.message || String(vmResult.error) }
|
package/src/lang/from-ts.test.ts
CHANGED
|
@@ -19,13 +19,13 @@ describe('TypeScript to TJS Transpiler', () => {
|
|
|
19
19
|
expect(result.code).toContain('b: 0')
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
it('should convert optional params to
|
|
22
|
+
it('should convert optional params to union with undefined', () => {
|
|
23
23
|
const result = fromTS(
|
|
24
24
|
`function greet(name: string, title?: string) { return name }`,
|
|
25
25
|
{ emitTJS: true }
|
|
26
26
|
)
|
|
27
27
|
expect(result.code).toContain("name: ''")
|
|
28
|
-
expect(result.code).toContain("title
|
|
28
|
+
expect(result.code).toContain("title: '' | undefined")
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
it('should convert return type to -! annotation (skip signature test)', () => {
|
|
@@ -57,7 +57,7 @@ describe('TypeScript to TJS Transpiler', () => {
|
|
|
57
57
|
`function find(id: string): string | null { return null }`,
|
|
58
58
|
{ emitTJS: true }
|
|
59
59
|
)
|
|
60
|
-
expect(result.code).toContain("-! ''
|
|
60
|
+
expect(result.code).toContain("-! '' | null") // -! for TS-transpiled
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
it('should preserve default values', () => {
|
package/src/lang/inference.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* 10 -> { kind: 'number' }
|
|
7
7
|
* ['string'] -> { kind: 'array', items: { kind: 'string' } }
|
|
8
8
|
* { name: 'string' } -> { kind: 'object', shape: { name: { kind: 'string' } } }
|
|
9
|
-
* 'string'
|
|
10
|
-
* 'string'
|
|
9
|
+
* 'string' | null -> { kind: 'string', nullable: true }
|
|
10
|
+
* 'string' | 0 -> { kind: 'union', members: [{ kind: 'string' }, { kind: 'number' }] }
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { parseExpressionAt } from 'acorn'
|
|
@@ -71,24 +71,8 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
|
|
|
71
71
|
const { operator, left, right } = node as any
|
|
72
72
|
|
|
73
73
|
if (operator === '||') {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// type || null means nullable type
|
|
78
|
-
if (rightType.kind === 'null') {
|
|
79
|
-
return { ...leftType, nullable: true }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// null || type means nullable type (reverse)
|
|
83
|
-
if (leftType.kind === 'null') {
|
|
84
|
-
return { ...rightType, nullable: true }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// type1 || type2 means union
|
|
88
|
-
return {
|
|
89
|
-
kind: 'union',
|
|
90
|
-
members: [leftType, rightType],
|
|
91
|
-
}
|
|
74
|
+
// || is JavaScript logical OR — infer type from left operand
|
|
75
|
+
return inferTypeFromValue(left)
|
|
92
76
|
}
|
|
93
77
|
|
|
94
78
|
if (operator === '&&') {
|
|
@@ -106,6 +90,27 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
|
|
|
106
90
|
return { kind: 'any' }
|
|
107
91
|
}
|
|
108
92
|
|
|
93
|
+
case 'BinaryExpression': {
|
|
94
|
+
const { operator, left, right } = node as any
|
|
95
|
+
// | means union type (e.g., 0 | null, '' | undefined)
|
|
96
|
+
if (operator === '|') {
|
|
97
|
+
const leftType = inferTypeFromValue(left)
|
|
98
|
+
const rightType = inferTypeFromValue(right)
|
|
99
|
+
|
|
100
|
+
if (rightType.kind === 'null') {
|
|
101
|
+
return { ...leftType, nullable: true }
|
|
102
|
+
}
|
|
103
|
+
if (leftType.kind === 'null') {
|
|
104
|
+
return { ...rightType, nullable: true }
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
kind: 'union',
|
|
108
|
+
members: [leftType, rightType],
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { kind: 'any' }
|
|
112
|
+
}
|
|
113
|
+
|
|
109
114
|
case 'Identifier': {
|
|
110
115
|
// Handle undefined as a type
|
|
111
116
|
if ((node as any).name === 'undefined') {
|
|
@@ -282,6 +287,15 @@ export function extractLiteralValue(node: Expression): any {
|
|
|
282
287
|
}
|
|
283
288
|
return undefined
|
|
284
289
|
|
|
290
|
+
case 'BinaryExpression': {
|
|
291
|
+
const { operator, left } = node as any
|
|
292
|
+
// | is union type — extract the left (primary) example value
|
|
293
|
+
if (operator === '|') {
|
|
294
|
+
return extractLiteralValue(left)
|
|
295
|
+
}
|
|
296
|
+
return undefined
|
|
297
|
+
}
|
|
298
|
+
|
|
285
299
|
case 'LogicalExpression': {
|
|
286
300
|
const { operator, left, right } = node as any
|
|
287
301
|
if (operator === '&&') {
|
package/src/lang/parser.test.ts
CHANGED
|
@@ -294,9 +294,9 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
294
294
|
expect(signature.parameters.limit.default).toBe(10)
|
|
295
295
|
})
|
|
296
296
|
|
|
297
|
-
it('should handle nullable types with
|
|
297
|
+
it('should handle nullable types with | syntax', () => {
|
|
298
298
|
const { signature } = transpile(`
|
|
299
|
-
function test(filter: 'default'
|
|
299
|
+
function test(filter: 'default' | null) {
|
|
300
300
|
return { filter }
|
|
301
301
|
}
|
|
302
302
|
`)
|
|
@@ -304,9 +304,9 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
304
304
|
expect(signature.parameters.filter.type.nullable).toBe(true)
|
|
305
305
|
})
|
|
306
306
|
|
|
307
|
-
it('should handle union types with
|
|
307
|
+
it('should handle union types with | syntax', () => {
|
|
308
308
|
const { signature } = transpile(`
|
|
309
|
-
function test(id: 'abc123'
|
|
309
|
+
function test(id: 'abc123' | 42) {
|
|
310
310
|
return { id }
|
|
311
311
|
}
|
|
312
312
|
`)
|
|
@@ -37,14 +37,16 @@ describe('TJS Emitter', () => {
|
|
|
37
37
|
expect(result.types.test.params.optional.required).toBe(false)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
it('should
|
|
40
|
+
it('should strip colon type annotations from output (no default for required params)', () => {
|
|
41
41
|
const result = transpileToJS(`
|
|
42
42
|
function greet(name: 'world') {
|
|
43
43
|
return name
|
|
44
44
|
}
|
|
45
45
|
`)
|
|
46
|
-
|
|
46
|
+
// Required `:` params have no default in JS output
|
|
47
|
+
expect(result.code).toContain('function greet(name)')
|
|
47
48
|
expect(result.code).not.toContain("name: 'world'")
|
|
49
|
+
expect(result.code).not.toContain("name = 'world'")
|
|
48
50
|
})
|
|
49
51
|
|
|
50
52
|
it('should handle object type annotations', () => {
|
|
@@ -225,7 +227,7 @@ function greet(name: 'world') {
|
|
|
225
227
|
return x
|
|
226
228
|
}
|
|
227
229
|
`)
|
|
228
|
-
expect(result.code).toContain('function test(x
|
|
230
|
+
expect(result.code).toContain('function test(x)')
|
|
229
231
|
expect(result.code).not.toContain('!')
|
|
230
232
|
})
|
|
231
233
|
|