tjs-lang 0.7.3 → 0.7.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tjs-lang",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "Type-safe JavaScript dialect with runtime validation, sandboxed VM execution, and AI agent orchestration. Transpiles TypeScript to validated JS with fuel-metered execution for untrusted code.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -31,22 +31,27 @@
31
31
  "./eval": {
32
32
  "bun": "./src/lang/eval.ts",
33
33
  "types": "./dist/src/lang/eval.d.ts",
34
- "default": "./dist/lang/eval.js"
34
+ "default": "./dist/tjs-eval.js"
35
35
  },
36
36
  "./lang": {
37
- "bun": "./src/lang/index.ts",
38
- "types": "./dist/src/lang/index.d.ts",
39
- "default": "./dist/lang/index.js"
40
- },
41
- "./lang/eval": {
42
- "bun": "./src/lang/eval.ts",
43
- "types": "./dist/src/lang/eval.d.ts",
44
- "default": "./dist/lang/eval.js"
37
+ "bun": "./src/lang/transpiler.ts",
38
+ "types": "./dist/src/lang/transpiler.d.ts",
39
+ "default": "./dist/tjs-lang.js"
45
40
  },
46
41
  "./lang/from-ts": {
47
42
  "bun": "./src/lang/emitters/from-ts.ts",
48
43
  "types": "./dist/src/lang/emitters/from-ts.d.ts",
49
- "default": "./dist/lang/emitters/from-ts.js"
44
+ "default": "./dist/tjs-from-ts.js"
45
+ },
46
+ "./vm": {
47
+ "bun": "./src/vm/index.ts",
48
+ "types": "./dist/src/vm/index.d.ts",
49
+ "default": "./dist/tjs-vm.js"
50
+ },
51
+ "./batteries": {
52
+ "bun": "./src/batteries/index.ts",
53
+ "types": "./dist/src/batteries/index.d.ts",
54
+ "default": "./dist/tjs-batteries.js"
50
55
  },
51
56
  "./src": "./src/index.ts",
52
57
  "./editors/monaco": "./editors/monaco/ajs-monarch.js",
@@ -94,6 +99,7 @@
94
99
  "acorn-walk": "^8.3.4",
95
100
  "chokidar": "^4.0.3",
96
101
  "codemirror": "^6.0.2",
102
+ "esbuild": "^0.28.0",
97
103
  "eslint": "^8.57.1",
98
104
  "firebase": "^10.12.0",
99
105
  "firebase-admin": "^13.6.0",
@@ -822,8 +822,8 @@ export function transpileToJS(
822
822
  // Core: MonadicError + typeError (needed by almost all validated functions)
823
823
  if (needsTypeError) {
824
824
  inlineParts.push(
825
- `class MonadicError extends Error{constructor(m,p,e,a,c){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c}}`,
826
- `function typeError(p,e,v){const a=v===null?'null':typeof v;const err=new MonadicError('Expected '+e+" for '"+p+"', got "+a,p,e,a);const c=globalThis.__tjs?.getConfig?.();if(c?.logTypeErrors)console.error('[TJS TypeError] '+err.message);if(c?.throwTypeErrors)throw err;return err}`,
825
+ `class MonadicError extends Error{constructor(m,p,e,a,c,r){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c;this.reason=r}}`,
826
+ `function typeError(p,e,v,r){const a=v===null?'null':typeof v;const m=r?'Expected '+e+" for '"+p+"': "+r:'Expected '+e+" for '"+p+"', got "+a;const err=new MonadicError(m,p,e,a,undefined,r);const c=globalThis.__tjs?.getConfig?.();if(c?.logTypeErrors)console.error('[TJS TypeError] '+err.message);if(c?.throwTypeErrors)throw err;return err}`,
827
827
  `function isMonadicError(v){return v instanceof Error&&v.name==='MonadicError'&&'path' in v}`
828
828
  )
829
829
  }
@@ -892,8 +892,8 @@ export function transpileToJS(
892
892
  // bang depends on typeError and isMonadicError — ensure they're inlined
893
893
  if (!needsTypeError) {
894
894
  inlineParts.push(
895
- `class MonadicError extends Error{constructor(m,p,e,a,c){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c}}`,
896
- `function typeError(p,e,v){const a=v===null?'null':typeof v;const err=new MonadicError('Expected '+e+" for '"+p+"', got "+a,p,e,a);const c=globalThis.__tjs?.getConfig?.();if(c?.logTypeErrors)console.error('[TJS TypeError] '+err.message);if(c?.throwTypeErrors)throw err;return err}`,
895
+ `class MonadicError extends Error{constructor(m,p,e,a,c,r){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c;this.reason=r}}`,
896
+ `function typeError(p,e,v,r){const a=v===null?'null':typeof v;const m=r?'Expected '+e+" for '"+p+"': "+r:'Expected '+e+" for '"+p+"', got "+a;const err=new MonadicError(m,p,e,a,undefined,r);const c=globalThis.__tjs?.getConfig?.();if(c?.logTypeErrors)console.error('[TJS TypeError] '+err.message);if(c?.throwTypeErrors)throw err;return err}`,
897
897
  `function isMonadicError(v){return v instanceof Error&&v.name==='MonadicError'&&'path' in v}`
898
898
  )
899
899
  }
@@ -12,9 +12,11 @@ describe('FunctionPredicate runtime', () => {
12
12
  })
13
13
  expect(Callback.check(() => {})).toBe(true)
14
14
  expect(Callback.check((x: number) => String(x))).toBe(true)
15
- expect(Callback.check(42)).toBe(false)
16
- expect(Callback.check('not a function')).toBe(false)
17
- expect(Callback.check(null)).toBe(false)
15
+ expect(Callback.check(42)).toBe('expected function, got number')
16
+ expect(Callback.check('not a function')).toBe(
17
+ 'expected function, got string'
18
+ )
19
+ expect(Callback.check(null)).toBe('expected function, got null')
18
20
  })
