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
|
@@ -105,7 +105,7 @@ function typeMatches(
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
// Primitive types - check type only (used by
|
|
108
|
+
// Primitive types - check type only (used by :? runtime validation)
|
|
109
109
|
if (typeof pattern === 'number') {
|
|
110
110
|
if (typeof actual === 'number') return { matches: true }
|
|
111
111
|
return {
|
|
@@ -451,6 +451,10 @@ interface SignatureTestInfo {
|
|
|
451
451
|
defaults?: Record<string, unknown>
|
|
452
452
|
line: number
|
|
453
453
|
isAsync?: boolean
|
|
454
|
+
/** For class method tests: the class name */
|
|
455
|
+
className?: string
|
|
456
|
+
/** For class method tests: args to pass to the first constructor */
|
|
457
|
+
constructorArgs?: unknown[]
|
|
454
458
|
}
|
|
455
459
|
|
|
456
460
|
/**
|
|
@@ -464,10 +468,10 @@ export function extractSignatureTestInfos(
|
|
|
464
468
|
// Strip comments to avoid matching functions inside doc comments/code examples
|
|
465
469
|
const sourceWithoutComments = stripComments(originalSource)
|
|
466
470
|
|
|
467
|
-
// Match function declarations with return type marker (
|
|
468
|
-
// Skip
|
|
469
|
-
// Pattern: [async] function name(params)
|
|
470
|
-
const funcRegex = /(async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(
|
|
471
|
+
// Match function declarations with return type marker (: or :?)
|
|
472
|
+
// Skip :! which means "don't test"
|
|
473
|
+
// Pattern: [async] function name(params): returnExample {
|
|
474
|
+
const funcRegex = /(async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(:[?!]?)\s*/g
|
|
471
475
|
|
|
472
476
|
let match
|
|
473
477
|
while ((match = funcRegex.exec(sourceWithoutComments)) !== null) {
|
|
@@ -481,8 +485,8 @@ export function extractSignatureTestInfos(
|
|
|
481
485
|
.slice(0, match.index)
|
|
482
486
|
.split('\n').length
|
|
483
487
|
|
|
484
|
-
//
|
|
485
|
-
if (returnMarker === '
|
|
488
|
+
// :! means skip test
|
|
489
|
+
if (returnMarker === ':!') continue
|
|
486
490
|
|
|
487
491
|
// Extract return example - handle nested braces/brackets
|
|
488
492
|
const afterMarker = sourceWithoutComments.slice(
|
|
@@ -516,6 +520,100 @@ export function extractSignatureTestInfos(
|
|
|
516
520
|
}
|
|
517
521
|
}
|
|
518
522
|
|
|
523
|
+
// Extract class method signature tests
|
|
524
|
+
// Find class declarations and their first constructor's params,
|
|
525
|
+
// then find methods with return type markers (: or :?)
|
|
526
|
+
const classRegex = /class\s+(\w+)(?:\s+extends\s+\w+)?\s*\{/g
|
|
527
|
+
let classMatch
|
|
528
|
+
while ((classMatch = classRegex.exec(sourceWithoutComments)) !== null) {
|
|
529
|
+
const className = classMatch[1]
|
|
530
|
+
const classBodyStart = classMatch.index + classMatch[0].length
|
|
531
|
+
|
|
532
|
+
// Find the matching closing brace for the class body
|
|
533
|
+
let braceDepth = 1
|
|
534
|
+
let classBodyEnd = classBodyStart
|
|
535
|
+
for (let i = classBodyStart; i < sourceWithoutComments.length; i++) {
|
|
536
|
+
if (sourceWithoutComments[i] === '{') braceDepth++
|
|
537
|
+
else if (sourceWithoutComments[i] === '}') {
|
|
538
|
+
braceDepth--
|
|
539
|
+
if (braceDepth === 0) {
|
|
540
|
+
classBodyEnd = i
|
|
541
|
+
break
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const classBody = sourceWithoutComments.slice(classBodyStart, classBodyEnd)
|
|
546
|
+
|
|
547
|
+
// Find the first constructor's params
|
|
548
|
+
const ctorRegex = /constructor\s*\(([^)]*)\)/
|
|
549
|
+
const ctorMatch = ctorRegex.exec(classBody)
|
|
550
|
+
if (!ctorMatch) continue
|
|
551
|
+
|
|
552
|
+
const ctorParamsStr = ctorMatch[1]
|
|
553
|
+
const ctorParamExamples = extractParamExamples(ctorParamsStr)
|
|
554
|
+
if (ctorParamsStr.trim() && ctorParamExamples.length === 0) continue
|
|
555
|
+
|
|
556
|
+
let ctorArgs: unknown[]
|
|
557
|
+
try {
|
|
558
|
+
ctorArgs = ctorParamExamples.map((p) => new Function(`return ${p}`)())
|
|
559
|
+
} catch {
|
|
560
|
+
continue
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Find methods with return type markers inside the class body
|
|
564
|
+
const methodRegex = /(async\s+)?(\w+)\s*\(([^)]*)\)\s*(:[?!]?)\s*/g
|
|
565
|
+
let methodMatch
|
|
566
|
+
while ((methodMatch = methodRegex.exec(classBody)) !== null) {
|
|
567
|
+
const methodName = methodMatch[2]
|
|
568
|
+
// Skip constructors and special names
|
|
569
|
+
if (methodName === 'constructor') continue
|
|
570
|
+
|
|
571
|
+
const isAsync = !!methodMatch[1]
|
|
572
|
+
const paramsStr = methodMatch[3]
|
|
573
|
+
const returnMarker = methodMatch[4]
|
|
574
|
+
|
|
575
|
+
if (returnMarker === ':!') continue
|
|
576
|
+
|
|
577
|
+
// Calculate line number from position in original source
|
|
578
|
+
const methodPosInSource = classBodyStart + methodMatch.index
|
|
579
|
+
const lineNumber = sourceWithoutComments
|
|
580
|
+
.slice(0, methodPosInSource)
|
|
581
|
+
.split('\n').length
|
|
582
|
+
|
|
583
|
+
const afterMarker = classBody.slice(
|
|
584
|
+
methodMatch.index + methodMatch[0].length
|
|
585
|
+
)
|
|
586
|
+
const returnExample = extractReturnExampleFromSource(afterMarker)
|
|
587
|
+
if (!returnExample) continue
|
|
588
|
+
|
|
589
|
+
const paramExamples = extractParamExamples(paramsStr)
|
|
590
|
+
if (paramsStr.trim() && paramExamples.length === 0) continue
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
const parsed = parseReturnExample(returnExample)
|
|
594
|
+
if (!parsed) continue
|
|
595
|
+
|
|
596
|
+
const args = paramExamples.map((p) => new Function(`return ${p}`)())
|
|
597
|
+
|
|
598
|
+
infos.push({
|
|
599
|
+
funcName: methodName,
|
|
600
|
+
args,
|
|
601
|
+
expected: parsed.pattern,
|
|
602
|
+
defaults:
|
|
603
|
+
Object.keys(parsed.defaults).length > 0
|
|
604
|
+
? parsed.defaults
|
|
605
|
+
: undefined,
|
|
606
|
+
line: lineNumber,
|
|
607
|
+
isAsync,
|
|
608
|
+
className,
|
|
609
|
+
constructorArgs: ctorArgs,
|
|
610
|
+
})
|
|
611
|
+
} catch {
|
|
612
|
+
// Skip if parsing fails
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
519
617
|
return infos
|
|
520
618
|
}
|
|
521
619
|
|
|
@@ -586,13 +684,23 @@ export function runAllTests(
|
|
|
586
684
|
|
|
587
685
|
// Build signature test execution code
|
|
588
686
|
const sigTestBodies = syncSigTestInfos
|
|
589
|
-
.map(
|
|
590
|
-
|
|
591
|
-
|
|
687
|
+
.map((info, i) => {
|
|
688
|
+
const testLabel = info.className
|
|
689
|
+
? `${info.className}.${info.funcName}`
|
|
690
|
+
: info.funcName
|
|
691
|
+
const callExpr = info.className
|
|
692
|
+
? `new ${info.className}(${(info.constructorArgs || [])
|
|
693
|
+
.map((a) => JSON.stringify(a))
|
|
694
|
+
.join(', ')}).${info.funcName}(${info.args
|
|
695
|
+
.map((a) => JSON.stringify(a))
|
|
696
|
+
.join(', ')})`
|
|
697
|
+
: `${info.funcName}(${info.args
|
|
698
|
+
.map((a) => JSON.stringify(a))
|
|
699
|
+
.join(', ')})`
|
|
700
|
+
return `
|
|
701
|
+
// Signature test ${i}: ${testLabel}
|
|
592
702
|
try {
|
|
593
|
-
let __actual = ${
|
|
594
|
-
.map((a) => JSON.stringify(a))
|
|
595
|
-
.join(', ')});
|
|
703
|
+
let __actual = ${callExpr};
|
|
596
704
|
const __expected = ${JSON.stringify(info.expected)};${
|
|
597
705
|
info.defaults
|
|
598
706
|
? `
|
|
@@ -603,15 +711,13 @@ export function runAllTests(
|
|
|
603
711
|
if (__deepEqual(__actual, __expected)) {
|
|
604
712
|
__sigTestResults.push({ idx: ${i}, passed: true });
|
|
605
713
|
} else {
|
|
606
|
-
__sigTestResults.push({ idx: ${i}, passed: false, error: 'Expected ' + __format(__expected) + ' at \\'${
|
|
607
|
-
info.funcName
|
|
608
|
-
}\\', got ' + __format(__actual) });
|
|
714
|
+
__sigTestResults.push({ idx: ${i}, passed: false, error: 'Expected ' + __format(__expected) + ' at \\'${testLabel}\\', got ' + __format(__actual) });
|
|
609
715
|
}
|
|
610
716
|
} catch (e) {
|
|
611
717
|
__sigTestResults.push({ idx: ${i}, passed: false, error: e.message || String(e) });
|
|
612
718
|
}
|
|
613
719
|
`
|
|
614
|
-
)
|
|
720
|
+
})
|
|
615
721
|
.join('\n')
|
|
616
722
|
|
|
617
723
|
// Install real TJS runtime for test execution
|
|
@@ -747,8 +853,11 @@ export function runAllTests(
|
|
|
747
853
|
!r.passed &&
|
|
748
854
|
r.error &&
|
|
749
855
|
/is not defined$/.test(r.error)
|
|
856
|
+
const label = info.className
|
|
857
|
+
? `${info.className}.${info.funcName}`
|
|
858
|
+
: info.funcName
|
|
750
859
|
results.push({
|
|
751
|
-
description: `${
|
|
860
|
+
description: `${label} signature example`,
|
|
752
861
|
passed: isImportError ? true : r.passed,
|
|
753
862
|
error: isImportError ? undefined : r.error,
|
|
754
863
|
isSignatureTest: true,
|
|
@@ -771,8 +880,11 @@ export function runAllTests(
|
|
|
771
880
|
})
|
|
772
881
|
}
|
|
773
882
|
for (const info of syncSigTestInfos) {
|
|
883
|
+
const label = info.className
|
|
884
|
+
? `${info.className}.${info.funcName}`
|
|
885
|
+
: info.funcName
|
|
774
886
|
results.push({
|
|
775
|
-
description: `${
|
|
887
|
+
description: `${label} signature example`,
|
|
776
888
|
passed: isUnresolvedRef,
|
|
777
889
|
error: isUnresolvedRef
|
|
778
890
|
? undefined
|
|
@@ -785,8 +897,11 @@ export function runAllTests(
|
|
|
785
897
|
|
|
786
898
|
// Add skipped results for async signature tests
|
|
787
899
|
for (const info of asyncSigTestInfos) {
|
|
900
|
+
const label = info.className
|
|
901
|
+
? `${info.className}.${info.funcName}`
|
|
902
|
+
: info.funcName
|
|
788
903
|
results.push({
|
|
789
|
-
description: `${
|
|
904
|
+
description: `${label} signature example`,
|
|
790
905
|
passed: true,
|
|
791
906
|
isSignatureTest: true,
|
|
792
907
|
line: info.line,
|
|
@@ -971,14 +1086,14 @@ function evalArrayExpression(node: any): unknown[] {
|
|
|
971
1086
|
}
|
|
972
1087
|
|
|
973
1088
|
/**
|
|
974
|
-
* Extract and run signature tests for ALL functions with
|
|
1089
|
+
* Extract and run signature tests for ALL functions with return type annotations
|
|
975
1090
|
* Parses the original source to find function signatures
|
|
976
1091
|
*
|
|
977
1092
|
* Current limitations (future work):
|
|
978
1093
|
* - Only tests top-level `function` declarations (not arrow functions yet)
|
|
979
1094
|
* - Nested functions (inside other functions/blocks) are not excluded yet
|
|
980
1095
|
* and will fail if tested since they're not in global scope
|
|
981
|
-
* - Arrow functions like `Foo = (x: 5)
|
|
1096
|
+
* - Arrow functions like `Foo = (x: 5): 10 => {}` not yet supported
|
|
982
1097
|
*/
|
|
983
1098
|
function runAllSignatureTests(
|
|
984
1099
|
originalSource: string,
|
|
@@ -990,10 +1105,10 @@ function runAllSignatureTests(
|
|
|
990
1105
|
// Strip comments to avoid matching functions inside doc comments/code examples
|
|
991
1106
|
const sourceWithoutComments = stripComments(originalSource)
|
|
992
1107
|
|
|
993
|
-
// Match function declarations with return type marker (
|
|
994
|
-
// Skip
|
|
995
|
-
// Pattern: function name(params)
|
|
996
|
-
const funcRegex = /function\s+(\w+)\s*\(([^)]*)\)\s*(
|
|
1108
|
+
// Match function declarations with return type marker (: or :?)
|
|
1109
|
+
// Skip :! which means "don't test"
|
|
1110
|
+
// Pattern: function name(params): returnExample {
|
|
1111
|
+
const funcRegex = /function\s+(\w+)\s*\(([^)]*)\)\s*(:[?!]?)\s*/g
|
|
997
1112
|
|
|
998
1113
|
let match
|
|
999
1114
|
while ((match = funcRegex.exec(sourceWithoutComments)) !== null) {
|
|
@@ -1006,8 +1121,8 @@ function runAllSignatureTests(
|
|
|
1006
1121
|
.slice(0, match.index)
|
|
1007
1122
|
.split('\n').length
|
|
1008
1123
|
|
|
1009
|
-
//
|
|
1010
|
-
if (returnMarker === '
|
|
1124
|
+
// :! means skip test
|
|
1125
|
+
if (returnMarker === ':!') continue
|
|
1011
1126
|
|
|
1012
1127
|
// Extract return example - handle nested braces/brackets
|
|
1013
1128
|
// Use stripped source since match.index is from that
|
package/src/lang/emitters/js.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Unlike the AST emitter (for AgentJS), this outputs executable JS code.
|
|
6
6
|
*
|
|
7
7
|
* Input:
|
|
8
|
-
* function greet(name: 'world')
|
|
8
|
+
* function greet(name: 'world'): '' {
|
|
9
9
|
* return `Hello, ${name}!`
|
|
10
10
|
* }
|
|
11
11
|
*
|
|
@@ -392,14 +392,13 @@ function generateInlineValidationCode(
|
|
|
392
392
|
|
|
393
393
|
if (lines.length === 0) return null
|
|
394
394
|
|
|
395
|
-
//
|
|
396
|
-
// finally
|
|
395
|
+
// pushStack is a no-op unless callStacks/debug is enabled at runtime.
|
|
396
|
+
// No try/finally needed — the ring buffer tolerates missed popStack.
|
|
397
397
|
lines.unshift(`__tjs.pushStack('${stackEntry}');`)
|
|
398
|
-
lines.unshift(`try {`)
|
|
399
398
|
|
|
400
399
|
return {
|
|
401
400
|
preamble: lines.join('\n '),
|
|
402
|
-
suffix: '
|
|
401
|
+
suffix: '__tjs.popStack();',
|
|
403
402
|
}
|
|
404
403
|
}
|
|
405
404
|
|
|
@@ -436,14 +435,13 @@ function generateInlineValidationCode(
|
|
|
436
435
|
|
|
437
436
|
if (lines.length === 0) return null
|
|
438
437
|
|
|
439
|
-
//
|
|
440
|
-
// finally
|
|
438
|
+
// pushStack is a no-op unless callStacks/debug is enabled at runtime.
|
|
439
|
+
// No try/finally needed — the ring buffer tolerates missed popStack.
|
|
441
440
|
lines.unshift(`__tjs.pushStack('${stackEntry}');`)
|
|
442
|
-
lines.unshift(`try {`)
|
|
443
441
|
|
|
444
442
|
return {
|
|
445
443
|
preamble: lines.join('\n '),
|
|
446
|
-
suffix: '
|
|
444
|
+
suffix: '__tjs.popStack();',
|
|
447
445
|
}
|
|
448
446
|
}
|
|
449
447
|
|
|
@@ -495,11 +493,11 @@ function extractFunctionReturnType(
|
|
|
495
493
|
source: string,
|
|
496
494
|
funcName: string
|
|
497
495
|
): string | null {
|
|
498
|
-
// Match: function funcName(params)
|
|
499
|
-
// or: function funcName(params)
|
|
500
|
-
// or: function funcName(params)
|
|
496
|
+
// Match: function funcName(params): returnExample {
|
|
497
|
+
// or: function funcName(params):? returnExample {
|
|
498
|
+
// or: function funcName(params):! returnExample {
|
|
501
499
|
const regex = new RegExp(
|
|
502
|
-
`function\\s+${funcName}\\s*\\([^)]*\\)\\s*(
|
|
500
|
+
`function\\s+${funcName}\\s*\\([^)]*\\)\\s*(:[?!]?)\\s*`,
|
|
503
501
|
'g'
|
|
504
502
|
)
|
|
505
503
|
const match = regex.exec(source)
|
|
@@ -511,15 +509,14 @@ function extractFunctionReturnType(
|
|
|
511
509
|
|
|
512
510
|
/**
|
|
513
511
|
* Extract return safety marker for a specific function from source
|
|
514
|
-
* Returns 'safe' for
|
|
512
|
+
* Returns 'safe' for :?, 'unsafe' for :!, undefined for : or no marker
|
|
515
513
|
*/
|
|
516
514
|
function extractFunctionReturnSafety(
|
|
517
515
|
source: string,
|
|
518
516
|
funcName: string
|
|
519
517
|
): 'safe' | 'unsafe' | undefined {
|
|
520
|
-
// Match: function funcName(params) -X where X is >, ?, or !
|
|
521
518
|
const regex = new RegExp(
|
|
522
|
-
`function\\s+${funcName}\\s*\\([^)]*\\)\\s
|
|
519
|
+
`function\\s+${funcName}\\s*\\([^)]*\\)\\s*:([?!]?)`,
|
|
523
520
|
'g'
|
|
524
521
|
)
|
|
525
522
|
const match = regex.exec(source)
|
|
@@ -528,7 +525,7 @@ function extractFunctionReturnSafety(
|
|
|
528
525
|
const marker = match[1]
|
|
529
526
|
if (marker === '?') return 'safe'
|
|
530
527
|
if (marker === '!') return 'unsafe'
|
|
531
|
-
return undefined //
|
|
528
|
+
return undefined // : is the default, no special safety flag
|
|
532
529
|
}
|
|
533
530
|
|
|
534
531
|
/**
|
|
@@ -586,7 +583,7 @@ export function transpileToJS(
|
|
|
586
583
|
// Extract test/mock blocks before parsing (they're not valid JS)
|
|
587
584
|
const { code: cleanSource, tests, mocks, testRunner } = extractTests(source)
|
|
588
585
|
|
|
589
|
-
// Parse the cleaned source (handles TJS syntax like x: 'type' and
|
|
586
|
+
// Parse the cleaned source (handles TJS syntax like x: 'type' and : ReturnType)
|
|
590
587
|
const {
|
|
591
588
|
ast: program,
|
|
592
589
|
originalSource,
|
|
@@ -747,11 +744,12 @@ export function transpileToJS(
|
|
|
747
744
|
position: func.body.start + 1,
|
|
748
745
|
text: `\n ${validation.preamble}\n`,
|
|
749
746
|
})
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
747
|
+
if (validation.suffix) {
|
|
748
|
+
insertions.push({
|
|
749
|
+
position: func.body.end - 1,
|
|
750
|
+
text: `\n ${validation.suffix}\n`,
|
|
751
|
+
})
|
|
752
|
+
}
|
|
755
753
|
}
|
|
756
754
|
}
|
|
757
755
|
}
|
|
@@ -796,6 +794,7 @@ export function transpileToJS(
|
|
|
796
794
|
const needsFunctionPredicate = /\bFunctionPredicate\(/.test(code)
|
|
797
795
|
const needsEnum = /\bEnum\(/.test(code)
|
|
798
796
|
const needsUnion = /\bUnion\(/.test(code)
|
|
797
|
+
const needsBang = code.includes('__tjs.bang(')
|
|
799
798
|
const needsSafeEval = preprocessed.tjsModes.tjsSafeEval
|
|
800
799
|
|
|
801
800
|
const needsRuntime =
|
|
@@ -811,6 +810,7 @@ export function transpileToJS(
|
|
|
811
810
|
needsFunctionPredicate ||
|
|
812
811
|
needsEnum ||
|
|
813
812
|
needsUnion ||
|
|
813
|
+
needsBang ||
|
|
814
814
|
needsSafeEval
|
|
815
815
|
|
|
816
816
|
if (needsRuntime) {
|
|
@@ -838,7 +838,7 @@ export function transpileToJS(
|
|
|
838
838
|
// Eq/NotEq (honest equality)
|
|
839
839
|
if (needsEq) {
|
|
840
840
|
inlineParts.push(
|
|
841
|
-
`function Eq(a,b){if(a instanceof String||a instanceof Number||a instanceof Boolean)a=a.valueOf();if(b instanceof String||b instanceof Number||b instanceof Boolean)b=b.valueOf();if(a===b)return true;if((a===null||a===undefined)&&(b===null||b===undefined))return true;return false}`
|
|
841
|
+
`function Eq(a,b){if(a instanceof String||a instanceof Number||a instanceof Boolean)a=a.valueOf();if(b instanceof String||b instanceof Number||b instanceof Boolean)b=b.valueOf();if(a===b)return true;if(typeof a==='number'&&typeof b==='number'&&isNaN(a)&&isNaN(b))return true;if((a===null||a===undefined)&&(b===null||b===undefined))return true;return false}`
|
|
842
842
|
)
|
|
843
843
|
}
|
|
844
844
|
if (needsNotEq) {
|
|
@@ -853,7 +853,7 @@ export function transpileToJS(
|
|
|
853
853
|
// Is/IsNot (structural equality)
|
|
854
854
|
if (needsIs) {
|
|
855
855
|
inlineParts.push(
|
|
856
|
-
`const tjsEquals=Symbol.for('tjs.equals');function Is(a,b){if(a!=null&&typeof a==='object'&&typeof a[tjsEquals]==='function')return a[tjsEquals](b);if(b!=null&&typeof b==='object'&&typeof b[tjsEquals]==='function')return b[tjsEquals](a);if(a!=null&&typeof a==='object'&&typeof a.Equals==='function')return a.Equals(b);if(b!=null&&typeof b==='object'&&typeof b.Equals==='function')return b.Equals(a);if(a instanceof String||a instanceof Number||a instanceof Boolean)a=a.valueOf();if(b instanceof String||b instanceof Number||b instanceof Boolean)b=b.valueOf();if(a===b)return true;if((a==null)&&(b==null))return true;if(a==null||b==null)return false;if(typeof a!==typeof b)return false;if(typeof a!=='object')return false;if(a instanceof Set&&b instanceof Set){if(a.size!==b.size)return false;for(const v of a)if(!b.has(v))return false;return true}if(a instanceof Map&&b instanceof Map){if(a.size!==b.size)return false;for(const[k,v]of a)if(!b.has(k)||!Is(v,b.get(k)))return false;return true}if(a instanceof Date&&b instanceof Date)return a.getTime()===b.getTime();if(a instanceof RegExp&&b instanceof RegExp)return a.toString()===b.toString();if(Array.isArray(a)&&Array.isArray(b)){if(a.length!==b.length)return false;return a.every((v,i)=>Is(v,b[i]))}if(Array.isArray(a)!==Array.isArray(b))return false;const ka=Object.keys(a),kb=Object.keys(b);if(ka.length!==kb.length)return false;return ka.every(k=>Is(a[k],b[k]))}`
|
|
856
|
+
`const tjsEquals=Symbol.for('tjs.equals');function Is(a,b){if(a!=null&&typeof a==='object'&&typeof a[tjsEquals]==='function')return a[tjsEquals](b);if(b!=null&&typeof b==='object'&&typeof b[tjsEquals]==='function')return b[tjsEquals](a);if(a!=null&&typeof a==='object'&&typeof a.Equals==='function')return a.Equals(b);if(b!=null&&typeof b==='object'&&typeof b.Equals==='function')return b.Equals(a);if(a instanceof String||a instanceof Number||a instanceof Boolean)a=a.valueOf();if(b instanceof String||b instanceof Number||b instanceof Boolean)b=b.valueOf();if(a===b)return true;if(typeof a==='number'&&typeof b==='number'&&isNaN(a)&&isNaN(b))return true;if((a==null)&&(b==null))return true;if(a==null||b==null)return false;if(typeof a!==typeof b)return false;if(typeof a!=='object')return false;if(a instanceof Set&&b instanceof Set){if(a.size!==b.size)return false;for(const v of a)if(!b.has(v))return false;return true}if(a instanceof Map&&b instanceof Map){if(a.size!==b.size)return false;for(const[k,v]of a)if(!b.has(k)||!Is(v,b.get(k)))return false;return true}if(a instanceof Date&&b instanceof Date)return a.getTime()===b.getTime();if(a instanceof RegExp&&b instanceof RegExp)return a.toString()===b.toString();if(Array.isArray(a)&&Array.isArray(b)){if(a.length!==b.length)return false;return a.every((v,i)=>Is(v,b[i]))}if(Array.isArray(a)!==Array.isArray(b))return false;const ka=Object.keys(a),kb=Object.keys(b);if(ka.length!==kb.length)return false;return ka.every(k=>Is(a[k],b[k]))}`
|
|
857
857
|
)
|
|
858
858
|
}
|
|
859
859
|
if (needsIsNot) {
|
|
@@ -887,6 +887,20 @@ export function transpileToJS(
|
|
|
887
887
|
`function Union(d,...v){const vals=v.flat();return{description:d,check:x=>vals.includes(x),values:vals,__runtimeType:true}}`
|
|
888
888
|
)
|
|
889
889
|
}
|
|
890
|
+
// Bang access (!.) — asserted non-null member access
|
|
891
|
+
if (needsBang) {
|
|
892
|
+
// bang depends on typeError and isMonadicError — ensure they're inlined
|
|
893
|
+
if (!needsTypeError) {
|
|
894
|
+
inlineParts.push(
|
|
895
|
+
`class MonadicError extends Error{constructor(m,p,e,a,c){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c}}`,
|
|
896
|
+
`function typeError(p,e,v){const a=v===null?'null':typeof v;const err=new MonadicError('Expected '+e+" for '"+p+"', got "+a,p,e,a);const c=globalThis.__tjs?.getConfig?.();if(c?.logTypeErrors)console.error('[TJS TypeError] '+err.message);if(c?.throwTypeErrors)throw err;return err}`,
|
|
897
|
+
`function isMonadicError(v){return v instanceof Error&&v.name==='MonadicError'&&'path' in v}`
|
|
898
|
+
)
|
|
899
|
+
}
|
|
900
|
+
inlineParts.push(
|
|
901
|
+
`function bang(o,p){if(o===null||o===undefined)return typeError('bang.'+p,'non-null',o);if(isMonadicError(o))return o;return o[p]}`
|
|
902
|
+
)
|
|
903
|
+
}
|
|
890
904
|
|
|
891
905
|
// Build preamble: inline functions are declared at module scope,
|
|
892
906
|
// then __tjs either uses the shared runtime or references the inlined ones.
|
|
@@ -907,6 +921,13 @@ export function transpileToJS(
|
|
|
907
921
|
if (needsFunctionPredicate) fallbackEntries.push('FunctionPredicate')
|
|
908
922
|
if (needsEnum) fallbackEntries.push('Enum')
|
|
909
923
|
if (needsUnion) fallbackEntries.push('Union')
|
|
924
|
+
if (needsBang) {
|
|
925
|
+
fallbackEntries.push('bang')
|
|
926
|
+
// Ensure typeError/isMonadicError are in fallback even if not otherwise needed
|
|
927
|
+
if (!needsTypeError) {
|
|
928
|
+
fallbackEntries.push('typeError', 'isMonadicError')
|
|
929
|
+
}
|
|
930
|
+
}
|
|
910
931
|
|
|
911
932
|
const fallbackObj =
|
|
912
933
|
fallbackEntries.length > 0
|
|
@@ -1044,7 +1065,7 @@ interface SafetyOptions {
|
|
|
1044
1065
|
unsafe?: boolean
|
|
1045
1066
|
/** Function marked with (?) - always validate inputs */
|
|
1046
1067
|
safe?: boolean
|
|
1047
|
-
/** Return type safety: 'safe' (
|
|
1068
|
+
/** Return type safety: 'safe' (:?) or 'unsafe' (:!) */
|
|
1048
1069
|
returnSafety?: 'safe' | 'unsafe'
|
|
1049
1070
|
}
|
|
1050
1071
|
|
|
@@ -1106,9 +1127,9 @@ function generateTypeMetadata(
|
|
|
1106
1127
|
}
|
|
1107
1128
|
// Add return safety flags
|
|
1108
1129
|
if (safety.returnSafety === 'safe') {
|
|
1109
|
-
metadata.safeReturn = true //
|
|
1130
|
+
metadata.safeReturn = true // :? forces output validation
|
|
1110
1131
|
} else if (safety.returnSafety === 'unsafe') {
|
|
1111
|
-
metadata.unsafeReturn = true //
|
|
1132
|
+
metadata.unsafeReturn = true // :! skips output validation
|
|
1112
1133
|
}
|
|
1113
1134
|
}
|
|
1114
1135
|
|