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/CLAUDE.md +1 -1
- package/demo/docs.json +7 -7
- package/dist/experiments/predicates/css.predicates.d.ts +13 -0
- package/dist/experiments/predicates/verify.d.ts +39 -0
- package/dist/index.js +101 -97
- package/dist/index.js.map +4 -4
- package/dist/src/lang/index.d.ts +2 -0
- package/dist/src/lang/predicate-schema.d.ts +39 -0
- package/dist/src/lang/predicate.d.ts +103 -0
- package/dist/src/lang/transpiler.d.ts +2 -0
- package/dist/src/vm/runtime.d.ts +22 -0
- package/dist/tjs-eval.js +29 -29
- package/dist/tjs-eval.js.map +3 -3
- package/dist/tjs-from-ts.js +1 -1
- package/dist/tjs-from-ts.js.map +2 -2
- package/dist/tjs-lang.js +86 -82
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +45 -45
- package/dist/tjs-vm.js.map +3 -3
- package/package.json +1 -1
- package/src/lang/runtime.test.ts +46 -0
- package/src/runtime.test.ts +6 -6
- package/src/vm/equality.test.ts +59 -0
- package/src/vm/runtime.ts +26 -59
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tjs-lang",
|
|
3
|
-
"version": "0.8.
|
|
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",
|
package/src/lang/runtime.test.ts
CHANGED
|
@@ -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
|
+
})
|
package/src/runtime.test.ts
CHANGED
|
@@ -596,8 +596,8 @@ describe('Edge Cases', () => {
|
|
|
596
596
|
expect(result.result.res).toEqual({ value: 42 })
|
|
597
597
|
})
|
|
598
598
|
|
|
599
|
-
describe('
|
|
600
|
-
it('==
|
|
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(
|
|
624
|
+
expect(result.result.res).toBe(false) // distinct arrays — NOT structural
|
|
625
625
|
})
|
|
626
626
|
|
|
627
|
-
it('==
|
|
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(
|
|
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
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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 (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
1174
|
+
return eqValue(left, right)
|
|
1208
1175
|
case '!=':
|
|
1209
|
-
return !
|
|
1176
|
+
return !eqValue(left, right)
|
|
1210
1177
|
case '===':
|
|
1211
1178
|
return left === right
|
|
1212
1179
|
case '!==':
|