@soundor/expression 0.0.0-stage → 0.17.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/LICENSE +21 -0
- package/dist/check-DMbokSSO.d.mts +185 -0
- package/dist/check-DMbokSSO.d.mts.map +1 -0
- package/dist/check-Db6XRMc8.mjs +395 -0
- package/dist/check-Db6XRMc8.mjs.map +1 -0
- package/dist/compiler/cpp.d.mts +40 -0
- package/dist/compiler/cpp.d.mts.map +1 -0
- package/dist/compiler/cpp.mjs +498 -0
- package/dist/compiler/cpp.mjs.map +1 -0
- package/dist/compiler/js.d.mts +36 -0
- package/dist/compiler/js.d.mts.map +1 -0
- package/dist/compiler/js.mjs +258 -0
- package/dist/compiler/js.mjs.map +1 -0
- package/dist/index.d.mts +146 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +334 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +45 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dmitrii Balakin and Egor Smirnov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The types of the expression language. They say what a value is (a 32-bit
|
|
3
|
+
* float, a 16-bit unsigned integer), not what one language calls it, so the
|
|
4
|
+
* same expression means the same thing in every language it compiles to.
|
|
5
|
+
*/
|
|
6
|
+
/** Fixed-width integers. Up to 32 bits a JavaScript number, 64 bits a bigint. */
|
|
7
|
+
declare const INTEGER_TYPES: readonly ['int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', 'uint64'];
|
|
8
|
+
/** IEEE 754 binary32 and binary64. */
|
|
9
|
+
declare const FLOAT_TYPES: readonly ['float32', 'float64'];
|
|
10
|
+
declare const NUMERIC_TYPES: readonly ["int8", "uint8", "int16", "uint16", "int32", "uint32", "int64", "uint64", "float32", "float64"];
|
|
11
|
+
declare const SCALAR_TYPES: readonly ["int8", "uint8", "int16", "uint16", "int32", "uint32", "int64", "uint64", "float32", "float64", "boolean", "string"];
|
|
12
|
+
type IntegerType = (typeof INTEGER_TYPES)[number];
|
|
13
|
+
type FloatType = (typeof FLOAT_TYPES)[number];
|
|
14
|
+
type NumericType = (typeof NUMERIC_TYPES)[number];
|
|
15
|
+
/** The integers JavaScript holds as a bigint. */
|
|
16
|
+
type BigIntType = 'int64' | 'uint64';
|
|
17
|
+
type ScalarType = (typeof SCALAR_TYPES)[number];
|
|
18
|
+
/** A number or `null`: what parsing text gives. */
|
|
19
|
+
type NullableType = `${NumericType}?`;
|
|
20
|
+
/** The type of an expression's value. */
|
|
21
|
+
type ExpressionType = ScalarType | NullableType;
|
|
22
|
+
/** `float32` of `float32?`; a non-nullable type stays as it is. */
|
|
23
|
+
type BaseType<T extends ExpressionType> = T extends `${infer B}?` ? B & NumericType : T;
|
|
24
|
+
/** The JavaScript value of a type: what compiled JavaScript computes. */
|
|
25
|
+
type JsValue<T extends ExpressionType> = T extends BigIntType ? bigint : T extends NumericType ? number : T extends 'boolean' ? boolean : T extends 'string' ? string : T extends `${infer B}?` ? JsValue<B & NumericType> | null : never;
|
|
26
|
+
/** What a literal of a type is written as in the builder. */
|
|
27
|
+
type LiteralValue<T extends ExpressionType> = T extends BigIntType ? bigint : T extends NumericType ? number : T extends 'boolean' ? boolean : T extends 'string' ? string : never;
|
|
28
|
+
interface IntegerInfo {
|
|
29
|
+
readonly bits: 8 | 16 | 32 | 64;
|
|
30
|
+
readonly signed: boolean;
|
|
31
|
+
readonly min: bigint;
|
|
32
|
+
readonly max: bigint;
|
|
33
|
+
}
|
|
34
|
+
/** Width, signedness and bounds of every integer type. */
|
|
35
|
+
declare const INTEGERS: Readonly<Record<IntegerType, IntegerInfo>>;
|
|
36
|
+
declare function isIntegerType(type: string): type is IntegerType;
|
|
37
|
+
declare function isFloatType(type: string): type is FloatType;
|
|
38
|
+
declare function isNumericType(type: string): type is NumericType;
|
|
39
|
+
declare function isBigIntType(type: string): type is BigIntType;
|
|
40
|
+
declare function isNullableType(type: string): type is NullableType;
|
|
41
|
+
declare function isExpressionType(type: unknown): type is ExpressionType;
|
|
42
|
+
/** The base type of a nullable type, or the type itself. */
|
|
43
|
+
declare function baseType(type: ExpressionType): ScalarType;
|
|
44
|
+
/** Operators of one operand. */
|
|
45
|
+
declare const UNARY_OPERATORS: readonly ['neg', 'abs', 'sqrt', 'exp', 'log', 'floor', 'ceil', 'round', 'not', 'trim', 'lower'];
|
|
46
|
+
/** Operators of two operands. */
|
|
47
|
+
declare const BINARY_OPERATORS: readonly ['add', 'sub', 'mul', 'div', 'min', 'max', 'pow', 'eq', 'ne', 'lt', 'lte', 'gt', 'gte', 'and', 'or', 'endsWith', 'removeSuffix'];
|
|
48
|
+
type UnaryOperator = (typeof UNARY_OPERATORS)[number];
|
|
49
|
+
type BinaryOperator = (typeof BINARY_OPERATORS)[number];
|
|
50
|
+
/**
|
|
51
|
+
* A literal. Numbers of up to 32 bits and floats are JSON numbers (finite);
|
|
52
|
+
* 64-bit integers are decimal strings, so a tree survives JSON. `null` only
|
|
53
|
+
* for a nullable type.
|
|
54
|
+
*/
|
|
55
|
+
interface LiteralNode {
|
|
56
|
+
readonly kind: 'literal';
|
|
57
|
+
readonly type: ExpressionType;
|
|
58
|
+
readonly value: number | string | boolean | null;
|
|
59
|
+
}
|
|
60
|
+
/** A value the expression is evaluated with, bound by its context. */
|
|
61
|
+
interface InputNode {
|
|
62
|
+
readonly kind: 'input';
|
|
63
|
+
readonly type: ExpressionType;
|
|
64
|
+
readonly name: string;
|
|
65
|
+
}
|
|
66
|
+
/** The value a `map` binds. */
|
|
67
|
+
interface VariableNode {
|
|
68
|
+
readonly kind: 'variable';
|
|
69
|
+
readonly type: ExpressionType;
|
|
70
|
+
readonly name: string;
|
|
71
|
+
}
|
|
72
|
+
interface UnaryNode {
|
|
73
|
+
readonly kind: 'unary';
|
|
74
|
+
readonly type: ExpressionType;
|
|
75
|
+
readonly operator: UnaryOperator;
|
|
76
|
+
readonly operand: ExpressionNode;
|
|
77
|
+
}
|
|
78
|
+
interface BinaryNode {
|
|
79
|
+
readonly kind: 'binary';
|
|
80
|
+
readonly type: ExpressionType;
|
|
81
|
+
readonly operator: BinaryOperator;
|
|
82
|
+
readonly left: ExpressionNode;
|
|
83
|
+
readonly right: ExpressionNode;
|
|
84
|
+
}
|
|
85
|
+
interface IfNode {
|
|
86
|
+
readonly kind: 'if';
|
|
87
|
+
readonly type: ExpressionType;
|
|
88
|
+
readonly condition: ExpressionNode;
|
|
89
|
+
readonly then: ExpressionNode;
|
|
90
|
+
readonly else: ExpressionNode;
|
|
91
|
+
}
|
|
92
|
+
/** A numeric value as another numeric type. */
|
|
93
|
+
interface ConvertNode {
|
|
94
|
+
readonly kind: 'convert';
|
|
95
|
+
readonly type: NumericType;
|
|
96
|
+
readonly operand: ExpressionNode;
|
|
97
|
+
}
|
|
98
|
+
/** A number in decimal notation with a fixed number of decimals. */
|
|
99
|
+
interface FixedNode {
|
|
100
|
+
readonly kind: 'fixed';
|
|
101
|
+
readonly type: 'string';
|
|
102
|
+
readonly operand: ExpressionNode;
|
|
103
|
+
readonly digits: number;
|
|
104
|
+
}
|
|
105
|
+
interface ConcatNode {
|
|
106
|
+
readonly kind: 'concat';
|
|
107
|
+
readonly type: 'string';
|
|
108
|
+
readonly operands: readonly ExpressionNode[];
|
|
109
|
+
}
|
|
110
|
+
/** Text read as a number of `type`'s base type, or `null`. */
|
|
111
|
+
interface ParseNode {
|
|
112
|
+
readonly kind: 'parse';
|
|
113
|
+
readonly type: NullableType;
|
|
114
|
+
readonly operand: ExpressionNode;
|
|
115
|
+
}
|
|
116
|
+
/** A nullable's value, or `fallback` when it is null. */
|
|
117
|
+
interface CoalesceNode {
|
|
118
|
+
readonly kind: 'coalesce';
|
|
119
|
+
readonly type: ExpressionType;
|
|
120
|
+
readonly operand: ExpressionNode;
|
|
121
|
+
readonly fallback: ExpressionNode;
|
|
122
|
+
}
|
|
123
|
+
interface IsNullNode {
|
|
124
|
+
readonly kind: 'isNull';
|
|
125
|
+
readonly type: 'boolean';
|
|
126
|
+
readonly operand: ExpressionNode;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* `null` when `operand` is null; otherwise `body`, in which `variable` is
|
|
130
|
+
* the operand's value.
|
|
131
|
+
*/
|
|
132
|
+
interface MapNode {
|
|
133
|
+
readonly kind: 'map';
|
|
134
|
+
readonly type: NullableType;
|
|
135
|
+
readonly operand: ExpressionNode;
|
|
136
|
+
readonly variable: string;
|
|
137
|
+
readonly body: ExpressionNode;
|
|
138
|
+
}
|
|
139
|
+
/** `body`, in which `variable` is the value of `value` (computed once). */
|
|
140
|
+
interface LetNode {
|
|
141
|
+
readonly kind: 'let';
|
|
142
|
+
readonly type: ExpressionType;
|
|
143
|
+
readonly value: ExpressionNode;
|
|
144
|
+
readonly variable: string;
|
|
145
|
+
readonly body: ExpressionNode;
|
|
146
|
+
}
|
|
147
|
+
type ExpressionNode = LiteralNode | InputNode | VariableNode | UnaryNode | BinaryNode | IfNode | ConvertNode | FixedNode | ConcatNode | ParseNode | CoalesceNode | IsNullNode | MapNode | LetNode;
|
|
148
|
+
/**
|
|
149
|
+
* An expression whose value is of type `T`. Its `type` keeps expressions of
|
|
150
|
+
* different types apart even where both are a JavaScript `number`.
|
|
151
|
+
*/
|
|
152
|
+
type Expression<T extends ExpressionType = ExpressionType> = ExpressionNode & {
|
|
153
|
+
readonly type: T;
|
|
154
|
+
};
|
|
155
|
+
type ExpressionKind = ExpressionNode['kind'];
|
|
156
|
+
/** Whether `value` looks like an expression node (its shape is checked by `checkExpression`). */
|
|
157
|
+
declare function isExpression(value: unknown): value is Expression;
|
|
158
|
+
/** A problem in a tree, at a path such as `left.operand`. */
|
|
159
|
+
interface ExpressionIssue {
|
|
160
|
+
readonly path: string;
|
|
161
|
+
readonly message: string;
|
|
162
|
+
}
|
|
163
|
+
/** Thrown by compilers for a tree that fails {@link checkExpression}. */
|
|
164
|
+
declare class ExpressionError extends Error {
|
|
165
|
+
readonly issues: readonly ExpressionIssue[];
|
|
166
|
+
constructor(message: string, issues: readonly ExpressionIssue[]);
|
|
167
|
+
}
|
|
168
|
+
/** The inputs an expression is evaluated with, by name. */
|
|
169
|
+
type ExpressionInputs = Readonly<Record<string, ExpressionType>>;
|
|
170
|
+
interface CheckOptions {
|
|
171
|
+
/** The type the expression must have. */
|
|
172
|
+
readonly expected?: ExpressionType;
|
|
173
|
+
}
|
|
174
|
+
/** The deepest tree accepted; compilers recurse. */
|
|
175
|
+
declare const MAX_DEPTH = 128;
|
|
176
|
+
/** The most nodes in one tree. */
|
|
177
|
+
declare const MAX_NODES = 10000;
|
|
178
|
+
/** The most decimals `fixed` writes. */
|
|
179
|
+
declare const MAX_FIXED_DIGITS = 15;
|
|
180
|
+
/** Every problem in `node`, evaluated with `inputs`; empty when it is valid. */
|
|
181
|
+
declare function checkExpression(node: unknown, inputs: ExpressionInputs, options?: CheckOptions): ExpressionIssue[];
|
|
182
|
+
/** Throws {@link ExpressionError} when `node` is not valid. */
|
|
183
|
+
declare function assertExpression(node: unknown, inputs: ExpressionInputs, options?: CheckOptions): asserts node is ExpressionNode;
|
|
184
|
+
export { isNullableType as $, VariableNode as A, IntegerType as B, LetNode as C, UNARY_OPERATORS as D, ParseNode as E, FLOAT_TYPES as F, NumericType as G, LiteralValue as H, FloatType as I, baseType as J, SCALAR_TYPES as K, INTEGERS as L, BaseType as M, BigIntType as N, UnaryNode as O, ExpressionType as P, isIntegerType as Q, INTEGER_TYPES as R, IsNullNode as S, MapNode as T, NUMERIC_TYPES as U, JsValue as V, NullableType as W, isExpressionType as X, isBigIntType as Y, isFloatType as Z, ExpressionKind as _, MAX_DEPTH as a, IfNode as b, assertExpression as c, BinaryNode as d, isNumericType as et, BinaryOperator as f, Expression as g, ConvertNode as h, ExpressionIssue as i, isExpression as j, UnaryOperator as k, checkExpression as l, ConcatNode as m, ExpressionError as n, MAX_FIXED_DIGITS as o, CoalesceNode as p, ScalarType as q, ExpressionInputs as r, MAX_NODES as s, CheckOptions as t, BINARY_OPERATORS as u, ExpressionNode as v, LiteralNode as w, InputNode as x, FixedNode as y, IntegerInfo as z };
|
|
185
|
+
//# sourceMappingURL=check-DMbokSSO.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-DMbokSSO.d.mts","names":[],"sources":["../src/types.ts","../src/ast.ts","../src/check.ts"],"mappings":";;;;;;cAOa;;cAYA;cAEA;cAEA;KAED,sBAAsB;KACtB,oBAAoB;KACpB,sBAAsB;;KAEtB;KACA,qBAAqB;;KAErB,kBAAkB;;KAElB,iBAAiB,aAAa;;KAG9B,SAAS,UAAU,kBAAkB,mBAAmB,OAChE,IAAI,cACJ;;KAGQ,QAAQ,UAAU,kBAAkB,UAAU,sBAEtD,UAAU,uBAER,gCAEE,8BAEE,mBAAmB,OACjB,QAAQ,IAAI;;KAIZ,aAAa,UAAU,kBAAkB,UAAU,sBAE3D,UAAU,uBAER,gCAEE;UAIS;WACN;WACA;WACA;WACA;;;cAgBE,UAAU,SAAS,OAAO,aAAa;iBAWpC,cAAc,eAAe,QAAQ;iBAIrC,YAAY,eAAe,QAAQ;iBAInC,cAAc,eAAe,QAAQ;iBAIrC,aAAa,eAAe,QAAQ;iBAIpC,eAAe,eAAe,QAAQ;iBAItC,iBAAiB,gBAAgB,QAAQ;;iBAQzC,SAAS,MAAM,iBAAiB;;cC9GnC;;cAeA;KAoBD,wBAAwB;KACxB,yBAAyB;;;;;;UAOpB;WACN;WACA,MAAM;WACN;;;UAIM;WACN;WACA,MAAM;WACN;;;UAIM;WACN;WACA,MAAM;WACN;;UAGM;WACN;WACA,MAAM;WACN,UAAU;WACV,SAAS;;UAGH;WACN;WACA,MAAM;WACN,UAAU;WACV,MAAM;WACN,OAAO;;UAGD;WACN;WACA,MAAM;WACN,WAAW;WACX,MAAM;WACN,MAAM;;;UAIA;WACN;WACA,MAAM;WACN,SAAS;;;UAIH;WACN;WACA;WACA,SAAS;WACT;;UAGM;WACN;WACA;WACA,mBAAmB;;;UAIb;WACN;WACA,MAAM;WACN,SAAS;;;UAIH;WACN;WACA,MAAM;WACN,SAAS;WACT,UAAU;;UAGJ;WACN;WACA;WACA,SAAS;;;;;;UAOH;WACN;WACA,MAAM;WACN,SAAS;WACT;WACA,MAAM;;;UAIA;WACN;WACA,MAAM;WACN,OAAO;WACP;WACA,MAAM;;KAGL,iBACR,cACA,YACA,eACA,YACA,aACA,SACA,cACA,YACA,aACA,YACA,eACA,aACA,UACA;;;;;KAMQ,WAAW,UAAU,iBAAiB,kBAChD;WAA4B,MAAM;;KAExB,iBAAiB;;iBAGb,aAAa,iBAAiB,SAAS;;UCxKtC;WACN;WACA;;;cAIE,wBAAwB;WAC1B,iBAAiB;EAE1B,YAAY,iBAAiB,iBAAiB;;;KAUpC,mBAAmB,SAAS,eAAe;UAEtC;;WAEN,WAAW;;;cAIT;;cAEA;;cAEA;;iBAGG,gBACd,eACA,QAAQ,kBACR,UAAS,eACR;;iBAkBa,iBACd,eACA,QAAQ,kBACR,UAAS,uBACA,QAAQ"}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// src/ast.ts
|
|
2
|
+
/** Operators of one operand. */
|
|
3
|
+
const UNARY_OPERATORS = [
|
|
4
|
+
"neg",
|
|
5
|
+
"abs",
|
|
6
|
+
"sqrt",
|
|
7
|
+
"exp",
|
|
8
|
+
"log",
|
|
9
|
+
"floor",
|
|
10
|
+
"ceil",
|
|
11
|
+
"round",
|
|
12
|
+
"not",
|
|
13
|
+
"trim",
|
|
14
|
+
"lower"
|
|
15
|
+
];
|
|
16
|
+
/** Operators of two operands. */
|
|
17
|
+
const BINARY_OPERATORS = [
|
|
18
|
+
"add",
|
|
19
|
+
"sub",
|
|
20
|
+
"mul",
|
|
21
|
+
"div",
|
|
22
|
+
"min",
|
|
23
|
+
"max",
|
|
24
|
+
"pow",
|
|
25
|
+
"eq",
|
|
26
|
+
"ne",
|
|
27
|
+
"lt",
|
|
28
|
+
"lte",
|
|
29
|
+
"gt",
|
|
30
|
+
"gte",
|
|
31
|
+
"and",
|
|
32
|
+
"or",
|
|
33
|
+
"endsWith",
|
|
34
|
+
"removeSuffix"
|
|
35
|
+
];
|
|
36
|
+
/** Whether `value` looks like an expression node (its shape is checked by `checkExpression`). */
|
|
37
|
+
function isExpression(value) {
|
|
38
|
+
return typeof value === "object" && value !== null && typeof value.kind === "string" && typeof value.type === "string";
|
|
39
|
+
}
|
|
40
|
+
// src/types.ts
|
|
41
|
+
/**
|
|
42
|
+
* The types of the expression language. They say what a value is (a 32-bit
|
|
43
|
+
* float, a 16-bit unsigned integer), not what one language calls it, so the
|
|
44
|
+
* same expression means the same thing in every language it compiles to.
|
|
45
|
+
*/
|
|
46
|
+
/** Fixed-width integers. Up to 32 bits a JavaScript number, 64 bits a bigint. */
|
|
47
|
+
const INTEGER_TYPES = [
|
|
48
|
+
"int8",
|
|
49
|
+
"uint8",
|
|
50
|
+
"int16",
|
|
51
|
+
"uint16",
|
|
52
|
+
"int32",
|
|
53
|
+
"uint32",
|
|
54
|
+
"int64",
|
|
55
|
+
"uint64"
|
|
56
|
+
];
|
|
57
|
+
/** IEEE 754 binary32 and binary64. */
|
|
58
|
+
const FLOAT_TYPES = ["float32", "float64"];
|
|
59
|
+
const NUMERIC_TYPES = [...INTEGER_TYPES, ...FLOAT_TYPES];
|
|
60
|
+
const SCALAR_TYPES = [
|
|
61
|
+
...NUMERIC_TYPES,
|
|
62
|
+
"boolean",
|
|
63
|
+
"string"
|
|
64
|
+
];
|
|
65
|
+
function integer(bits, signed) {
|
|
66
|
+
const width = BigInt(bits);
|
|
67
|
+
return signed ? {
|
|
68
|
+
bits,
|
|
69
|
+
signed,
|
|
70
|
+
min: -(1n << width - 1n),
|
|
71
|
+
max: (1n << width - 1n) - 1n
|
|
72
|
+
} : {
|
|
73
|
+
bits,
|
|
74
|
+
signed,
|
|
75
|
+
min: 0n,
|
|
76
|
+
max: (1n << width) - 1n
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Width, signedness and bounds of every integer type. */
|
|
80
|
+
const INTEGERS = {
|
|
81
|
+
int8: integer(8, true),
|
|
82
|
+
uint8: integer(8, false),
|
|
83
|
+
int16: integer(16, true),
|
|
84
|
+
uint16: integer(16, false),
|
|
85
|
+
int32: integer(32, true),
|
|
86
|
+
uint32: integer(32, false),
|
|
87
|
+
int64: integer(64, true),
|
|
88
|
+
uint64: integer(64, false)
|
|
89
|
+
};
|
|
90
|
+
function isIntegerType(type) {
|
|
91
|
+
return INTEGER_TYPES.includes(type);
|
|
92
|
+
}
|
|
93
|
+
function isFloatType(type) {
|
|
94
|
+
return type === "float32" || type === "float64";
|
|
95
|
+
}
|
|
96
|
+
function isNumericType(type) {
|
|
97
|
+
return isIntegerType(type) || isFloatType(type);
|
|
98
|
+
}
|
|
99
|
+
function isBigIntType(type) {
|
|
100
|
+
return type === "int64" || type === "uint64";
|
|
101
|
+
}
|
|
102
|
+
function isNullableType(type) {
|
|
103
|
+
return type.endsWith("?") && isNumericType(type.slice(0, -1));
|
|
104
|
+
}
|
|
105
|
+
function isExpressionType(type) {
|
|
106
|
+
return typeof type === "string" && (SCALAR_TYPES.includes(type) || isNullableType(type));
|
|
107
|
+
}
|
|
108
|
+
/** The base type of a nullable type, or the type itself. */
|
|
109
|
+
function baseType(type) {
|
|
110
|
+
return isNullableType(type) ? type.slice(0, -1) : type;
|
|
111
|
+
}
|
|
112
|
+
// src/check.ts
|
|
113
|
+
/**
|
|
114
|
+
* Build-time validation of an expression tree. TypeScript keeps the builder's
|
|
115
|
+
* output well typed, but a tree can also come from JSON or be built by hand,
|
|
116
|
+
* so compilers check every tree they are given: its shape, every node's type,
|
|
117
|
+
* literals within their type, inputs and variables that exist.
|
|
118
|
+
*/
|
|
119
|
+
/** Thrown by compilers for a tree that fails {@link checkExpression}. */
|
|
120
|
+
var ExpressionError = class extends Error {
|
|
121
|
+
issues;
|
|
122
|
+
constructor(message, issues) {
|
|
123
|
+
super(`${message}\n${issues.map((issue) => ` ${issue.path || "(root)"}: ${issue.message}`).join("\n")}`);
|
|
124
|
+
this.name = "ExpressionError";
|
|
125
|
+
this.issues = issues;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
/** The deepest tree accepted; compilers recurse. */
|
|
129
|
+
const MAX_DEPTH = 128;
|
|
130
|
+
/** The most nodes in one tree. */
|
|
131
|
+
const MAX_NODES = 1e4;
|
|
132
|
+
/** The most decimals `fixed` writes. */
|
|
133
|
+
const MAX_FIXED_DIGITS = 15;
|
|
134
|
+
/** Every problem in `node`, evaluated with `inputs`; empty when it is valid. */
|
|
135
|
+
function checkExpression(node, inputs, options = {}) {
|
|
136
|
+
const issues = [];
|
|
137
|
+
const type = visit(node, "", 0, inputs, /* @__PURE__ */ new Map(), issues, { nodes: 0 });
|
|
138
|
+
if (type !== void 0 && options.expected !== void 0 && type !== options.expected) issues.push({
|
|
139
|
+
path: "",
|
|
140
|
+
message: `expected an expression of type '${options.expected}', got '${type}'`
|
|
141
|
+
});
|
|
142
|
+
return issues;
|
|
143
|
+
}
|
|
144
|
+
/** Throws {@link ExpressionError} when `node` is not valid. */
|
|
145
|
+
function assertExpression(node, inputs, options = {}) {
|
|
146
|
+
const issues = checkExpression(node, inputs, options);
|
|
147
|
+
if (issues.length > 0) throw new ExpressionError("Invalid expression.", issues);
|
|
148
|
+
}
|
|
149
|
+
const join = (path, key) => path === "" ? key : `${path}.${key}`;
|
|
150
|
+
function visit(value, path, depth, inputs, scope, issues, state) {
|
|
151
|
+
const fail = (message, at = path) => {
|
|
152
|
+
issues.push({
|
|
153
|
+
path: at,
|
|
154
|
+
message
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
if (++state.nodes > 1e4) {
|
|
158
|
+
if (state.nodes === 10001) return fail(`an expression has at most ${MAX_NODES} nodes`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (depth > 128) return fail(`an expression nests at most 128 levels deep`);
|
|
162
|
+
if (typeof value !== "object" || value === null) return fail("expected an expression node");
|
|
163
|
+
const node = value;
|
|
164
|
+
const type = node["type"];
|
|
165
|
+
if (!isExpressionType(type)) return fail(`unknown expression type ${JSON.stringify(type)}`);
|
|
166
|
+
const child = (key) => visit(node[key], join(path, key), depth + 1, inputs, scope, issues, state);
|
|
167
|
+
const expect = (actual, message) => {
|
|
168
|
+
if (actual !== type) fail(`${message}: the node says '${type}', its value is '${actual}'`);
|
|
169
|
+
};
|
|
170
|
+
switch (node.kind) {
|
|
171
|
+
case "literal": return checkLiteral(type, node["value"], path, issues);
|
|
172
|
+
case "input":
|
|
173
|
+
case "variable": {
|
|
174
|
+
const name = node["name"];
|
|
175
|
+
if (typeof name !== "string") return fail("expected a name");
|
|
176
|
+
const declared = node.kind === "input" ? inputs[name] : scope.get(name);
|
|
177
|
+
if (declared === void 0) return fail(node.kind === "input" ? `unknown input '${name}'; this expression has ${Object.keys(inputs).length === 0 ? "no inputs" : Object.keys(inputs).map((input) => `'${input}'`).join(", ")}` : `variable '${name}' is not bound by an enclosing let or map`);
|
|
178
|
+
if (declared !== type) return fail(`'${name}' is '${declared}', not '${type}'`);
|
|
179
|
+
return type;
|
|
180
|
+
}
|
|
181
|
+
case "unary": {
|
|
182
|
+
const operator = node["operator"];
|
|
183
|
+
if (!UNARY_OPERATORS.includes(operator)) return fail(`unknown unary operator ${JSON.stringify(operator)}`);
|
|
184
|
+
const operand = child("operand");
|
|
185
|
+
if (operand === void 0) return void 0;
|
|
186
|
+
let result;
|
|
187
|
+
switch (operator) {
|
|
188
|
+
case "neg":
|
|
189
|
+
case "abs":
|
|
190
|
+
if (isNumericType(operand)) result = operand;
|
|
191
|
+
break;
|
|
192
|
+
case "sqrt":
|
|
193
|
+
case "exp":
|
|
194
|
+
case "log":
|
|
195
|
+
case "floor":
|
|
196
|
+
case "ceil":
|
|
197
|
+
case "round":
|
|
198
|
+
if (isFloatType(operand)) result = operand;
|
|
199
|
+
break;
|
|
200
|
+
case "not":
|
|
201
|
+
if (operand === "boolean") result = operand;
|
|
202
|
+
break;
|
|
203
|
+
case "trim":
|
|
204
|
+
case "lower": if (operand === "string") result = operand;
|
|
205
|
+
}
|
|
206
|
+
if (result === void 0) return fail(`'${String(operator)}' does not take a '${operand}'`);
|
|
207
|
+
expect(result, `'${String(operator)}'`);
|
|
208
|
+
return type;
|
|
209
|
+
}
|
|
210
|
+
case "binary": {
|
|
211
|
+
const operator = node["operator"];
|
|
212
|
+
if (!BINARY_OPERATORS.includes(operator)) return fail(`unknown binary operator ${JSON.stringify(operator)}`);
|
|
213
|
+
const left = child("left");
|
|
214
|
+
const right = child("right");
|
|
215
|
+
if (left === void 0 || right === void 0) return void 0;
|
|
216
|
+
if (left !== right) return fail(`'${String(operator)}' takes two operands of one type, got '${left}' and '${right}'; convert one explicitly`);
|
|
217
|
+
let result;
|
|
218
|
+
switch (operator) {
|
|
219
|
+
case "add":
|
|
220
|
+
case "sub":
|
|
221
|
+
case "mul":
|
|
222
|
+
case "div":
|
|
223
|
+
case "min":
|
|
224
|
+
case "max":
|
|
225
|
+
if (isNumericType(left)) result = left;
|
|
226
|
+
break;
|
|
227
|
+
case "pow":
|
|
228
|
+
if (isFloatType(left)) result = left;
|
|
229
|
+
break;
|
|
230
|
+
case "eq":
|
|
231
|
+
case "ne":
|
|
232
|
+
if (!isNullableType(left)) result = "boolean";
|
|
233
|
+
break;
|
|
234
|
+
case "lt":
|
|
235
|
+
case "lte":
|
|
236
|
+
case "gt":
|
|
237
|
+
case "gte":
|
|
238
|
+
if (isNumericType(left)) result = "boolean";
|
|
239
|
+
break;
|
|
240
|
+
case "and":
|
|
241
|
+
case "or":
|
|
242
|
+
if (left === "boolean") result = "boolean";
|
|
243
|
+
break;
|
|
244
|
+
case "endsWith":
|
|
245
|
+
if (left === "string") result = "boolean";
|
|
246
|
+
break;
|
|
247
|
+
case "removeSuffix": if (left === "string") result = "string";
|
|
248
|
+
}
|
|
249
|
+
if (result === void 0) return fail(`'${String(operator)}' does not take '${left}' operands`);
|
|
250
|
+
expect(result, `'${String(operator)}'`);
|
|
251
|
+
return type;
|
|
252
|
+
}
|
|
253
|
+
case "if": {
|
|
254
|
+
const condition = child("condition");
|
|
255
|
+
const then = child("then");
|
|
256
|
+
const otherwise = child("else");
|
|
257
|
+
if (condition === void 0 || then === void 0 || otherwise === void 0) return void 0;
|
|
258
|
+
if (condition !== "boolean") return fail(`a condition is 'boolean', got '${condition}'`, join(path, "condition"));
|
|
259
|
+
const joined = joinTypes(then, otherwise);
|
|
260
|
+
if (joined === void 0) return fail(`the branches have different types, '${then}' and '${otherwise}'`);
|
|
261
|
+
expect(joined, "if");
|
|
262
|
+
return type;
|
|
263
|
+
}
|
|
264
|
+
case "convert": {
|
|
265
|
+
const operand = child("operand");
|
|
266
|
+
if (operand === void 0) return void 0;
|
|
267
|
+
if (!isNumericType(operand) || !isNumericType(type)) return fail(`convert turns a number into another number, not '${operand}' into '${type}'`);
|
|
268
|
+
return type;
|
|
269
|
+
}
|
|
270
|
+
case "fixed": {
|
|
271
|
+
const operand = child("operand");
|
|
272
|
+
if (operand === void 0) return void 0;
|
|
273
|
+
const digits = node["digits"];
|
|
274
|
+
if (typeof digits !== "number" || !Number.isInteger(digits) || digits < 0 || digits > 15) return fail(`fixed writes 0 to 15 decimals, got ${JSON.stringify(digits)}`, join(path, "digits"));
|
|
275
|
+
if (!isNumericType(operand)) return fail(`fixed formats a number, not a '${operand}'`);
|
|
276
|
+
expect("string", "fixed");
|
|
277
|
+
return type;
|
|
278
|
+
}
|
|
279
|
+
case "concat": {
|
|
280
|
+
const operands = node["operands"];
|
|
281
|
+
if (!Array.isArray(operands) || operands.length === 0) return fail("concat takes at least one string");
|
|
282
|
+
let ok = true;
|
|
283
|
+
operands.forEach((operand, index) => {
|
|
284
|
+
const at = join(path, `operands[${index}]`);
|
|
285
|
+
const operandType = visit(operand, at, depth + 1, inputs, scope, issues, state);
|
|
286
|
+
if (operandType === void 0) ok = false;
|
|
287
|
+
else if (operandType !== "string") {
|
|
288
|
+
ok = false;
|
|
289
|
+
fail(`concat takes strings, got '${operandType}'; format numbers with fixed`, at);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
if (!ok) return void 0;
|
|
293
|
+
expect("string", "concat");
|
|
294
|
+
return type;
|
|
295
|
+
}
|
|
296
|
+
case "parse": {
|
|
297
|
+
const operand = child("operand");
|
|
298
|
+
if (operand === void 0) return void 0;
|
|
299
|
+
if (operand !== "string") return fail(`parse reads a string, not a '${operand}'`);
|
|
300
|
+
if (!isNullableType(type)) return fail(`parse gives a nullable number, not '${type}'`);
|
|
301
|
+
return type;
|
|
302
|
+
}
|
|
303
|
+
case "coalesce": {
|
|
304
|
+
const operand = child("operand");
|
|
305
|
+
const fallback = child("fallback");
|
|
306
|
+
if (operand === void 0 || fallback === void 0) return void 0;
|
|
307
|
+
if (!isNullableType(operand)) return fail(`coalesce takes a nullable value, not a '${operand}'`);
|
|
308
|
+
if (fallback !== baseType(operand)) return fail(`the fallback of a '${operand}' is a '${baseType(operand)}', not a '${fallback}'`);
|
|
309
|
+
expect(fallback, "coalesce");
|
|
310
|
+
return type;
|
|
311
|
+
}
|
|
312
|
+
case "isNull": {
|
|
313
|
+
const operand = child("operand");
|
|
314
|
+
if (operand === void 0) return void 0;
|
|
315
|
+
if (!isNullableType(operand)) return fail(`isNull takes a nullable value, not a '${operand}'`);
|
|
316
|
+
expect("boolean", "isNull");
|
|
317
|
+
return type;
|
|
318
|
+
}
|
|
319
|
+
case "map": {
|
|
320
|
+
const operand = child("operand");
|
|
321
|
+
if (operand === void 0) return void 0;
|
|
322
|
+
const variable = node["variable"];
|
|
323
|
+
if (typeof variable !== "string" || variable === "") return fail("a map names its variable", join(path, "variable"));
|
|
324
|
+
if (scope.has(variable)) return fail(`variable '${variable}' is already bound`, join(path, "variable"));
|
|
325
|
+
if (!isNullableType(operand)) return fail(`map takes a nullable value, not a '${operand}'`);
|
|
326
|
+
const inner = new Map(scope).set(variable, baseType(operand));
|
|
327
|
+
const body = visit(node["body"], join(path, "body"), depth + 1, inputs, inner, issues, state);
|
|
328
|
+
if (body === void 0) return void 0;
|
|
329
|
+
if (!isNumericType(body) && !isNullableType(body)) return fail(`a map gives a number, not a '${body}'`, join(path, "body"));
|
|
330
|
+
expect(isNullableType(body) ? body : `${body}?`, "map");
|
|
331
|
+
return type;
|
|
332
|
+
}
|
|
333
|
+
case "let": {
|
|
334
|
+
const value = child("value");
|
|
335
|
+
if (value === void 0) return void 0;
|
|
336
|
+
const variable = node["variable"];
|
|
337
|
+
if (typeof variable !== "string" || variable === "") return fail("a let names its variable", join(path, "variable"));
|
|
338
|
+
if (scope.has(variable)) return fail(`variable '${variable}' is already bound`, join(path, "variable"));
|
|
339
|
+
const inner = new Map(scope).set(variable, value);
|
|
340
|
+
const body = visit(node["body"], join(path, "body"), depth + 1, inputs, inner, issues, state);
|
|
341
|
+
if (body === void 0) return void 0;
|
|
342
|
+
expect(body, "let");
|
|
343
|
+
return type;
|
|
344
|
+
}
|
|
345
|
+
default: return fail(`unknown expression kind ${JSON.stringify(node.kind)}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** The type of `if (c) a else b`: equal types, or a number and its nullable. */
|
|
349
|
+
function joinTypes(a, b) {
|
|
350
|
+
if (a === b) return a;
|
|
351
|
+
if (baseType(a) === baseType(b) && isNumericType(baseType(a))) return `${baseType(a)}?`;
|
|
352
|
+
}
|
|
353
|
+
function checkLiteral(type, value, path, issues) {
|
|
354
|
+
const problem = literalProblem(type, value);
|
|
355
|
+
if (problem !== void 0) {
|
|
356
|
+
issues.push({
|
|
357
|
+
path,
|
|
358
|
+
message: problem
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
return type;
|
|
363
|
+
}
|
|
364
|
+
/** Why `value` is not a literal of `type`, or undefined when it is. */
|
|
365
|
+
function literalProblem(type, value) {
|
|
366
|
+
if (isNullableType(type)) return value === null ? void 0 : literalProblem(baseType(type), value);
|
|
367
|
+
if (type === "boolean") return typeof value === "boolean" ? void 0 : `expected a boolean, got ${describe(value)}`;
|
|
368
|
+
if (type === "string") return typeof value === "string" ? void 0 : `expected a string, got ${describe(value)}`;
|
|
369
|
+
if (isFloatType(type)) {
|
|
370
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return `a '${type}' literal is a finite number, got ${describe(value)}`;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (isIntegerType(type)) {
|
|
374
|
+
const { min, max } = INTEGERS[type];
|
|
375
|
+
let integer;
|
|
376
|
+
if (isBigIntType(type)) {
|
|
377
|
+
if (typeof value !== "string" || !/^-?(0|[1-9][0-9]*)$/.test(value)) return `a '${type}' literal is a decimal string, got ${describe(value)}`;
|
|
378
|
+
integer = BigInt(value);
|
|
379
|
+
} else {
|
|
380
|
+
if (typeof value !== "number" || !Number.isInteger(value) || Object.is(value, -0)) return `a '${type}' literal is an integer, got ${describe(value)}`;
|
|
381
|
+
integer = BigInt(value);
|
|
382
|
+
}
|
|
383
|
+
if (integer < min || integer > max) return `${String(integer)} is outside '${type}' (${min} to ${max})`;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
return `no literal of type '${type}'`;
|
|
387
|
+
}
|
|
388
|
+
function describe(value) {
|
|
389
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
390
|
+
if (typeof value === "number" && Object.is(value, -0)) return "-0";
|
|
391
|
+
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
392
|
+
}
|
|
393
|
+
export { isExpression as C, UNARY_OPERATORS as S, isFloatType as _, assertExpression as a, isNumericType as b, literalProblem as c, INTEGER_TYPES as d, NUMERIC_TYPES as f, isExpressionType as g, isBigIntType as h, MAX_NODES as i, FLOAT_TYPES as l, baseType as m, MAX_DEPTH as n, checkExpression as o, SCALAR_TYPES as p, MAX_FIXED_DIGITS as r, joinTypes as s, ExpressionError as t, INTEGERS as u, isIntegerType as v, BINARY_OPERATORS as x, isNullableType as y };
|
|
394
|
+
|
|
395
|
+
//# sourceMappingURL=check-Db6XRMc8.mjs.map
|