@venn-lang/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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +303 -0
  3. package/dist/index.d.ts +3654 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +13137 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +61 -0
  8. package/src/ast/call-args.ts +15 -0
  9. package/src/ast/dotted-path.ts +22 -0
  10. package/src/ast/index.ts +7 -0
  11. package/src/ast/is-statement.ts +40 -0
  12. package/src/ast/split-call.ts +28 -0
  13. package/src/ast/walk.ts +6 -0
  14. package/src/codes/build-problem.ts +24 -0
  15. package/src/codes/catalog.ts +35 -0
  16. package/src/codes/code.types.ts +7 -0
  17. package/src/codes/index.ts +3 -0
  18. package/src/compile/compile.ts +169 -0
  19. package/src/compile/compile.types.ts +40 -0
  20. package/src/compile/index.ts +3 -0
  21. package/src/compile/lex-scope.ts +78 -0
  22. package/src/compile/nodes/binary.ts +56 -0
  23. package/src/compile/nodes/call.ts +50 -0
  24. package/src/compile/nodes/collection.ts +83 -0
  25. package/src/compile/nodes/const-lit.ts +30 -0
  26. package/src/compile/nodes/fast-binary.ts +71 -0
  27. package/src/compile/nodes/fn.ts +87 -0
  28. package/src/compile/nodes/index.ts +8 -0
  29. package/src/compile/nodes/literal.ts +86 -0
  30. package/src/compile/nodes/map-fields.ts +79 -0
  31. package/src/compile/nodes/member.ts +32 -0
  32. package/src/events/envelope.types.ts +17 -0
  33. package/src/events/event-data.types.ts +25 -0
  34. package/src/events/ids.types.ts +10 -0
  35. package/src/events/index.ts +5 -0
  36. package/src/events/run-plan.types.ts +18 -0
  37. package/src/events/status.types.ts +2 -0
  38. package/src/expand/accepted-kinds.ts +23 -0
  39. package/src/expand/deco/deco-decorator.ts +77 -0
  40. package/src/expand/deco/deco-env.ts +24 -0
  41. package/src/expand/deco/deco.types.ts +43 -0
  42. package/src/expand/deco/document-decos.ts +64 -0
  43. package/src/expand/deco/index.ts +7 -0
  44. package/src/expand/deco/read-signature.ts +57 -0
  45. package/src/expand/deco/run-body.ts +119 -0
  46. package/src/expand/decorate-callable.ts +49 -0
  47. package/src/expand/decorations.ts +41 -0
  48. package/src/expand/expand.ts +108 -0
  49. package/src/expand/expand.types.ts +78 -0
  50. package/src/expand/handles/around-verbs.ts +35 -0
  51. package/src/expand/handles/binding-verbs.ts +27 -0
  52. package/src/expand/handles/common-verbs.ts +40 -0
  53. package/src/expand/handles/fn-verbs.ts +58 -0
  54. package/src/expand/handles/handle.types.ts +33 -0
  55. package/src/expand/handles/index.ts +4 -0
  56. package/src/expand/handles/kind-of.ts +36 -0
  57. package/src/expand/handles/make-handle.ts +100 -0
  58. package/src/expand/handles/missing-verb.ts +27 -0
  59. package/src/expand/handles/runnable-verbs.ts +13 -0
  60. package/src/expand/handles/type-verbs.ts +56 -0
  61. package/src/expand/index.ts +40 -0
  62. package/src/expand/make-context.ts +69 -0
  63. package/src/expand/node-meta.ts +30 -0
  64. package/src/expand/node-span.ts +17 -0
  65. package/src/expand/swap-node.ts +28 -0
  66. package/src/expand/wrong-kind.ts +102 -0
  67. package/src/expr/cell.types.ts +25 -0
  68. package/src/expr/closure.ts +53 -0
  69. package/src/expr/closure.types.ts +31 -0
  70. package/src/expr/eval-env.types.ts +9 -0
  71. package/src/expr/evaluate.ts +18 -0
  72. package/src/expr/frame.ts +60 -0
  73. package/src/expr/index.ts +14 -0
  74. package/src/expr/invoke.ts +140 -0
  75. package/src/expr/member-value.ts +91 -0
  76. package/src/expr/methods/index.ts +65 -0
  77. package/src/expr/methods/list-grouping.ts +107 -0
  78. package/src/expr/methods/list-methods.ts +57 -0
  79. package/src/expr/methods/list-selection.ts +142 -0
  80. package/src/expr/methods/map-extras.ts +87 -0
  81. package/src/expr/methods/map-methods.ts +17 -0
  82. package/src/expr/methods/number-methods.ts +29 -0
  83. package/src/expr/methods/string-extras.ts +66 -0
  84. package/src/expr/methods/string-methods.ts +32 -0
  85. package/src/expr/methods/unit-methods.ts +54 -0
  86. package/src/expr/namespace.ts +20 -0
  87. package/src/expr/native.types.ts +40 -0
  88. package/src/expr/operators.ts +91 -0
  89. package/src/expr/pending.ts +47 -0
  90. package/src/expr/prelude.ts +75 -0
  91. package/src/expr/task.ts +71 -0
  92. package/src/format/format-text.ts +22 -0
  93. package/src/format/format.types.ts +18 -0
  94. package/src/format/from-settings.ts +40 -0
  95. package/src/format/index.ts +5 -0
  96. package/src/format/organize-header.ts +66 -0
  97. package/src/format/reindent.ts +35 -0
  98. package/src/generated/ast.ts +2383 -0
  99. package/src/generated/grammar.ts +5236 -0
  100. package/src/generated/module.ts +25 -0
  101. package/src/grammar/venn.langium +291 -0
  102. package/src/graph/graph.types.ts +20 -0
  103. package/src/graph/index.ts +2 -0
  104. package/src/graph/to-graph.ts +87 -0
  105. package/src/index.ts +93 -0
  106. package/src/interpolation/compile-template.ts +40 -0
  107. package/src/interpolation/index.ts +4 -0
  108. package/src/interpolation/interpolation.types.ts +18 -0
  109. package/src/interpolation/scan-interpolations.ts +47 -0
  110. package/src/interpolation/template.types.ts +19 -0
  111. package/src/lang/default-services.ts +14 -0
  112. package/src/lang/index.ts +3 -0
  113. package/src/lang/venn-lexer.ts +34 -0
  114. package/src/lang/venn-module.ts +51 -0
  115. package/src/module/index.ts +5 -0
  116. package/src/module/specifier.ts +21 -0
  117. package/src/parse/error-to-problem.ts +43 -0
  118. package/src/parse/index.ts +3 -0
  119. package/src/parse/parse-expression.ts +27 -0
  120. package/src/parse/parse-output.types.ts +8 -0
  121. package/src/parse/parse.ts +26 -0
  122. package/src/problem/build-diff.ts +116 -0
  123. package/src/problem/diff.types.ts +20 -0
  124. package/src/problem/format-value.ts +40 -0
  125. package/src/problem/index.ts +8 -0
  126. package/src/problem/problem-error.ts +15 -0
  127. package/src/problem/problem.types.ts +21 -0
  128. package/src/problem/related.types.ts +7 -0
  129. package/src/problem/severity.types.ts +2 -0
  130. package/src/problem/span.types.ts +8 -0
  131. package/src/typecheck/action-signature.ts +53 -0
  132. package/src/typecheck/builtin-types.ts +63 -0
  133. package/src/typecheck/builtins.ts +267 -0
  134. package/src/typecheck/catalog.types.ts +16 -0
  135. package/src/typecheck/check-deco.ts +174 -0
  136. package/src/typecheck/check-stmts.ts +151 -0
  137. package/src/typecheck/check-types.ts +232 -0
  138. package/src/typecheck/context.ts +31 -0
  139. package/src/typecheck/imported-types.ts +127 -0
  140. package/src/typecheck/index.ts +26 -0
  141. package/src/typecheck/infer.ts +416 -0
  142. package/src/typecheck/kind-types.ts +79 -0
  143. package/src/typecheck/member-docs.ts +211 -0
  144. package/src/typecheck/named-types.ts +57 -0
  145. package/src/typecheck/prelude-types.ts +152 -0
  146. package/src/typecheck/reshaped-fns.ts +58 -0
  147. package/src/typecheck/scheme.ts +75 -0
  148. package/src/typecheck/seed-params.ts +72 -0
  149. package/src/typecheck/seed-values.ts +64 -0
  150. package/src/typecheck/show.ts +64 -0
  151. package/src/typecheck/solidify.ts +60 -0
  152. package/src/typecheck/spec-to-type.ts +88 -0
  153. package/src/typecheck/type-env.ts +46 -0
  154. package/src/typecheck/type-ref.ts +62 -0
  155. package/src/typecheck/type.types.ts +168 -0
  156. package/src/typecheck/unify.ts +192 -0
  157. package/src/typecheck/unit-types.ts +49 -0
  158. package/src/units/combine.ts +97 -0
  159. package/src/units/index.ts +6 -0
  160. package/src/units/parse-instant.ts +12 -0
  161. package/src/units/parse-number.ts +48 -0
  162. package/src/units/unit-guards.ts +16 -0
  163. package/src/units/unit.types.ts +38 -0
  164. package/src/value/equals.ts +4 -0
  165. package/src/value/index.ts +4 -0
  166. package/src/value/is-numeric.ts +6 -0
  167. package/src/value/truthiness.ts +9 -0
  168. package/src/value/value.types.ts +12 -0
