tjs-lang 0.8.3 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tjs-lang",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
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",
@@ -27,6 +27,8 @@ import {
27
27
  isNativeType,
28
28
  Is,
29
29
  IsNot,
30
+ Eq,
31
+ NotEq,
30
32
  tjsEquals,
31
33
  checkType,
32
34
  MonadicError,
@@ -1178,3 +1180,47 @@ describe('predicate reason strings', () => {
1178
1180
  expect(err!.reason).toBeUndefined()
1179
1181
  })
1180
1182
  })
1183
+
1184
+ // `==`/`!=` (which transpile to Eq/NotEq under TjsEquals) are **footgun-free
1185
+ // `===`**, NOT structural. They unwrap boxed primitives and treat null/undefined
1186
+ // as equal — but distinct objects/arrays are genuinely distinct (a real
1187
+ // distinction, and structural `==` would be a silent O(n) perf hit). Deep
1188
+ // structural comparison is the separate `Is`/`IsNot` function. This block pins
1189
+ // that design so docs/behavior can't drift back to "== is structural".
1190
+ describe('equality semantics — == (Eq) is footgun-free, NOT structural', () => {
1191
+ it('fixes the boxed-primitive footgun (unlike ===)', () => {
1192
+ expect(Eq(new Boolean(false), false)).toBe(true)
1193
+ expect(Eq(new Number(5), 5)).toBe(true)
1194
+ expect(Eq(new String('x'), 'x')).toBe(true)
1195
+ })
1196
+
1197
+ it('does NOT coerce across types (unlike JS ==)', () => {
1198
+ expect(Eq('5', 5)).toBe(false)
1199
+ expect(Eq('', false)).toBe(false)
1200
+ expect(Eq(0, false)).toBe(false)
1201
+ expect(Eq(1, true)).toBe(false)
1202
+ expect(Eq(null, 0)).toBe(false)
1203
+ })
1204
+
1205
+ it('treats null and undefined as equal; NaN as equal to itself', () => {
1206
+ expect(Eq(null, undefined)).toBe(true)
1207
+ expect(Eq(NaN, NaN)).toBe(true)
1208
+ })
1209
+
1210
+ it('is NOT structural — distinct objects/arrays are distinct', () => {
1211
+ expect(Eq({ a: 1 }, { a: 1 })).toBe(false)
1212
+ expect(Eq([1, 2], [1, 2])).toBe(false)
1213
+ const shared = { a: 1 }
1214
+ expect(Eq(shared, shared)).toBe(true) // identity still holds
1215
+ expect(NotEq([1, 2], [1, 2])).toBe(true)
1216
+ })
1217
+
1218
+ it('Is/IsNot ARE structural — that is the deep-comparison path', () => {
1219
+ expect(Is({ a: 1 }, { a: 1 })).toBe(true)
1220
+ expect(Is([1, 2], [1, 2])).toBe(true)
1221
+ expect(Is({ a: { b: [1] } }, { a: { b: [1] } })).toBe(true)
1222
+ expect(IsNot([1, 2], [1, 3])).toBe(true)
1223
+ // and Is is not fooled into structural-comparing unequal shapes
1224
+ expect(Is({ a: 1 }, { a: 2 })).toBe(false)
1225
+ })
1226
+ })
@@ -596,8 +596,8 @@ describe('Edge Cases', () => {
596
596
  expect(result.result.res).toEqual({ value: 42 })
597
597
  })
598
598
 