19
21
 
20
22
  it('should create from existing typed function', () => {
@@ -73,7 +75,7 @@ describe('FunctionPredicate runtime', () => {
73
75
  x: { type: { kind: 'integer' }, example: 0 },
74
76
  },
75
77
  }
76
- expect(Binop.check(wrongArity)).toBe(false)
78
+ expect(Binop.check(wrongArity)).toBe('expected 2 params, got 1')
77
79
  })
78
80
 
79
81
  it('should reject function with wrong param types via __tjs metadata', () => {
@@ -93,7 +95,7 @@ describe('FunctionPredicate runtime', () => {
93
95
  ;(badFn as any).__tjs = {
94
96
  params: { n: { type: { kind: 'integer' }, example: 0 } },
95
97
  }
96
- expect(StrFn.check(badFn)).toBe(false)
98
+ expect(StrFn.check(badFn)).toBe("param 'name' expected string, got integer")
97
99
  })
98
100
 
99
101
  it('should accept function with any-typed params', () => {
@@ -230,7 +232,7 @@ describe('Generic FunctionPredicate runtime', () => {
230
232
  expect(StringCreator.returns).toBe('')
231
233
  expect(StringCreator.params.x).toBe('')
232
234
  expect(StringCreator.check(() => {})).toBe(true)
233
- expect(StringCreator.check(42)).toBe(false)
235
+ expect(StringCreator.check(42)).toBe('expected function, got number')
234
236
  })
235
237
 
236
238
  it('should use default type arg when none provided', () => {
@@ -28,6 +28,9 @@ import {
28
28
  Is,
29
29
  IsNot,
30
30
  tjsEquals,
31
+ checkType,
32
+ MonadicError,
33
+ isMonadicError,
31
34
  } from './runtime'
32
35
  import { Eval, SafeFunction } from './eval'
33
36
 
@@ -1120,3 +1123,58 @@ function add(a: 1, b: 2): 3 {
1120
1123
  }
1121
1124
  })
1122
1125
  })
1126
+
1127
+ describe('predicate reason strings', () => {
1128
+ it('typeError includes reason in message and field', () => {
1129
+ const err = typeError('foo.bar', 'even number', 3, 'value is odd')
1130
+ expect(err.message).toContain('value is odd')
1131
+ expect(err.message).toContain('foo.bar')
1132
+ expect(err.reason).toBe('value is odd')
1133
+ expect(isMonadicError(err)).toBe(true)
1134
+ })
1135
+
1136
+ it('typeError without reason works as before', () => {
1137
+ const err = typeError('foo.bar', 'string', 42)
1138
+ expect(err.message).toContain('got number')
1139
+ expect(err.reason).toBeUndefined()
1140
+ })
1141
+
1142
+ it('checkType captures reason from predicate', () => {
1143
+ const EvenType = {
1144
+ check: (v: unknown): boolean | string => {
1145
+ if (typeof v !== 'number') return `expected number, got ${typeof v}`
1146
+ if (v % 2 !== 0) return `${v} is odd`
1147
+ return true
1148
+ },
1149
+ description: 'even number',
1150
+ }
1151
+
1152
+ // Pass
1153
+ expect(checkType(4, EvenType, 'test.val')).toBeNull()
1154
+
1155
+ // Fail with reason
1156
+ const err = checkType(3, EvenType, 'test.val')
1157
+ expect(err).not.toBeNull()
1158
+ expect(err!.message).toContain('3 is odd')
1159
+ expect(err!.reason).toBe('3 is odd')
1160
+
1161
+ // Fail with different reason
1162
+ const err2 = checkType('x', EvenType, 'test.val')
1163
+ expect(err2).not.toBeNull()
1164
+ expect(err2!.message).toContain('expected number, got string')
1165
+ })
1166
+
1167
+ it('checkType works with boolean-only predicates', () => {
1168
+ const PositiveType = {
1169
+ check: (v: unknown) => typeof v === 'number' && v > 0,
1170
+ description: 'positive number',
1171
+ }
1172
+
1173
+ expect(checkType(5, PositiveType, 'x')).toBeNull()
1174
+
1175
+ const err = checkType(-1, PositiveType, 'x')
1176
+ expect(err).not.toBeNull()
1177
+ expect(err!.message).toContain('positive number')
1178
+ expect(err!.reason).toBeUndefined()
1179
+ })
1180
+ })
@@ -142,13 +142,16 @@ export class MonadicError extends Error {
142
142
  readonly actual?: string
143
143
  /** TJS call stack (only in debug mode) - shows source locations */
144
144
  readonly callStack?: string[]
145
+ /** Why the check failed (from predicate reason strings) */
146
+ readonly reason?: string
145
147
 
146
148
  constructor(
147
149
  message: string,
148
150
  path: string,
149
151
  expected?: string,
150
152
  actual?: string,
151
- callStack?: string[]
153
+ callStack?: string[],
154
+ reason?: string
152
155
  ) {
153
156
  super(message)
154
157
  this.name = 'MonadicError'
@@ -156,6 +159,7 @@ export class MonadicError extends Error {
156
159
  this.expected = expected
157
160
  this.actual = actual
158
161
  this.callStack = callStack
162
+ this.reason = reason
159
163
  // Maintains proper stack trace in V8 environments
160
164
  if (Error.captureStackTrace) {
161
165
  Error.captureStackTrace(this, MonadicError)
@@ -177,18 +181,16 @@ export class MonadicError extends Error {
177
181
  export function typeError(
178
182
  path: string,
179
183
  expected: string,
180
- value: unknown
184
+ value: unknown,
185
+ reason?: string
181
186
  ): MonadicError {
182
187
  const actual = value === null ? 'null' : typeof value
183
188
  // Capture call stack in debug mode (getStack returns [] if not in debug mode)
184
189
  const stack = config.callStacks || config.debug ? getStack() : undefined
185
- const err = new MonadicError(
186
- `Expected ${expected} for '${path}', got ${actual}`,
187
- path,
188
- expected,
189
- actual,
190
- stack
191
- )
190
+ const msg = reason
191
+ ? `Expected ${expected} for '${path}': ${reason}`
192
+ : `Expected ${expected} for '${path}', got ${actual}`
193
+ const err = new MonadicError(msg, path, expected, actual, stack, reason)
192
194
 
193
195
  // Track in error history ring buffer (zero cost on happy path)
194
196
  if (config.trackErrors !== false) {
@@ -238,6 +240,8 @@ export interface TJSError {
238
240
  stack?: string[]
239
241
  expected?: string
240
242
  actual?: string
243
+ /** Why the check failed (from predicate reason strings) */
244
+ reason?: string
241
245
  cause?: Error | TJSError
242
246
  /** Source location for error reporting */
243
247
  loc?: { start: number; end: number }
@@ -779,7 +783,9 @@ export function isNativeType(value: unknown, typeName: string): boolean {
779
783
  */
780
784
  export function checkType(
781
785
  value: unknown,
782
- expected: string | { check: (v: unknown) => boolean; description: string },
786
+ expected:
787
+ | string
788
+ | { check: (v: unknown) => boolean | string; description: string },
783
789
  path?: string
784
790
  ): TJSError | null {
785
791
  // If value is already an error, propagate it
@@ -791,11 +797,17 @@ export function checkType(
791
797
  expected !== null &&
792
798
  'check' in expected
793
799
  ) {
794
- if (expected.check(value)) return null
795
- return error(`Expected ${expected.description} but got ${typeOf(value)}`, {
800
+ const result = expected.check(value)
801
+ if (result === true) return null
802
+ const reason = typeof result === 'string' ? result : undefined
803
+ const msg = reason
804
+ ? `Expected ${expected.description} for '${path}': ${reason}`
805
+ : `Expected ${expected.description} but got ${typeOf(value)}`
806
+ return error(msg, {
796
807
  path,
797
808
  expected: expected.description,
798
809
  actual: typeOf(value),
810
+ reason,
799
811
  })
800
812
  }
801
813
 
@@ -828,7 +840,9 @@ export function checkType(
828
840
  }
829
841
 
830
842
  /** Type specifier - either a string name or a RuntimeType */
831
- type TypeSpec = string | { check: (v: unknown) => boolean; description: string }
843
+ type TypeSpec =
844
+ | string
845
+ | { check: (v: unknown) => boolean | string; description: string }
832
846
 
833
847
  /** Parameter metadata with optional location */
834
848
  interface ParamMeta {
@@ -207,8 +207,8 @@ describe('Built-in Types', () => {
207
207
  it('TString validates strings', () => {
208
208
  expect(TString.check('hello')).toBe(true)
209
209
  expect(TString.check('')).toBe(true)
210
- expect(TString.check(123)).toBe(false)
211
- expect(TString.check(null)).toBe(false)
210
+ expect(TString.check(123)).toBe('expected string, got number')
211
+ expect(TString.check(null)).toBe('expected string, got null')
212
212
  })
213
213
 
214
214
  it('TNumber validates numbers', () => {
@@ -216,60 +216,64 @@ describe('Built-in Types', () => {
216
216
  expect(TNumber.check(0)).toBe(true)
217
217
  expect(TNumber.check(-1.5)).toBe(true)
218
218
  expect(TNumber.check(NaN)).toBe(true) // NaN is typeof number
219
- expect(TNumber.check('123')).toBe(false)
219
+ expect(TNumber.check('123')).toBe('expected number, got string')
220
220
  })
221
221
 
222
222
  it('TBoolean validates booleans', () => {
223
223
  expect(TBoolean.check(true)).toBe(true)
224
224
  expect(TBoolean.check(false)).toBe(true)
225
- expect(TBoolean.check(1)).toBe(false)
226
- expect(TBoolean.check('true')).toBe(false)
225
+ expect(TBoolean.check(1)).toBe('expected boolean, got number')
226
+ expect(TBoolean.check('true')).toBe('expected boolean, got string')
227
227
  })
228
228
 
229
229
  it('TInteger validates integers', () => {
230
230
  expect(TInteger.check(1)).toBe(true)
231
231
  expect(TInteger.check(0)).toBe(true)
232
232
  expect(TInteger.check(-5)).toBe(true)
233
- expect(TInteger.check(1.5)).toBe(false)
234
- expect(TInteger.check('1')).toBe(false)
233
+ expect(TInteger.check(1.5)).toBe('1.5 is not an integer')
234
+ expect(TInteger.check('1')).toBe('expected integer, got string')
235
235
  })
236
236
 
237
237
  it('TPositiveInt validates positive integers', () => {
238
238
  expect(TPositiveInt.check(1)).toBe(true)
239
239
  expect(TPositiveInt.check(100)).toBe(true)
240
- expect(TPositiveInt.check(0)).toBe(false)
241
- expect(TPositiveInt.check(-1)).toBe(false)
242
- expect(TPositiveInt.check(1.5)).toBe(false)
240
+ expect(TPositiveInt.check(0)).toBe('0 is not positive')
241
+ expect(TPositiveInt.check(-1)).toBe('-1 is not positive')
242
+ expect(TPositiveInt.check(1.5)).toBe('1.5 is not an integer')
243
243
  })
244
244
 
245
245
  it('TNonEmptyString validates non-empty strings', () => {
246
246
  expect(TNonEmptyString.check('hello')).toBe(true)
247
247
  expect(TNonEmptyString.check('a')).toBe(true)
248
- expect(TNonEmptyString.check('')).toBe(false)
249
- expect(TNonEmptyString.check(123)).toBe(false)
248
+ expect(TNonEmptyString.check('')).toBe('string is empty')
249
+ expect(TNonEmptyString.check(123)).toBe('expected string, got number')
250
250
  })
251
251
 
252
252
  it('TEmail validates email addresses', () => {
253
253
  expect(TEmail.check('user@example.com')).toBe(true)
254
254
  expect(TEmail.check('a@b.c')).toBe(true)
255
- expect(TEmail.check('invalid')).toBe(false)
256
- expect(TEmail.check('no@domain')).toBe(false)
257
- expect(TEmail.check('@example.com')).toBe(false)
255
+ expect(TEmail.check('invalid')).toBe('"invalid" is not a valid email')
256
+ expect(TEmail.check('no@domain')).toBe('"no@domain" is not a valid email')
257
+ expect(TEmail.check('@example.com')).toBe(
258
+ '"@example.com" is not a valid email'
259
+ )
258
260
  })
259
261
 
260
262
  it('TUrl validates URLs', () => {
261
263
  expect(TUrl.check('https://example.com')).toBe(true)
262
264
  expect(TUrl.check('http://localhost:3000')).toBe(true)
263
265
  expect(TUrl.check('ftp://files.example.com')).toBe(true)
264
- expect(TUrl.check('not-a-url')).toBe(false)
265
- expect(TUrl.check('example.com')).toBe(false)
266
+ expect(TUrl.check('not-a-url')).toBe('"not-a-url" is not a valid URL')
267
+ expect(TUrl.check('example.com')).toBe('"example.com" is not a valid URL')
266
268
  })
267
269
 
268
270
  it('TUuid validates UUIDs', () => {
269
271
  expect(TUuid.check('550e8400-e29b-41d4-a716-446655440000')).toBe(true)
270
272
  expect(TUuid.check('550E8400-E29B-41D4-A716-446655440000')).toBe(true)
271
- expect(TUuid.check('not-a-uuid')).toBe(false)
272
- expect(TUuid.check('550e8400-e29b-41d4-a716')).toBe(false)
273
+ expect(TUuid.check('not-a-uuid')).toBe('"not-a-uuid" is not a valid UUID')
274
+ expect(TUuid.check('550e8400-e29b-41d4-a716')).toBe(
275
+ '"550e8400-e29b-41d4-a716" is not a valid UUID'
276
+ )
273
277
  })
274
278
  })
275
279
 
@@ -676,3 +680,48 @@ describe('Enum', () => {
676
680
  expect(HttpStatus.members.NotFound).toBe(404)
677
681
  })
678
682
  })
683
+
684
+ describe('predicate reason strings', () => {
685
+ it('Type() passes through reason strings from predicates', () => {
686
+ const EvenNumber = Type<number>('even number', (v: unknown) => {
687
+ if (typeof v !== 'number') return 'not a number'
688
+ if (v % 2 !== 0) return `${v} is odd`
689
+ return true
690
+ })
691
+
692
+ expect(EvenNumber.check(4)).toBe(true)
693
+ expect(EvenNumber.check(3)).toBe('3 is odd')
694
+ expect(EvenNumber.check('x')).toBe('not a number')
695
+ })
696
+
697
+ it('schema-based types still return boolean', () => {
698
+ const StringType = Type('a string', s.string)
699
+ expect(StringType.check('hello')).toBe(true)
700
+ expect(StringType.check(42)).toBe(false)
701
+ })
702
+
703
+ it('Nullable correctly handles reason-returning predicates', () => {
704
+ const EvenNumber = Type<number>('even number', (v: unknown) => {
705
+ if (typeof v !== 'number') return 'not a number'
706
+ if (v % 2 !== 0) return `${v} is odd`
707
+ return true
708
+ })
709
+ const NullableEven = Nullable(EvenNumber)
710
+
711
+ expect(NullableEven.check(null)).toBe(true)
712
+ expect(NullableEven.check(4)).toBe(true)
713
+ expect(NullableEven.check(3)).toBe(false) // reason lost in combinator, but correctly fails
714
+ })
715
+
716
+ it('TArray correctly handles reason-returning predicates', () => {
717
+ const Positive = Type<number>('positive', (v: unknown) => {
718
+ if (typeof v !== 'number') return 'not a number'
719
+ if (v <= 0) return `${v} is not positive`
720
+ return true
721
+ })
722
+ const PositiveArray = TArray(Positive)
723
+
724
+ expect(PositiveArray.check([1, 2, 3])).toBe(true)
725
+ expect(PositiveArray.check([1, -1, 3])).toBe(false)
726
+ })
727
+ })