tjs-lang 0.7.7 → 0.7.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 +90 -33
- package/bin/docs.js +4 -1
- package/demo/docs.json +45 -11
- package/demo/src/examples.test.ts +1 -0
- package/demo/src/imports.test.ts +16 -4
- package/demo/src/imports.ts +60 -15
- package/demo/src/playground-shared.ts +9 -8
- package/demo/src/tfs-worker.js +205 -147
- package/demo/src/tjs-playground.ts +34 -10
- package/demo/src/ts-playground.ts +24 -8
- package/dist/index.js +118 -101
- package/dist/index.js.map +4 -4
- package/dist/src/lang/bool-coercion.d.ts +50 -0
- package/dist/src/lang/docs.d.ts +31 -6
- package/dist/src/lang/linter.d.ts +8 -0
- package/dist/src/lang/parser-transforms.d.ts +18 -0
- package/dist/src/lang/parser-types.d.ts +2 -0
- package/dist/src/lang/parser.d.ts +3 -0
- package/dist/src/lang/runtime.d.ts +34 -0
- package/dist/src/lang/types.d.ts +9 -1
- package/dist/src/rbac/index.d.ts +1 -1
- package/dist/src/vm/runtime.d.ts +1 -1
- package/dist/tjs-eval.js +38 -36
- package/dist/tjs-eval.js.map +4 -4
- package/dist/tjs-from-ts.js +20 -20
- package/dist/tjs-from-ts.js.map +3 -3
- package/dist/tjs-lang.js +85 -83
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +47 -45
- package/dist/tjs-vm.js.map +4 -4
- package/llms.txt +79 -0
- package/package.json +3 -2
- package/src/cli/commands/convert.test.ts +16 -21
- package/src/lang/bool-coercion.test.ts +203 -0
- package/src/lang/bool-coercion.ts +314 -0
- package/src/lang/codegen.test.ts +137 -0
- package/src/lang/docs.test.ts +328 -1
- package/src/lang/docs.ts +424 -24
- package/src/lang/emitters/ast.ts +11 -12
- package/src/lang/emitters/dts.test.ts +41 -0
- package/src/lang/emitters/dts.ts +9 -0
- package/src/lang/emitters/js-tests.ts +9 -4
- package/src/lang/emitters/js.ts +182 -2
- package/src/lang/inference.ts +54 -0
- package/src/lang/linter.test.ts +104 -1
- package/src/lang/linter.ts +124 -1
- package/src/lang/parser-params.ts +31 -0
- package/src/lang/parser-transforms.ts +304 -0
- package/src/lang/parser-types.ts +2 -0
- package/src/lang/parser.test.ts +73 -1
- package/src/lang/parser.ts +34 -1
- package/src/lang/runtime.ts +98 -0
- package/src/lang/types.ts +6 -0
- package/src/rbac/index.ts +2 -2
- package/src/rbac/rules.tjs.d.ts +9 -0
- package/src/vm/atoms/batteries.ts +2 -2
- package/src/vm/runtime.ts +10 -3
- package/dist/src/rbac/rules.d.ts +0 -184
- package/src/rbac/rules.js +0 -338
|
@@ -3595,3 +3595,307 @@ function findMatchingOpen(
|
|
|
3595
3595
|
}
|
|
3596
3596
|
return pos
|
|
3597
3597
|
}
|
|
3598
|
+
|
|
3599
|
+
/**
|
|
3600
|
+
* Transform `let x: <example>` and `let x: <example> = value` declarations.
|
|
3601
|
+
*
|
|
3602
|
+
* Strips the `: <example>` annotation so Acorn can parse, and records the
|
|
3603
|
+
* variable name + example text so the linter and (later) type inference can
|
|
3604
|
+
* use the annotation. Acorn rejects the colon since it is not valid JS.
|
|
3605
|
+
*
|
|
3606
|
+
* let x: '' → let x (annotation: x → '')
|
|
3607
|
+
* let x: 0 = 5 → let x = 5 (annotation: x → 0)
|
|
3608
|
+
* let result: { ok: false } = ... (annotation: result → { ok: false })
|
|
3609
|
+
*
|
|
3610
|
+
* Only `let` is processed. `const` always has an initializer, so the type
|
|
3611
|
+
* is always inferable. `var` is rejected by TjsNoVar mode.
|
|
3612
|
+
*/
|
|
3613
|
+
export function transformLetTypeAnnotations(source: string): {
|
|
3614
|
+
source: string
|
|
3615
|
+
annotations: Map<string, string>
|
|
3616
|
+
} {
|
|
3617
|
+
const annotations = new Map<string, string>()
|
|
3618
|
+
if (!source.includes('let ')) return { source, annotations }
|
|
3619
|
+
|
|
3620
|
+
type Replacement = { start: number; end: number; replacement: string }
|
|
3621
|
+
const replacements: Replacement[] = []
|
|
3622
|
+
|
|
3623
|
+
let i = 0
|
|
3624
|
+
let state: TokenizerState = 'normal'
|
|
3625
|
+
const templateStack: number[] = []
|
|
3626
|
+
|
|
3627
|
+
while (i < source.length) {
|
|
3628
|
+
const char = source[i]
|
|
3629
|
+
const nextChar = source[i + 1]
|
|
3630
|
+
|
|
3631
|
+
switch (state) {
|
|
3632
|
+
case 'single-string':
|
|
3633
|
+
if (char === '\\' && i + 1 < source.length) {
|
|
3634
|
+
i += 2
|
|
3635
|
+
continue
|
|
3636
|
+
}
|
|
3637
|
+
if (char === "'") state = 'normal'
|
|
3638
|
+
i++
|
|
3639
|
+
continue
|
|
3640
|
+
case 'double-string':
|
|
3641
|
+
if (char === '\\' && i + 1 < source.length) {
|
|
3642
|
+
i += 2
|
|
3643
|
+
continue
|
|
3644
|
+
}
|
|
3645
|
+
if (char === '"') state = 'normal'
|
|
3646
|
+
i++
|
|
3647
|
+
continue
|
|
3648
|
+
case 'template-string':
|
|
3649
|
+
if (char === '\\' && i + 1 < source.length) {
|
|
3650
|
+
i += 2
|
|
3651
|
+
continue
|
|
3652
|
+
}
|
|
3653
|
+
if (char === '$' && nextChar === '{') {
|
|
3654
|
+
i += 2
|
|
3655
|
+
templateStack.push(1)
|
|
3656
|
+
state = 'normal'
|
|
3657
|
+
continue
|
|
3658
|
+
}
|
|
3659
|
+
if (char === '`') state = 'normal'
|
|
3660
|
+
i++
|
|
3661
|
+
continue
|
|
3662
|
+
case 'line-comment':
|
|
3663
|
+
if (char === '\n') state = 'normal'
|
|
3664
|
+
i++
|
|
3665
|
+
continue
|
|
3666
|
+
case 'block-comment':
|
|
3667
|
+
if (char === '*' && nextChar === '/') {
|
|
3668
|
+
i += 2
|
|
3669
|
+
state = 'normal'
|
|
3670
|
+
continue
|
|
3671
|
+
}
|
|
3672
|
+
i++
|
|
3673
|
+
continue
|
|
3674
|
+
case 'regex':
|
|
3675
|
+
if (char === '\\' && i + 1 < source.length) {
|
|
3676
|
+
i += 2
|
|
3677
|
+
continue
|
|
3678
|
+
}
|
|
3679
|
+
if (char === '[') {
|
|
3680
|
+
i++
|
|
3681
|
+
while (i < source.length && source[i] !== ']') {
|
|
3682
|
+
if (source[i] === '\\' && i + 1 < source.length) i += 2
|
|
3683
|
+
else i++
|
|
3684
|
+
}
|
|
3685
|
+
if (i < source.length) i++
|
|
3686
|
+
continue
|
|
3687
|
+
}
|
|
3688
|
+
if (char === '/') {
|
|
3689
|
+
i++
|
|
3690
|
+
while (i < source.length && /[gimsuy]/.test(source[i])) i++
|
|
3691
|
+
state = 'normal'
|
|
3692
|
+
continue
|
|
3693
|
+
}
|
|
3694
|
+
i++
|
|
3695
|
+
continue
|
|
3696
|
+
case 'normal':
|
|
3697
|
+
if (templateStack.length > 0) {
|
|
3698
|
+
if (char === '{') {
|
|
3699
|
+
templateStack[templateStack.length - 1]++
|
|
3700
|
+
} else if (char === '}') {
|
|
3701
|
+
templateStack[templateStack.length - 1]--
|
|
3702
|
+
if (templateStack[templateStack.length - 1] === 0) {
|
|
3703
|
+
templateStack.pop()
|
|
3704
|
+
i++
|
|
3705
|
+
state = 'template-string'
|
|
3706
|
+
continue
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
if (char === "'") {
|
|
3711
|
+
i++
|
|
3712
|
+
state = 'single-string'
|
|
3713
|
+
continue
|
|
3714
|
+
}
|
|
3715
|
+
if (char === '"') {
|
|
3716
|
+
i++
|
|
3717
|
+
state = 'double-string'
|
|
3718
|
+
continue
|
|
3719
|
+
}
|
|
3720
|
+
if (char === '`') {
|
|
3721
|
+
i++
|
|
3722
|
+
state = 'template-string'
|
|
3723
|
+
continue
|
|
3724
|
+
}
|
|
3725
|
+
if (char === '/' && nextChar === '/') {
|
|
3726
|
+
i += 2
|
|
3727
|
+
state = 'line-comment'
|
|
3728
|
+
continue
|
|
3729
|
+
}
|
|
3730
|
+
if (char === '/' && nextChar === '*') {
|
|
3731
|
+
i += 2
|
|
3732
|
+
state = 'block-comment'
|
|
3733
|
+
continue
|
|
3734
|
+
}
|
|
3735
|
+
if (char === '/') {
|
|
3736
|
+
let j = i - 1
|
|
3737
|
+
while (j >= 0 && /\s/.test(source[j])) j--
|
|
3738
|
+
const beforeChar = j >= 0 ? source[j] : ''
|
|
3739
|
+
const isRegexContext =
|
|
3740
|
+
!beforeChar ||
|
|
3741
|
+
/[=(!,;:{[&|?+\-*%<>~^]/.test(beforeChar) ||
|
|
3742
|
+
(j >= 5 &&
|
|
3743
|
+
/\b(return|case|throw|in|of|typeof|instanceof|new|delete|void)$/.test(
|
|
3744
|
+
source.slice(Math.max(0, j - 10), j + 1)
|
|
3745
|
+
))
|
|
3746
|
+
if (isRegexContext) {
|
|
3747
|
+
i++
|
|
3748
|
+
state = 'regex'
|
|
3749
|
+
continue
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
// Detect `let <ident> :` at top-level normal state
|
|
3754
|
+
if (
|
|
3755
|
+
char === 'l' &&
|
|
3756
|
+
source.slice(i, i + 4) === 'let ' &&
|
|
3757
|
+
(i === 0 || !/[\w$]/.test(source[i - 1]))
|
|
3758
|
+
) {
|
|
3759
|
+
// Skip past `let` and whitespace
|
|
3760
|
+
let j = i + 4
|
|
3761
|
+
while (j < source.length && /\s/.test(source[j])) j++
|
|
3762
|
+
// Match identifier
|
|
3763
|
+
if (j < source.length && /[a-zA-Z_$]/.test(source[j])) {
|
|
3764
|
+
const nameStart = j
|
|
3765
|
+
while (j < source.length && /[\w$]/.test(source[j])) j++
|
|
3766
|
+
const nameEnd = j
|
|
3767
|
+
const varName = source.slice(nameStart, nameEnd)
|
|
3768
|
+
// Skip whitespace; require a `:` (not `::` or part of `?:`)
|
|
3769
|
+
let k = j
|
|
3770
|
+
while (k < source.length && /\s/.test(source[k])) k++
|
|
3771
|
+
if (
|
|
3772
|
+
k < source.length &&
|
|
3773
|
+
source[k] === ':' &&
|
|
3774
|
+
source[k + 1] !== ':'
|
|
3775
|
+
) {
|
|
3776
|
+
const colonPos = k
|
|
3777
|
+
// Skip whitespace after colon
|
|
3778
|
+
let exStart = colonPos + 1
|
|
3779
|
+
while (exStart < source.length && /[ \t]/.test(source[exStart])) {
|
|
3780
|
+
exStart++
|
|
3781
|
+
}
|
|
3782
|
+
// Scan example expression until `=`, `,`, `;`, or newline at depth 0
|
|
3783
|
+
const exEnd = scanExampleEnd(source, exStart)
|
|
3784
|
+
if (exEnd > exStart) {
|
|
3785
|
+
const example = source.slice(exStart, exEnd).trim()
|
|
3786
|
+
annotations.set(varName, example)
|
|
3787
|
+
replacements.push({
|
|
3788
|
+
start: nameEnd,
|
|
3789
|
+
end: exEnd,
|
|
3790
|
+
replacement: '',
|
|
3791
|
+
})
|
|
3792
|
+
i = exEnd
|
|
3793
|
+
continue
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
break
|
|
3799
|
+
}
|
|
3800
|
+
i++
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
if (replacements.length === 0) return { source, annotations }
|
|
3804
|
+
|
|
3805
|
+
// Apply right-to-left to preserve positions
|
|
3806
|
+
let result = source
|
|
3807
|
+
for (let k = replacements.length - 1; k >= 0; k--) {
|
|
3808
|
+
const r = replacements[k]
|
|
3809
|
+
result = result.slice(0, r.start) + r.replacement + result.slice(r.end)
|
|
3810
|
+
}
|
|
3811
|
+
return { source: result, annotations }
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
/**
|
|
3815
|
+
* Scan forward from `start` and return the position where the example
|
|
3816
|
+
* expression ends. Stops at `=`, `,`, `;`, or a newline when paren/brace/
|
|
3817
|
+
* bracket depth is 0. Skips through nested brackets, strings, and templates.
|
|
3818
|
+
*/
|
|
3819
|
+
function scanExampleEnd(source: string, start: number): number {
|
|
3820
|
+
let i = start
|
|
3821
|
+
let parens = 0
|
|
3822
|
+
let braces = 0
|
|
3823
|
+
let brackets = 0
|
|
3824
|
+
let state: 'normal' | 'sq' | 'dq' | 'tpl' = 'normal'
|
|
3825
|
+
const templateStack: number[] = []
|
|
3826
|
+
while (i < source.length) {
|
|
3827
|
+
const c = source[i]
|
|
3828
|
+
if (state === 'sq') {
|
|
3829
|
+
if (c === '\\') {
|
|
3830
|
+
i += 2
|
|
3831
|
+
continue
|
|
3832
|
+
}
|
|
3833
|
+
if (c === "'") state = 'normal'
|
|
3834
|
+
i++
|
|
3835
|
+
continue
|
|
3836
|
+
}
|
|
3837
|
+
if (state === 'dq') {
|
|
3838
|
+
if (c === '\\') {
|
|
3839
|
+
i += 2
|
|
3840
|
+
continue
|
|
3841
|
+
}
|
|
3842
|
+
if (c === '"') state = 'normal'
|
|
3843
|
+
i++
|
|
3844
|
+
continue
|
|
3845
|
+
}
|
|
3846
|
+
if (state === 'tpl') {
|
|
3847
|
+
if (c === '\\') {
|
|
3848
|
+
i += 2
|
|
3849
|
+
continue
|
|
3850
|
+
}
|
|
3851
|
+
if (c === '$' && source[i + 1] === '{') {
|
|
3852
|
+
templateStack.push(1)
|
|
3853
|
+
state = 'normal'
|
|
3854
|
+
i += 2
|
|
3855
|
+
continue
|
|
3856
|
+
}
|
|
3857
|
+
if (c === '`') state = 'normal'
|
|
3858
|
+
i++
|
|
3859
|
+
continue
|
|
3860
|
+
}
|
|
3861
|
+
// normal
|
|
3862
|
+
if (templateStack.length > 0) {
|
|
3863
|
+
if (c === '{') templateStack[templateStack.length - 1]++
|
|
3864
|
+
else if (c === '}') {
|
|
3865
|
+
templateStack[templateStack.length - 1]--
|
|
3866
|
+
if (templateStack[templateStack.length - 1] === 0) {
|
|
3867
|
+
templateStack.pop()
|
|
3868
|
+
state = 'tpl'
|
|
3869
|
+
i++
|
|
3870
|
+
continue
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
if (c === "'") {
|
|
3875
|
+
state = 'sq'
|
|
3876
|
+
i++
|
|
3877
|
+
continue
|
|
3878
|
+
}
|
|
3879
|
+
if (c === '"') {
|
|
3880
|
+
state = 'dq'
|
|
3881
|
+
i++
|
|
3882
|
+
continue
|
|
3883
|
+
}
|
|
3884
|
+
if (c === '`') {
|
|
3885
|
+
state = 'tpl'
|
|
3886
|
+
i++
|
|
3887
|
+
continue
|
|
3888
|
+
}
|
|
3889
|
+
if (c === '(') parens++
|
|
3890
|
+
else if (c === ')') parens--
|
|
3891
|
+
else if (c === '{') braces++
|
|
3892
|
+
else if (c === '}') braces--
|
|
3893
|
+
else if (c === '[') brackets++
|
|
3894
|
+
else if (c === ']') brackets--
|
|
3895
|
+
if (parens === 0 && braces === 0 && brackets === 0) {
|
|
3896
|
+
if (c === '=' || c === ',' || c === ';' || c === '\n') return i
|
|
3897
|
+
}
|
|
3898
|
+
i++
|
|
3899
|
+
}
|
|
3900
|
+
return i
|
|
3901
|
+
}
|
package/src/lang/parser-types.ts
CHANGED
|
@@ -144,6 +144,8 @@ export interface TjsModes {
|
|
|
144
144
|
tjsSafeEval: boolean
|
|
145
145
|
/** TjsNoVar: var declarations are syntax errors */
|
|
146
146
|
tjsNoVar: boolean
|
|
147
|
+
/** TjsSafeAssign: let declarations need an initializer or `: example` annotation; literal undefined/null/void 0 assigned to typed lets is flagged */
|
|
148
|
+
tjsSafeAssign: boolean
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
/**
|
package/src/lang/parser.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'bun:test'
|
|
2
2
|
import { transpile, ajs, tjs } from './index'
|
|
3
|
-
import { preprocess } from './parser'
|
|
3
|
+
import { preprocess, parse } from './parser'
|
|
4
4
|
import { createRuntime, isMonadicError } from './runtime'
|
|
5
5
|
|
|
6
6
|
describe('Transpiler', () => {
|
|
@@ -1067,4 +1067,76 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
1067
1067
|
expect(varSetStep.value.op).toBe('??')
|
|
1068
1068
|
})
|
|
1069
1069
|
})
|
|
1070
|
+
|
|
1071
|
+
describe('stray `=>` on function declarations', () => {
|
|
1072
|
+
it('errors clearly on `function f(...): T => { body }`', () => {
|
|
1073
|
+
// A common mistake — writing arrow-function syntax on a regular
|
|
1074
|
+
// function declaration. Without the dedicated check, the `=>` falls
|
|
1075
|
+
// through to Acorn which complains at a misleading position.
|
|
1076
|
+
expect(() =>
|
|
1077
|
+
tjs(`function f(s: '', n = 0): 0 => {
|
|
1078
|
+
return s.length + n
|
|
1079
|
+
}`)
|
|
1080
|
+
).toThrow(/Unexpected '=>' after function declaration/)
|
|
1081
|
+
})
|
|
1082
|
+
|
|
1083
|
+
it('errors with column pointing at the `=>` (not earlier in the line)', () => {
|
|
1084
|
+
let caught: any
|
|
1085
|
+
try {
|
|
1086
|
+
tjs(`function f(): 0 => { return 0 }`)
|
|
1087
|
+
} catch (e) {
|
|
1088
|
+
caught = e
|
|
1089
|
+
}
|
|
1090
|
+
expect(caught).toBeDefined()
|
|
1091
|
+
// The `=>` is at column 17 (1-indexed): `function f(): 0 ` is 16 chars
|
|
1092
|
+
expect(caught.column).toBe(16)
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
it('does not error on regular function declarations', () => {
|
|
1096
|
+
expect(() =>
|
|
1097
|
+
tjs(`function f(s: ''): 0 { return s.length }`)
|
|
1098
|
+
).not.toThrow()
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
it('does not error on real arrow function expressions', () => {
|
|
1102
|
+
expect(() => tjs(`const f = (s = '') => s.length`)).not.toThrow()
|
|
1103
|
+
})
|
|
1104
|
+
})
|
|
1105
|
+
|
|
1106
|
+
describe('let type annotations (TjsSafeAssign)', () => {
|
|
1107
|
+
it("strips `: <example>` from `let x: ''` and records annotation", () => {
|
|
1108
|
+
const r = parse(`let x: ''`)
|
|
1109
|
+
expect(r.letAnnotations.get('x')).toBe("''")
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
it('strips `: <example>` from `let x: 0 = 5` and keeps the initializer', () => {
|
|
1113
|
+
const r = parse(`let x: 0 = 5`)
|
|
1114
|
+
expect(r.letAnnotations.get('x')).toBe('0')
|
|
1115
|
+
const decl = r.ast.body[0] as any
|
|
1116
|
+
expect(decl.type).toBe('VariableDeclaration')
|
|
1117
|
+
expect(decl.declarations[0].init).not.toBeNull()
|
|
1118
|
+
})
|
|
1119
|
+
|
|
1120
|
+
it('handles object-literal example with nested braces', () => {
|
|
1121
|
+
const r = parse(
|
|
1122
|
+
`function f() { let result: { ok: false }; return result }`
|
|
1123
|
+
)
|
|
1124
|
+
expect(r.letAnnotations.get('result')).toBe('{ ok: false }')
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
it('does not strip `:` inside string literals', () => {
|
|
1128
|
+
const r = parse(`let s = 'a:b:c'`)
|
|
1129
|
+
expect(r.letAnnotations.size).toBe(0)
|
|
1130
|
+
})
|
|
1131
|
+
|
|
1132
|
+
it('TjsSafeAssign mode is on by default in native TJS', () => {
|
|
1133
|
+
const r = parse(`let x = 0`)
|
|
1134
|
+
expect(r.tjsModes.tjsSafeAssign).toBe(true)
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
it('TjsCompat directive disables TjsSafeAssign', () => {
|
|
1138
|
+
const r = parse(`TjsCompat\nlet x = 0`)
|
|
1139
|
+
expect(r.tjsModes.tjsSafeAssign).toBe(false)
|
|
1140
|
+
})
|
|
1141
|
+
})
|
|
1070
1142
|
})
|
package/src/lang/parser.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
transformConstBang,
|
|
52
52
|
transformBangAccess,
|
|
53
53
|
transformExtensionCalls,
|
|
54
|
+
transformLetTypeAnnotations,
|
|
54
55
|
} from './parser-transforms'
|
|
55
56
|
|
|
56
57
|
// Re-export transformExtensionCalls for js.ts
|
|
@@ -120,6 +121,7 @@ export function preprocess(
|
|
|
120
121
|
testErrors: string[]
|
|
121
122
|
polymorphicNames: Set<string>
|
|
122
123
|
extensions: Map<string, Set<string>>
|
|
124
|
+
letAnnotations: Map<string, string>
|
|
123
125
|
} {
|
|
124
126
|
const originalSource = source
|
|
125
127
|
let moduleSafety: 'none' | 'inputs' | 'all' | undefined
|
|
@@ -143,6 +145,7 @@ export function preprocess(
|
|
|
143
145
|
tjsStandard: false,
|
|
144
146
|
tjsSafeEval: false,
|
|
145
147
|
tjsNoVar: false,
|
|
148
|
+
tjsSafeAssign: false,
|
|
146
149
|
}
|
|
147
150
|
: {
|
|
148
151
|
tjsEquals: true,
|
|
@@ -152,6 +155,7 @@ export function preprocess(
|
|
|
152
155
|
tjsStandard: true,
|
|
153
156
|
tjsSafeEval: false, // opt-in only (adds import)
|
|
154
157
|
tjsNoVar: true,
|
|
158
|
+
tjsSafeAssign: true,
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
// Safety: native TJS defaults to 'inputs' (runtime default),
|
|
@@ -180,7 +184,7 @@ export function preprocess(
|
|
|
180
184
|
// TjsCompat disables all TJS modes (useful for native TJS opting out)
|
|
181
185
|
// Individual modes: TjsEquals, TjsClass, TjsDate, TjsNoeval, TjsStandard, TjsSafeEval
|
|
182
186
|
const directivePattern =
|
|
183
|
-
/^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)\s*(TjsStrict|TjsCompat|TjsEquals|TjsClass|TjsDate|TjsNoeval|TjsNoVar|TjsStandard|TjsSafeEval)\b/
|
|
187
|
+
/^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)\s*(TjsStrict|TjsCompat|TjsEquals|TjsClass|TjsDate|TjsNoeval|TjsNoVar|TjsStandard|TjsSafeEval|TjsSafeAssign)\b/
|
|
184
188
|
|
|
185
189
|
let match
|
|
186
190
|
while ((match = source.match(directivePattern))) {
|
|
@@ -194,6 +198,7 @@ export function preprocess(
|
|
|
194
198
|
tjsModes.tjsNoeval = true
|
|
195
199
|
tjsModes.tjsNoVar = true
|
|
196
200
|
tjsModes.tjsStandard = true
|
|
201
|
+
tjsModes.tjsSafeAssign = true
|
|
197
202
|
} else if (directive === 'TjsCompat') {
|
|
198
203
|
// Disable all TJS modes (JS-compatible)
|
|
199
204
|
tjsModes.tjsEquals = false
|
|
@@ -203,6 +208,7 @@ export function preprocess(
|
|
|
203
208
|
tjsModes.tjsNoVar = false
|
|
204
209
|
tjsModes.tjsStandard = false
|
|
205
210
|
tjsModes.tjsSafeEval = false
|
|
211
|
+
tjsModes.tjsSafeAssign = false
|
|
206
212
|
} else if (directive === 'TjsEquals') {
|
|
207
213
|
tjsModes.tjsEquals = true
|
|
208
214
|
} else if (directive === 'TjsClass') {
|
|
@@ -217,6 +223,8 @@ export function preprocess(
|
|
|
217
223
|
tjsModes.tjsStandard = true
|
|
218
224
|
} else if (directive === 'TjsSafeEval') {
|
|
219
225
|
tjsModes.tjsSafeEval = true
|
|
226
|
+
} else if (directive === 'TjsSafeAssign') {
|
|
227
|
+
tjsModes.tjsSafeAssign = true
|
|
220
228
|
}
|
|
221
229
|
|
|
222
230
|
// Remove the directive from source
|
|
@@ -247,6 +255,13 @@ export function preprocess(
|
|
|
247
255
|
// Must happen before acorn parsing since !. is not valid JS
|
|
248
256
|
source = transformBangAccess(source)
|
|
249
257
|
|
|
258
|
+
// Transform `let x: <example>` declarations: strip annotation and record
|
|
259
|
+
// varName -> example. Must happen before paren transforms so the colon
|
|
260
|
+
// is not confused with TS-style annotations on params/returns.
|
|
261
|
+
const letAnnoResult = transformLetTypeAnnotations(source)
|
|
262
|
+
source = letAnnoResult.source
|
|
263
|
+
const letAnnotations = letAnnoResult.annotations
|
|
264
|
+
|
|
250
265
|
// Transform Is/IsNot infix operators to function calls
|
|
251
266
|
// a Is b -> Is(a, b)
|
|
252
267
|
// a IsNot b -> IsNot(a, b)
|
|
@@ -371,6 +386,7 @@ export function preprocess(
|
|
|
371
386
|
testErrors: testResult.errors,
|
|
372
387
|
polymorphicNames: polyResult.polymorphicNames,
|
|
373
388
|
extensions: extResult.extensions,
|
|
389
|
+
letAnnotations,
|
|
374
390
|
}
|
|
375
391
|
}
|
|
376
392
|
|
|
@@ -392,6 +408,8 @@ export function parse(
|
|
|
392
408
|
wasmBlocks: WasmBlock[]
|
|
393
409
|
tests: TestBlock[]
|
|
394
410
|
testErrors: string[]
|
|
411
|
+
letAnnotations: Map<string, string>
|
|
412
|
+
tjsModes: TjsModes
|
|
395
413
|
} {
|
|
396
414
|
const {
|
|
397
415
|
filename = '<source>',
|
|
@@ -412,6 +430,8 @@ export function parse(
|
|
|
412
430
|
wasmBlocks,
|
|
413
431
|
tests,
|
|
414
432
|
testErrors,
|
|
433
|
+
letAnnotations,
|
|
434
|
+
tjsModes,
|
|
415
435
|
} = colonShorthand
|
|
416
436
|
? preprocess(source, { vmTarget })
|
|
417
437
|
: {
|
|
@@ -426,6 +446,17 @@ export function parse(
|
|
|
426
446
|
wasmBlocks: [] as WasmBlock[],
|
|
427
447
|
tests: [] as TestBlock[],
|
|
428
448
|
testErrors: [] as string[],
|
|
449
|
+
letAnnotations: new Map<string, string>(),
|
|
450
|
+
tjsModes: {
|
|
451
|
+
tjsEquals: false,
|
|
452
|
+
tjsClass: false,
|
|
453
|
+
tjsDate: false,
|
|
454
|
+
tjsNoeval: false,
|
|
455
|
+
tjsStandard: false,
|
|
456
|
+
tjsSafeEval: false,
|
|
457
|
+
tjsNoVar: false,
|
|
458
|
+
tjsSafeAssign: false,
|
|
459
|
+
} as TjsModes,
|
|
429
460
|
}
|
|
430
461
|
|
|
431
462
|
try {
|
|
@@ -448,6 +479,8 @@ export function parse(
|
|
|
448
479
|
wasmBlocks,
|
|
449
480
|
tests,
|
|
450
481
|
testErrors,
|
|
482
|
+
letAnnotations,
|
|
483
|
+
tjsModes,
|
|
451
484
|
}
|
|
452
485
|
} catch (e: any) {
|
|
453
486
|
// Convert Acorn error to our error type
|
package/src/lang/runtime.ts
CHANGED
|
@@ -605,6 +605,26 @@ export function TypeOf(value: unknown): string {
|
|
|
605
605
|
return typeof value
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
+
/**
|
|
609
|
+
* Honest boolean coercion. Like `Boolean(x)` but unwraps boxed primitives
|
|
610
|
+
* first, fixing the JS footgun `Boolean(new Boolean(false)) === true`.
|
|
611
|
+
*
|
|
612
|
+
* Under TjsStandard, the source rewriter wraps every truthiness context
|
|
613
|
+
* (if/while/for/do-while conditions, `!`, `&&`, `||`, ternary, and
|
|
614
|
+
* top-level `Boolean(x)` calls) with this function so a boxed `false`
|
|
615
|
+
* actually behaves as `false`.
|
|
616
|
+
*/
|
|
617
|
+
export function toBool(value: unknown): boolean {
|
|
618
|
+
if (
|
|
619
|
+
value instanceof Boolean ||
|
|
620
|
+
value instanceof Number ||
|
|
621
|
+
value instanceof String
|
|
622
|
+
) {
|
|
623
|
+
return Boolean((value as any).valueOf())
|
|
624
|
+
}
|
|
625
|
+
return Boolean(value)
|
|
626
|
+
}
|
|
627
|
+
|
|
608
628
|
export function Eq(a: unknown, b: unknown): boolean {
|
|
609
629
|
// Unwrap boxed primitives
|
|
610
630
|
if (a instanceof String || a instanceof Number || a instanceof Boolean) {
|
|
@@ -844,6 +864,78 @@ type TypeSpec =
|
|
|
844
864
|
| string
|
|
845
865
|
| { check: (v: unknown) => boolean | string; description: string }
|
|
846
866
|
|
|
867
|
+
/**
|
|
868
|
+
* Check that a passed-in function's declared shape matches the expected
|
|
869
|
+
* shape. Returns the function unchanged on a match, or a MonadicError on
|
|
870
|
+
* mismatch. Untyped functions (no `__tjs` metadata — anonymous arrows
|
|
871
|
+
* like `x => false`) pass through unchanged on the assumption that the
|
|
872
|
+
* caller knows what they're doing; they accept any args and return
|
|
873
|
+
* whatever they return.
|
|
874
|
+
*
|
|
875
|
+
* This is a ONE-SHOT check at pass time, NOT a per-call wrapper. The TJS
|
|
876
|
+
* design call: a wrong-shape callback is ONE error at the boundary, not
|
|
877
|
+
* N errors when the receiving function invokes the callback N times.
|
|
878
|
+
*
|
|
879
|
+
* Compatibility rules (deliberately permissive — strict subtyping is a
|
|
880
|
+
* separate, larger feature):
|
|
881
|
+
* - For each expected param: the actual function may declare fewer
|
|
882
|
+
* params (extras simply not used). If both declare a kind, they
|
|
883
|
+
* must match exactly. Either side being `any` always matches.
|
|
884
|
+
* - For the return type: same exact-match rule when both are known.
|
|
885
|
+
*/
|
|
886
|
+
export function checkFnShape(
|
|
887
|
+
fn: unknown,
|
|
888
|
+
expectedParamKinds: string[],
|
|
889
|
+
expectedReturnKind: string,
|
|
890
|
+
path: string
|
|
891
|
+
): unknown {
|
|
892
|
+
if (typeof fn !== 'function') return fn // outer "is callable" check already ran
|
|
893
|
+
const meta = (fn as any).__tjs
|
|
894
|
+
if (!meta || !meta.params) return fn // untyped — let it run
|
|
895
|
+
|
|
896
|
+
const actualEntries = Object.entries(meta.params) as Array<
|
|
897
|
+
[string, { type?: { kind?: string } }]
|
|
898
|
+
>
|
|
899
|
+
for (let i = 0; i < expectedParamKinds.length; i++) {
|
|
900
|
+
const expectedKind = expectedParamKinds[i]
|
|
901
|
+
if (expectedKind === 'any') continue
|
|
902
|
+
const actual = actualEntries[i]
|
|
903
|
+
if (!actual) continue // function takes fewer params, OK
|
|
904
|
+
const actualKind = actual[1]?.type?.kind
|
|
905
|
+
if (!actualKind || actualKind === 'any') continue
|
|
906
|
+
if (actualKind !== expectedKind) {
|
|
907
|
+
return new MonadicError(
|
|
908
|
+
`Expected (...arg${i}: ${expectedKind}, ...) for '${path}', ` +
|
|
909
|
+
`but callback declares arg${i} as ${actualKind}`,
|
|
910
|
+
`${path}(arg${i})`,
|
|
911
|
+
expectedKind,
|
|
912
|
+
actualKind
|
|
913
|
+
)
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (expectedReturnKind !== 'any' && meta.returns) {
|
|
918
|
+
// Metadata's `returns` is `{ type: TypeDescriptor, defaults?: ... }`,
|
|
919
|
+
// but defensively also accept a bare TypeDescriptor.
|
|
920
|
+
const actualReturnKind = meta.returns.type?.kind ?? meta.returns.kind
|
|
921
|
+
if (
|
|
922
|
+
actualReturnKind &&
|
|
923
|
+
actualReturnKind !== 'any' &&
|
|
924
|
+
actualReturnKind !== expectedReturnKind
|
|
925
|
+
) {
|
|
926
|
+
return new MonadicError(
|
|
927
|
+
`Expected callback returning ${expectedReturnKind} for '${path}', ` +
|
|
928
|
+
`but callback returns ${actualReturnKind}`,
|
|
929
|
+
`${path}(return)`,
|
|
930
|
+
expectedReturnKind,
|
|
931
|
+
actualReturnKind
|
|
932
|
+
)
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return fn
|
|
937
|
+
}
|
|
938
|
+
|
|
847
939
|
/** Parameter metadata with optional location */
|
|
848
940
|
interface ParamMeta {
|
|
849
941
|
type: TypeSpec
|
|
@@ -1478,6 +1570,7 @@ export function createRuntime() {
|
|
|
1478
1570
|
checkType,
|
|
1479
1571
|
validateArgs,
|
|
1480
1572
|
wrap,
|
|
1573
|
+
checkFnShape,
|
|
1481
1574
|
wrapClass,
|
|
1482
1575
|
compareVersions,
|
|
1483
1576
|
versionsCompatible,
|
|
@@ -1528,6 +1621,8 @@ export function createRuntime() {
|
|
|
1528
1621
|
NotEq,
|
|
1529
1622
|
// Honest typeof (typeof with TjsEquals)
|
|
1530
1623
|
TypeOf,
|
|
1624
|
+
// Honest truthiness (unwraps boxed primitives)
|
|
1625
|
+
toBool,
|
|
1531
1626
|
tjsEquals,
|
|
1532
1627
|
// Extensions
|
|
1533
1628
|
registerExtension: instanceRegisterExtension,
|
|
@@ -1559,6 +1654,7 @@ export const runtime = {
|
|
|
1559
1654
|
checkType,
|
|
1560
1655
|
validateArgs,
|
|
1561
1656
|
wrap,
|
|
1657
|
+
checkFnShape,
|
|
1562
1658
|
wrapClass,
|
|
1563
1659
|
compareVersions,
|
|
1564
1660
|
versionsCompatible,
|
|
@@ -1612,6 +1708,8 @@ export const runtime = {
|
|
|
1612
1708
|
NotEq,
|
|
1613
1709
|
// Honest typeof (used by typeof with TjsEquals)
|
|
1614
1710
|
TypeOf,
|
|
1711
|
+
// Honest truthiness (used in TjsStandard for boxed-primitive coercion)
|
|
1712
|
+
toBool,
|
|
1615
1713
|
}
|
|
1616
1714
|
|
|
1617
1715
|
/**
|
package/src/lang/types.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface TypeDescriptor {
|
|
|
22
22
|
| 'array'
|
|
23
23
|
| 'object'
|
|
24
24
|
| 'union'
|
|
25
|
+
| 'function'
|
|
25
26
|
| 'any'
|
|
26
27
|
nullable?: boolean
|
|
27
28
|
/** For arrays: the element type */
|
|
@@ -32,6 +33,11 @@ export interface TypeDescriptor {
|
|
|
32
33
|
members?: TypeDescriptor[]
|
|
33
34
|
/** For destructured parameters: full parameter descriptors */
|
|
34
35
|
destructuredParams?: Record<string, ParameterDescriptor>
|
|
36
|
+
/** For functions: declared parameters with names and inferred types */
|
|
37
|
+
params?: Array<{ name: string; type: TypeDescriptor }>
|
|
38
|
+
/** For functions: inferred return type. Concise arrow bodies infer from
|
|
39
|
+
* the expression; block bodies and complex expressions stay `any`. */
|
|
40
|
+
returns?: TypeDescriptor
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/** Describes a function parameter */
|