599
- describe('structural equality in expressions', () => {
600
- it('== compares arrays structurally', async () => {
599
+ describe('footgun-free equality in expressions (== is NOT structural)', () => {
600
+ it('distinct arrays are NOT equal (== matches TJS, not structural)', async () => {
601
601
  const ast = {
602
602
  op: 'seq',
603
603
  steps: [
@@ -621,10 +621,10 @@ describe('Edge Cases', () => {
621
621
  ],
622
622
  } as any
623
623
  const result = await vm.run(ast, {})
624
- expect(result.result.res).toBe(true)
624
+ expect(result.result.res).toBe(false) // distinct arrays — NOT structural
625
625
  })
626
626
 
627
- it('== compares objects structurally', async () => {
627
+ it('distinct objects are NOT equal (== matches TJS, not structural)', async () => {
628
628
  const ast = {
629
629
  op: 'seq',
630
630
  steps: [
@@ -648,7 +648,7 @@ describe('Edge Cases', () => {
648
648
  ],
649
649
  } as any
650
650
  const result = await vm.run(ast, {})
651
- expect(result.result.res).toBe(true)
651
+ expect(result.result.res).toBe(false) // distinct objects — NOT structural
652
652
  })
653
653
 
654
654
  it('== does not coerce types', async () => {
@@ -678,7 +678,7 @@ describe('Edge Cases', () => {
678
678
  expect(result.result.res).toBe(false) // no coercion
679
679
  })
680
680
 
681
- it('!= returns true for structurally different objects', async () => {
681
+ it('!= returns true for distinct objects', async () => {
682
682
  const ast = {
683
683
  op: 'seq',
684
684
  steps: [
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { evaluateExpr } from './runtime'
3
+
4
+ /**
5
+ * AJS `==`/`!=` must match TJS `==` (Eq): footgun-free `===`, NOT structural.
6
+ * This used to do deep structural comparison in the VM — an early, unconsidered
7
+ * divergence from TJS `==` (so `[1,2] == [1,2]` was `true` in AJS but `false` in
8
+ * TJS). Now consistent: distinct objects/arrays are distinct; structural
9
+ * equality is an explicit operation, never `==`.
10
+ */
11
+ const lit = (value: unknown) => ({ $expr: 'literal' as const, value })
12
+ const evalEq = (op: '==' | '!=', a: unknown, b: unknown) =>
13
+ evaluateExpr(
14
+ { $expr: 'binary', op, left: lit(a), right: lit(b) } as any,
15
+ { state: {}, args: {} } as any
16
+ )
17
+
18
+ describe('AJS == / != — footgun-free, NOT structural (consistent with TJS)', () => {
19
+ it('distinct arrays/objects are NOT equal (the divergence fix)', () => {
20
+ expect(evalEq('==', [1, 2], [1, 2])).toBe(false)
21
+ expect(evalEq('==', { a: 1 }, { a: 1 })).toBe(false)
22
+ expect(evalEq('!=', [1, 2], [1, 2])).toBe(true)
23
+ })
24
+
25
+ it('identity still holds for the same reference', () => {
26
+ const shared = { a: 1 }
27
+ expect(evalEq('==', shared, shared)).toBe(true)
28
+ const arr = [1, 2]
29
+ expect(evalEq('==', arr, arr)).toBe(true)
30
+ })
31
+
32
+ it('no type coercion', () => {
33
+ expect(evalEq('==', '5', 5)).toBe(false)
34
+ expect(evalEq('==', '', false)).toBe(false)
35
+ expect(evalEq('==', 0, false)).toBe(false)
36
+ })
37
+
38
+ it('unwraps boxed primitives', () => {
39
+ expect(evalEq('==', new Boolean(false) as any, false)).toBe(true)
40
+ expect(evalEq('==', new Number(5) as any, 5)).toBe(true)
41
+ })
42
+
43
+ it('null/undefined equal; NaN equal to itself', () => {
44
+ expect(evalEq('==', null, undefined)).toBe(true)
45
+ expect(evalEq('==', NaN, NaN)).toBe(true)
46
+ expect(evalEq('==', null, 0)).toBe(false)
47
+ })
48
+
49
+ it('scalars compare as expected', () => {
50
+ expect(evalEq('==', 1, 1)).toBe(true)
51
+ expect(evalEq('==', 'x', 'x')).toBe(true)
52
+ expect(evalEq('!=', 1, 2)).toBe(true)
53
+ })
54
+
55
+ it('=== / !== remain strict identity (unchanged)', () => {
56
+ expect(evalEq('===' as any, [1], [1])).toBe(false)
57
+ expect(evalEq('===' as any, 5, 5)).toBe(true)
58
+ })
59
+ })
package/src/vm/runtime.ts CHANGED
@@ -1,69 +1,36 @@
1
1
  import { s, validate, filter as schemaFilter } from 'tosijs-schema'
2
2
 
3
- /** Well-known symbol for custom equality (matches src/lang/runtime.ts) */
4
- const tjsEquals = Symbol.for('tjs.equals')
5
-
6
3
  /**
7
- * Structural equality for AJS expressions.
8
- * Mirrors the TJS Is() function: symbol .Equals structural.
4
+ * AJS `==`/`!=` equality **footgun-free `===`**, consistent with TJS `Eq`
5
+ * (`src/lang/runtime.ts`). NOT structural: it unwraps boxed primitives, treats
6
+ * `null`/`undefined` as equal and `NaN` as equal to itself, but does NOT coerce
7
+ * across types, and distinct objects/arrays are distinct (`[1,2] != [1,2]`).
8
+ *
9
+ * (Originally the VM did deep structural comparison here — an early, unconsidered
10
+ * divergence from TJS `==`. It was also a SECURITY hole: a single `==` node costs
11
+ * a flat EXPR_FUEL_COST, but the structural walk charged nothing per element, so
12
+ * `==` on an attacker-controlled large structure was an unbounded-work / fuel-
13
+ * bypass DoS — exactly what AJS exists to prevent. `eqValue` is O(1).
14
+ *
15
+ * Structural equality is an explicit operation: in TJS it's the `Is`/`IsNot`
16
+ * function; AJS can grow an `Is` atom if/when needed — but it MUST be fuel-metered
17
+ * per element compared, or it reintroduces the same DoS.)
9
18
  */
10
- function isStructurallyEqual(a: unknown, b: unknown): boolean {
11
- // Symbol protocol
12
- if (
13
- a !== null &&
14
- typeof a === 'object' &&
15
- typeof (a as any)[tjsEquals] === 'function'
16
- ) {
17
- return (a as any)[tjsEquals](b)
18
- }
19
- if (
20
- b !== null &&
21
- typeof b === 'object' &&
22
- typeof (b as any)[tjsEquals] === 'function'
23
- ) {
24
- return (b as any)[tjsEquals](a)
19
+ function eqValue(a: unknown, b: unknown): boolean {
20
+ if (a instanceof String || a instanceof Number || a instanceof Boolean) {
21
+ a = a.valueOf()
25
22
  }
26
-
27
- // .Equals method
28
- if (
29
- a !== null &&
30
- typeof a === 'object' &&
31
- typeof (a as any).Equals === 'function'
32
- ) {
33
- return (a as any).Equals(b)
34
- }
35
- if (
36
- b !== null &&
37
- typeof b === 'object' &&
38
- typeof (b as any).Equals === 'function'
39
- ) {
40
- return (b as any).Equals(a)
23
+ if (b instanceof String || b instanceof Number || b instanceof Boolean) {
24
+ b = b.valueOf()
41
25
  }
42
-
43
26
  if (a === b) return true
44
-
45
- // Nullish equality (null == undefined)
46
- if ((a === null || a === undefined) && (b === null || b === undefined))
27
+ if (typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b)) {
47
28
  return true
48
-
49
- if (a === null || a === undefined || b === null || b === undefined)
50
- return false
51
-
52
- if (typeof a !== typeof b) return false
53
- if (typeof a !== 'object') return false
54
-
55
- // Arrays
56
- if (Array.isArray(a) && Array.isArray(b)) {
57
- if (a.length !== b.length) return false
58
- return a.every((v, i) => isStructurallyEqual(v, b[i]))
59
29
  }
60
- if (Array.isArray(a) !== Array.isArray(b)) return false
61
-
62
- // Objects
63
- const keysA = Object.keys(a as object)
64
- const keysB = Object.keys(b as object)
65
- if (keysA.length !== keysB.length) return false
66
- return keysA.every((k) => isStructurallyEqual((a as any)[k], (b as any)[k]))
30
+ if ((a === null || a === undefined) && (b === null || b === undefined)) {
31
+ return true
32
+ }
33
+ return false
67
34
  }
68
35
 
69
36
  // --- Monadic Error Type ---
@@ -1204,9 +1171,9 @@ export function evaluateExpr(node: ExprNode, ctx: RuntimeContext): any {
1204
1171
  case '<=':
1205
1172
  return left <= right
1206
1173
  case '==':
1207
- return isStructurallyEqual(left, right)
1174
+ return eqValue(left, right)
1208
1175
  case '!=':
1209
- return !isStructurallyEqual(left, right)
1176
+ return !eqValue(left, right)
1210
1177
  case '===':
1211
1178
  return left === right
1212
1179
  case '!==':