tjs-lang 0.6.45 → 0.7.4

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 (95) hide show
  1. package/CLAUDE.md +70 -444
  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 +743 -47
  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 +176 -175
  18. package/dist/index.js.map +5 -43
  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 -20
  32. package/dist/src/types/Type.d.ts +5 -0
  33. package/dist/tjs-batteries.js +3 -4
  34. package/dist/tjs-batteries.js.map +5 -13
  35. package/dist/tjs-eval.js +47 -0
  36. package/dist/tjs-eval.js.map +7 -0
  37. package/dist/tjs-from-ts.js +58 -0
  38. package/dist/tjs-from-ts.js.map +7 -0
  39. package/dist/tjs-lang.js +349 -0
  40. package/dist/tjs-lang.js.map +7 -0
  41. package/dist/tjs-vm.js +51 -51
  42. package/dist/tjs-vm.js.map +4 -19
  43. package/docs/README.md +21 -20
  44. package/docs/WASM-QUICKSTART.md +283 -0
  45. package/docs/diagrams/architecture-shift.svg +117 -0
  46. package/docs/diagrams/compile-runtime.svg +130 -0
  47. package/docs/diagrams/icon-riff-1.svg +55 -0
  48. package/docs/diagrams/icon-riff-2.svg +62 -0
  49. package/docs/diagrams/icon-riff-3.svg +61 -0
  50. package/docs/diagrams/platform-overview.svg +114 -0
  51. package/docs/diagrams/safe-eval.svg +147 -0
  52. package/docs/eval-v4/arch-comparison.svg +277 -0
  53. package/docs/eval-v4/bundler-tree.svg +250 -0
  54. package/docs/eval-v4/http-lifecycle.svg +148 -0
  55. package/docs/function-predicate-design.md +8 -8
  56. package/docs/native-engine-integration.md +2 -2
  57. package/editors/codemirror/autocomplete.test.ts +29 -29
  58. package/package.json +24 -12
  59. package/src/cli/commands/convert.test.ts +11 -8
  60. package/src/lang/codegen.test.ts +117 -112
  61. package/src/lang/docs.test.ts +22 -22
  62. package/src/lang/docs.ts +5 -8
  63. package/src/lang/emitters/dts.test.ts +13 -13
  64. package/src/lang/emitters/from-ts.ts +36 -9
  65. package/src/lang/emitters/js-tests.ts +143 -28
  66. package/src/lang/emitters/js.ts +44 -31
  67. package/src/lang/features.test.ts +259 -43
  68. package/src/lang/from-ts.test.ts +3 -3
  69. package/src/lang/function-predicate.test.ts +1 -1
  70. package/src/lang/index.ts +8 -47
  71. package/src/lang/json-schema.test.ts +261 -0
  72. package/src/lang/json-schema.ts +167 -0
  73. package/src/lang/parser-params.ts +28 -44
  74. package/src/lang/parser-transforms.ts +255 -0
  75. package/src/lang/parser.test.ts +32 -13
  76. package/src/lang/parser.ts +49 -11
  77. package/src/lang/perf.test.ts +11 -11
  78. package/src/lang/roundtrip.test.ts +3 -3
  79. package/src/lang/runtime.test.ts +167 -0
  80. package/src/lang/runtime.ts +213 -64
  81. package/src/lang/transpiler.test.ts +21 -21
  82. package/src/lang/typescript-syntax.test.ts +11 -9
  83. package/src/types/Type.ts +38 -1
  84. package/src/use-cases/bootstrap.test.ts +7 -7
  85. package/src/use-cases/client-server.test.ts +1 -1
  86. package/src/use-cases/malicious-actor.test.ts +1 -1
  87. package/src/use-cases/rag-processor.test.ts +1 -1
  88. package/src/use-cases/sophisticated-agents.test.ts +2 -2
  89. package/src/use-cases/transpiler-llm.test.ts +1 -1
  90. package/src/use-cases/unbundled-imports.test.ts +9 -9
  91. package/tjs-lang.svg +17 -25
  92. package/dist/tjs-full.js +0 -435
  93. package/dist/tjs-full.js.map +0 -45
  94. package/dist/tjs-transpiler.js +0 -3
  95. package/dist/tjs-transpiler.js.map +0 -11
@@ -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
 
@@ -522,20 +601,6 @@ export function TypeOf(value: unknown): string {
522
601
  return typeof value
523
602
  }
524
603
 
