tjs-lang 0.6.44 → 0.7.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 +85 -422
- package/README.md +15 -82
- package/bin/benchmarks.ts +7 -7
- package/bin/dev.ts +2 -1
- package/demo/autocomplete.test.ts +1 -1
- package/demo/docs.json +744 -48
- package/demo/src/demo-nav.ts +5 -5
- package/demo/src/index.ts +28 -36
- package/demo/src/module-sw.ts +1 -1
- package/demo/src/playground-shared.ts +17 -17
- package/demo/src/playground.ts +13 -1
- package/demo/src/style.ts +4 -1
- package/demo/src/tjs-playground.ts +5 -5
- package/demo/src/user-store.ts +2 -1
- package/demo/static/favicon.svg +17 -24
- package/demo/static/tosi-platform.json +9304 -0
- package/dist/index.js +158 -156
- package/dist/index.js.map +14 -13
- package/dist/scripts/compat-effect.d.ts +16 -0
- package/dist/scripts/compat-kysely.d.ts +13 -0
- package/dist/scripts/compat-radash.d.ts +13 -0
- package/dist/scripts/compat-superstruct.d.ts +13 -0
- package/dist/scripts/compat-ts-pattern.d.ts +13 -0
- package/dist/scripts/compat-zod.d.ts +12 -0
- package/dist/src/lang/emitters/from-ts.d.ts +1 -1
- package/dist/src/lang/emitters/js-tests.d.ts +4 -0
- package/dist/src/lang/emitters/js.d.ts +2 -2
- package/dist/src/lang/index.d.ts +1 -0
- package/dist/src/lang/json-schema.d.ts +40 -0
- package/dist/src/lang/parser-transforms.d.ts +14 -0
- package/dist/src/lang/runtime.d.ts +39 -6
- package/dist/src/types/Type.d.ts +5 -0
- package/dist/tjs-full.js +158 -156
- package/dist/tjs-full.js.map +14 -13
- package/dist/tjs-vm.js +44 -43
- package/dist/tjs-vm.js.map +5 -5
- package/docs/README.md +21 -20
- package/docs/WASM-QUICKSTART.md +283 -0
- package/docs/diagrams/architecture-shift.svg +117 -0
- package/docs/diagrams/compile-runtime.svg +130 -0
- package/docs/diagrams/icon-riff-1.svg +55 -0
- package/docs/diagrams/icon-riff-2.svg +62 -0
- package/docs/diagrams/icon-riff-3.svg +61 -0
- package/docs/diagrams/platform-overview.svg +114 -0
- package/docs/diagrams/safe-eval.svg +147 -0
- package/docs/eval-v4/arch-comparison.svg +277 -0
- package/docs/eval-v4/bundler-tree.svg +250 -0
- package/docs/eval-v4/http-lifecycle.svg +148 -0
- package/docs/function-predicate-design.md +8 -8
- package/docs/native-engine-integration.md +2 -2
- package/editors/codemirror/autocomplete.test.ts +29 -29
- package/package.json +10 -4
- package/src/cli/commands/convert.test.ts +11 -8
- package/src/cli/tjs.ts +1 -1
- package/src/lang/codegen.test.ts +117 -112
- package/src/lang/docs.test.ts +22 -22
- package/src/lang/docs.ts +5 -8
- package/src/lang/emitters/dts.test.ts +13 -13
- package/src/lang/emitters/from-ts.ts +36 -9
- package/src/lang/emitters/js-tests.ts +143 -28
- package/src/lang/emitters/js.ts +49 -28
- package/src/lang/features.test.ts +259 -43
- package/src/lang/from-ts.test.ts +3 -3
- package/src/lang/function-predicate.test.ts +1 -1
- package/src/lang/index.ts +8 -47
- package/src/lang/json-schema.test.ts +261 -0
- package/src/lang/json-schema.ts +167 -0
- package/src/lang/parser-params.ts +28 -44
- package/src/lang/parser-transforms.ts +255 -0
- package/src/lang/parser.test.ts +32 -13
- package/src/lang/parser.ts +49 -11
- package/src/lang/perf.test.ts +11 -11
- package/src/lang/roundtrip.test.ts +3 -3
- package/src/lang/runtime.test.ts +167 -0
- package/src/lang/runtime.ts +234 -46
- package/src/lang/transpiler.test.ts +21 -21
- package/src/lang/typescript-syntax.test.ts +11 -9
- package/src/types/Type.ts +38 -1
- package/src/use-cases/bootstrap.test.ts +7 -7
- package/src/use-cases/client-server.test.ts +1 -1
- package/src/use-cases/malicious-actor.test.ts +1 -1
- package/src/use-cases/rag-processor.test.ts +1 -1
- package/src/use-cases/sophisticated-agents.test.ts +2 -2
- package/src/use-cases/transpiler-llm.test.ts +1 -1
- package/src/use-cases/unbundled-imports.test.ts +9 -9
- package/tjs-lang.svg +17 -25
|
@@ -539,11 +539,31 @@ export function insertAsiProtection(source: string): string {
|
|
|
539
539
|
|
|
540
540
|
const lines = source.split('\n')
|
|
541
541
|
const result: string[] = []
|
|
542
|
+
let inBlockComment = false
|
|
542
543
|
|
|
543
544
|
for (let i = 0; i < lines.length; i++) {
|
|
544
545
|
const line = lines[i]
|
|
545
546
|
const prevLine = i > 0 ? lines[i - 1] : ''
|
|
546
547
|
|
|
548
|
+
// Track block comment state
|
|
549
|
+
if (inBlockComment) {
|
|
550
|
+
result.push(line)
|
|
551
|
+
if (line.includes('*/')) inBlockComment = false
|
|
552
|
+
continue
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Check if this line opens a block comment
|
|
556
|
+
const commentOpen = line.indexOf('/*')
|
|
557
|
+
const commentClose = line.indexOf('*/')
|
|
558
|
+
if (
|
|
559
|
+
commentOpen !== -1 &&
|
|
560
|
+
(commentClose === -1 || commentClose < commentOpen)
|
|
561
|
+
) {
|
|
562
|
+
inBlockComment = true
|
|
563
|
+
result.push(line)
|
|
564
|
+
continue
|
|
565
|
+
}
|
|
566
|
+
|
|
547
567
|
// Check if this line starts with a problematic character
|
|
548
568
|
if (i > 0 && continuationStarts.test(line)) {
|
|
549
569
|
// Get the previous line without trailing comment
|
|
@@ -3109,3 +3129,238 @@ export function validateNoEval(source: string): string {
|
|
|
3109
3129
|
|
|
3110
3130
|
return source
|
|
3111
3131
|
}
|
|
3132
|
+
|
|
3133
|
+
/**
|
|
3134
|
+
* Transform bang access (!.) to __tjs.bang() calls.
|
|
3135
|
+
*
|
|
3136
|
+
* x!.foo → __tjs.bang(x,'foo')
|
|
3137
|
+
* x.y!.foo → __tjs.bang(x.y,'foo')
|
|
3138
|
+
* fn()!.foo → __tjs.bang(fn(),'foo')
|
|
3139
|
+
* arr[0]!.foo → __tjs.bang(arr[0],'foo')
|
|
3140
|
+
* x!.foo!.bar → __tjs.bang(__tjs.bang(x,'foo'),'bar')
|
|
3141
|
+
*
|
|
3142
|
+
* If the source is null/undefined, returns MonadicError.
|
|
3143
|
+
* If the source is a MonadicError, propagates it.
|
|
3144
|
+
* Otherwise, performs a bare property access (throws as usual).
|
|
3145
|
+
*/
|
|
3146
|
+
export function transformBangAccess(source: string): string {
|
|
3147
|
+
// Quick bail — no !. in source at all
|
|
3148
|
+
if (!source.includes('!.')) return source
|
|
3149
|
+
|
|
3150
|
+
let result = ''
|
|
3151
|
+
let i = 0
|
|
3152
|
+
|
|
3153
|
+
// State tracking for strings/comments
|
|
3154
|
+
type State =
|
|
3155
|
+
| 'normal'
|
|
3156
|
+
| 'string-single'
|
|
3157
|
+
| 'string-double'
|
|
3158
|
+
| 'string-template'
|
|
3159
|
+
| 'line-comment'
|
|
3160
|
+
| 'block-comment'
|
|
3161
|
+
let state: State = 'normal'
|
|
3162
|
+
let templateDepth = 0
|
|
3163
|
+
|
|
3164
|
+
while (i < source.length) {
|
|
3165
|
+
const ch = source[i]
|
|
3166
|
+
const next = source[i + 1]
|
|
3167
|
+
|
|
3168
|
+
// State transitions
|
|
3169
|
+
if (state === 'normal') {
|
|
3170
|
+
if (ch === '/' && next === '/') {
|
|
3171
|
+
state = 'line-comment'
|
|
3172
|
+
result += ch
|
|
3173
|
+
i++
|
|
3174
|
+
continue
|
|
3175
|
+
}
|
|
3176
|
+
if (ch === '/' && next === '*') {
|
|
3177
|
+
state = 'block-comment'
|
|
3178
|
+
result += ch
|
|
3179
|
+
i++
|
|
3180
|
+
continue
|
|
3181
|
+
}
|
|
3182
|
+
if (ch === "'") {
|
|
3183
|
+
state = 'string-single'
|
|
3184
|
+
result += ch
|
|
3185
|
+
i++
|
|
3186
|
+
continue
|
|
3187
|
+
}
|
|
3188
|
+
if (ch === '"') {
|
|
3189
|
+
state = 'string-double'
|
|
3190
|
+
result += ch
|
|
3191
|
+
i++
|
|
3192
|
+
continue
|
|
3193
|
+
}
|
|
3194
|
+
if (ch === '`') {
|
|
3195
|
+
state = 'string-template'
|
|
3196
|
+
templateDepth++
|
|
3197
|
+
result += ch
|
|
3198
|
+
i++
|
|
3199
|
+
continue
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
// Detect bang access: ! followed by . followed by a word char (not digit)
|
|
3203
|
+
if (
|
|
3204
|
+
ch === '!' &&
|
|
3205
|
+
next === '.' &&
|
|
3206
|
+
i + 2 < source.length &&
|
|
3207
|
+
/[a-zA-Z_$]/.test(source[i + 2])
|
|
3208
|
+
) {
|
|
3209
|
+
// Scan backward in `result` to find the expression start
|
|
3210
|
+
const exprEnd = result.length
|
|
3211
|
+
const exprStart = findExprStartBackward(result)
|
|
3212
|
+
|
|
3213
|
+
if (exprStart < exprEnd) {
|
|
3214
|
+
const expr = result.slice(exprStart)
|
|
3215
|
+
result = result.slice(0, exprStart)
|
|
3216
|
+
|
|
3217
|
+
// Scan forward to capture the property name after !.
|
|
3218
|
+
let j = i + 2
|
|
3219
|
+
while (j < source.length && /[\w$]/.test(source[j])) j++
|
|
3220
|
+
const prop = source.slice(i + 2, j)
|
|
3221
|
+
|
|
3222
|
+
result += `__tjs.bang(${expr},'${prop}')`
|
|
3223
|
+
i = j
|
|
3224
|
+
continue
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
result += ch
|
|
3229
|
+
i++
|
|
3230
|
+
} else if (state === 'line-comment') {
|
|
3231
|
+
result += ch
|
|
3232
|
+
if (ch === '\n') state = 'normal'
|
|
3233
|
+
i++
|
|
3234
|
+
} else if (state === 'block-comment') {
|
|
3235
|
+
result += ch
|
|
3236
|
+
if (ch === '*' && next === '/') {
|
|
3237
|
+
result += next
|
|
3238
|
+
state = 'normal'
|
|
3239
|
+
i += 2
|
|
3240
|
+
} else {
|
|
3241
|
+
i++
|
|
3242
|
+
}
|
|
3243
|
+
} else if (state === 'string-single') {
|
|
3244
|
+
result += ch
|
|
3245
|
+
if (ch === '\\') {
|
|
3246
|
+
result += next || ''
|
|
3247
|
+
i += 2
|
|
3248
|
+
} else if (ch === "'") {
|
|
3249
|
+
state = 'normal'
|
|
3250
|
+
i++
|
|
3251
|
+
} else {
|
|
3252
|
+
i++
|
|
3253
|
+
}
|
|
3254
|
+
} else if (state === 'string-double') {
|
|
3255
|
+
result += ch
|
|
3256
|
+
if (ch === '\\') {
|
|
3257
|
+
result += next || ''
|
|
3258
|
+
i += 2
|
|
3259
|
+
} else if (ch === '"') {
|
|
3260
|
+
state = 'normal'
|
|
3261
|
+
i++
|
|
3262
|
+
} else {
|
|
3263
|
+
i++
|
|
3264
|
+
}
|
|
3265
|
+
} else if (state === 'string-template') {
|
|
3266
|
+
result += ch
|
|
3267
|
+
if (ch === '\\') {
|
|
3268
|
+
result += next || ''
|
|
3269
|
+
i += 2
|
|
3270
|
+
} else if (ch === '`') {
|
|
3271
|
+
templateDepth--
|
|
3272
|
+
state = templateDepth > 0 ? 'string-template' : 'normal'
|
|
3273
|
+
i++
|
|
3274
|
+
} else if (ch === '$' && next === '{') {
|
|
3275
|
+
result += next
|
|
3276
|
+
i += 2
|
|
3277
|
+
state = 'normal'
|
|
3278
|
+
} else {
|
|
3279
|
+
i++
|
|
3280
|
+
}
|
|
3281
|
+
} else {
|
|
3282
|
+
result += ch
|
|
3283
|
+
i++
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
return result
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
/**
|
|
3291
|
+
* Scan backward through `text` to find the start of the expression
|
|
3292
|
+
* that ends at the last character of `text`.
|
|
3293
|
+
*
|
|
3294
|
+
* Handles: identifiers, member chains (. and ?.), function calls (),
|
|
3295
|
+
* computed access [], and nested combinations.
|
|
3296
|
+
*/
|
|
3297
|
+
function findExprStartBackward(text: string): number {
|
|
3298
|
+
let pos = text.length - 1
|
|
3299
|
+
|
|
3300
|
+
// Skip trailing whitespace
|
|
3301
|
+
while (pos >= 0 && /\s/.test(text[pos])) pos--
|
|
3302
|
+
if (pos < 0) return text.length
|
|
3303
|
+
|
|
3304
|
+
// Walk backward consuming expression parts
|
|
3305
|
+
while (pos >= 0) {
|
|
3306
|
+
const ch = text[pos]
|
|
3307
|
+
|
|
3308
|
+
if (/[\w$]/.test(ch)) {
|
|
3309
|
+
// Identifier — consume word chars
|
|
3310
|
+
while (pos >= 0 && /[\w$]/.test(text[pos])) pos--
|
|
3311
|
+
// Check if preceded by . or ?. (member chain continues)
|
|
3312
|
+
if (pos >= 0 && text[pos] === '.') {
|
|
3313
|
+
if (pos >= 1 && text[pos - 1] === '?') {
|
|
3314
|
+
pos -= 2
|
|
3315
|
+
} else {
|
|
3316
|
+
pos--
|
|
3317
|
+
}
|
|
3318
|
+
continue
|
|
3319
|
+
}
|
|
3320
|
+
return pos + 1
|
|
3321
|
+
} else if (ch === ')') {
|
|
3322
|
+
pos = findMatchingOpen(text, pos, '(', ')')
|
|
3323
|
+
if (pos < 0) return 0
|
|
3324
|
+
pos--
|
|
3325
|
+
if (pos >= 0 && /[\w$]/.test(text[pos])) continue
|
|
3326
|
+
if (pos >= 0 && text[pos] === '.') {
|
|
3327
|
+
if (pos >= 1 && text[pos - 1] === '?') pos -= 2
|
|
3328
|
+
else pos--
|
|
3329
|
+
continue
|
|
3330
|
+
}
|
|
3331
|
+
return pos + 1
|
|
3332
|
+
} else if (ch === ']') {
|
|
3333
|
+
pos = findMatchingOpen(text, pos, '[', ']')
|
|
3334
|
+
if (pos < 0) return 0
|
|
3335
|
+
pos--
|
|
3336
|
+
if (pos >= 0 && /[\w$]/.test(text[pos])) continue
|
|
3337
|
+
if (pos >= 0 && text[pos] === '.') {
|
|
3338
|
+
if (pos >= 1 && text[pos - 1] === '?') pos -= 2
|
|
3339
|
+
else pos--
|
|
3340
|
+
continue
|
|
3341
|
+
}
|
|
3342
|
+
return pos + 1
|
|
3343
|
+
} else {
|
|
3344
|
+
return pos + 1
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
return 0
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
/** Find the matching opening bracket/paren scanning backward from `pos`. */
|
|
3352
|
+
function findMatchingOpen(
|
|
3353
|
+
text: string,
|
|
3354
|
+
pos: number,
|
|
3355
|
+
open: string,
|
|
3356
|
+
close: string
|
|
3357
|
+
): number {
|
|
3358
|
+
let depth = 1
|
|
3359
|
+
pos--
|
|
3360
|
+
while (pos >= 0 && depth > 0) {
|
|
3361
|
+
if (text[pos] === close) depth++
|
|
3362
|
+
else if (text[pos] === open) depth--
|
|
3363
|
+
if (depth > 0) pos--
|
|
3364
|
+
}
|
|
3365
|
+
return pos
|
|
3366
|
+
}
|
package/src/lang/parser.test.ts
CHANGED
|
@@ -13,12 +13,30 @@ describe('Transpiler', () => {
|
|
|
13
13
|
|
|
14
14
|
it('should extract return type annotation', () => {
|
|
15
15
|
const result = preprocess(
|
|
16
|
-
`function foo(x: 'string')
|
|
16
|
+
`function foo(x: 'string'): { result: 'string' } { }`
|
|
17
17
|
)
|
|
18
18
|
expect(result.returnType).toBe(`{ result: 'string' }`)
|
|
19
19
|
expect(result.source).not.toContain('->')
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
+
it('should extract colon-style return type annotation', () => {
|
|
23
|
+
const result = preprocess(
|
|
24
|
+
`function foo(x: 'string'): { result: 'string' } { }`
|
|
25
|
+
)
|
|
26
|
+
expect(result.returnType).toBe(`{ result: 'string' }`)
|
|
27
|
+
expect(result.source).not.toContain('): ')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should extract colon-style return type with safety markers', () => {
|
|
31
|
+
const safe = preprocess(`function foo(x: 0):? 0 { }`)
|
|
32
|
+
expect(safe.returnType).toBe('0')
|
|
33
|
+
expect(safe.returnSafety).toBe('safe')
|
|
34
|
+
|
|
35
|
+
const unsafe = preprocess(`function foo(x: 0):! 0 { }`)
|
|
36
|
+
expect(unsafe.returnType).toBe('0')
|
|
37
|
+
expect(unsafe.returnSafety).toBe('unsafe')
|
|
38
|
+
})
|
|
39
|
+
|
|
22
40
|
it('should handle multiple parameters', () => {
|
|
23
41
|
const result = preprocess(`function foo(a: 'string', b: 0, c = 10) { }`)
|
|
24
42
|
expect(result.source).toContain(`a = 'string'`)
|
|
@@ -44,7 +62,7 @@ describe('Transpiler', () => {
|
|
|
44
62
|
|
|
45
63
|
it('should handle rest param with array type example', () => {
|
|
46
64
|
const result = preprocess(
|
|
47
|
-
`function mean(...values: [1.0, 2.0, 3.0])
|
|
65
|
+
`function mean(...values: [1.0, 2.0, 3.0]): 2.0 { return 0 }`
|
|
48
66
|
)
|
|
49
67
|
expect(result.source).toContain('...values)')
|
|
50
68
|
expect(result.source).not.toContain('[1.0')
|
|
@@ -417,20 +435,20 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
417
435
|
})
|
|
418
436
|
|
|
419
437
|
it('should generate correct runtime validation for integer', () => {
|
|
420
|
-
const result = tjs(`function test(n: 1)
|
|
438
|
+
const result = tjs(`function test(n: 1): 1 { return n }`)
|
|
421
439
|
// Should check Number.isInteger
|
|
422
440
|
expect(result.code).toContain('Number.isInteger')
|
|
423
441
|
})
|
|
424
442
|
|
|
425
443
|
it('should generate correct runtime validation for non-negative-integer', () => {
|
|
426
|
-
const result = tjs(`function test(n: +1)
|
|
444
|
+
const result = tjs(`function test(n: +1): 1 { return n }`)
|
|
427
445
|
// Should check Number.isInteger AND >= 0
|
|
428
446
|
expect(result.code).toContain('Number.isInteger')
|
|
429
447
|
expect(result.code).toContain('< 0')
|
|
430
448
|
})
|
|
431
449
|
|
|
432
450
|
it('should validate integer at runtime', () => {
|
|
433
|
-
const result = tjs(`function check(n: 1)
|
|
451
|
+
const result = tjs(`function check(n: 1): 1 { return n }`)
|
|
434
452
|
const savedTjs = globalThis.__tjs
|
|
435
453
|
globalThis.__tjs = createRuntime()
|
|
436
454
|
try {
|
|
@@ -446,7 +464,7 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
446
464
|
})
|
|
447
465
|
|
|
448
466
|
it('should validate non-negative-integer at runtime', () => {
|
|
449
|
-
const result = tjs(`function check(n: +1)
|
|
467
|
+
const result = tjs(`function check(n: +1): 1 { return n }`)
|
|
450
468
|
const savedTjs = globalThis.__tjs
|
|
451
469
|
globalThis.__tjs = createRuntime()
|
|
452
470
|
try {
|
|
@@ -466,7 +484,7 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
466
484
|
})
|
|
467
485
|
|
|
468
486
|
it('should validate float (number) accepts all numbers at runtime', () => {
|
|
469
|
-
const result = tjs(`function check(n: 0.0)
|
|
487
|
+
const result = tjs(`function check(n: 0.0): 0.0 { return n }`)
|
|
470
488
|
const savedTjs = globalThis.__tjs
|
|
471
489
|
globalThis.__tjs = createRuntime()
|
|
472
490
|
try {
|
|
@@ -695,7 +713,7 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
695
713
|
|
|
696
714
|
This demonstrates TJS doc comments.
|
|
697
715
|
*/
|
|
698
|
-
function greet(name: 'World')
|
|
716
|
+
function greet(name: 'World'): '' {
|
|
699
717
|
return 'Hello, ' + name + '!'
|
|
700
718
|
}
|
|
701
719
|
`)
|
|
@@ -715,7 +733,7 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
715
733
|
|
|
716
734
|
\`code example\`
|
|
717
735
|
*/
|
|
718
|
-
function test(x: 0)
|
|
736
|
+
function test(x: 0): 0 {
|
|
719
737
|
return x
|
|
720
738
|
}
|
|
721
739
|
`)
|
|
@@ -730,7 +748,7 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
730
748
|
Indented content here.
|
|
731
749
|
More indented content.
|
|
732
750
|
*/
|
|
733
|
-
function test(x: 0)
|
|
751
|
+
function test(x: 0): 0 {
|
|
734
752
|
return x
|
|
735
753
|
}
|
|
736
754
|
`)
|
|
@@ -748,7 +766,7 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
748
766
|
/*#
|
|
749
767
|
TJS doc description
|
|
750
768
|
*/
|
|
751
|
-
function test(x: 0)
|
|
769
|
+
function test(x: 0): 0 {
|
|
752
770
|
return x
|
|
753
771
|
}
|
|
754
772
|
`)
|
|
@@ -768,7 +786,7 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
768
786
|
/*#
|
|
769
787
|
Function-specific documentation.
|
|
770
788
|
*/
|
|
771
|
-
function test(x: 0)
|
|
789
|
+
function test(x: 0): 0 {
|
|
772
790
|
return x
|
|
773
791
|
}
|
|
774
792
|
`)
|
|
@@ -786,7 +804,7 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
786
804
|
|
|
787
805
|
const someVar = 123
|
|
788
806
|
|
|
789
|
-
function test(x: 0)
|
|
807
|
+
function test(x: 0): 0 {
|
|
790
808
|
return x
|
|
791
809
|
}
|
|
792
810
|
`)
|
|
@@ -810,6 +828,7 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
810
828
|
// Classes are not supported in AJS/VM - they're a TJS-only feature
|
|
811
829
|
expect(() =>
|
|
812
830
|
transpile(`
|
|
831
|
+
TjsCompat
|
|
813
832
|
class Foo {}
|
|
814
833
|
`)
|
|
815
834
|
).toThrow('Classes are not supported')
|
package/src/lang/parser.ts
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
validateNoEval,
|
|
50
50
|
validateNoVar,
|
|
51
51
|
transformConstBang,
|
|
52
|
+
transformBangAccess,
|
|
52
53
|
transformExtensionCalls,
|
|
53
54
|
} from './parser-transforms'
|
|
54
55
|
|
|
@@ -80,19 +81,42 @@ export function preprocess(
|
|
|
80
81
|
const unsafeFunctions = new Set<string>()
|
|
81
82
|
const safeFunctions = new Set<string>()
|
|
82
83
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
// Detect whether this source was emitted by fromTS (TS-originated)
|
|
85
|
+
// The /* tjs <- filename */ annotation is the signal
|
|
86
|
+
const isFromTS = /\/\*\s*tjs\s*<-\s*\S+\s*\*\//.test(source)
|
|
87
|
+
|
|
88
|
+
// Native TJS: all modes ON by default (TJS is its own language)
|
|
89
|
+
// TS-originated or VM target (AJS): all modes OFF, safety none (JS-compatible)
|
|
90
|
+
const isCompat = isFromTS || options.vmTarget
|
|
91
|
+
const tjsModes: TjsModes = isCompat
|
|
92
|
+
? {
|
|
93
|
+
tjsEquals: false,
|
|
94
|
+
tjsClass: false,
|
|
95
|
+
tjsDate: false,
|
|
96
|
+
tjsNoeval: false,
|
|
97
|
+
tjsStandard: false,
|
|
98
|
+
tjsSafeEval: false,
|
|
99
|
+
tjsNoVar: false,
|
|
100
|
+
}
|
|
101
|
+
: {
|
|
102
|
+
tjsEquals: true,
|
|
103
|
+
tjsClass: true,
|
|
104
|
+
tjsDate: true,
|
|
105
|
+
tjsNoeval: true,
|
|
106
|
+
tjsStandard: true,
|
|
107
|
+
tjsSafeEval: false, // opt-in only (adds import)
|
|
108
|
+
tjsNoVar: true,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Safety: native TJS defaults to 'inputs' (runtime default),
|
|
112
|
+
// TS-originated and VM targets default to 'none'
|
|
113
|
+
if (isCompat) {
|
|
114
|
+
moduleSafety = 'none'
|
|
92
115
|
}
|
|
93
116
|
|
|
94
117
|
// Handle module-level safety directive: safety none | safety inputs | safety all
|
|
95
118
|
// Must be at the start of the file (possibly after comments/whitespace)
|
|
119
|
+
// Explicit directive always overrides the default
|
|
96
120
|
const safetyMatch = source.match(
|
|
97
121
|
/^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)\s*safety\s+(none|inputs|all)\b/
|
|
98
122
|
)
|
|
@@ -106,10 +130,11 @@ export function preprocess(
|
|
|
106
130
|
}
|
|
107
131
|
|
|
108
132
|
// Handle TJS mode directives (can appear in any order after safety)
|
|
109
|
-
// TjsStrict enables all TJS modes
|
|
133
|
+
// TjsStrict enables all TJS modes (useful for TS-originated code opting in)
|
|
134
|
+
// TjsCompat disables all TJS modes (useful for native TJS opting out)
|
|
110
135
|
// Individual modes: TjsEquals, TjsClass, TjsDate, TjsNoeval, TjsStandard, TjsSafeEval
|
|
111
136
|
const directivePattern =
|
|
112
|
-
/^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)\s*(TjsStrict|TjsEquals|TjsClass|TjsDate|TjsNoeval|TjsNoVar|TjsStandard|TjsSafeEval)\b/
|
|
137
|
+
/^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)\s*(TjsStrict|TjsCompat|TjsEquals|TjsClass|TjsDate|TjsNoeval|TjsNoVar|TjsStandard|TjsSafeEval)\b/
|
|
113
138
|
|
|
114
139
|
let match
|
|
115
140
|
while ((match = source.match(directivePattern))) {
|
|
@@ -123,6 +148,15 @@ export function preprocess(
|
|
|
123
148
|
tjsModes.tjsNoeval = true
|
|
124
149
|
tjsModes.tjsNoVar = true
|
|
125
150
|
tjsModes.tjsStandard = true
|
|
151
|
+
} else if (directive === 'TjsCompat') {
|
|
152
|
+
// Disable all TJS modes (JS-compatible)
|
|
153
|
+
tjsModes.tjsEquals = false
|
|
154
|
+
tjsModes.tjsClass = false
|
|
155
|
+
tjsModes.tjsDate = false
|
|
156
|
+
tjsModes.tjsNoeval = false
|
|
157
|
+
tjsModes.tjsNoVar = false
|
|
158
|
+
tjsModes.tjsStandard = false
|
|
159
|
+
tjsModes.tjsSafeEval = false
|
|
126
160
|
} else if (directive === 'TjsEquals') {
|
|
127
161
|
tjsModes.tjsEquals = true
|
|
128
162
|
} else if (directive === 'TjsClass') {
|
|
@@ -158,6 +192,10 @@ export function preprocess(
|
|
|
158
192
|
// Must happen before acorn parsing since const! is not valid JS
|
|
159
193
|
source = transformConstBang(source)
|
|
160
194
|
|
|
195
|
+
// Transform !. bang access to __tjs.bang() calls
|
|
196
|
+
// Must happen before acorn parsing since !. is not valid JS
|
|
197
|
+
source = transformBangAccess(source)
|
|
198
|
+
|
|
161
199
|
// Transform Is/IsNot infix operators to function calls
|
|
162
200
|
// a Is b -> Is(a, b)
|
|
163
201
|
// a IsNot b -> IsNot(a, b)
|
package/src/lang/perf.test.ts
CHANGED
|
@@ -38,7 +38,7 @@ describe('TJS Performance', () => {
|
|
|
38
38
|
// Create a simple test file
|
|
39
39
|
const testFile = '/tmp/perf-test.tjs'
|
|
40
40
|
const { writeFileSync } = await import('fs')
|
|
41
|
-
writeFileSync(testFile, `function add(a: 1, b: 2)
|
|
41
|
+
writeFileSync(testFile, `function add(a: 1, b: 2): 3 { return a + b }`)
|
|
42
42
|
|
|
43
43
|
const cliPath = path.join(import.meta.dir, '../cli/tjsx.ts')
|
|
44
44
|
|
|
@@ -120,7 +120,7 @@ describe('TJS Performance', () => {
|
|
|
120
120
|
|
|
121
121
|
// TJS transpiled
|
|
122
122
|
const tjsResult = tjs(`
|
|
123
|
-
function tjsDouble(x: 0)
|
|
123
|
+
function tjsDouble(x: 0): 0 {
|
|
124
124
|
return x * 2
|
|
125
125
|
}
|
|
126
126
|
`)
|
|
@@ -128,7 +128,7 @@ describe('TJS Performance', () => {
|
|
|
128
128
|
|
|
129
129
|
// TJS with unsafe (!) - no validation wrapper
|
|
130
130
|
const unsafeResult = tjs(`
|
|
131
|
-
function unsafeDouble(! x: 0)
|
|
131
|
+
function unsafeDouble(! x: 0): 0 {
|
|
132
132
|
return x * 2
|
|
133
133
|
}
|
|
134
134
|
`)
|
|
@@ -172,7 +172,7 @@ describe('TJS Performance', () => {
|
|
|
172
172
|
|
|
173
173
|
// TJS transpiled
|
|
174
174
|
const tjsResult = tjs(`
|
|
175
|
-
function tjsTransform(x: 0, y: 0)
|
|
175
|
+
function tjsTransform(x: 0, y: 0): { sum: 0, product: 0 } {
|
|
176
176
|
return { sum: x + y, product: x * y }
|
|
177
177
|
}
|
|
178
178
|
`)
|
|
@@ -182,7 +182,7 @@ describe('TJS Performance', () => {
|
|
|
182
182
|
|
|
183
183
|
// TJS with unsafe (!) - no validation wrapper
|
|
184
184
|
const unsafeResult = tjs(`
|
|
185
|
-
function unsafeTransform(! x: 0, y: 0)
|
|
185
|
+
function unsafeTransform(! x: 0, y: 0): { sum: 0, product: 0 } {
|
|
186
186
|
return { sum: x + y, product: x * y }
|
|
187
187
|
}
|
|
188
188
|
`)
|
|
@@ -225,7 +225,7 @@ describe('TJS Performance', () => {
|
|
|
225
225
|
|
|
226
226
|
// TJS transpiled
|
|
227
227
|
const tjsResult = tjs(`
|
|
228
|
-
function tjsSum(arr: [0])
|
|
228
|
+
function tjsSum(arr: [0]): 0 {
|
|
229
229
|
let sum = 0
|
|
230
230
|
for (const n of arr) sum += n
|
|
231
231
|
return sum
|
|
@@ -235,7 +235,7 @@ describe('TJS Performance', () => {
|
|
|
235
235
|
|
|
236
236
|
// TJS with unsafe (!) - no validation wrapper
|
|
237
237
|
const unsafeResult = tjs(`
|
|
238
|
-
function unsafeSum(! arr: [0])
|
|
238
|
+
function unsafeSum(! arr: [0]): 0 {
|
|
239
239
|
let sum = 0
|
|
240
240
|
for (const n of arr) sum += n
|
|
241
241
|
return sum
|
|
@@ -281,7 +281,7 @@ describe('TJS Performance', () => {
|
|
|
281
281
|
|
|
282
282
|
// TJS - loop inside function body
|
|
283
283
|
const tjsResult = tjs(`
|
|
284
|
-
function tjsIntensive(n: 0)
|
|
284
|
+
function tjsIntensive(n: 0): 0 {
|
|
285
285
|
let sum = 0
|
|
286
286
|
for (let i = 0; i < n; i++) {
|
|
287
287
|
sum += i * i
|
|
@@ -295,7 +295,7 @@ describe('TJS Performance', () => {
|
|
|
295
295
|
|
|
296
296
|
// TJS unsafe (!) - no validation wrapper
|
|
297
297
|
const unsafeResult = tjs(`
|
|
298
|
-
function unsafeIntensive(! n: 0)
|
|
298
|
+
function unsafeIntensive(! n: 0): 0 {
|
|
299
299
|
let sum = 0
|
|
300
300
|
for (let i = 0; i < n; i++) {
|
|
301
301
|
sum += i * i
|
|
@@ -338,7 +338,7 @@ describe('TJS Performance', () => {
|
|
|
338
338
|
it('should measure error path overhead for try block', () => {
|
|
339
339
|
// TJS try-without-catch converts exceptions to monadic errors
|
|
340
340
|
const tryResult = tjs(`
|
|
341
|
-
function tryThrow(x: 0)
|
|
341
|
+
function tryThrow(x: 0): 0 {
|
|
342
342
|
try {
|
|
343
343
|
if (x < 0) throw new Error('negative')
|
|
344
344
|
return x
|
|
@@ -456,7 +456,7 @@ describe('TJS Performance', () => {
|
|
|
456
456
|
|
|
457
457
|
// TJS unsafe (!) version - no wrapper at all
|
|
458
458
|
const unsafeResult = tjs(`
|
|
459
|
-
function unsafeAdd(! a: 0, b: 0)
|
|
459
|
+
function unsafeAdd(! a: 0, b: 0): 0 {
|
|
460
460
|
return a + b
|
|
461
461
|
}
|
|
462
462
|
`)
|
|
@@ -47,7 +47,7 @@ function execCode(code: string): any[] {
|
|
|
47
47
|
describe('TJS roundtrip - code should just work', () => {
|
|
48
48
|
test('basic function with type annotations', () => {
|
|
49
49
|
const source = `
|
|
50
|
-
function add(a: 0, b: 0)
|
|
50
|
+
function add(a: 0, b: 0): 0 {
|
|
51
51
|
return a + b
|
|
52
52
|
}
|
|
53
53
|
console.log(add(2, 3))
|
|
@@ -61,7 +61,7 @@ console.log(add(2, 3))
|
|
|
61
61
|
|
|
62
62
|
test('template literals (backticks)', () => {
|
|
63
63
|
const source = `
|
|
64
|
-
function greet(name: 'World')
|
|
64
|
+
function greet(name: 'World'): '' {
|
|
65
65
|
return \`Hello, \${name}!\`
|
|
66
66
|
}
|
|
67
67
|
console.log(greet('TJS'))
|
|
@@ -75,7 +75,7 @@ console.log(greet('TJS'))
|
|
|
75
75
|
|
|
76
76
|
test('inline tests execute at transpile time', () => {
|
|
77
77
|
const source = `
|
|
78
|
-
function double(x: 0)
|
|
78
|
+
function double(x: 0): 0 {
|
|
79
79
|
return x * 2
|
|
80
80
|
}
|
|
81
81
|
|