@nudojs/core 0.1.0
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/CHANGELOG.md +7 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +440 -0
- package/package.json +12 -0
- package/src/__tests__/environment.test.ts +48 -0
- package/src/__tests__/ops.test.ts +109 -0
- package/src/__tests__/type-value-phase2.test.ts +117 -0
- package/src/__tests__/type-value-phase3.test.ts +94 -0
- package/src/__tests__/type-value.test.ts +213 -0
- package/src/environment.ts +69 -0
- package/src/index.ts +22 -0
- package/src/ops.ts +147 -0
- package/src/type-value.ts +332 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { Ops, applyBinaryOp } from "../ops.ts";
|
|
3
|
+
import { T, typeValueEquals } from "../type-value.ts";
|
|
4
|
+
|
|
5
|
+
describe("Ops.add", () => {
|
|
6
|
+
it("adds two literal numbers", () => {
|
|
7
|
+
expect(typeValueEquals(Ops.add(T.literal(1), T.literal(2)), T.literal(3))).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("concatenates two literal strings", () => {
|
|
11
|
+
expect(typeValueEquals(Ops.add(T.literal("a"), T.literal("b")), T.literal("ab"))).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns number for abstract numbers", () => {
|
|
15
|
+
expect(typeValueEquals(Ops.add(T.number, T.number), T.number)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns string when one side is string", () => {
|
|
19
|
+
expect(typeValueEquals(Ops.add(T.number, T.string), T.string)).toBe(true);
|
|
20
|
+
expect(typeValueEquals(Ops.add(T.string, T.literal(1)), T.string)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("Ops.sub / mul / div / mod", () => {
|
|
25
|
+
it("computes literal arithmetic", () => {
|
|
26
|
+
expect(typeValueEquals(Ops.sub(T.literal(5), T.literal(3)), T.literal(2))).toBe(true);
|
|
27
|
+
expect(typeValueEquals(Ops.mul(T.literal(3), T.literal(4)), T.literal(12))).toBe(true);
|
|
28
|
+
expect(typeValueEquals(Ops.div(T.literal(10), T.literal(2)), T.literal(5))).toBe(true);
|
|
29
|
+
expect(typeValueEquals(Ops.mod(T.literal(7), T.literal(3)), T.literal(1))).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns number for abstract operands", () => {
|
|
33
|
+
expect(typeValueEquals(Ops.sub(T.number, T.number), T.number)).toBe(true);
|
|
34
|
+
expect(typeValueEquals(Ops.mul(T.number, T.literal(2)), T.number)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("Ops.strictEq / strictNeq", () => {
|
|
39
|
+
it("compares literals", () => {
|
|
40
|
+
expect(typeValueEquals(Ops.strictEq(T.literal(1), T.literal(1)), T.literal(true))).toBe(true);
|
|
41
|
+
expect(typeValueEquals(Ops.strictEq(T.literal(1), T.literal(2)), T.literal(false))).toBe(true);
|
|
42
|
+
expect(typeValueEquals(Ops.strictNeq(T.literal(1), T.literal(2)), T.literal(true))).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns boolean for abstract operands", () => {
|
|
46
|
+
expect(typeValueEquals(Ops.strictEq(T.number, T.number), T.boolean)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("Ops.gt / lt / gte / lte", () => {
|
|
51
|
+
it("compares literal numbers", () => {
|
|
52
|
+
expect(typeValueEquals(Ops.gt(T.literal(3), T.literal(2)), T.literal(true))).toBe(true);
|
|
53
|
+
expect(typeValueEquals(Ops.lt(T.literal(1), T.literal(2)), T.literal(true))).toBe(true);
|
|
54
|
+
expect(typeValueEquals(Ops.gte(T.literal(2), T.literal(2)), T.literal(true))).toBe(true);
|
|
55
|
+
expect(typeValueEquals(Ops.lte(T.literal(3), T.literal(2)), T.literal(false))).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns boolean for abstract operands", () => {
|
|
59
|
+
expect(typeValueEquals(Ops.gt(T.number, T.number), T.boolean)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("Ops.typeof_", () => {
|
|
64
|
+
it("returns literal typeof for known types", () => {
|
|
65
|
+
expect(typeValueEquals(Ops.typeof_(T.literal(1)), T.literal("number"))).toBe(true);
|
|
66
|
+
expect(typeValueEquals(Ops.typeof_(T.literal("hi")), T.literal("string"))).toBe(true);
|
|
67
|
+
expect(typeValueEquals(Ops.typeof_(T.literal(null)), T.literal("object"))).toBe(true);
|
|
68
|
+
expect(typeValueEquals(Ops.typeof_(T.number), T.literal("number"))).toBe(true);
|
|
69
|
+
expect(typeValueEquals(Ops.typeof_(T.object({})), T.literal("object"))).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("Ops.not", () => {
|
|
74
|
+
it("negates literal booleans", () => {
|
|
75
|
+
expect(typeValueEquals(Ops.not(T.literal(true)), T.literal(false))).toBe(true);
|
|
76
|
+
expect(typeValueEquals(Ops.not(T.literal(false)), T.literal(true))).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("negates truthy/falsy literals", () => {
|
|
80
|
+
expect(typeValueEquals(Ops.not(T.literal(0)), T.literal(true))).toBe(true);
|
|
81
|
+
expect(typeValueEquals(Ops.not(T.literal("")), T.literal(true))).toBe(true);
|
|
82
|
+
expect(typeValueEquals(Ops.not(T.literal(1)), T.literal(false))).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns boolean for abstract types", () => {
|
|
86
|
+
expect(typeValueEquals(Ops.not(T.number), T.boolean)).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("Ops.neg", () => {
|
|
91
|
+
it("negates literal number", () => {
|
|
92
|
+
expect(typeValueEquals(Ops.neg(T.literal(5)), T.literal(-5))).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns number for abstract", () => {
|
|
96
|
+
expect(typeValueEquals(Ops.neg(T.number), T.number)).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("applyBinaryOp", () => {
|
|
101
|
+
it("dispatches to correct op", () => {
|
|
102
|
+
expect(typeValueEquals(applyBinaryOp("+", T.literal(1), T.literal(2)), T.literal(3))).toBe(true);
|
|
103
|
+
expect(typeValueEquals(applyBinaryOp("===", T.literal(1), T.literal(1)), T.literal(true))).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns unknown for unsupported op", () => {
|
|
107
|
+
expect(applyBinaryOp("**", T.number, T.number)).toEqual(T.unknown);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
T,
|
|
4
|
+
isSubtypeOf,
|
|
5
|
+
deepCloneTypeValue,
|
|
6
|
+
mergeObjectProperties,
|
|
7
|
+
typeValueEquals,
|
|
8
|
+
} from "../type-value.ts";
|
|
9
|
+
|
|
10
|
+
describe("isSubtypeOf: structural subtyping", () => {
|
|
11
|
+
it("object subtype: a has all properties of b with subtypes", () => {
|
|
12
|
+
const a = T.object({ x: T.literal(1), y: T.string, z: T.boolean });
|
|
13
|
+
const b = T.object({ x: T.number, y: T.string });
|
|
14
|
+
expect(isSubtypeOf(a, b)).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("object not subtype: missing property", () => {
|
|
18
|
+
const a = T.object({ x: T.number });
|
|
19
|
+
const b = T.object({ x: T.number, y: T.string });
|
|
20
|
+
expect(isSubtypeOf(a, b)).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("object not subtype: property type mismatch", () => {
|
|
24
|
+
const a = T.object({ x: T.string });
|
|
25
|
+
const b = T.object({ x: T.number });
|
|
26
|
+
expect(isSubtypeOf(a, b)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("array subtype: element is subtype", () => {
|
|
30
|
+
const a = T.array(T.literal(1));
|
|
31
|
+
const b = T.array(T.number);
|
|
32
|
+
expect(isSubtypeOf(a, b)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("array not subtype: element mismatch", () => {
|
|
36
|
+
const a = T.array(T.string);
|
|
37
|
+
const b = T.array(T.number);
|
|
38
|
+
expect(isSubtypeOf(a, b)).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("tuple subtype: element-wise", () => {
|
|
42
|
+
const a = T.tuple([T.literal(1), T.literal("hi")]);
|
|
43
|
+
const b = T.tuple([T.number, T.string]);
|
|
44
|
+
expect(isSubtypeOf(a, b)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("tuple not subtype: length mismatch", () => {
|
|
48
|
+
const a = T.tuple([T.number]);
|
|
49
|
+
const b = T.tuple([T.number, T.string]);
|
|
50
|
+
expect(isSubtypeOf(a, b)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("tuple is subtype of array if all elements match", () => {
|
|
54
|
+
const a = T.tuple([T.literal(1), T.literal(2)]);
|
|
55
|
+
const b = T.array(T.number);
|
|
56
|
+
expect(isSubtypeOf(a, b)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("tuple not subtype of array if element mismatch", () => {
|
|
60
|
+
const a = T.tuple([T.literal(1), T.literal("hi")]);
|
|
61
|
+
const b = T.array(T.number);
|
|
62
|
+
expect(isSubtypeOf(a, b)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("deepCloneTypeValue", () => {
|
|
67
|
+
it("clones object with new id", () => {
|
|
68
|
+
const obj = T.object({ x: T.number, y: T.string });
|
|
69
|
+
const cloned = deepCloneTypeValue(obj);
|
|
70
|
+
expect(cloned.kind).toBe("object");
|
|
71
|
+
if (cloned.kind === "object" && obj.kind === "object") {
|
|
72
|
+
expect(cloned.id).not.toBe(obj.id);
|
|
73
|
+
expect(cloned.properties.x).toEqual(obj.properties.x);
|
|
74
|
+
expect(cloned.properties.y).toEqual(obj.properties.y);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("clones nested objects", () => {
|
|
79
|
+
const inner = T.object({ a: T.literal(1) });
|
|
80
|
+
const outer = T.object({ nested: inner });
|
|
81
|
+
const cloned = deepCloneTypeValue(outer);
|
|
82
|
+
if (cloned.kind === "object" && outer.kind === "object") {
|
|
83
|
+
const clonedInner = cloned.properties.nested;
|
|
84
|
+
expect(clonedInner.kind).toBe("object");
|
|
85
|
+
if (clonedInner.kind === "object" && inner.kind === "object") {
|
|
86
|
+
expect(clonedInner.id).not.toBe(inner.id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("returns primitives unchanged", () => {
|
|
92
|
+
expect(deepCloneTypeValue(T.number)).toBe(T.number);
|
|
93
|
+
expect(typeValueEquals(deepCloneTypeValue(T.literal(1)), T.literal(1))).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("mergeObjectProperties", () => {
|
|
98
|
+
it("merges two objects with overlapping keys", () => {
|
|
99
|
+
const a = T.object({ x: T.literal(1), y: T.literal("a") }) as any;
|
|
100
|
+
const b = T.object({ x: T.literal(2), z: T.boolean }) as any;
|
|
101
|
+
const merged = mergeObjectProperties(a, b);
|
|
102
|
+
if (merged.kind === "object") {
|
|
103
|
+
expect(Object.keys(merged.properties).sort()).toEqual(["x", "y", "z"]);
|
|
104
|
+
expect(merged.properties.y).toEqual(T.literal("a"));
|
|
105
|
+
expect(merged.properties.z).toEqual(T.boolean);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("creates union for overlapping keys with different values", () => {
|
|
110
|
+
const a = T.object({ x: T.literal(1) }) as any;
|
|
111
|
+
const b = T.object({ x: T.literal(2) }) as any;
|
|
112
|
+
const merged = mergeObjectProperties(a, b);
|
|
113
|
+
if (merged.kind === "object") {
|
|
114
|
+
expect(merged.properties.x.kind).toBe("union");
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
T,
|
|
4
|
+
typeValueToString,
|
|
5
|
+
typeValueEquals,
|
|
6
|
+
isSubtypeOf,
|
|
7
|
+
getPrimitiveTypeOf,
|
|
8
|
+
deepCloneTypeValue,
|
|
9
|
+
} from "../type-value.ts";
|
|
10
|
+
|
|
11
|
+
describe("PromiseType", () => {
|
|
12
|
+
it("creates a promise type", () => {
|
|
13
|
+
const p = T.promise(T.number);
|
|
14
|
+
expect(p.kind).toBe("promise");
|
|
15
|
+
if (p.kind === "promise") {
|
|
16
|
+
expect(p.value).toBe(T.number);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("toString formats correctly", () => {
|
|
21
|
+
expect(typeValueToString(T.promise(T.number))).toBe("Promise<number>");
|
|
22
|
+
expect(typeValueToString(T.promise(T.string))).toBe("Promise<string>");
|
|
23
|
+
expect(typeValueToString(T.promise(T.union(T.number, T.string)))).toBe(
|
|
24
|
+
"Promise<number | string>",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("equals works", () => {
|
|
29
|
+
expect(typeValueEquals(T.promise(T.number), T.promise(T.number))).toBe(true);
|
|
30
|
+
expect(typeValueEquals(T.promise(T.number), T.promise(T.string))).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("isSubtypeOf works", () => {
|
|
34
|
+
expect(isSubtypeOf(T.promise(T.literal(1)), T.promise(T.number))).toBe(true);
|
|
35
|
+
expect(isSubtypeOf(T.promise(T.number), T.promise(T.string))).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("getPrimitiveTypeOf returns object", () => {
|
|
39
|
+
expect(getPrimitiveTypeOf(T.promise(T.number))).toBe("object");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("deepClone works", () => {
|
|
43
|
+
const p = T.promise(T.object({ x: T.number }));
|
|
44
|
+
const cloned = deepCloneTypeValue(p);
|
|
45
|
+
expect(typeValueEquals(cloned, p)).toBe(false);
|
|
46
|
+
expect(cloned.kind).toBe("promise");
|
|
47
|
+
if (cloned.kind === "promise" && cloned.value.kind === "object") {
|
|
48
|
+
expect(cloned.value.properties.x).toBe(T.number);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("InstanceType", () => {
|
|
54
|
+
it("creates an instance type", () => {
|
|
55
|
+
const inst = T.instanceOf("Error", { message: T.literal("oops") });
|
|
56
|
+
expect(inst.kind).toBe("instance");
|
|
57
|
+
if (inst.kind === "instance") {
|
|
58
|
+
expect(inst.className).toBe("Error");
|
|
59
|
+
expect(inst.properties.message).toEqual(T.literal("oops"));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("toString formats correctly", () => {
|
|
64
|
+
expect(typeValueToString(T.instanceOf("Error"))).toBe("Error");
|
|
65
|
+
expect(typeValueToString(T.instanceOf("Error", { message: T.literal("oops") }))).toBe(
|
|
66
|
+
'Error { message: "oops" }',
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("equals checks className", () => {
|
|
71
|
+
expect(typeValueEquals(T.instanceOf("Error"), T.instanceOf("Error"))).toBe(true);
|
|
72
|
+
expect(typeValueEquals(T.instanceOf("Error"), T.instanceOf("TypeError"))).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("isSubtypeOf handles error hierarchy", () => {
|
|
76
|
+
expect(isSubtypeOf(T.instanceOf("TypeError"), T.instanceOf("Error"))).toBe(true);
|
|
77
|
+
expect(isSubtypeOf(T.instanceOf("SyntaxError"), T.instanceOf("Error"))).toBe(true);
|
|
78
|
+
expect(isSubtypeOf(T.instanceOf("Error"), T.instanceOf("TypeError"))).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("getPrimitiveTypeOf returns object", () => {
|
|
82
|
+
expect(getPrimitiveTypeOf(T.instanceOf("Error"))).toBe("object");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("deepClone works", () => {
|
|
86
|
+
const inst = T.instanceOf("Error", { message: T.string });
|
|
87
|
+
const cloned = deepCloneTypeValue(inst);
|
|
88
|
+
expect(cloned.kind).toBe("instance");
|
|
89
|
+
if (cloned.kind === "instance") {
|
|
90
|
+
expect(cloned.className).toBe("Error");
|
|
91
|
+
expect(cloned.properties.message).toBe(T.string);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
T,
|
|
4
|
+
typeValueEquals,
|
|
5
|
+
simplifyUnion,
|
|
6
|
+
widenLiteral,
|
|
7
|
+
isSubtypeOf,
|
|
8
|
+
typeValueToString,
|
|
9
|
+
narrowType,
|
|
10
|
+
subtractType,
|
|
11
|
+
getPrimitiveTypeOf,
|
|
12
|
+
} from "../type-value.ts";
|
|
13
|
+
|
|
14
|
+
describe("T factory", () => {
|
|
15
|
+
it("creates literal type values", () => {
|
|
16
|
+
expect(T.literal(1)).toEqual({ kind: "literal", value: 1 });
|
|
17
|
+
expect(T.literal("hello")).toEqual({ kind: "literal", value: "hello" });
|
|
18
|
+
expect(T.literal(true)).toEqual({ kind: "literal", value: true });
|
|
19
|
+
expect(T.literal(null)).toEqual({ kind: "literal", value: null });
|
|
20
|
+
expect(T.literal(undefined)).toEqual({ kind: "literal", value: undefined });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("provides primitive singletons", () => {
|
|
24
|
+
expect(T.number).toEqual({ kind: "primitive", type: "number" });
|
|
25
|
+
expect(T.string).toEqual({ kind: "primitive", type: "string" });
|
|
26
|
+
expect(T.boolean).toEqual({ kind: "primitive", type: "boolean" });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("creates object type values", () => {
|
|
30
|
+
const obj = T.object({ x: T.number, y: T.string });
|
|
31
|
+
expect(obj.kind).toBe("object");
|
|
32
|
+
if (obj.kind === "object") {
|
|
33
|
+
expect(obj.properties.x).toBe(T.number);
|
|
34
|
+
expect(obj.properties.y).toBe(T.string);
|
|
35
|
+
expect(typeof obj.id).toBe("symbol");
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("creates union type values with simplification", () => {
|
|
40
|
+
const u = T.union(T.literal(1), T.literal(2));
|
|
41
|
+
expect(u.kind).toBe("union");
|
|
42
|
+
if (u.kind === "union") {
|
|
43
|
+
expect(u.members).toHaveLength(2);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("simplifies single-member union to the member", () => {
|
|
48
|
+
const u = T.union(T.number);
|
|
49
|
+
expect(u).toBe(T.number);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("simplifies empty union to never", () => {
|
|
53
|
+
const u = T.union(T.never, T.never);
|
|
54
|
+
expect(u).toEqual(T.never);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("typeValueEquals", () => {
|
|
59
|
+
it("compares literals", () => {
|
|
60
|
+
expect(typeValueEquals(T.literal(1), T.literal(1))).toBe(true);
|
|
61
|
+
expect(typeValueEquals(T.literal(1), T.literal(2))).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("compares primitives", () => {
|
|
65
|
+
expect(typeValueEquals(T.number, T.number)).toBe(true);
|
|
66
|
+
expect(typeValueEquals(T.number, T.string)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("compares never and unknown", () => {
|
|
70
|
+
expect(typeValueEquals(T.never, T.never)).toBe(true);
|
|
71
|
+
expect(typeValueEquals(T.unknown, T.unknown)).toBe(true);
|
|
72
|
+
expect(typeValueEquals(T.never, T.unknown)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("simplifyUnion", () => {
|
|
77
|
+
it("deduplicates members", () => {
|
|
78
|
+
const result = simplifyUnion([T.literal(1), T.literal(1), T.literal(2)]);
|
|
79
|
+
expect(result.kind).toBe("union");
|
|
80
|
+
if (result.kind === "union") {
|
|
81
|
+
expect(result.members).toHaveLength(2);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("flattens nested unions", () => {
|
|
86
|
+
const inner = T.union(T.literal(1), T.literal(2));
|
|
87
|
+
const result = simplifyUnion([inner, T.literal(3)]);
|
|
88
|
+
expect(result.kind).toBe("union");
|
|
89
|
+
if (result.kind === "union") {
|
|
90
|
+
expect(result.members).toHaveLength(3);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("absorbs unknown", () => {
|
|
95
|
+
const result = simplifyUnion([T.number, T.unknown]);
|
|
96
|
+
expect(result).toEqual(T.unknown);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("removes never", () => {
|
|
100
|
+
const result = simplifyUnion([T.number, T.never]);
|
|
101
|
+
expect(result).toBe(T.number);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("widenLiteral", () => {
|
|
106
|
+
it("widens number literal to number", () => {
|
|
107
|
+
expect(widenLiteral(T.literal(42))).toEqual(T.number);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("widens string literal to string", () => {
|
|
111
|
+
expect(widenLiteral(T.literal("hi"))).toEqual(T.string);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("widens boolean literal to boolean", () => {
|
|
115
|
+
expect(widenLiteral(T.literal(true))).toEqual(T.boolean);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("returns non-literal unchanged", () => {
|
|
119
|
+
expect(widenLiteral(T.number)).toBe(T.number);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("isSubtypeOf", () => {
|
|
124
|
+
it("everything is subtype of unknown", () => {
|
|
125
|
+
expect(isSubtypeOf(T.number, T.unknown)).toBe(true);
|
|
126
|
+
expect(isSubtypeOf(T.literal(1), T.unknown)).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("never is subtype of everything", () => {
|
|
130
|
+
expect(isSubtypeOf(T.never, T.number)).toBe(true);
|
|
131
|
+
expect(isSubtypeOf(T.never, T.never)).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("literal is subtype of its primitive", () => {
|
|
135
|
+
expect(isSubtypeOf(T.literal(1), T.number)).toBe(true);
|
|
136
|
+
expect(isSubtypeOf(T.literal("hi"), T.string)).toBe(true);
|
|
137
|
+
expect(isSubtypeOf(T.literal(true), T.boolean)).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("literal is not subtype of wrong primitive", () => {
|
|
141
|
+
expect(isSubtypeOf(T.literal(1), T.string)).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("union is subtype if all members are", () => {
|
|
145
|
+
const u = T.union(T.literal(1), T.literal(2));
|
|
146
|
+
expect(isSubtypeOf(u, T.number)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("value is subtype of union containing it", () => {
|
|
150
|
+
const u = T.union(T.number, T.string);
|
|
151
|
+
expect(isSubtypeOf(T.number, u)).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("typeValueToString", () => {
|
|
156
|
+
it("formats literals", () => {
|
|
157
|
+
expect(typeValueToString(T.literal(1))).toBe("1");
|
|
158
|
+
expect(typeValueToString(T.literal("hi"))).toBe('"hi"');
|
|
159
|
+
expect(typeValueToString(T.literal(true))).toBe("true");
|
|
160
|
+
expect(typeValueToString(T.literal(null))).toBe("null");
|
|
161
|
+
expect(typeValueToString(T.literal(undefined))).toBe("undefined");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("formats primitives", () => {
|
|
165
|
+
expect(typeValueToString(T.number)).toBe("number");
|
|
166
|
+
expect(typeValueToString(T.string)).toBe("string");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("formats unions", () => {
|
|
170
|
+
const u = T.union(T.number, T.string);
|
|
171
|
+
expect(typeValueToString(u)).toBe("number | string");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("formats objects", () => {
|
|
175
|
+
const obj = T.object({ x: T.number });
|
|
176
|
+
expect(typeValueToString(obj)).toBe("{ x: number }");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("formats never and unknown", () => {
|
|
180
|
+
expect(typeValueToString(T.never)).toBe("never");
|
|
181
|
+
expect(typeValueToString(T.unknown)).toBe("unknown");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("narrowType", () => {
|
|
186
|
+
it("narrows union by predicate", () => {
|
|
187
|
+
const u = T.union(T.literal(1), T.literal("a"), T.literal(true));
|
|
188
|
+
const result = narrowType(u, (m) => m.kind === "literal" && typeof m.value === "number");
|
|
189
|
+
expect(typeValueEquals(result, T.literal(1))).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("returns never if nothing matches", () => {
|
|
193
|
+
const result = narrowType(T.number, (m) => m.kind === "literal");
|
|
194
|
+
expect(result).toEqual(T.never);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("getPrimitiveTypeOf", () => {
|
|
199
|
+
it("returns typeof for literals", () => {
|
|
200
|
+
expect(getPrimitiveTypeOf(T.literal(1))).toBe("number");
|
|
201
|
+
expect(getPrimitiveTypeOf(T.literal("hi"))).toBe("string");
|
|
202
|
+
expect(getPrimitiveTypeOf(T.literal(null))).toBe("object");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("returns type for primitives", () => {
|
|
206
|
+
expect(getPrimitiveTypeOf(T.number)).toBe("number");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("returns object for object/array/tuple", () => {
|
|
210
|
+
expect(getPrimitiveTypeOf(T.object({}))).toBe("object");
|
|
211
|
+
expect(getPrimitiveTypeOf(T.array(T.number))).toBe("object");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type TypeValue, T } from "./type-value.ts";
|
|
2
|
+
|
|
3
|
+
export type Environment = {
|
|
4
|
+
lookup(name: string): TypeValue;
|
|
5
|
+
bind(name: string, value: TypeValue): Environment;
|
|
6
|
+
update(name: string, value: TypeValue): boolean;
|
|
7
|
+
extend(bindings: Record<string, TypeValue>): Environment;
|
|
8
|
+
has(name: string): boolean;
|
|
9
|
+
snapshot(): Environment;
|
|
10
|
+
getOwnBindings(): Record<string, TypeValue>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function createEnvironment(
|
|
14
|
+
parent?: Environment,
|
|
15
|
+
bindings: Map<string, TypeValue> = new Map(),
|
|
16
|
+
): Environment {
|
|
17
|
+
const store = new Map(bindings);
|
|
18
|
+
|
|
19
|
+
const env: Environment = {
|
|
20
|
+
lookup(name) {
|
|
21
|
+
const val = store.get(name);
|
|
22
|
+
if (val !== undefined) return val;
|
|
23
|
+
if (parent) return parent.lookup(name);
|
|
24
|
+
return T.undefined;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
bind(name, value) {
|
|
28
|
+
store.set(name, value);
|
|
29
|
+
return env;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
update(name, value) {
|
|
33
|
+
if (store.has(name)) {
|
|
34
|
+
store.set(name, value);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (parent) return parent.update(name, value);
|
|
38
|
+
return false;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
extend(newBindings) {
|
|
42
|
+
const childMap = new Map<string, TypeValue>();
|
|
43
|
+
for (const [k, v] of Object.entries(newBindings)) {
|
|
44
|
+
childMap.set(k, v);
|
|
45
|
+
}
|
|
46
|
+
return createEnvironment(env, childMap);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
has(name) {
|
|
50
|
+
return store.has(name) || (parent?.has(name) ?? false);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
snapshot() {
|
|
54
|
+
const clonedStore = new Map(store);
|
|
55
|
+
const clonedParent = parent?.snapshot();
|
|
56
|
+
return createEnvironment(clonedParent, clonedStore);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
getOwnBindings() {
|
|
60
|
+
const result: Record<string, TypeValue> = {};
|
|
61
|
+
for (const [k, v] of store) {
|
|
62
|
+
result[k] = v;
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return env;
|
|
69
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type TypeValue,
|
|
3
|
+
type LiteralValue,
|
|
4
|
+
T,
|
|
5
|
+
typeValueEquals,
|
|
6
|
+
simplifyUnion,
|
|
7
|
+
widenLiteral,
|
|
8
|
+
isSubtypeOf,
|
|
9
|
+
typeValueToString,
|
|
10
|
+
narrowType,
|
|
11
|
+
subtractType,
|
|
12
|
+
getPrimitiveTypeOf,
|
|
13
|
+
deepCloneTypeValue,
|
|
14
|
+
mergeObjectProperties,
|
|
15
|
+
} from "./type-value.ts";
|
|
16
|
+
|
|
17
|
+
export { Ops, applyBinaryOp } from "./ops.ts";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
type Environment,
|
|
21
|
+
createEnvironment,
|
|
22
|
+
} from "./environment.ts";
|