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.
Files changed (86) hide show
  1. package/CLAUDE.md +85 -422
  2. package/README.md +15 -82
  3. package/bin/benchmarks.ts +7 -7
  4. package/bin/dev.ts +2 -1
  5. package/demo/autocomplete.test.ts +1 -1
  6. package/demo/docs.json +744 -48
  7. package/demo/src/demo-nav.ts +5 -5
  8. package/demo/src/index.ts +28 -36
  9. package/demo/src/module-sw.ts +1 -1
  10. package/demo/src/playground-shared.ts +17 -17
  11. package/demo/src/playground.ts +13 -1
  12. package/demo/src/style.ts +4 -1
  13. package/demo/src/tjs-playground.ts +5 -5
  14. package/demo/src/user-store.ts +2 -1
  15. package/demo/static/favicon.svg +17 -24
  16. package/demo/static/tosi-platform.json +9304 -0
  17. package/dist/index.js +158 -156
  18. package/dist/index.js.map +14 -13
  19. package/dist/scripts/compat-effect.d.ts +16 -0
  20. package/dist/scripts/compat-kysely.d.ts +13 -0
  21. package/dist/scripts/compat-radash.d.ts +13 -0
  22. package/dist/scripts/compat-superstruct.d.ts +13 -0
  23. package/dist/scripts/compat-ts-pattern.d.ts +13 -0
  24. package/dist/scripts/compat-zod.d.ts +12 -0
  25. package/dist/src/lang/emitters/from-ts.d.ts +1 -1
  26. package/dist/src/lang/emitters/js-tests.d.ts +4 -0
  27. package/dist/src/lang/emitters/js.d.ts +2 -2
  28. package/dist/src/lang/index.d.ts +1 -0
  29. package/dist/src/lang/json-schema.d.ts +40 -0
  30. package/dist/src/lang/parser-transforms.d.ts +14 -0
  31. package/dist/src/lang/runtime.d.ts +39 -6
  32. package/dist/src/types/Type.d.ts +5 -0
  33. package/dist/tjs-full.js +158 -156
  34. package/dist/tjs-full.js.map +14 -13
  35. package/dist/tjs-vm.js +44 -43
  36. package/dist/tjs-vm.js.map +5 -5
  37. package/docs/README.md +21 -20
  38. package/docs/WASM-QUICKSTART.md +283 -0
  39. package/docs/diagrams/architecture-shift.svg +117 -0
  40. package/docs/diagrams/compile-runtime.svg +130 -0
  41. package/docs/diagrams/icon-riff-1.svg +55 -0
  42. package/docs/diagrams/icon-riff-2.svg +62 -0
  43. package/docs/diagrams/icon-riff-3.svg +61 -0
  44. package/docs/diagrams/platform-overview.svg +114 -0
  45. package/docs/diagrams/safe-eval.svg +147 -0
  46. package/docs/eval-v4/arch-comparison.svg +277 -0
  47. package/docs/eval-v4/bundler-tree.svg +250 -0
  48. package/docs/eval-v4/http-lifecycle.svg +148 -0
  49. package/docs/function-predicate-design.md +8 -8
  50. package/docs/native-engine-integration.md +2 -2
  51. package/editors/codemirror/autocomplete.test.ts +29 -29
  52. package/package.json +10 -4
  53. package/src/cli/commands/convert.test.ts +11 -8
  54. package/src/cli/tjs.ts +1 -1
  55. package/src/lang/codegen.test.ts +117 -112
  56. package/src/lang/docs.test.ts +22 -22
  57. package/src/lang/docs.ts +5 -8
  58. package/src/lang/emitters/dts.test.ts +13 -13
  59. package/src/lang/emitters/from-ts.ts +36 -9
  60. package/src/lang/emitters/js-tests.ts +143 -28
  61. package/src/lang/emitters/js.ts +49 -28
  62. package/src/lang/features.test.ts +259 -43
  63. package/src/lang/from-ts.test.ts +3 -3
  64. package/src/lang/function-predicate.test.ts +1 -1
  65. package/src/lang/index.ts +8 -47
  66. package/src/lang/json-schema.test.ts +261 -0
  67. package/src/lang/json-schema.ts +167 -0
  68. package/src/lang/parser-params.ts +28 -44
  69. package/src/lang/parser-transforms.ts +255 -0
  70. package/src/lang/parser.test.ts +32 -13
  71. package/src/lang/parser.ts +49 -11
  72. package/src/lang/perf.test.ts +11 -11
  73. package/src/lang/roundtrip.test.ts +3 -3
  74. package/src/lang/runtime.test.ts +167 -0
  75. package/src/lang/runtime.ts +234 -46
  76. package/src/lang/transpiler.test.ts +21 -21
  77. package/src/lang/typescript-syntax.test.ts +11 -9
  78. package/src/types/Type.ts +38 -1
  79. package/src/use-cases/bootstrap.test.ts +7 -7
  80. package/src/use-cases/client-server.test.ts +1 -1
  81. package/src/use-cases/malicious-actor.test.ts +1 -1
  82. package/src/use-cases/rag-processor.test.ts +1 -1
  83. package/src/use-cases/sophisticated-agents.test.ts +2 -2
  84. package/src/use-cases/transpiler-llm.test.ts +1 -1
  85. package/src/use-cases/unbundled-imports.test.ts +9 -9
  86. 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 -? runtime validation)
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 (-> or -?)
468
- // Skip -! which means "don't test"
469
- // Pattern: [async] function name(params) -> returnExample {
470
- const funcRegex = /(async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(-[>?])\s*/g
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
- // -! means skip test
485
- if (returnMarker === '-!') continue
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
- (info, i) => `
591
- // Signature test ${i}: ${info.funcName}
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 = ${info.funcName}(${info.args
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: `${info.funcName} signature example`,
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: `${info.funcName} signature example`,
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: `${info.funcName} signature example`,
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 -> return types
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) -> 10 => {}` not yet supported
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 (-> or -?)
994
- // Skip -! which means "don't test"
995
- // Pattern: function name(params) -> returnExample {
996
- const funcRegex = /function\s+(\w+)\s*\(([^)]*)\)\s*(-[>?])\s*/g
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
- // -! means skip test
1010
- if (returnMarker === '-!') continue
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
@@ -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
- // Push stack first, then validate callStack includes current function
396
- // finally block ensures popStack on all exit paths
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: '} finally { __tjs.popStack(); }',
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
- // Push stack first, then validate callStack includes current function
440
- // finally block ensures popStack on all exit paths
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: '} finally { __tjs.popStack(); }',
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) -> returnExample {
499
- // or: function funcName(params) -? returnExample {
500
- // or: function funcName(params) -! returnExample {
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*(-[>?!])\\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 -?, 'unsafe' for -!, undefined for -> or no marker
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 // -> is the default, no special safety flag
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 -> ReturnType)
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
- // Insert suffix (popStack) right before the closing brace
751
- insertions.push({
752
- position: func.body.end - 1,
753
- text: `\n ${validation.suffix}\n`,
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' (-?) or 'unsafe' (-!) */
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 // -? forces output validation
1130
+ metadata.safeReturn = true // :? forces output validation
1110
1131
  } else if (safety.returnSafety === 'unsafe') {
1111
- metadata.unsafeReturn = true // -! skips output validation
1132
+ metadata.unsafeReturn = true // :! skips output validation
1112
1133
  }
1113
1134
  }
1114
1135