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.
- package/CLAUDE.md +70 -444
- 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 +743 -47
- 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 +176 -175
- package/dist/index.js.map +5 -43
- 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 -20
- package/dist/src/types/Type.d.ts +5 -0
- package/dist/tjs-batteries.js +3 -4
- package/dist/tjs-batteries.js.map +5 -13
- package/dist/tjs-eval.js +47 -0
- package/dist/tjs-eval.js.map +7 -0
- package/dist/tjs-from-ts.js +58 -0
- package/dist/tjs-from-ts.js.map +7 -0
- package/dist/tjs-lang.js +349 -0
- package/dist/tjs-lang.js.map +7 -0
- package/dist/tjs-vm.js +51 -51
- package/dist/tjs-vm.js.map +4 -19
- 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 +24 -12
- package/src/cli/commands/convert.test.ts +11 -8
- 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 +44 -31
- 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 +213 -64
- 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
- package/dist/tjs-full.js +0 -435
- package/dist/tjs-full.js.map +0 -45
- package/dist/tjs-transpiler.js +0 -3
- package/dist/tjs-transpiler.js.map +0 -11
package/src/lang/runtime.test.ts
CHANGED
|
@@ -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
|
+
})
|
package/src/lang/runtime.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
277
|
-
const
|
|
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
|
|
320
|
-
*
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
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
|
-
//
|
|
604
|
-
if (config.debug &&
|
|
605
|
-
|
|
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
|
-
? [...
|
|
608
|
-
:
|
|
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
|
-
//
|
|
1039
|
-
|
|
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
|
|
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
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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 (
|
|
1174
|
-
|
|
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 [
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
1425
|
+
if (
|
|
1426
|
+
(instanceConfig.callStacks || instanceConfig.debug) &&
|
|
1427
|
+
instanceStackCount > 0
|
|
1428
|
+
) {
|
|
1299
1429
|
const fullStack = details?.path
|
|
1300
|
-
? [...
|
|
1301
|
-
:
|
|
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
|
/**
|