tjs-lang 0.6.44 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +85 -422
- package/README.md +15 -82
- package/bin/benchmarks.ts +7 -7
- package/bin/dev.ts +2 -1
- package/demo/autocomplete.test.ts +1 -1
- package/demo/docs.json +744 -48
- package/demo/src/demo-nav.ts +5 -5
- package/demo/src/index.ts +28 -36
- package/demo/src/module-sw.ts +1 -1
- package/demo/src/playground-shared.ts +17 -17
- package/demo/src/playground.ts +13 -1
- package/demo/src/style.ts +4 -1
- package/demo/src/tjs-playground.ts +5 -5
- package/demo/src/user-store.ts +2 -1
- package/demo/static/favicon.svg +17 -24
- package/demo/static/tosi-platform.json +9304 -0
- package/dist/index.js +158 -156
- package/dist/index.js.map +14 -13
- package/dist/scripts/compat-effect.d.ts +16 -0
- package/dist/scripts/compat-kysely.d.ts +13 -0
- package/dist/scripts/compat-radash.d.ts +13 -0
- package/dist/scripts/compat-superstruct.d.ts +13 -0
- package/dist/scripts/compat-ts-pattern.d.ts +13 -0
- package/dist/scripts/compat-zod.d.ts +12 -0
- package/dist/src/lang/emitters/from-ts.d.ts +1 -1
- package/dist/src/lang/emitters/js-tests.d.ts +4 -0
- package/dist/src/lang/emitters/js.d.ts +2 -2
- package/dist/src/lang/index.d.ts +1 -0
- package/dist/src/lang/json-schema.d.ts +40 -0
- package/dist/src/lang/parser-transforms.d.ts +14 -0
- package/dist/src/lang/runtime.d.ts +39 -6
- package/dist/src/types/Type.d.ts +5 -0
- package/dist/tjs-full.js +158 -156
- package/dist/tjs-full.js.map +14 -13
- package/dist/tjs-vm.js +44 -43
- package/dist/tjs-vm.js.map +5 -5
- package/docs/README.md +21 -20
- package/docs/WASM-QUICKSTART.md +283 -0
- package/docs/diagrams/architecture-shift.svg +117 -0
- package/docs/diagrams/compile-runtime.svg +130 -0
- package/docs/diagrams/icon-riff-1.svg +55 -0
- package/docs/diagrams/icon-riff-2.svg +62 -0
- package/docs/diagrams/icon-riff-3.svg +61 -0
- package/docs/diagrams/platform-overview.svg +114 -0
- package/docs/diagrams/safe-eval.svg +147 -0
- package/docs/eval-v4/arch-comparison.svg +277 -0
- package/docs/eval-v4/bundler-tree.svg +250 -0
- package/docs/eval-v4/http-lifecycle.svg +148 -0
- package/docs/function-predicate-design.md +8 -8
- package/docs/native-engine-integration.md +2 -2
- package/editors/codemirror/autocomplete.test.ts +29 -29
- package/package.json +10 -4
- package/src/cli/commands/convert.test.ts +11 -8
- package/src/cli/tjs.ts +1 -1
- package/src/lang/codegen.test.ts +117 -112
- package/src/lang/docs.test.ts +22 -22
- package/src/lang/docs.ts +5 -8
- package/src/lang/emitters/dts.test.ts +13 -13
- package/src/lang/emitters/from-ts.ts +36 -9
- package/src/lang/emitters/js-tests.ts +143 -28
- package/src/lang/emitters/js.ts +49 -28
- package/src/lang/features.test.ts +259 -43
- package/src/lang/from-ts.test.ts +3 -3
- package/src/lang/function-predicate.test.ts +1 -1
- package/src/lang/index.ts +8 -47
- package/src/lang/json-schema.test.ts +261 -0
- package/src/lang/json-schema.ts +167 -0
- package/src/lang/parser-params.ts +28 -44
- package/src/lang/parser-transforms.ts +255 -0
- package/src/lang/parser.test.ts +32 -13
- package/src/lang/parser.ts +49 -11
- package/src/lang/perf.test.ts +11 -11
- package/src/lang/roundtrip.test.ts +3 -3
- package/src/lang/runtime.test.ts +167 -0
- package/src/lang/runtime.ts +234 -46
- package/src/lang/transpiler.test.ts +21 -21
- package/src/lang/typescript-syntax.test.ts +11 -9
- package/src/types/Type.ts +38 -1
- package/src/use-cases/bootstrap.test.ts +7 -7
- package/src/use-cases/client-server.test.ts +1 -1
- package/src/use-cases/malicious-actor.test.ts +1 -1
- package/src/use-cases/rag-processor.test.ts +1 -1
- package/src/use-cases/sophisticated-agents.test.ts +2 -2
- package/src/use-cases/transpiler-llm.test.ts +1 -1
- package/src/use-cases/unbundled-imports.test.ts +9 -9
- package/tjs-lang.svg +17 -25
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
|
|
|
@@ -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
|
-
//
|
|
569
|
-
if (config.debug &&
|
|
570
|
-
|
|
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
|
-
? [...
|
|
573
|
-
:
|
|
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
|
-
//
|
|
1004
|
-
|
|
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
|
|
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
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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 (
|
|
1139
|
-
|
|
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 [
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
1425
|
+
if (
|
|
1426
|
+
(instanceConfig.callStacks || instanceConfig.debug) &&
|
|
1427
|
+
instanceStackCount > 0
|
|
1428
|
+
) {
|
|
1264
1429
|
const fullStack = details?.path
|
|
1265
|
-
? [...
|
|
1266
|
-
:
|
|
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,
|