tjs-lang 0.6.7 → 0.6.8
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 +5 -1
- package/demo/docs.json +3 -3
- package/dist/index.js +53 -53
- package/dist/index.js.map +6 -6
- package/dist/tjs-full.js +53 -53
- package/dist/tjs-full.js.map +6 -6
- package/dist/tjs-transpiler.js +2 -2
- package/dist/tjs-transpiler.js.map +3 -3
- package/dist/tjs-vm.js +21 -21
- package/dist/tjs-vm.js.map +4 -4
- package/package.json +1 -1
- package/src/cli/tjs.ts +1 -1
- package/src/lang/codegen.test.ts +44 -0
- package/src/lang/emitters/js-tests.ts +22 -1
- package/src/lang/emitters/js.ts +41 -0
- package/src/lang/inference.ts +20 -3
- package/src/lang/parser-params.ts +11 -0
- package/src/lang/parser.test.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tjs-lang",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
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/cli/tjs.ts
CHANGED
package/src/lang/codegen.test.ts
CHANGED
|
@@ -2043,3 +2043,47 @@ describe('TS overloads → TJS → JS full pipeline', () => {
|
|
|
2043
2043
|
}
|
|
2044
2044
|
})
|
|
2045
2045
|
})
|
|
2046
|
+
|
|
2047
|
+
describe('rest parameter metadata', () => {
|
|
2048
|
+
it('should capture typed rest param in metadata', () => {
|
|
2049
|
+
const result = tjs(`function sum(...nums: [0]) -> 0 { return 0 }`, {
|
|
2050
|
+
runTests: false,
|
|
2051
|
+
})
|
|
2052
|
+
const info = result.types.sum
|
|
2053
|
+
expect(info.params.nums).toBeDefined()
|
|
2054
|
+
expect(info.params.nums.type.kind).toBe('array')
|
|
2055
|
+
expect(info.params.nums.type.items?.kind).toBe('integer')
|
|
2056
|
+
expect(info.params.nums.required).toBe(false)
|
|
2057
|
+
})
|
|
2058
|
+
|
|
2059
|
+
it('should capture rest param with float array type', () => {
|
|
2060
|
+
const result = tjs(
|
|
2061
|
+
`function mean(...values: [1.0, 2.0]) -> 0.0 { return 0 }`,
|
|
2062
|
+
{ runTests: false }
|
|
2063
|
+
)
|
|
2064
|
+
const info = result.types.mean
|
|
2065
|
+
expect(info.params.values).toBeDefined()
|
|
2066
|
+
expect(info.params.values.type.kind).toBe('array')
|
|
2067
|
+
expect(info.params.values.type.items?.kind).toBe('number')
|
|
2068
|
+
})
|
|
2069
|
+
|
|
2070
|
+
it('should capture heterogeneous rest param as union', () => {
|
|
2071
|
+
const result = tjs(
|
|
2072
|
+
`function log(...args: ['hello', 42, true]) -> 0 { return 0 }`,
|
|
2073
|
+
{ runTests: false }
|
|
2074
|
+
)
|
|
2075
|
+
const info = result.types.log
|
|
2076
|
+
expect(info.params.args.type.kind).toBe('array')
|
|
2077
|
+
expect(info.params.args.type.items?.kind).toBe('union')
|
|
2078
|
+
expect(info.params.args.type.items?.members).toHaveLength(3)
|
|
2079
|
+
})
|
|
2080
|
+
|
|
2081
|
+
it('should capture untyped rest param as bare array', () => {
|
|
2082
|
+
const result = tjs(`function collect(...args) { return args }`, {
|
|
2083
|
+
runTests: false,
|
|
2084
|
+
})
|
|
2085
|
+
const info = result.types.collect
|
|
2086
|
+
expect(info.params.args).toBeDefined()
|
|
2087
|
+
expect(info.params.args.type.kind).toBe('array')
|
|
2088
|
+
})
|
|
2089
|
+
})
|
|
@@ -1105,9 +1105,30 @@ function extractParamExamples(paramsStr: string): string[] {
|
|
|
1105
1105
|
const params = splitParams(paramsStr)
|
|
1106
1106
|
|
|
1107
1107
|
for (const param of params) {
|
|
1108
|
+
const trimmed = param.trim()
|
|
1109
|
+
|
|
1110
|
+
// Rest parameter: ...name: [examples] — spread the array elements as individual args
|
|
1111
|
+
const restMatch = trimmed.match(/^\.\.\.(\w+)\s*[:=]\s*(\[.+\])$/)
|
|
1112
|
+
if (restMatch) {
|
|
1113
|
+
try {
|
|
1114
|
+
const arr = new Function(`return ${restMatch[2]}`)()
|
|
1115
|
+
if (Array.isArray(arr)) {
|
|
1116
|
+
for (const el of arr) {
|
|
1117
|
+
examples.push(JSON.stringify(el))
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
} catch {
|
|
1121
|
+
// Can't parse — skip rest param examples
|
|
1122
|
+
}
|
|
1123
|
+
continue
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// Bare rest param without type: ...name — skip (no example to use)
|
|
1127
|
+
if (trimmed.startsWith('...')) continue
|
|
1128
|
+
|
|
1108
1129
|
// Match: name: example or name = example (with optional safety markers)
|
|
1109
1130
|
// Handle: (? name: example) or (! name: example)
|
|
1110
|
-
const match =
|
|
1131
|
+
const match = trimmed.match(/(?:\(\s*[?!]\s*)?(\w+)\s*[:=]\s*(.+?)(?:\))?$/)
|
|
1111
1132
|
if (match) {
|
|
1112
1133
|
examples.push(match[2].trim())
|
|
1113
1134
|
} else {
|
package/src/lang/emitters/js.ts
CHANGED
|
@@ -257,6 +257,47 @@ function extractFunctionTypeInfo(
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
|
+
} else if (
|
|
261
|
+
param.type === 'RestElement' &&
|
|
262
|
+
param.argument?.type === 'Identifier'
|
|
263
|
+
) {
|
|
264
|
+
// Handle rest parameters: ...args: [0]
|
|
265
|
+
// The type annotation was stripped by preprocessing (JS forbids
|
|
266
|
+
// defaults on rest params), so extract it from the original source
|
|
267
|
+
const restName = param.argument.name
|
|
268
|
+
const restTypeMatch = originalSource.match(
|
|
269
|
+
new RegExp(`\\.\\.\\.${restName}\\s*:\\s*([^)]+?)\\s*\\)`)
|
|
270
|
+
)
|
|
271
|
+
if (restTypeMatch) {
|
|
272
|
+
try {
|
|
273
|
+
const typeExpr = parseExpressionAt(restTypeMatch[1].trim(), 0, {
|
|
274
|
+
ecmaVersion: 2022,
|
|
275
|
+
})
|
|
276
|
+
const restItemType = inferTypeFromValue(typeExpr as any)
|
|
277
|
+
params[restName] = {
|
|
278
|
+
name: restName,
|
|
279
|
+
type: restItemType,
|
|
280
|
+
required: false,
|
|
281
|
+
description: tdoc.params[restName],
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
// If we can't parse the type, emit as any array
|
|
285
|
+
params[restName] = {
|
|
286
|
+
name: restName,
|
|
287
|
+
type: { kind: 'array' },
|
|
288
|
+
required: false,
|
|
289
|
+
description: tdoc.params[restName],
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
// No type annotation — bare rest param
|
|
294
|
+
params[restName] = {
|
|
295
|
+
name: restName,
|
|
296
|
+
type: { kind: 'array' },
|
|
297
|
+
required: false,
|
|
298
|
+
description: tdoc.params[restName],
|
|
299
|
+
}
|
|
300
|
+
}
|
|
260
301
|
}
|
|
261
302
|
}
|
|
262
303
|
}
|
package/src/lang/inference.ts
CHANGED
|
@@ -48,9 +48,26 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
|
|
|
48
48
|
if (elements.length === 0) {
|
|
49
49
|
return { kind: 'array', items: { kind: 'any' } }
|
|
50
50
|
}
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
// Infer type from all elements — if homogeneous, use that type;
|
|
52
|
+
// if heterogeneous, produce a union of distinct kinds
|
|
53
|
+
const itemTypes = elements
|
|
54
|
+
.filter((el) => el != null)
|
|
55
|
+
.map((el) => inferTypeFromValue(el))
|
|
56
|
+
if (itemTypes.length === 0) {
|
|
57
|
+
return { kind: 'array', items: { kind: 'any' } }
|
|
58
|
+
}
|
|
59
|
+
// Deduplicate by structure
|
|
60
|
+
const seen = new Map<string, TypeDescriptor>()
|
|
61
|
+
for (const t of itemTypes) {
|
|
62
|
+
const key = JSON.stringify(t)
|
|
63
|
+
if (!seen.has(key)) seen.set(key, t)
|
|
64
|
+
}
|
|
65
|
+
const unique = [...seen.values()]
|
|
66
|
+
const items =
|
|
67
|
+
unique.length === 1
|
|
68
|
+
? unique[0]
|
|
69
|
+
: { kind: 'union' as const, members: unique }
|
|
70
|
+
return { kind: 'array', items }
|
|
54
71
|
}
|
|
55
72
|
|
|
56
73
|
case 'ObjectExpression': {
|
|
@@ -933,6 +933,17 @@ function processParamString(
|
|
|
933
933
|
return `[ ${processedInner} ]`
|
|
934
934
|
}
|
|
935
935
|
|
|
936
|
+
// Handle rest parameters: ...args: [0] -> ...args (strip type, JS forbids defaults on rest)
|
|
937
|
+
// The type annotation is still captured by extractFunctionTypeInfo for __tjs metadata
|
|
938
|
+
if (trimmed.startsWith('...')) {
|
|
939
|
+
const restColonPos = findTopLevelColon(trimmed)
|
|
940
|
+
if (restColonPos !== -1) {
|
|
941
|
+
const restName = trimmed.slice(0, restColonPos).trim()
|
|
942
|
+
return restName
|
|
943
|
+
}
|
|
944
|
+
return param
|
|
945
|
+
}
|
|
946
|
+
|
|
936
947
|
// Handle optional param syntax: x?: type -> x = type (not required)
|
|
937
948
|
const optionalMatch = trimmed.match(/^(\w+)\s*\?\s*:\s*(.+)$/)
|
|
938
949
|
if (optionalMatch) {
|
package/src/lang/parser.test.ts
CHANGED
|
@@ -35,6 +35,21 @@ describe('Transpiler', () => {
|
|
|
35
35
|
)
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
+
it('should strip type annotation from rest params', () => {
|
|
39
|
+
// JS forbids default values on rest params, so : annotation must be stripped
|
|
40
|
+
const result = preprocess(`function sum(...nums: [0]) { }`)
|
|
41
|
+
expect(result.source).toContain('...nums)')
|
|
42
|
+
expect(result.source).not.toContain('...nums =')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should handle rest param with array type example', () => {
|
|
46
|
+
const result = preprocess(
|
|
47
|
+
`function mean(...values: [1.0, 2.0, 3.0]) -> 2.0 { return 0 }`
|
|
48
|
+
)
|
|
49
|
+
expect(result.source).toContain('...values)')
|
|
50
|
+
expect(result.source).not.toContain('[1.0')
|
|
51
|
+
})
|
|
52
|
+
|
|
38
53
|
it('should allow required params after optional params', () => {
|
|
39
54
|
// TypeScript permits this pattern, and fromTS can produce it when
|
|
40
55
|
// earlier params degrade to any. TJS should accept it.
|