@@ -0,0 +1,192 @@
1
+ import type {
2
+ FnType,
3
+ ListType,
4
+ LiteralType,
5
+ RecordType,
6
+ Type,
7
+ TypeVar,
8
+ UnionType,
9
+ } from "./type.types.js";
10
+ import { baseOf, DYNAMIC } from "./type.types.js";
11
+
12
+ /** Follow solved variables to the type they stand for. */
13
+ export function prune(type: Type): Type {
14
+ if (type.kind === "var" && type.ref) {
15
+ const resolved = prune(type.ref);
16
+ type.ref = resolved;
17
+ return resolved;
18
+ }
19
+ return type;
20
+ }
21
+
22
+ /**
23
+ * Make two types equal, solving variables along the way.
24
+ *
25
+ * `dynamic` unifies with anything without constraining it. That is the escape
26
+ * hatch keeping the effectful world from forcing annotations.
27
+ *
28
+ * @returns true when they were made equal. Variables solved before a later step
29
+ * failed stay solved, so a false answer is not a rollback.
30
+ */
31
+ export function unify(left: Type, right: Type): boolean {
32
+ const a = prune(left);
33
+ const b = prune(right);
34
+ if (a === b) return true;
35
+ if (a.kind === "dynamic" || b.kind === "dynamic") return true;
36
+ if (a.kind === "var") return bind(a, b);
37
+ if (b.kind === "var") return bind(b, a);
38
+ if (a.kind === "union" || b.kind === "union") return unifyUnion(a, b);
39
+ if (a.kind === "literal" || b.kind === "literal") return unifyLiteral(a, b);
40
+ if (a.kind === "opaque" && b.kind === "opaque") return a.name === b.name;
41
+ if (a.kind === "prim" && b.kind === "prim") return a.name === b.name;
42
+ if (a.kind === "list" && b.kind === "list") return unify(a.element, b.element);
43
+ if (a.kind === "fn" && b.kind === "fn") return unifyFn(a, b);
44
+ if (a.kind === "record" && b.kind === "record") return unifyRecord(a, b);
45
+ return false;
46
+ }
47
+
48
+ /** `"GET"` is a string wherever a string is wanted, and itself where it is not. */
49
+ function unifyLiteral(a: Type, b: Type): boolean {
50
+ if (a.kind === "literal" && b.kind === "literal") return a.value === b.value;
51
+ const lit: LiteralType = a.kind === "literal" ? a : (b as LiteralType);
52
+ const other = a.kind === "literal" ? b : a;
53
+ return other.kind === "prim" && other.name === baseOf(lit.value);
54
+ }
55
+
56
+ /**
57
+ * A union is satisfied by any one of its members.
58
+ *
59
+ * Membership is decided by {@link matches}, which never binds a variable: trying
60
+ * a member and rolling back would leave half-solved variables behind, and a
61
+ * checker that guesses wrong is worse than one that stays quiet.
62
+ */
63
+ function unifyUnion(a: Type, b: Type): boolean {
64
+ const [set, other] = a.kind === "union" ? [a, b] : [b as UnionType, a];
65
+ if (other.kind === "union") {
66
+ return other.members.every((m) => set.members.some((option) => matches(option, m)));
67
+ }
68
+ return set.members.some((option) => matches(option, other));
69
+ }
70
+
71
+ /**
72
+ * Could these be the same, without deciding anything?
73
+ *
74
+ * Used to pick a union member. Anything unknown, a variable or `dynamic`, counts
75
+ * as compatible, so a union never turns an open question into an error.
76
+ */
77
+ function matches(left: Type, right: Type): boolean {
78
+ const a = prune(left);
79
+ const b = prune(right);
80
+ if (a.kind === "dynamic" || b.kind === "dynamic") return true;
81
+ if (a.kind === "var" || b.kind === "var") return true;
82
+ if (a.kind === "union") return a.members.some((m) => matches(m, b));
83
+ if (b.kind === "union") return b.members.some((m) => matches(a, m));
84
+ return matchesConcrete(a, b);
85
+ }
86
+
87
+ function matchesConcrete(a: Type, b: Type): boolean {
88
+ if (a.kind === "literal" || b.kind === "literal") return unifyLiteral(a, b);
89
+ if (a.kind === "list" && b.kind === "list") return matches(a.element, b.element);
90
+ if (a.kind === "opaque" && b.kind === "opaque") return a.name === b.name;
91
+ if (a.kind === "prim" && b.kind === "prim") return a.name === b.name;
92
+ // Shape is left to real unification; here it is enough that the family agrees.
93
+ return a.kind === b.kind && (a.kind === "record" || a.kind === "fn");
94
+ }
95
+
96
+ /** Bind a variable to a type, refusing the infinite type `t = list<t>`. */
97
+ function bind(variable: TypeVar, type: Type): boolean {
98
+ if (occurs(variable, type)) return false;
99
+ variable.ref = type;
100
+ return true;
101
+ }
102
+
103
+ function occurs(variable: TypeVar, type: Type): boolean {
104
+ const t = prune(type);
105
+ if (t === variable) return true;
106
+ if (t.kind === "list") return occurs(variable, t.element);
107
+ if (t.kind === "union") return t.members.some((m) => occurs(variable, m));
108
+ if (t.kind === "fn")
109
+ return t.params.some((p) => occurs(variable, p)) || occurs(variable, t.result);
110
+ if (t.kind === "record")
111
+ return (
112
+ [...t.fields.values()].some((f) => occurs(variable, f)) ||
113
+ (t.rest !== undefined && occurs(variable, t.rest))
114
+ );
115
+ return false;
116
+ }
117
+
118
+ function unifyFn(a: FnType, b: FnType): boolean {
119
+ // A variadic function accepts any argument list; only its result must agree.
120
+ if (a.variadic || b.variadic) return unify(a.result, b.result);
121
+ const shared = sharedArity(a, b);
122
+ if (shared === undefined) return false;
123
+ for (let at = 0; at < shared; at += 1) {
124
+ if (!unify(a.params[at] as Type, b.params[at] as Type)) return false;
125
+ }
126
+ return unify(a.result, b.result);
127
+ }
128
+
129
+ /**
130
+ * How many parameters the two sides must agree on. Normally all of them, but a
131
+ * callback offered more than it takes agrees on the ones it took, which is how
132
+ * `people.map(p => p.age)` type-checks against a `map` that also passes an index.
133
+ */
134
+ function sharedArity(a: FnType, b: FnType): number | undefined {
135
+ if (a.params.length === b.params.length) return a.params.length;
136
+ const [shorter, longer] = a.params.length < b.params.length ? [a, b] : [b, a];
137
+ const ignorable = longer.ignorableFrom;
138
+ if (ignorable === undefined || shorter.params.length < ignorable) return undefined;
139
+ return shorter.params.length;
140
+ }
141
+
142
+ /**
143
+ * Records unify field by field. An open record (a map used loosely) tolerates
144
+ * fields the other side lacks; a closed one requires the same shape, except for
145
+ * fields that may be absent.
146
+ */
147
+ function unifyRecord(a: RecordType, b: RecordType): boolean {
148
+ const open = a.open || b.open;
149
+ if (a.rest && b.rest && !unify(a.rest, b.rest)) return false;
150
+ for (const [name, type] of a.fields) {
151
+ const other = b.fields.get(name);
152
+ if (other) {
153
+ if (!unify(type, other)) return false;
154
+ } else if (!open && !omittable(type)) {
155
+ return false;
156
+ }
157
+ }
158
+ return open || everyPresent(b, a);
159
+ }
160
+
161
+ /**
162
+ * May this field simply not be there?
163
+ *
164
+ * `nickname?: string` is read as `string | null`, a value that may be nothing.
165
+ * Absent and null are the same thing to a reader of the map, so a record that
166
+ * left it out still satisfies one that allows it.
167
+ */
168
+ function omittable(type: Type): boolean {
169
+ const t = prune(type);
170
+ if (t.kind === "prim") return t.name === "null";
171
+ return t.kind === "union" && t.members.some(omittable);
172
+ }
173
+
174
+ function everyPresent(from: RecordType, into: RecordType): boolean {
175
+ for (const [name, type] of from.fields) {
176
+ if (!into.fields.has(name) && !omittable(type)) return false;
177
+ }
178
+ return true;
179
+ }
180
+
181
+ /**
182
+ * A field's type on a record. A key nobody listed is whatever the record says
183
+ * its other keys hold, or `dynamic` when it says nothing, which is a loose map.
184
+ */
185
+ export function fieldType(record: RecordType, name: string): Type | undefined {
186
+ const known = record.fields.get(name);
187
+ if (known) return known;
188
+ if (!record.open) return undefined;
189
+ return record.rest ?? DYNAMIC;
190
+ }
191
+
192
+ export type { ListType };
@@ -0,0 +1,49 @@
1
+ import { combine, isUnitValue, type Numeric, parseNumber } from "../units/index.js";
2
+ import { BOOL, NUMBER, prim, type Type } from "./type.types.js";
3
+
4
+ /** One value of each kind, to ask `combine` what an operator does with them. */
5
+ const SAMPLES: Record<string, Numeric> = {
6
+ number: 1,
7
+ duration: { kind: "duration", ms: 1 },
8
+ size: { kind: "size", bytes: 1 },
9
+ percent: { kind: "percent", ratio: 1 },
10
+ };
11
+
12
+ /** The type a NUMBER lexeme has: `200` is a number, `300ms` a duration, `2mb` a size. */
13
+ export function literalType(raw: string): Type {
14
+ const value = parseNumber(raw);
15
+ return typeof value === "number" ? NUMBER : prim(value.kind);
16
+ }
17
+
18
+ /** What an operator produces, or that its operands cannot be combined. */
19
+ export type UnitOutcome = { ok: true; type: Type } | { ok: false };
20
+
21
+ /**
22
+ * The type `left op right` produces, decided by the very `combine` the evaluator
23
+ * uses, handed one value of each kind rather than the real ones.
24
+ *
25
+ * Reusing it is the point. Unit compatibility is a single rule (`300ms + 1s`
26
+ * yes, `300ms + 2mb` no), and writing it a second time for the checker would be
27
+ * two rules that agree only until one of them changes.
28
+ *
29
+ * @returns undefined when either side is not a concrete number or unit: a type
30
+ * variable still to be solved, `dynamic`, a string. Those keep the ordinary
31
+ * path, which is what lets `fn double(x) => x * 2` learn that `x` is a number.
32
+ */
33
+ export function combinedType(op: string, left: Type, right: Type): UnitOutcome | undefined {
34
+ const a = sampleOf(left);
35
+ const b = sampleOf(right);
36
+ if (a === undefined || b === undefined) return undefined;
37
+ const result = combine({ op, left: a, right: b });
38
+ return result.ok ? { ok: true, type: typeOfValue(result.value) } : { ok: false };
39
+ }
40
+
41
+ function sampleOf(type: Type): Numeric | undefined {
42
+ return type.kind === "prim" ? SAMPLES[type.name] : undefined;
43
+ }
44
+
45
+ function typeOfValue(value: Numeric | boolean): Type {
46
+ if (typeof value === "boolean") return BOOL;
47
+ if (isUnitValue(value)) return prim(value.kind);
48
+ return NUMBER;
49
+ }
@@ -0,0 +1,97 @@
1
+ import type { UnitKind, UnitValue } from "./unit.types.js";
2
+
3
+ /** Anything arithmetic accepts: a plain number, or a number carrying a unit. */
4
+ export type Numeric = number | UnitValue;
5
+
6
+ /** Why two operands would not combine: the operator, and the kind of each side. */
7
+ export type UnitMismatch = { op: string; left: string; right: string };
8
+
9
+ /** The outcome of {@link combine}: the value, or the mismatch behind VN3012. */
10
+ export type CombineResult =
11
+ | { ok: true; value: Numeric | boolean }
12
+ | { ok: false; mismatch: UnitMismatch };
13
+
14
+ type Norm = { kind: UnitKind | "scalar"; base: number };
15
+
16
+ const COMPARE = new Set(["<", "<=", ">", ">=", "==", "!="]);
17
+
18
+ /**
19
+ * Combine two numerics with an operator, checking that their units agree.
20
+ *
21
+ * `300ms + 1s` succeeds; `300ms + 2mb` reports a mismatch. Never throws: the
22
+ * caller decides whether a mismatch is a problem and where to point at it.
23
+ *
24
+ * @returns The combined value, or the mismatch that stopped it.
25
+ */
26
+ export function combine(args: { op: string; left: Numeric; right: Numeric }): CombineResult {
27
+ const left = norm(args.left);
28
+ const right = norm(args.right);
29
+ const value = COMPARE.has(args.op) ? compare(args.op, left, right) : arith(args.op, left, right);
30
+ if (value === null)
31
+ return { ok: false, mismatch: { op: args.op, left: left.kind, right: right.kind } };
32
+ return { ok: true, value };
33
+ }
34
+
35
+ /**
36
+ * Ordering needs a shared unit; equality does not.
37
+ *
38
+ * Asking whether a duration is *less than* a size is a question with no answer,
39
+ * so it reports a mismatch. Asking whether they are *equal* has an obvious one:
40
+ * they are not. Reporting a mismatch there made `a == b` able to fail, so no
41
+ * program could compare two values whose units it did not already know.
42
+ */
43
+ function compare(op: string, l: Norm, r: Norm): boolean | null {
44
+ if (l.kind === r.kind) return applyCompare(op, l.base, r.base);
45
+ if (op === "==") return false;
46
+ return op === "!=" ? true : null;
47
+ }
48
+
49
+ function arith(op: string, l: Norm, r: Norm): Numeric | null {
50
+ if (l.kind === "scalar" && r.kind === "scalar") return applyScalar(op, l.base, r.base);
51
+ if (op === "+" || op === "-")
52
+ return l.kind === r.kind ? fromBase(l.kind, applyScalar(op, l.base, r.base)) : null;
53
+ if (op === "*") return scale(l, r);
54
+ return divide(op, l, r);
55
+ }
56
+
57
+ function scale(l: Norm, r: Norm): Numeric | null {
58
+ if (l.kind === "scalar") return fromBase(r.kind, l.base * r.base);
59
+ if (r.kind === "scalar") return fromBase(l.kind, l.base * r.base);
60
+ return null;
61
+ }
62
+
63
+ function divide(op: string, l: Norm, r: Norm): Numeric | null {
64
+ if (op !== "/" && op !== "%") return null;
65
+ if (r.kind === "scalar") return fromBase(l.kind, applyScalar(op, l.base, r.base));
66
+ return l.kind === r.kind ? applyScalar(op, l.base, r.base) : null;
67
+ }
68
+
69
+ function norm(value: Numeric): Norm {
70
+ if (typeof value === "number") return { kind: "scalar", base: value };
71
+ if (value.kind === "duration") return { kind: "duration", base: value.ms };
72
+ if (value.kind === "size") return { kind: "size", base: value.bytes };
73
+ return { kind: "percent", base: value.ratio };
74
+ }
75
+
76
+ function fromBase(kind: UnitKind | "scalar", base: number): Numeric {
77
+ if (kind === "duration") return { kind, ms: base };
78
+ if (kind === "size") return { kind, bytes: base };
79
+ if (kind === "percent") return { kind, ratio: base };
80
+ return base;
81
+ }
82
+
83
+ function applyScalar(op: string, a: number, b: number): number {
84
+ if (op === "+") return a + b;
85
+ if (op === "-") return a - b;
86
+ if (op === "*") return a * b;
87
+ if (op === "/") return a / b;
88
+ return a % b;
89
+ }
90
+
91
+ function applyCompare(op: string, a: number, b: number): boolean {
92
+ if (op === "<") return a < b;
93
+ if (op === "<=") return a <= b;
94
+ if (op === ">") return a > b;
95
+ if (op === ">=") return a >= b;
96
+ return op === "==" ? a === b : a !== b;
97
+ }
@@ -0,0 +1,6 @@
1
+ export type { CombineResult, Numeric, UnitMismatch } from "./combine.js";
2
+ export { combine } from "./combine.js";
3
+ export { parseInstant } from "./parse-instant.js";
4
+ export { parseNumber } from "./parse-number.js";
5
+ export type { Duration, Instant, Percent, Size, UnitKind, UnitValue } from "./unit.types.js";
6
+ export { isInstant, isUnitValue } from "./unit-guards.js";
@@ -0,0 +1,12 @@
1
+ import type { Instant } from "./unit.types.js";
2
+
3
+ /**
4
+ * Read an ISO-8601 lexeme into an {@link Instant}, keeping the source text.
5
+ *
6
+ * A lexeme that does not parse takes epoch 0 rather than NaN, so comparing two
7
+ * instants stays total. The grammar has already accepted the shape by here.
8
+ */
9
+ export function parseInstant(iso: string): Instant {
10
+ const epochMs = Date.parse(iso);
11
+ return { kind: "instant", epochMs: Number.isNaN(epochMs) ? 0 : epochMs, iso };
12
+ }
@@ -0,0 +1,48 @@
1
+ import type { UnitValue } from "./unit.types.js";
2
+
3
+ const DURATION_MS: Record<string, number> = { ms: 1, s: 1000, m: 60000, h: 3600000 };
4
+ const SIZE_BYTES: Record<string, number> = { b: 1, kb: 1024, mb: 1048576, gb: 1073741824 };
5
+
6
+ /**
7
+ * Lexemes already read, keyed by their text.
8
+ *
9
+ * Callers pass `NumberLit.raw`, so this holds one entry per literal written in
10
+ * the loaded programs. Handing the same parsed value to every caller is safe:
11
+ * numbers are primitives and a `UnitValue` is readonly throughout, so `combine`
12
+ * builds new values rather than mutating.
13
+ */
14
+ const parsed = new Map<string, number | UnitValue>();
15
+
16
+ /**
17
+ * Read a NUMBER lexeme into the value it denotes.
18
+ *
19
+ * Accepts digit grouping and an optional unit suffix: "200", "1_000", "300ms",
20
+ * "2mb", "50%". A lexeme with no recognised suffix reads as a plain number.
21
+ *
22
+ * @returns A number, or the `UnitValue` the suffix asks for.
23
+ */
24
+ export function parseNumber(raw: string): number | UnitValue {
25
+ const known = parsed.get(raw);
26
+ if (known !== undefined) return known;
27
+ const value = readLexeme(raw);
28
+ parsed.set(raw, value);
29
+ return value;
30
+ }
31
+
32
+ function readLexeme(raw: string): number | UnitValue {
33
+ // `_` groups digits for the reader and means nothing to the value: the
34
+ // grammar only lets it sit between them, so dropping it is safe here.
35
+ const digits = raw.includes("_") ? raw.replaceAll("_", "") : raw;
36
+ const match = /^([0-9]+(?:\.[0-9]+)?)(ms|kb|mb|gb|b|s|m|h|%)?$/.exec(digits);
37
+ if (!match) return Number(digits);
38
+ const value = Number(match[1]);
39
+ return match[2] ? toUnitValue(value, match[2]) : value;
40
+ }
41
+
42
+ function toUnitValue(value: number, unit: string): UnitValue {
43
+ const ms = DURATION_MS[unit];
44
+ if (ms !== undefined) return { kind: "duration", ms: value * ms };
45
+ const bytes = SIZE_BYTES[unit];
46
+ if (bytes !== undefined) return { kind: "size", bytes: value * bytes };
47
+ return { kind: "percent", ratio: value / 100 };
48
+ }
@@ -0,0 +1,16 @@
1
+ import type { Instant, UnitValue } from "./unit.types.js";
2
+
3
+ /** Whether this value carries a unit: a duration, a size or a percent. */
4
+ export function isUnitValue(value: unknown): value is UnitValue {
5
+ return hasKind(value, ["duration", "size", "percent"]);
6
+ }
7
+
8
+ /** Whether this value is a point in time. */
9
+ export function isInstant(value: unknown): value is Instant {
10
+ return hasKind(value, ["instant"]);
11
+ }
12
+
13
+ function hasKind(value: unknown, kinds: readonly string[]): boolean {
14
+ if (typeof value !== "object" || value === null || !("kind" in value)) return false;
15
+ return kinds.includes((value as { kind: string }).kind);
16
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Unit-typed values.
3
+ *
4
+ * Each holds a canonical base unit (milliseconds, bytes, a ratio in 0..1, epoch
5
+ * milliseconds), so arithmetic never converts and only display does. The `kind`
6
+ * field is the brand that keeps a unit from reading as an ordinary map.
7
+ */
8
+
9
+ /** Which unit a value carries. */
10
+ export type UnitKind = "duration" | "size" | "percent";
11
+
12
+ /** A length of time, held in milliseconds. */
13
+ export interface Duration {
14
+ readonly kind: "duration";
15
+ readonly ms: number;
16
+ }
17
+
18
+ /** A quantity of data, held in bytes. */
19
+ export interface Size {
20
+ readonly kind: "size";
21
+ readonly bytes: number;
22
+ }
23
+
24
+ /** A proportion, held as a ratio in 0..1 rather than as 0..100. */
25
+ export interface Percent {
26
+ readonly kind: "percent";
27
+ readonly ratio: number;
28
+ }
29
+
30
+ /** A point in time. The source ISO text is kept so printing it back is exact. */
31
+ export interface Instant {
32
+ readonly kind: "instant";
33
+ readonly epochMs: number;
34
+ readonly iso: string;
35
+ }
36
+
37
+ /** Any value carrying a unit. */
38
+ export type UnitValue = Duration | Size | Percent;
@@ -0,0 +1,4 @@
1
+ /** Strict equality with no coercion: "99.00" (string) never equals 99 (number). */
2
+ export function strictEquals(left: unknown, right: unknown): boolean {
3
+ return left === right;
4
+ }
@@ -0,0 +1,4 @@
1
+ export { strictEquals } from "./equals.js";
2
+ export { isNumeric } from "./is-numeric.js";
3
+ export { truthy } from "./truthiness.js";
4
+ export type { Value } from "./value.types.js";
@@ -0,0 +1,6 @@
1
+ import { isUnitValue, type Numeric } from "../units/index.js";
2
+
3
+ /** True for plain numbers and unit values (duration/size/percent). */
4
+ export function isNumeric(value: unknown): value is Numeric {
5
+ return typeof value === "number" || isUnitValue(value);
6
+ }
@@ -0,0 +1,9 @@
1
+ import { isUnitValue } from "../units/index.js";
2
+
3
+ /** Boolean coercion for `!`, `&&`, `||`, and `if` conditions. */
4
+ export function truthy(value: unknown): boolean {
5
+ if (value === null || value === undefined || value === false) return false;
6
+ if (value === 0 || value === "") return false;
7
+ if (isUnitValue(value)) return true;
8
+ return Boolean(value);
9
+ }
@@ -0,0 +1,12 @@
1
+ import type { Instant, UnitValue } from "../units/index.js";
2
+
3
+ /** A runtime value in the language: primitives, unit values, lists, and maps. */
4
+ export type Value =
5
+ | null
6
+ | boolean
7
+ | number
8
+ | string
9
+ | UnitValue
10
+ | Instant
11
+ | readonly Value[]
12
+ | { readonly [key: string]: Value };