525
- /**
526
- * Check if a number is bounded (finite and not NaN).
527
- * The question you're actually asking when you reach for isNaN or isFinite.
528
- *
529
- * IsBounded(42) → true
530
- * IsBounded(3.14) → true
531
- * IsBounded(NaN) → false
532
- * IsBounded(Infinity) → false
533
- * IsBounded(-Infinity) → false
534
- * IsBounded('hello') → false
535
- */
536
- export function IsBounded(value: unknown): boolean {
537
- return typeof value === 'number' && isFinite(value) && !isNaN(value)
538
- }
539
604
  export function Eq(a: unknown, b: unknown): boolean {
540
605
  // Unwrap boxed primitives
541
606
  if (a instanceof String || a instanceof Number || a instanceof Boolean) {
@@ -600,12 +665,12 @@ export function error(
600
665
  ...details,
601
666
  }
602
667
 
603
- // In debug mode, capture the call stack
604
- if (config.debug && callStack.length > 0) {
605
- // 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()
606
671
  const fullStack = details?.path
607
- ? [...callStack, details.path]
608
- : [...callStack]
672
+ ? [...currentStack, details.path]
673
+ : currentStack
609
674
  err.stack = fullStack
610
675
  }
611
676
 
@@ -891,6 +956,8 @@ export function wrap<T extends (...args: any[]) => any>(
891
956
  ): T {
892
957
  // Always attach metadata for introspection/autocomplete
893
958
  ;(fn as any).__tjs = meta
959
+ // Lazy JSON Schema generation — only computed when called
960
+ ;(fn as any).__tjs.schema = () => functionMetaToJSONSchema(meta)
894
961
 
895
962
  // Determine if we need a wrapper at all
896
963
  // Polymorphic dispatchers handle their own routing — no wrapping needed
@@ -1035,8 +1102,9 @@ export function wrap<T extends (...args: any[]) => any>(
1035
1102
  }
1036
1103
  }
1037
1104
 
1038
- // Push onto call stack in debug mode
1039
- 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)
1040
1108
 
1041
1109
  try {
1042
1110
  // Execute function
@@ -1055,15 +1123,15 @@ export function wrap<T extends (...args: any[]) => any>(
1055
1123
  `${funcName}()`
1056
1124
  )
1057
1125
  if (returnError) {
1058
- popStack()
1126
+ if (trackStack) popStack()
1059
1127
  return returnError as ReturnType<T>
1060
1128
  }
1061
1129
  }
1062
1130
 
1063
- popStack()
1131
+ if (trackStack) popStack()
1064
1132
  return result
1065
1133
  } catch (e) {
1066
- popStack()
1134
+ if (trackStack) popStack()
1067
1135
  // Convert thrown errors to TJS errors
1068
1136
  return error((e as Error).message || String(e), {
1069
1137
  path: funcName,
@@ -1075,6 +1143,7 @@ export function wrap<T extends (...args: any[]) => any>(
1075
1143
  // Preserve function name and metadata
1076
1144
  Object.defineProperty(wrapped, 'name', { value: fn.name })
1077
1145
  ;(wrapped as any).__tjs = meta
1146
+ ;(wrapped as any).__tjs.schema = () => functionMetaToJSONSchema(meta)
1078
1147
 
1079
1148
  return wrapped as T
1080
1149
  }
@@ -1147,7 +1216,15 @@ export function wrapClass<T extends new (...args: any[]) => any>(
1147
1216
  export function createRuntime() {
1148
1217
  // Per-instance state - inherit current global config
1149
1218
  let instanceConfig: TJSConfig = { ...config }
1150
- 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
1151
1228
  let instanceUnsafeDepth = 0
1152
1229
 
1153
1230
  // Per-instance stateful functions
@@ -1160,28 +1237,42 @@ export function createRuntime() {
1160
1237
  }
1161
1238
 
1162
1239
  function instancePushStack(name: string): void {
1163
- if (instanceConfig.debug && name) {
1164
- instanceCallStack.push(name)
1165
- const maxSize = instanceConfig.maxStackSize ?? 100
1166
- while (instanceCallStack.length > maxSize) {
1167
- instanceCallStack.shift()
1168
- }
1240
+ if ((instanceConfig.callStacks || instanceConfig.debug) && name) {
1241
+ instanceStackBuffer[instanceStackHead] = name
1242
+ instanceStackHead = (instanceStackHead + 1) % instStackSize
1243
+ if (instanceStackCount < instStackSize) instanceStackCount++
1169
1244
  }
1170
1245
  }
1171
1246
 
1172
1247
  function instancePopStack(): void {
1173
- if (instanceConfig.debug) {
1174
- instanceCallStack.pop()
1248
+ if (
1249
+ (instanceConfig.callStacks || instanceConfig.debug) &&
1250
+ instanceStackCount > 0
1251
+ ) {
1252
+ instanceStackHead =
1253
+ (instanceStackHead - 1 + instStackSize) % instStackSize
1254
+ instanceStackCount--
1175
1255
  }
1176
1256
  }
1177
1257
 
1178
1258
  function instanceGetStack(): string[] {
1179
- 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
1180
1267
  }
1181
1268
 
1182
1269
  function instanceResetRuntime(): void {
1183
1270
  instanceConfig = { ...DEFAULT_CONFIG }
1184
- instanceCallStack.length = 0
1271
+ instanceStackHead = 0
1272
+ instanceStackCount = 0
1273
+ instanceErrorHead = 0
1274
+ instanceErrorBufCount = 0
1275
+ instanceErrorTotal = 0
1185
1276
  instanceUnsafeDepth = 0
1186
1277
  }
1187
1278
 
@@ -1267,7 +1358,10 @@ export function createRuntime() {
1267
1358
  value: unknown
1268
1359
  ): MonadicError {
1269
1360
  const actual = value === null ? 'null' : typeof value
1270
- const stack = instanceConfig.debug ? instanceGetStack() : undefined
1361
+ const stack =
1362
+ instanceConfig.callStacks || instanceConfig.debug
1363
+ ? instanceGetStack()
1364
+ : undefined
1271
1365
  const err = new MonadicError(
1272
1366
  `Expected ${expected} for '${path}', got ${actual}`,
1273
1367
  path,
@@ -1276,6 +1370,14 @@ export function createRuntime() {
1276
1370
  stack
1277
1371
  )
1278
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
+
1279
1381
  if (instanceConfig.logTypeErrors) {
1280
1382
  console.error(`[TJS TypeError] ${err.message}`)
1281
1383
  }
@@ -1286,6 +1388,31 @@ export function createRuntime() {
1286
1388
  return err
1287
1389
  }
1288
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
+
1289
1416
  function instanceError(
1290
1417
  message: string,
1291
1418
  details?: Partial<Omit<TJSError, '$error' | 'message'>>
@@ -1295,21 +1422,39 @@ export function createRuntime() {
1295
1422
  message,
1296
1423
  ...details,
1297
1424
  }
1298
- if (instanceConfig.debug && instanceCallStack.length > 0) {
1425
+ if (
1426
+ (instanceConfig.callStacks || instanceConfig.debug) &&
1427
+ instanceStackCount > 0
1428
+ ) {
1299
1429
  const fullStack = details?.path
1300
- ? [...instanceCallStack, details.path]
1301
- : [...instanceCallStack]
1430
+ ? [...instanceGetStack(), details.path]
1431
+ : instanceGetStack()
1302
1432
  err.stack = fullStack
1303
1433
  }
1304
1434
  return err
1305
1435
  }
1306
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
+
1307
1450
  return {
1308
1451
  version: TJS_VERSION,
1309
1452
  // Monadic error handling
1310
1453
  MonadicError,
1311
1454
  typeError: instanceTypeError,
1312
1455
  isMonadicError,
1456
+ // Bang access (!.)
1457
+ bang: instanceBang,
1313
1458
  // Legacy error handling
1314
1459
  isError,
1315
1460
  error: instanceError,
@@ -1322,12 +1467,17 @@ export function createRuntime() {
1322
1467
  wrapClass,
1323
1468
  compareVersions,
1324
1469
  versionsCompatible,
1470
+ // Create child runtime instances
1471
+ createRuntime,
1325
1472
  // Debug mode (instance-specific)
1326
1473
  configure: instanceConfigure,
1327
1474
  getConfig: instanceGetConfig,
1328
1475
  pushStack: instancePushStack,
1329
1476
  popStack: instancePopStack,
1330
1477
  getStack: instanceGetStack,
1478
+ errors: instanceErrors,
1479
+ clearErrors: instanceClearErrors,
1480
+ getErrorCount: instanceGetErrorCount,
1331
1481
  resetRuntime: instanceResetRuntime,
1332
1482
  // Unsafe mode (instance-specific)
1333
1483
  enterUnsafe: instanceEnterUnsafe,
@@ -1364,8 +1514,6 @@ export function createRuntime() {
1364
1514
  NotEq,
1365
1515
  // Honest typeof (typeof with TjsEquals)
1366
1516
  TypeOf,
1367
- // Number utilities
1368
- IsBounded,
1369
1517
  tjsEquals,
1370
1518
  // Extensions
1371
1519
  registerExtension: instanceRegisterExtension,
@@ -1406,6 +1554,9 @@ export const runtime = {
1406
1554
  pushStack,
1407
1555
  popStack,
1408
1556
  getStack,
1557
+ errors,
1558
+ clearErrors,
1559
+ getErrorCount,
1409
1560
  resetRuntime,
1410
1561
  // Unsafe mode
1411
1562
  enterUnsafe,
@@ -1447,8 +1598,6 @@ export const runtime = {
1447
1598
  NotEq,
1448
1599
  // Honest typeof (used by typeof with TjsEquals)
1449
1600
  TypeOf,
1450
- // Number utilities
1451
- IsBounded,
1452
1601
  }
1453
1602
 
1454
1603
  /**