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
@@ -14,11 +14,15 @@ import {
14
14
  getStack,
15
15
  pushStack,
16
16
  popStack,
17
+ errors,
18
+ clearErrors,
19
+ getErrorCount,
17
20
  resetRuntime,
18
21
  enterUnsafe,
19
22
  exitUnsafe,
20
23
  isUnsafeMode,
21
24
  TJSError,
25
+ typeError,
22
26
  typeOf,
23
27
  isNativeType,
24
28
  Is,
@@ -953,3 +957,166 @@ describe('tjsEquals symbol protocol', () => {
953
957
  expect(tjsEquals).toBe(Symbol.for('tjs.equals'))
954
958
  })
955
959
  })
960
+
961
+ describe('Error history ring buffer', () => {
962
+ beforeEach(() => {
963
+ resetRuntime()
964
+ })
965
+
966
+ afterEach(() => {
967
+ resetRuntime()
968
+ })
969
+
970
+ it('tracks errors by default', () => {
971
+ const err = typeError('fn.x', 'string', 42)
972
+ const recent = errors()
973
+ expect(recent).toHaveLength(1)
974
+ expect(recent[0]).toBe(err)
975
+ })
976
+
977
+ it('tracks multiple errors', () => {
978
+ typeError('fn.a', 'string', 1)
979
+ typeError('fn.b', 'number', 'x')
980
+ typeError('fn.c', 'boolean', null)
981
+ const recent = errors()
982
+ expect(recent).toHaveLength(3)
983
+ expect(recent[0].path).toBe('fn.a')
984
+ expect(recent[2].path).toBe('fn.c')
985
+ })
986
+
987
+ it('ring buffer wraps at maxErrors', () => {
988
+ configure({ maxErrors: 4 })
989
+ for (let i = 0; i < 6; i++) {
990
+ typeError(`fn.x${i}`, 'string', i)
991
+ }
992
+ const recent = errors()
993
+ expect(recent).toHaveLength(4)
994
+ // Should have the last 4 errors (x2..x5)
995
+ expect(recent[0].path).toBe('fn.x2')
996
+ expect(recent[3].path).toBe('fn.x5')
997
+ })
998
+
999
+ it('getErrorCount tracks total even after buffer wraps', () => {
1000
+ configure({ maxErrors: 4 })
1001
+ for (let i = 0; i < 10; i++) {
1002
+ typeError('fn.x', 'string', i)
1003
+ }
1004
+ expect(getErrorCount()).toBe(10)
1005
+ expect(errors()).toHaveLength(4)
1006
+ })
1007
+
1008
+ it('clearErrors returns cleared errors and resets', () => {
1009
+ typeError('fn.a', 'string', 1)
1010
+ typeError('fn.b', 'number', 'x')
1011
+ const cleared = clearErrors()
1012
+ expect(cleared).toHaveLength(2)
1013
+ expect(cleared[0].path).toBe('fn.a')
1014
+ expect(errors()).toHaveLength(0)
1015
+ expect(getErrorCount()).toBe(0)
1016
+ })
1017
+
1018
+ it('can be disabled with configure', () => {
1019
+ configure({ trackErrors: false })
1020
+ typeError('fn.x', 'string', 42)
1021
+ expect(errors()).toHaveLength(0)
1022
+ expect(getErrorCount()).toBe(0)
1023
+ })
1024
+
1025
+ it('resetRuntime clears error history', () => {
1026
+ typeError('fn.x', 'string', 42)
1027
+ expect(errors()).toHaveLength(1)
1028
+ resetRuntime()
1029
+ expect(errors()).toHaveLength(0)
1030
+ expect(getErrorCount()).toBe(0)
1031
+ })
1032
+
1033
+ it('zero cost on happy path', () => {
1034
+ // No errors created — errors() should return empty
1035
+ expect(errors()).toHaveLength(0)
1036
+ expect(getErrorCount()).toBe(0)
1037
+ })
1038
+
1039
+ it('catches unhandled errors from transpiled functions', () => {
1040
+ const { tjs } = require('./index')
1041
+ const savedTjs = globalThis.__tjs
1042
+
1043
+ try {
1044
+ // installRuntime sets up globalThis.__tjs with the global runtime
1045
+ // Emitted code calls createRuntime() to get a child — we need to
1046
+ // access the child's error buffer via the __tjs local in the emitted code
1047
+ const runtime = require('./runtime').createRuntime()
1048
+ globalThis.__tjs = runtime
1049
+
1050
+ const result = tjs(`
1051
+ function greet(name: 'World'): 'Hello, World' {
1052
+ return 'Hello, ' + name
1053
+ }
1054
+
1055
+ function process(x: 0): 0 {
1056
+ return x * 2
1057
+ }
1058
+ `)
1059
+ // The emitted code creates its own child runtime via createRuntime().
1060
+ // We need to capture that child's __tjs to check its error buffer.
1061
+ const mod = new Function(
1062
+ result.code + '\nreturn { greet, process, __tjs }'
1063
+ )()
1064
+
1065
+ // Clear any errors from transpilation
1066
+ mod.__tjs.clearErrors()
1067
+
1068
+ // Call with correct types — no errors
1069
+ mod.greet('Alice')
1070
+ mod.process(5)
1071
+ expect(mod.__tjs.errors()).toHaveLength(0)
1072
+
1073
+ // Call with wrong type — error returned but not checked
1074
+ mod.greet(42) // returns MonadicError, caller ignores it
1075
+ mod.process('not a number') // same
1076
+
1077
+ // The errors are captured in history even though nobody checked them
1078
+ const recent = mod.__tjs.errors()
1079
+ expect(recent).toHaveLength(2)
1080
+ expect(recent[0].path).toContain('greet.name')
1081
+ expect(recent[0].expected).toBe('string')
1082
+ expect(recent[1].path).toContain('process.x')
1083
+ expect(recent[1].expected).toBe('integer')
1084
+ } finally {
1085
+ globalThis.__tjs = savedTjs
1086
+ }
1087
+ })
1088
+
1089
+ it('supports the test workflow: clear, run, check', () => {
1090
+ const { tjs } = require('./index')
1091
+ const savedTjs = globalThis.__tjs
1092
+
1093
+ try {
1094
+ const runtime = require('./runtime').createRuntime()
1095
+ globalThis.__tjs = runtime
1096
+
1097
+ const result = tjs(`
1098
+ function add(a: 1, b: 2): 3 {
1099
+ return a + b
1100
+ }
1101
+ `)
1102
+ const mod = new Function(result.code + '\nreturn { add, __tjs }')()
1103
+
1104
+ // Test workflow: clear → run → check for unexpected errors
1105
+ mod.__tjs.clearErrors()
1106
+
1107
+ mod.add(10, 20) // correct types
1108
+ mod.add(1, 2) // correct types
1109
+
1110
+ expect(mod.__tjs.errors()).toHaveLength(0)
1111
+ expect(mod.__tjs.getErrorCount()).toBe(0)
1112
+
1113
+ // Now introduce a bad call
1114
+ mod.add('x', 'y') // wrong types
1115
+
1116
+ expect(mod.__tjs.errors()).toHaveLength(1)
1117
+ expect(mod.__tjs.getErrorCount()).toBe(1)
1118
+ } finally {
1119
+ globalThis.__tjs = savedTjs
1120
+ }
1121
+ })
1122
+ })
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { validate, s } from 'tosijs-schema'
14
+ import { functionMetaToJSONSchema } from './json-schema'
14
15
  import {
15
16
  Type,
16
17
  isRuntimeType,
@@ -180,7 +181,7 @@ export function typeError(
180
181
  ): MonadicError {
181
182
  const actual = value === null ? 'null' : typeof value
182
183
  // Capture call stack in debug mode (getStack returns [] if not in debug mode)
183
- const stack = config.debug ? getStack() : undefined
184
+ const stack = config.callStacks || config.debug ? getStack() : undefined
184
185
  const err = new MonadicError(
185
186
  `Expected ${expected} for '${path}', got ${actual}`,
186
187
  path,
@@ -189,6 +190,15 @@ export function typeError(
189
190
  stack
190
191
  )
191
192
 
193
+ // Track in error history ring buffer (zero cost on happy path)
194
+ if (config.trackErrors !== false) {
195
+ const size = config.maxErrors ?? ERROR_BUF_SIZE
196
+ errorBuffer[errorHead] = err
197
+ errorHead = (errorHead + 1) % size
198
+ if (errorBufCount < size) errorBufCount++
199
+ errorTotal++
200
+ }
201
+
192
202
  // Log to console if configured (includes source location from path)
193
203
  if (config.logTypeErrors) {
194
204
  console.error(`[TJS TypeError] ${err.message}`)
@@ -253,8 +263,18 @@ export interface TJSConfig {
253
263
  safety?: SafetyLevel
254
264
  /** Require explicit return types (error if -> not specified) */
255
265
  requireReturnTypes?: boolean
256
- /** Maximum call stack size to prevent memory issues (default: 100) */
266
+ /** Track call stacks for error diagnostics (default: false).
267
+ * Useful for server-side logging and agent debugging without devtools.
268
+ * Uses a fixed ring buffer — no allocation pressure. */
269
+ callStacks?: boolean
270
+ /** Ring buffer size for call stack tracking (default: 64) */
257
271
  maxStackSize?: number
272
+ /** Track recent type errors in a ring buffer (default: true).
273
+ * Zero cost on happy path — only writes when an error occurs.
274
+ * Lets you catch monadic errors that were silently ignored. */
275
+ trackErrors?: boolean
276
+ /** Ring buffer size for error tracking (default: 64) */
277
+ maxErrors?: number
258
278
  /** Log type errors to console.error when they occur (default: false) */
259
279
  logTypeErrors?: boolean
260
280
  /** Throw type errors instead of returning them (default: false).
@@ -267,14 +287,27 @@ const DEFAULT_CONFIG: TJSConfig = {
267
287
  debug: false,
268
288
  safety: 'inputs',
269
289
  requireReturnTypes: false,
270
- maxStackSize: 100,
290
+ callStacks: false,
291
+ maxStackSize: 64,
292
+ trackErrors: true,
293
+ maxErrors: 64,
271
294
  }
272
295
 
273
296
  /** Current runtime configuration */
274
297
  let config: TJSConfig = { ...DEFAULT_CONFIG }
275
298
 
276
- /** Current call stack (only tracked in debug mode) */
277
- const callStack: string[] = []
299
+ /** Ring buffer for call stack tracking fixed size, zero allocation */
300
+ const STACK_SIZE = 64
301
+ const callStackBuffer: string[] = new Array(STACK_SIZE).fill('')
302
+ let callStackHead = 0
303
+ let callStackCount = 0
304
+
305
+ /** Ring buffer for error history — zero cost on happy path */
306
+ const ERROR_BUF_SIZE = 64
307
+ const errorBuffer: any[] = new Array(ERROR_BUF_SIZE).fill(null)
308
+ let errorHead = 0
309
+ let errorBufCount = 0
310
+ let errorTotal = 0
278
311
 
279
312
  /** Unsafe mode depth - when > 0, skip validation in wrap() */
280
313
  let unsafeDepth = 0
@@ -316,45 +349,91 @@ export function getConfig(): TJSConfig {
316
349
  }
317
350
 
318
351
  /**
319
- * Push a function onto the call stack (debug mode only)
320
- * Respects maxStackSize to prevent unbounded memory growth
352
+ * Push a function onto the call stack ring buffer.
353
+ * Only tracks when callStacks or debug is enabled.
354
+ * O(1), no allocation.
321
355
  */
322
356
  export function pushStack(name: string): void {
323
- if (config.debug && name) {
324
- callStack.push(name)
325
- // Enforce max stack size by removing oldest entries
326
- const maxSize = config.maxStackSize ?? 100
327
- while (callStack.length > maxSize) {
328
- callStack.shift()
329
- }
357
+ if ((config.callStacks || config.debug) && name) {
358
+ const size = config.maxStackSize ?? STACK_SIZE
359
+ callStackBuffer[callStackHead] = name
360
+ callStackHead = (callStackHead + 1) % size
361
+ if (callStackCount < size) callStackCount++
330
362
  }
331
363
  }
332
364
 
333
365
  /**
334
- * Pop a function from the call stack (debug mode only)
366
+ * Pop a function from the call stack ring buffer.
367
+ * O(1), no allocation.
335
368
  */
336
369
  export function popStack(): void {
337
- if (config.debug) {
338
- callStack.pop()
370
+ if ((config.callStacks || config.debug) && callStackCount > 0) {
371
+ const size = config.maxStackSize ?? STACK_SIZE
372
+ callStackHead = (callStackHead - 1 + size) % size
373
+ callStackCount--
339
374
  }
340
375
  }
341
376
 
342
377
  /**
343
- * Get current call stack snapshot
378
+ * Get current call stack snapshot (most recent entries, newest last)
344
379
  */
345
380
  export function getStack(): string[] {
346
- return [...callStack]
381
+ if (callStackCount === 0) return []
382
+ const size = config.maxStackSize ?? STACK_SIZE
383
+ const result: string[] = []
384
+ const start = (callStackHead - callStackCount + size) % size
385
+ for (let i = 0; i < callStackCount; i++) {
386
+ result.push(callStackBuffer[(start + i) % size])
387
+ }
388
+ return result
389
+ }
390
+
391
+ /**
392
+ * Get recent type errors (newest last).
393
+ * Only tracks when trackErrors is enabled (default: true).
394
+ */
395
+ export function errors(): MonadicError[] {
396
+ if (config.trackErrors === false || errorBufCount === 0) return []
397
+ const size = config.maxErrors ?? ERROR_BUF_SIZE
398
+ const result: MonadicError[] = []
399
+ const start = (errorHead - errorBufCount + size) % size
400
+ for (let i = 0; i < errorBufCount; i++) {
401
+ result.push(errorBuffer[(start + i) % size])
402
+ }
403
+ return result
404
+ }
405
+
406
+ /**
407
+ * Clear error history. Returns the cleared errors.
408
+ */
409
+ export function clearErrors(): MonadicError[] {
410
+ const cleared = errors()
411
+ errorHead = 0
412
+ errorBufCount = 0
413
+ errorTotal = 0
414
+ return cleared
415
+ }
416
+
417
+ /**
418
+ * Total error count since last clear (may exceed buffer size).
419
+ */
420
+ export function getErrorCount(): number {
421
+ return errorTotal
347
422
  }
348
423
 
349
424
  /**
350
425
  * Reset runtime state to defaults
351
426
  *
352
- * Resets: config, callStack, unsafeDepth
427
+ * Resets: config, callStack, errors, unsafeDepth
353
428
  * Use this in test teardown to prevent state leaking between tests.
354
429
  */
355
430
  export function resetRuntime(): void {
356
431
  config = { ...DEFAULT_CONFIG }
357
- callStack.length = 0
432
+ callStackHead = 0
433
+ callStackCount = 0
434
+ errorHead = 0
435
+ errorBufCount = 0
436
+ errorTotal = 0
358
437
  unsafeDepth = 0
359
438
  }
360
439
 
@@ -416,6 +495,16 @@ export function Is(a: unknown, b: unknown): boolean {
416
495
  // Identical references or primitives
417
496
  if (a === b) return true
418
497
 
498
+ // NaN === NaN (JS gets this wrong)
499
+ if (
500
+ typeof a === 'number' &&
501
+ typeof b === 'number' &&
502
+ isNaN(a as number) &&
503
+ isNaN(b as number)
504
+ ) {
505
+ return true
506
+ }
507
+
419
508
  // null and undefined are equal to each other (nullish equality)
420
509
  // This preserves the useful JS pattern: x == null checks for both
421
510
  if ((a === null || a === undefined) && (b === null || b === undefined)) {
@@ -511,6 +600,7 @@ export function TypeOf(value: unknown): string {
511
600
  if (value === null) return 'null'
512
601
  return typeof value
513
602
  }
603
+
514
604
  export function Eq(a: unknown, b: unknown): boolean {
515
605
  // Unwrap boxed primitives
516
606
  if (a instanceof String || a instanceof Number || a instanceof Boolean) {
@@ -523,6 +613,16 @@ export function Eq(a: unknown, b: unknown): boolean {
523
613
  // Identical references or primitives
524
614
  if (a === b) return true
525
615
 
616
+ // NaN === NaN (JS gets this wrong)
617
+ if (
618
+ typeof a === 'number' &&
619
+ typeof b === 'number' &&
620
+ isNaN(a as number) &&
621
+ isNaN(b as number)
622
+ ) {
623
+ return true
624
+ }
625
+
526
626
  // null and undefined are equal to each other
527
627
  if ((a === null || a === undefined) && (b === null || b === undefined)) {
528
628
  return true
@@ -565,12 +665,12 @@ export function error(
565
665
  ...details,
566
666
  }
567
667
 
568
- // In debug mode, capture the call stack
569
- if (config.debug && callStack.length > 0) {
570
- // Add the path to the stack if it exists
668
+ // Capture call stack when tracking is enabled
669
+ if ((config.callStacks || config.debug) && callStackCount > 0) {
670
+ const currentStack = getStack()
571
671
  const fullStack = details?.path
572
- ? [...callStack, details.path]
573
- : [...callStack]
672
+ ? [...currentStack, details.path]
673
+ : currentStack
574
674
  err.stack = fullStack
575
675
  }
576
676
 
@@ -856,6 +956,8 @@ export function wrap<T extends (...args: any[]) => any>(
856
956
  ): T {
857
957
  // Always attach metadata for introspection/autocomplete
858
958
  ;(fn as any).__tjs = meta
959
+ // Lazy JSON Schema generation — only computed when called
960
+ ;(fn as any).__tjs.schema = () => functionMetaToJSONSchema(meta)
859
961
 
860
962
  // Determine if we need a wrapper at all
861
963
  // Polymorphic dispatchers handle their own routing — no wrapping needed
@@ -1000,8 +1102,9 @@ export function wrap<T extends (...args: any[]) => any>(
1000
1102
  }
1001
1103
  }
1002
1104
 
1003
- // Push onto call stack in debug mode
1004
- pushStack(funcName)
1105
+ // Track call stack only when enabled (ring buffer, no allocation)
1106
+ const trackStack = config.callStacks || config.debug
1107
+ if (trackStack) pushStack(funcName)
1005
1108
 
1006
1109
  try {
1007
1110
  // Execute function
@@ -1020,15 +1123,15 @@ export function wrap<T extends (...args: any[]) => any>(
1020
1123
  `${funcName}()`
1021
1124
  )
1022
1125
  if (returnError) {
1023
- popStack()
1126
+ if (trackStack) popStack()
1024
1127
  return returnError as ReturnType<T>
1025
1128
  }
1026
1129
  }
1027
1130
 
1028
- popStack()
1131
+ if (trackStack) popStack()
1029
1132
  return result
1030
1133
  } catch (e) {
1031
- popStack()
1134
+ if (trackStack) popStack()
1032
1135
  // Convert thrown errors to TJS errors
1033
1136
  return error((e as Error).message || String(e), {
1034
1137
  path: funcName,
@@ -1040,6 +1143,7 @@ export function wrap<T extends (...args: any[]) => any>(
1040
1143
  // Preserve function name and metadata
1041
1144
  Object.defineProperty(wrapped, 'name', { value: fn.name })
1042
1145
  ;(wrapped as any).__tjs = meta
1146
+ ;(wrapped as any).__tjs.schema = () => functionMetaToJSONSchema(meta)
1043
1147
 
1044
1148
  return wrapped as T
1045
1149
  }
@@ -1112,7 +1216,15 @@ export function wrapClass<T extends new (...args: any[]) => any>(
1112
1216
  export function createRuntime() {
1113
1217
  // Per-instance state - inherit current global config
1114
1218
  let instanceConfig: TJSConfig = { ...config }
1115
- const instanceCallStack: string[] = []
1219
+ const instStackSize = instanceConfig.maxStackSize ?? STACK_SIZE
1220
+ const instanceStackBuffer: string[] = new Array(instStackSize).fill('')
1221
+ let instanceStackHead = 0
1222
+ let instanceStackCount = 0
1223
+ const instErrorSize = instanceConfig.maxErrors ?? ERROR_BUF_SIZE
1224
+ const instanceErrorBuffer: any[] = new Array(instErrorSize).fill(null)
1225
+ let instanceErrorHead = 0
1226
+ let instanceErrorBufCount = 0
1227
+ let instanceErrorTotal = 0
1116
1228
  let instanceUnsafeDepth = 0
1117
1229
 
1118
1230
  // Per-instance stateful functions
@@ -1125,28 +1237,42 @@ export function createRuntime() {
1125
1237
  }
1126
1238
 
1127
1239
  function instancePushStack(name: string): void {
1128
- if (instanceConfig.debug && name) {
1129
- instanceCallStack.push(name)
1130
- const maxSize = instanceConfig.maxStackSize ?? 100
1131
- while (instanceCallStack.length > maxSize) {
1132
- instanceCallStack.shift()
1133
- }
1240
+ if ((instanceConfig.callStacks || instanceConfig.debug) && name) {
1241
+ instanceStackBuffer[instanceStackHead] = name
1242
+ instanceStackHead = (instanceStackHead + 1) % instStackSize
1243
+ if (instanceStackCount < instStackSize) instanceStackCount++
1134
1244
  }
1135
1245
  }
1136
1246
 
1137
1247
  function instancePopStack(): void {
1138
- if (instanceConfig.debug) {
1139
- instanceCallStack.pop()
1248
+ if (
1249
+ (instanceConfig.callStacks || instanceConfig.debug) &&
1250
+ instanceStackCount > 0
1251
+ ) {
1252
+ instanceStackHead =
1253
+ (instanceStackHead - 1 + instStackSize) % instStackSize
1254
+ instanceStackCount--
1140
1255
  }
1141
1256
  }
1142
1257
 
1143
1258
  function instanceGetStack(): string[] {
1144
- return [...instanceCallStack]
1259
+ if (instanceStackCount === 0) return []
1260
+ const result: string[] = []
1261
+ const start =
1262
+ (instanceStackHead - instanceStackCount + instStackSize) % instStackSize
1263
+ for (let i = 0; i < instanceStackCount; i++) {
1264
+ result.push(instanceStackBuffer[(start + i) % instStackSize])
1265
+ }
1266
+ return result
1145
1267
  }
1146
1268
 
1147
1269
  function instanceResetRuntime(): void {
1148
1270
  instanceConfig = { ...DEFAULT_CONFIG }
1149
- instanceCallStack.length = 0
1271
+ instanceStackHead = 0
1272
+ instanceStackCount = 0
1273
+ instanceErrorHead = 0
1274
+ instanceErrorBufCount = 0
1275
+ instanceErrorTotal = 0
1150
1276
  instanceUnsafeDepth = 0
1151
1277
  }
1152
1278
 
@@ -1232,7 +1358,10 @@ export function createRuntime() {
1232
1358
  value: unknown
1233
1359
  ): MonadicError {
1234
1360
  const actual = value === null ? 'null' : typeof value
1235
- const stack = instanceConfig.debug ? instanceGetStack() : undefined
1361
+ const stack =
1362
+ instanceConfig.callStacks || instanceConfig.debug
1363
+ ? instanceGetStack()
1364
+ : undefined
1236
1365
  const err = new MonadicError(
1237
1366
  `Expected ${expected} for '${path}', got ${actual}`,
1238
1367
  path,
@@ -1241,6 +1370,14 @@ export function createRuntime() {
1241
1370
  stack
1242
1371
  )
1243
1372
 
1373
+ // Track in error history
1374
+ if (instanceConfig.trackErrors !== false) {
1375
+ instanceErrorBuffer[instanceErrorHead] = err
1376
+ instanceErrorHead = (instanceErrorHead + 1) % instErrorSize
1377
+ if (instanceErrorBufCount < instErrorSize) instanceErrorBufCount++
1378
+ instanceErrorTotal++
1379
+ }
1380
+
1244
1381
  if (instanceConfig.logTypeErrors) {
1245
1382
  console.error(`[TJS TypeError] ${err.message}`)
1246
1383
  }
@@ -1251,6 +1388,31 @@ export function createRuntime() {
1251
1388
  return err
1252
1389
  }
1253
1390
 
1391
+ function instanceErrors(): MonadicError[] {
1392
+ if (instanceConfig.trackErrors === false || instanceErrorBufCount === 0)
1393
+ return []
1394
+ const result: MonadicError[] = []
1395
+ const start =
1396
+ (instanceErrorHead - instanceErrorBufCount + instErrorSize) %
1397
+ instErrorSize
1398
+ for (let i = 0; i < instanceErrorBufCount; i++) {
1399
+ result.push(instanceErrorBuffer[(start + i) % instErrorSize])
1400
+ }
1401
+ return result
1402
+ }
1403
+
1404
+ function instanceClearErrors(): MonadicError[] {
1405
+ const cleared = instanceErrors()
1406
+ instanceErrorHead = 0
1407
+ instanceErrorBufCount = 0
1408
+ instanceErrorTotal = 0
1409
+ return cleared
1410
+ }
1411
+
1412
+ function instanceGetErrorCount(): number {
1413
+ return instanceErrorTotal
1414
+ }
1415
+
1254
1416
  function instanceError(
1255
1417
  message: string,
1256
1418
  details?: Partial<Omit<TJSError, '$error' | 'message'>>
@@ -1260,21 +1422,39 @@ export function createRuntime() {
1260
1422
  message,
1261
1423
  ...details,
1262
1424
  }
1263
- if (instanceConfig.debug && instanceCallStack.length > 0) {
1425
+ if (
1426
+ (instanceConfig.callStacks || instanceConfig.debug) &&
1427
+ instanceStackCount > 0
1428
+ ) {
1264
1429
  const fullStack = details?.path
1265
- ? [...instanceCallStack, details.path]
1266
- : [...instanceCallStack]
1430
+ ? [...instanceGetStack(), details.path]
1431
+ : instanceGetStack()
1267
1432
  err.stack = fullStack
1268
1433
  }
1269
1434
  return err
1270
1435
  }
1271
1436
 
1437
+ /**
1438
+ * Bang access (!.) — asserted non-null member access.
1439
+ * Returns MonadicError if obj is null, undefined, or already a MonadicError.
1440
+ * Otherwise returns obj[prop] (bare access — throws as usual on other errors).
1441
+ */
1442
+ function instanceBang(obj: unknown, prop: string): unknown {
1443
+ if (obj === null || obj === undefined) {
1444
+ return instanceTypeError(`bang.${prop}`, 'non-null', obj)
1445
+ }
1446
+ if (isMonadicError(obj)) return obj
1447
+ return (obj as any)[prop]
1448
+ }
1449
+
1272
1450
  return {
1273
1451
  version: TJS_VERSION,
1274
1452
  // Monadic error handling
1275
1453
  MonadicError,
1276
1454
  typeError: instanceTypeError,
1277
1455
  isMonadicError,
1456
+ // Bang access (!.)
1457
+ bang: instanceBang,
1278
1458
  // Legacy error handling
1279
1459
  isError,
1280
1460
  error: instanceError,
@@ -1287,12 +1467,17 @@ export function createRuntime() {
1287
1467
  wrapClass,
1288
1468
  compareVersions,
1289
1469
  versionsCompatible,
1470
+ // Create child runtime instances
1471
+ createRuntime,
1290
1472
  // Debug mode (instance-specific)
1291
1473
  configure: instanceConfigure,
1292
1474
  getConfig: instanceGetConfig,
1293
1475
  pushStack: instancePushStack,
1294
1476
  popStack: instancePopStack,
1295
1477
  getStack: instanceGetStack,
1478
+ errors: instanceErrors,
1479
+ clearErrors: instanceClearErrors,
1480
+ getErrorCount: instanceGetErrorCount,
1296
1481
  resetRuntime: instanceResetRuntime,
1297
1482
  // Unsafe mode (instance-specific)
1298
1483
  enterUnsafe: instanceEnterUnsafe,
@@ -1369,6 +1554,9 @@ export const runtime = {
1369
1554
  pushStack,
1370
1555
  popStack,
1371
1556
  getStack,
1557
+ errors,
1558
+ clearErrors,
1559
+ getErrorCount,
1372
1560
  resetRuntime,
1373
1561
  // Unsafe mode
1374
1562
  enterUnsafe,