@libraz/coverwise 1.0.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 +191 -0
- package/README.md +119 -0
- package/README.npm.md +119 -0
- package/dist/coverwise.js +2 -0
- package/dist/coverwise.wasm +0 -0
- package/dist/js/constraint.d.ts +78 -0
- package/dist/js/constraint.d.ts.map +1 -0
- package/dist/js/constraint.js +213 -0
- package/dist/js/constraint.js.map +1 -0
- package/dist/js/index.d.ts +94 -0
- package/dist/js/index.d.ts.map +1 -0
- package/dist/js/index.js +164 -0
- package/dist/js/index.js.map +1 -0
- package/dist/js/pure/adapter.d.ts +40 -0
- package/dist/js/pure/adapter.d.ts.map +1 -0
- package/dist/js/pure/adapter.js +207 -0
- package/dist/js/pure/adapter.js.map +1 -0
- package/dist/js/pure/index.d.ts +83 -0
- package/dist/js/pure/index.d.ts.map +1 -0
- package/dist/js/pure/index.js +132 -0
- package/dist/js/pure/index.js.map +1 -0
- package/dist/js/types.d.ts +132 -0
- package/dist/js/types.d.ts.map +1 -0
- package/dist/js/types.js +3 -0
- package/dist/js/types.js.map +1 -0
- package/dist/src/ts/algo/greedy.d.ts +9 -0
- package/dist/src/ts/algo/greedy.d.ts.map +1 -0
- package/dist/src/ts/algo/greedy.js +137 -0
- package/dist/src/ts/algo/greedy.js.map +1 -0
- package/dist/src/ts/algo/index.d.ts +2 -0
- package/dist/src/ts/algo/index.d.ts.map +1 -0
- package/dist/src/ts/algo/index.js +2 -0
- package/dist/src/ts/algo/index.js.map +1 -0
- package/dist/src/ts/core/coverage-engine.d.ts +40 -0
- package/dist/src/ts/core/coverage-engine.d.ts.map +1 -0
- package/dist/src/ts/core/coverage-engine.js +366 -0
- package/dist/src/ts/core/coverage-engine.js.map +1 -0
- package/dist/src/ts/core/generator.d.ts +6 -0
- package/dist/src/ts/core/generator.d.ts.map +1 -0
- package/dist/src/ts/core/generator.js +394 -0
- package/dist/src/ts/core/generator.js.map +1 -0
- package/dist/src/ts/core/index.d.ts +3 -0
- package/dist/src/ts/core/index.d.ts.map +1 -0
- package/dist/src/ts/core/index.js +3 -0
- package/dist/src/ts/core/index.js.map +1 -0
- package/dist/src/ts/model/boundary.d.ts +29 -0
- package/dist/src/ts/model/boundary.d.ts.map +1 -0
- package/dist/src/ts/model/boundary.js +102 -0
- package/dist/src/ts/model/boundary.js.map +1 -0
- package/dist/src/ts/model/constraint-ast.d.ts +152 -0
- package/dist/src/ts/model/constraint-ast.d.ts.map +1 -0
- package/dist/src/ts/model/constraint-ast.js +384 -0
- package/dist/src/ts/model/constraint-ast.js.map +1 -0
- package/dist/src/ts/model/constraint-parser.d.ts +49 -0
- package/dist/src/ts/model/constraint-parser.d.ts.map +1 -0
- package/dist/src/ts/model/constraint-parser.js +831 -0
- package/dist/src/ts/model/constraint-parser.js.map +1 -0
- package/dist/src/ts/model/error.d.ts +19 -0
- package/dist/src/ts/model/error.d.ts.map +1 -0
- package/dist/src/ts/model/error.js +19 -0
- package/dist/src/ts/model/error.js.map +1 -0
- package/dist/src/ts/model/generate-options.d.ts +82 -0
- package/dist/src/ts/model/generate-options.d.ts.map +1 -0
- package/dist/src/ts/model/generate-options.js +52 -0
- package/dist/src/ts/model/generate-options.js.map +1 -0
- package/dist/src/ts/model/index.d.ts +6 -0
- package/dist/src/ts/model/index.d.ts.map +1 -0
- package/dist/src/ts/model/index.js +6 -0
- package/dist/src/ts/model/index.js.map +1 -0
- package/dist/src/ts/model/parameter.d.ts +65 -0
- package/dist/src/ts/model/parameter.d.ts.map +1 -0
- package/dist/src/ts/model/parameter.js +157 -0
- package/dist/src/ts/model/parameter.js.map +1 -0
- package/dist/src/ts/model/test-case.d.ts +67 -0
- package/dist/src/ts/model/test-case.d.ts.map +1 -0
- package/dist/src/ts/model/test-case.js +28 -0
- package/dist/src/ts/model/test-case.js.map +1 -0
- package/dist/src/ts/util/bitset.d.ts +14 -0
- package/dist/src/ts/util/bitset.d.ts.map +1 -0
- package/dist/src/ts/util/bitset.js +66 -0
- package/dist/src/ts/util/bitset.js.map +1 -0
- package/dist/src/ts/util/combinatorics.d.ts +4 -0
- package/dist/src/ts/util/combinatorics.d.ts.map +1 -0
- package/dist/src/ts/util/combinatorics.js +60 -0
- package/dist/src/ts/util/combinatorics.js.map +1 -0
- package/dist/src/ts/util/index.d.ts +5 -0
- package/dist/src/ts/util/index.d.ts.map +1 -0
- package/dist/src/ts/util/index.js +7 -0
- package/dist/src/ts/util/index.js.map +1 -0
- package/dist/src/ts/util/rng.d.ts +13 -0
- package/dist/src/ts/util/rng.d.ts.map +1 -0
- package/dist/src/ts/util/rng.js +112 -0
- package/dist/src/ts/util/rng.js.map +1 -0
- package/dist/src/ts/util/string_util.d.ts +3 -0
- package/dist/src/ts/util/string_util.d.ts.map +1 -0
- package/dist/src/ts/util/string_util.js +25 -0
- package/dist/src/ts/util/string_util.js.map +1 -0
- package/dist/src/ts/validator/constraint-validator.d.ts +34 -0
- package/dist/src/ts/validator/constraint-validator.d.ts.map +1 -0
- package/dist/src/ts/validator/constraint-validator.js +51 -0
- package/dist/src/ts/validator/constraint-validator.js.map +1 -0
- package/dist/src/ts/validator/coverage-validator.d.ts +42 -0
- package/dist/src/ts/validator/coverage-validator.d.ts.map +1 -0
- package/dist/src/ts/validator/coverage-validator.js +230 -0
- package/dist/src/ts/validator/coverage-validator.js.map +1 -0
- package/dist/src/ts/validator/index.d.ts +3 -0
- package/dist/src/ts/validator/index.d.ts.map +1 -0
- package/dist/src/ts/validator/index.js +3 -0
- package/dist/src/ts/validator/index.js.map +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
/// Parser for human-readable constraint expressions.
|
|
2
|
+
import { AndNode, EqualsNode, IfThenElseNode, ImpliesNode, InNode, LikeNode, NotEqualsNode, NotNode, OrNode, ParamEqualsNode, ParamNotEqualsNode, RelationalNode, RelOp, } from './constraint-ast.js';
|
|
3
|
+
import { ErrorCode, okError } from './error.js';
|
|
4
|
+
const NOT_FOUND = 0xffffffff;
|
|
5
|
+
// --- Token types ---
|
|
6
|
+
var TokenType;
|
|
7
|
+
(function (TokenType) {
|
|
8
|
+
TokenType[TokenType["Identifier"] = 0] = "Identifier";
|
|
9
|
+
TokenType[TokenType["Number"] = 1] = "Number";
|
|
10
|
+
TokenType[TokenType["Equals"] = 2] = "Equals";
|
|
11
|
+
TokenType[TokenType["NotEquals"] = 3] = "NotEquals";
|
|
12
|
+
TokenType[TokenType["Less"] = 4] = "Less";
|
|
13
|
+
TokenType[TokenType["LessEqual"] = 5] = "LessEqual";
|
|
14
|
+
TokenType[TokenType["Greater"] = 6] = "Greater";
|
|
15
|
+
TokenType[TokenType["GreaterEqual"] = 7] = "GreaterEqual";
|
|
16
|
+
TokenType[TokenType["LParen"] = 8] = "LParen";
|
|
17
|
+
TokenType[TokenType["RParen"] = 9] = "RParen";
|
|
18
|
+
TokenType[TokenType["LBrace"] = 10] = "LBrace";
|
|
19
|
+
TokenType[TokenType["RBrace"] = 11] = "RBrace";
|
|
20
|
+
TokenType[TokenType["Comma"] = 12] = "Comma";
|
|
21
|
+
TokenType[TokenType["And"] = 13] = "And";
|
|
22
|
+
TokenType[TokenType["Or"] = 14] = "Or";
|
|
23
|
+
TokenType[TokenType["Not"] = 15] = "Not";
|
|
24
|
+
TokenType[TokenType["If"] = 16] = "If";
|
|
25
|
+
TokenType[TokenType["Then"] = 17] = "Then";
|
|
26
|
+
TokenType[TokenType["Else"] = 18] = "Else";
|
|
27
|
+
TokenType[TokenType["Implies"] = 19] = "Implies";
|
|
28
|
+
TokenType[TokenType["In"] = 20] = "In";
|
|
29
|
+
TokenType[TokenType["Like"] = 21] = "Like";
|
|
30
|
+
TokenType[TokenType["End"] = 22] = "End";
|
|
31
|
+
})(TokenType || (TokenType = {}));
|
|
32
|
+
// --- Tokenizer ---
|
|
33
|
+
function toUpper(s) {
|
|
34
|
+
return s.toUpperCase();
|
|
35
|
+
}
|
|
36
|
+
function isIdentChar(c) {
|
|
37
|
+
const code = c.charCodeAt(0);
|
|
38
|
+
return ((code >= 0x30 && code <= 0x39) || // 0-9
|
|
39
|
+
(code >= 0x41 && code <= 0x5a) || // A-Z
|
|
40
|
+
(code >= 0x61 && code <= 0x7a) || // a-z
|
|
41
|
+
c === '_' ||
|
|
42
|
+
c === '-' ||
|
|
43
|
+
c === '.' ||
|
|
44
|
+
code >= 0x80);
|
|
45
|
+
}
|
|
46
|
+
function isGlobPatternChar(c) {
|
|
47
|
+
return isIdentChar(c) || c === '*' || c === '?' || c === '.';
|
|
48
|
+
}
|
|
49
|
+
function isDigit(c) {
|
|
50
|
+
const code = c.charCodeAt(0);
|
|
51
|
+
return code >= 0x30 && code <= 0x39;
|
|
52
|
+
}
|
|
53
|
+
function isSpace(c) {
|
|
54
|
+
return c === ' ' || c === '\t' || c === '\n' || c === '\r';
|
|
55
|
+
}
|
|
56
|
+
function classifyKeyword(upper) {
|
|
57
|
+
switch (upper) {
|
|
58
|
+
case 'AND':
|
|
59
|
+
return TokenType.And;
|
|
60
|
+
case 'OR':
|
|
61
|
+
return TokenType.Or;
|
|
62
|
+
case 'NOT':
|
|
63
|
+
return TokenType.Not;
|
|
64
|
+
case 'IF':
|
|
65
|
+
return TokenType.If;
|
|
66
|
+
case 'THEN':
|
|
67
|
+
return TokenType.Then;
|
|
68
|
+
case 'ELSE':
|
|
69
|
+
return TokenType.Else;
|
|
70
|
+
case 'IMPLIES':
|
|
71
|
+
return TokenType.Implies;
|
|
72
|
+
case 'IN':
|
|
73
|
+
return TokenType.In;
|
|
74
|
+
case 'LIKE':
|
|
75
|
+
return TokenType.Like;
|
|
76
|
+
default:
|
|
77
|
+
return TokenType.Identifier;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function tokenize(expr) {
|
|
81
|
+
const tokens = [];
|
|
82
|
+
let i = 0;
|
|
83
|
+
const len = expr.length;
|
|
84
|
+
let expectPattern = false;
|
|
85
|
+
while (i < len) {
|
|
86
|
+
if (isSpace(expr[i])) {
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const start = i;
|
|
91
|
+
if (expr[i] === '(') {
|
|
92
|
+
tokens.push({ type: TokenType.LParen, text: '(', position: start });
|
|
93
|
+
i++;
|
|
94
|
+
expectPattern = false;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (expr[i] === ')') {
|
|
98
|
+
tokens.push({ type: TokenType.RParen, text: ')', position: start });
|
|
99
|
+
i++;
|
|
100
|
+
expectPattern = false;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (expr[i] === '{') {
|
|
104
|
+
tokens.push({ type: TokenType.LBrace, text: '{', position: start });
|
|
105
|
+
i++;
|
|
106
|
+
expectPattern = false;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (expr[i] === '}') {
|
|
110
|
+
tokens.push({ type: TokenType.RBrace, text: '}', position: start });
|
|
111
|
+
i++;
|
|
112
|
+
expectPattern = false;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (expr[i] === ',') {
|
|
116
|
+
tokens.push({ type: TokenType.Comma, text: ',', position: start });
|
|
117
|
+
i++;
|
|
118
|
+
expectPattern = false;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (expr[i] === '!' && i + 1 < len && expr[i + 1] === '=') {
|
|
122
|
+
tokens.push({ type: TokenType.NotEquals, text: '!=', position: start });
|
|
123
|
+
i += 2;
|
|
124
|
+
expectPattern = false;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (expr[i] === '<' && i + 1 < len && expr[i + 1] === '=') {
|
|
128
|
+
tokens.push({ type: TokenType.LessEqual, text: '<=', position: start });
|
|
129
|
+
i += 2;
|
|
130
|
+
expectPattern = false;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (expr[i] === '>' && i + 1 < len && expr[i + 1] === '=') {
|
|
134
|
+
tokens.push({ type: TokenType.GreaterEqual, text: '>=', position: start });
|
|
135
|
+
i += 2;
|
|
136
|
+
expectPattern = false;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (expr[i] === '<') {
|
|
140
|
+
tokens.push({ type: TokenType.Less, text: '<', position: start });
|
|
141
|
+
i++;
|
|
142
|
+
expectPattern = false;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (expr[i] === '>') {
|
|
146
|
+
tokens.push({ type: TokenType.Greater, text: '>', position: start });
|
|
147
|
+
i++;
|
|
148
|
+
expectPattern = false;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (expr[i] === '=') {
|
|
152
|
+
tokens.push({ type: TokenType.Equals, text: '=', position: start });
|
|
153
|
+
i++;
|
|
154
|
+
expectPattern = false;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// Negative number: '-' followed by digit, only if preceded by an operator
|
|
158
|
+
if (expr[i] === '-' && i + 1 < len && isDigit(expr[i + 1])) {
|
|
159
|
+
let isNegativeNum = tokens.length === 0;
|
|
160
|
+
if (!isNegativeNum && tokens.length > 0) {
|
|
161
|
+
const prev = tokens[tokens.length - 1].type;
|
|
162
|
+
isNegativeNum =
|
|
163
|
+
prev === TokenType.Equals ||
|
|
164
|
+
prev === TokenType.NotEquals ||
|
|
165
|
+
prev === TokenType.Less ||
|
|
166
|
+
prev === TokenType.LessEqual ||
|
|
167
|
+
prev === TokenType.Greater ||
|
|
168
|
+
prev === TokenType.GreaterEqual;
|
|
169
|
+
}
|
|
170
|
+
if (isNegativeNum) {
|
|
171
|
+
let j = i + 1;
|
|
172
|
+
while (j < len && (isDigit(expr[j]) || expr[j] === '.')) {
|
|
173
|
+
j++;
|
|
174
|
+
}
|
|
175
|
+
const num = expr.substring(i, j);
|
|
176
|
+
tokens.push({ type: TokenType.Number, text: num, position: start });
|
|
177
|
+
i = j;
|
|
178
|
+
expectPattern = false;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Number literal
|
|
183
|
+
if (isDigit(expr[i])) {
|
|
184
|
+
let j = i;
|
|
185
|
+
while (j < len && (isDigit(expr[j]) || expr[j] === '.')) {
|
|
186
|
+
j++;
|
|
187
|
+
}
|
|
188
|
+
// If followed by identifier chars, it's actually an identifier (e.g., "3d")
|
|
189
|
+
if (j < len && isIdentChar(expr[j]) && expr[j] !== '-') {
|
|
190
|
+
while (j < len && isIdentChar(expr[j])) {
|
|
191
|
+
j++;
|
|
192
|
+
}
|
|
193
|
+
const word = expr.substring(i, j);
|
|
194
|
+
tokens.push({ type: TokenType.Identifier, text: word, position: start });
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const num = expr.substring(i, j);
|
|
198
|
+
tokens.push({ type: TokenType.Number, text: num, position: start });
|
|
199
|
+
}
|
|
200
|
+
i = j;
|
|
201
|
+
expectPattern = false;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// LIKE pattern: after LIKE keyword, consume pattern with glob chars
|
|
205
|
+
if (expectPattern) {
|
|
206
|
+
let j = i;
|
|
207
|
+
while (j < len && isGlobPatternChar(expr[j])) {
|
|
208
|
+
j++;
|
|
209
|
+
}
|
|
210
|
+
if (j > i) {
|
|
211
|
+
const pattern = expr.substring(i, j);
|
|
212
|
+
tokens.push({ type: TokenType.Identifier, text: pattern, position: start });
|
|
213
|
+
i = j;
|
|
214
|
+
expectPattern = false;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (isIdentChar(expr[i])) {
|
|
219
|
+
let j = i;
|
|
220
|
+
while (j < len && isIdentChar(expr[j])) {
|
|
221
|
+
j++;
|
|
222
|
+
}
|
|
223
|
+
const word = expr.substring(i, j);
|
|
224
|
+
const upper = toUpper(word);
|
|
225
|
+
const type = classifyKeyword(upper);
|
|
226
|
+
tokens.push({ type, text: word, position: start });
|
|
227
|
+
i = j;
|
|
228
|
+
expectPattern = type === TokenType.Like;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
// Handle glob pattern chars at top level
|
|
232
|
+
if (expr[i] === '*' || expr[i] === '?') {
|
|
233
|
+
let j = i;
|
|
234
|
+
while (j < len && isGlobPatternChar(expr[j])) {
|
|
235
|
+
j++;
|
|
236
|
+
}
|
|
237
|
+
const pattern = expr.substring(i, j);
|
|
238
|
+
tokens.push({ type: TokenType.Identifier, text: pattern, position: start });
|
|
239
|
+
i = j;
|
|
240
|
+
expectPattern = false;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
tokens: [],
|
|
245
|
+
error: {
|
|
246
|
+
code: ErrorCode.ConstraintError,
|
|
247
|
+
message: `Unexpected character '${expr[i]}' at position ${start}`,
|
|
248
|
+
detail: '',
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
tokens.push({ type: TokenType.End, text: '', position: len });
|
|
253
|
+
return { tokens, error: okError() };
|
|
254
|
+
}
|
|
255
|
+
// --- Name resolution ---
|
|
256
|
+
function namesEqual(a, b, caseSensitive) {
|
|
257
|
+
if (caseSensitive) {
|
|
258
|
+
return a === b;
|
|
259
|
+
}
|
|
260
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
261
|
+
}
|
|
262
|
+
function resolveParam(paramName, params, caseSensitive) {
|
|
263
|
+
for (let i = 0; i < params.length; i++) {
|
|
264
|
+
if (namesEqual(params[i].name, paramName, caseSensitive)) {
|
|
265
|
+
return { paramIndex: i, error: okError() };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const available = params.map((p) => p.name).join(', ');
|
|
269
|
+
return {
|
|
270
|
+
paramIndex: 0,
|
|
271
|
+
error: {
|
|
272
|
+
code: ErrorCode.ConstraintError,
|
|
273
|
+
message: `Unknown parameter '${paramName}'`,
|
|
274
|
+
detail: `Available parameters: ${available}`,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function resolveComparison(paramName, valueName, params, caseSensitive) {
|
|
279
|
+
const rp = resolveParam(paramName, params, caseSensitive);
|
|
280
|
+
if (rp.error.code !== ErrorCode.Ok) {
|
|
281
|
+
return { paramIndex: 0, valueIndex: 0, error: rp.error };
|
|
282
|
+
}
|
|
283
|
+
const paramIdx = rp.paramIndex;
|
|
284
|
+
const valIdx = params[paramIdx].findValueIndex(valueName, caseSensitive);
|
|
285
|
+
if (valIdx === NOT_FOUND) {
|
|
286
|
+
const available = params[paramIdx].values.join(', ');
|
|
287
|
+
return {
|
|
288
|
+
paramIndex: 0,
|
|
289
|
+
valueIndex: 0,
|
|
290
|
+
error: {
|
|
291
|
+
code: ErrorCode.ConstraintError,
|
|
292
|
+
message: `Unknown value '${valueName}' for parameter '${paramName}'`,
|
|
293
|
+
detail: `Available values: ${available}`,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return { paramIndex: paramIdx, valueIndex: valIdx, error: okError() };
|
|
298
|
+
}
|
|
299
|
+
function resolveValue(paramIndex, valueName, params, caseSensitive) {
|
|
300
|
+
const idx = params[paramIndex].findValueIndex(valueName, caseSensitive);
|
|
301
|
+
if (idx !== NOT_FOUND) {
|
|
302
|
+
return { valueIndex: idx, error: okError() };
|
|
303
|
+
}
|
|
304
|
+
const available = params[paramIndex].values.join(', ');
|
|
305
|
+
return {
|
|
306
|
+
valueIndex: 0,
|
|
307
|
+
error: {
|
|
308
|
+
code: ErrorCode.ConstraintError,
|
|
309
|
+
message: `Unknown value '${valueName}' for parameter '${params[paramIndex].name}'`,
|
|
310
|
+
detail: `Available values: ${available}`,
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function isParameterName(name, params, caseSensitive) {
|
|
315
|
+
for (const p of params) {
|
|
316
|
+
if (namesEqual(p.name, name, caseSensitive)) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
function isValueOfParam(paramIndex, name, params, caseSensitive) {
|
|
323
|
+
return params[paramIndex].findValueIndex(name, caseSensitive) !== NOT_FOUND;
|
|
324
|
+
}
|
|
325
|
+
function isNumericString(s) {
|
|
326
|
+
if (s.length === 0) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
const n = Number(s);
|
|
330
|
+
return !Number.isNaN(n) && Number.isFinite(n);
|
|
331
|
+
}
|
|
332
|
+
// --- Recursive descent parser ---
|
|
333
|
+
class Parser {
|
|
334
|
+
constructor(tokens, params, options) {
|
|
335
|
+
this.tokens = tokens;
|
|
336
|
+
this.params = params;
|
|
337
|
+
this.options = options;
|
|
338
|
+
this.pos = 0;
|
|
339
|
+
}
|
|
340
|
+
parse() {
|
|
341
|
+
const result = this.parseExpression();
|
|
342
|
+
if (result.error.code !== ErrorCode.Ok) {
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
if (this.current().type !== TokenType.End) {
|
|
346
|
+
return {
|
|
347
|
+
constraint: null,
|
|
348
|
+
error: {
|
|
349
|
+
code: ErrorCode.ConstraintError,
|
|
350
|
+
message: `Unexpected token '${this.current().text}' at position ${this.current().position}`,
|
|
351
|
+
detail: 'Expected end of expression',
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
current() {
|
|
358
|
+
return this.tokens[this.pos];
|
|
359
|
+
}
|
|
360
|
+
advance() {
|
|
361
|
+
const tok = this.tokens[this.pos];
|
|
362
|
+
if (this.pos + 1 < this.tokens.length) {
|
|
363
|
+
this.pos++;
|
|
364
|
+
}
|
|
365
|
+
return tok;
|
|
366
|
+
}
|
|
367
|
+
match(type) {
|
|
368
|
+
if (this.current().type === type) {
|
|
369
|
+
this.advance();
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
parseExpression() {
|
|
375
|
+
return this.parseImpliesExpr();
|
|
376
|
+
}
|
|
377
|
+
parseImpliesExpr() {
|
|
378
|
+
if (this.current().type === TokenType.If) {
|
|
379
|
+
this.advance();
|
|
380
|
+
const antecedent = this.parseOrExpr();
|
|
381
|
+
if (antecedent.error.code !== ErrorCode.Ok) {
|
|
382
|
+
return antecedent;
|
|
383
|
+
}
|
|
384
|
+
if (this.current().type !== TokenType.Then) {
|
|
385
|
+
return {
|
|
386
|
+
constraint: null,
|
|
387
|
+
error: {
|
|
388
|
+
code: ErrorCode.ConstraintError,
|
|
389
|
+
message: `Expected 'THEN' after 'IF' clause at position ${this.current().position}`,
|
|
390
|
+
detail: 'Syntax: IF <condition> THEN <condition>',
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
this.advance();
|
|
395
|
+
const consequent = this.parseOrExpr();
|
|
396
|
+
if (consequent.error.code !== ErrorCode.Ok) {
|
|
397
|
+
return consequent;
|
|
398
|
+
}
|
|
399
|
+
// Check for optional ELSE clause
|
|
400
|
+
if (this.current().type === TokenType.Else) {
|
|
401
|
+
this.advance();
|
|
402
|
+
const elseBranch = this.parseOrExpr();
|
|
403
|
+
if (elseBranch.error.code !== ErrorCode.Ok) {
|
|
404
|
+
return elseBranch;
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
constraint: new IfThenElseNode(antecedent.constraint, consequent.constraint, elseBranch.constraint),
|
|
408
|
+
error: okError(),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
constraint: new ImpliesNode(antecedent.constraint, consequent.constraint),
|
|
413
|
+
error: okError(),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const left = this.parseOrExpr();
|
|
417
|
+
if (left.error.code !== ErrorCode.Ok) {
|
|
418
|
+
return left;
|
|
419
|
+
}
|
|
420
|
+
if (this.match(TokenType.Implies)) {
|
|
421
|
+
const right = this.parseOrExpr();
|
|
422
|
+
if (right.error.code !== ErrorCode.Ok) {
|
|
423
|
+
return right;
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
constraint: new ImpliesNode(left.constraint, right.constraint),
|
|
427
|
+
error: okError(),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
return left;
|
|
431
|
+
}
|
|
432
|
+
parseOrExpr() {
|
|
433
|
+
let left = this.parseAndExpr();
|
|
434
|
+
if (left.error.code !== ErrorCode.Ok) {
|
|
435
|
+
return left;
|
|
436
|
+
}
|
|
437
|
+
while (this.match(TokenType.Or)) {
|
|
438
|
+
const right = this.parseAndExpr();
|
|
439
|
+
if (right.error.code !== ErrorCode.Ok) {
|
|
440
|
+
return right;
|
|
441
|
+
}
|
|
442
|
+
left = {
|
|
443
|
+
constraint: new OrNode(left.constraint, right.constraint),
|
|
444
|
+
error: okError(),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
return left;
|
|
448
|
+
}
|
|
449
|
+
parseAndExpr() {
|
|
450
|
+
let left = this.parseUnaryExpr();
|
|
451
|
+
if (left.error.code !== ErrorCode.Ok) {
|
|
452
|
+
return left;
|
|
453
|
+
}
|
|
454
|
+
while (this.match(TokenType.And)) {
|
|
455
|
+
const right = this.parseUnaryExpr();
|
|
456
|
+
if (right.error.code !== ErrorCode.Ok) {
|
|
457
|
+
return right;
|
|
458
|
+
}
|
|
459
|
+
left = {
|
|
460
|
+
constraint: new AndNode(left.constraint, right.constraint),
|
|
461
|
+
error: okError(),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return left;
|
|
465
|
+
}
|
|
466
|
+
parseUnaryExpr() {
|
|
467
|
+
if (this.match(TokenType.Not)) {
|
|
468
|
+
const child = this.parseUnaryExpr();
|
|
469
|
+
if (child.error.code !== ErrorCode.Ok) {
|
|
470
|
+
return child;
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
constraint: new NotNode(child.constraint),
|
|
474
|
+
error: okError(),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return this.parseAtom();
|
|
478
|
+
}
|
|
479
|
+
isComparisonOp(type) {
|
|
480
|
+
return (type === TokenType.Equals ||
|
|
481
|
+
type === TokenType.NotEquals ||
|
|
482
|
+
type === TokenType.Less ||
|
|
483
|
+
type === TokenType.LessEqual ||
|
|
484
|
+
type === TokenType.Greater ||
|
|
485
|
+
type === TokenType.GreaterEqual);
|
|
486
|
+
}
|
|
487
|
+
parseAtom() {
|
|
488
|
+
if (this.match(TokenType.LParen)) {
|
|
489
|
+
const inner = this.parseExpression();
|
|
490
|
+
if (inner.error.code !== ErrorCode.Ok) {
|
|
491
|
+
return inner;
|
|
492
|
+
}
|
|
493
|
+
if (!this.match(TokenType.RParen)) {
|
|
494
|
+
return {
|
|
495
|
+
constraint: null,
|
|
496
|
+
error: {
|
|
497
|
+
code: ErrorCode.ConstraintError,
|
|
498
|
+
message: `Expected ')' at position ${this.current().position}`,
|
|
499
|
+
detail: 'Mismatched parentheses',
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
return inner;
|
|
504
|
+
}
|
|
505
|
+
if (this.current().type === TokenType.Identifier) {
|
|
506
|
+
const paramTok = this.advance();
|
|
507
|
+
// IN operator: ident IN { val1, val2, ... }
|
|
508
|
+
if (this.current().type === TokenType.In) {
|
|
509
|
+
return this.parseInExpr(paramTok);
|
|
510
|
+
}
|
|
511
|
+
// LIKE operator: ident LIKE pattern
|
|
512
|
+
if (this.current().type === TokenType.Like) {
|
|
513
|
+
return this.parseLikeExpr(paramTok);
|
|
514
|
+
}
|
|
515
|
+
if (!this.isComparisonOp(this.current().type)) {
|
|
516
|
+
return {
|
|
517
|
+
constraint: null,
|
|
518
|
+
error: {
|
|
519
|
+
code: ErrorCode.ConstraintError,
|
|
520
|
+
message: `Expected operator after '${paramTok.text}' at position ${this.current().position}`,
|
|
521
|
+
detail: 'Syntax: parameter=value, parameter!=value, parameter>value, ' +
|
|
522
|
+
'parameter IN {values}, or parameter LIKE pattern',
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
const opType = this.current().type;
|
|
527
|
+
this.advance();
|
|
528
|
+
// Relational operators (>, >=, <, <=) with number or param
|
|
529
|
+
if (opType === TokenType.Less ||
|
|
530
|
+
opType === TokenType.LessEqual ||
|
|
531
|
+
opType === TokenType.Greater ||
|
|
532
|
+
opType === TokenType.GreaterEqual) {
|
|
533
|
+
return this.parseRelationalRhs(paramTok, opType);
|
|
534
|
+
}
|
|
535
|
+
// = or != with identifier, number, or param-to-param
|
|
536
|
+
if (this.current().type !== TokenType.Identifier &&
|
|
537
|
+
this.current().type !== TokenType.Number) {
|
|
538
|
+
return {
|
|
539
|
+
constraint: null,
|
|
540
|
+
error: {
|
|
541
|
+
code: ErrorCode.ConstraintError,
|
|
542
|
+
message: `Expected value after operator at position ${this.current().position}`,
|
|
543
|
+
detail: 'Syntax: parameter=value or parameter!=value',
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
const valueTok = this.advance();
|
|
548
|
+
const isEquals = opType === TokenType.Equals;
|
|
549
|
+
// Resolve left parameter
|
|
550
|
+
const rp = resolveParam(paramTok.text, this.params, this.options.caseSensitive);
|
|
551
|
+
if (rp.error.code !== ErrorCode.Ok) {
|
|
552
|
+
return { constraint: null, error: rp.error };
|
|
553
|
+
}
|
|
554
|
+
const leftParam = rp.paramIndex;
|
|
555
|
+
// Determine if RHS is a value of the left param or a parameter name
|
|
556
|
+
const rhsIsValue = isValueOfParam(leftParam, valueTok.text, this.params, this.options.caseSensitive);
|
|
557
|
+
const rhsIsParam = isParameterName(valueTok.text, this.params, this.options.caseSensitive);
|
|
558
|
+
// If it's a value of the left param, prefer param=value interpretation
|
|
559
|
+
if (rhsIsValue) {
|
|
560
|
+
const rv = resolveValue(leftParam, valueTok.text, this.params, this.options.caseSensitive);
|
|
561
|
+
if (rv.error.code !== ErrorCode.Ok) {
|
|
562
|
+
return { constraint: null, error: rv.error };
|
|
563
|
+
}
|
|
564
|
+
if (isEquals) {
|
|
565
|
+
return {
|
|
566
|
+
constraint: new EqualsNode(leftParam, rv.valueIndex),
|
|
567
|
+
error: okError(),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
constraint: new NotEqualsNode(leftParam, rv.valueIndex),
|
|
572
|
+
error: okError(),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
// If it's a parameter name, do param-to-param comparison
|
|
576
|
+
if (rhsIsParam) {
|
|
577
|
+
const rp2 = resolveParam(valueTok.text, this.params, this.options.caseSensitive);
|
|
578
|
+
if (rp2.error.code !== ErrorCode.Ok) {
|
|
579
|
+
return { constraint: null, error: rp2.error };
|
|
580
|
+
}
|
|
581
|
+
if (isEquals) {
|
|
582
|
+
return {
|
|
583
|
+
constraint: new ParamEqualsNode(leftParam, rp2.paramIndex, this.params[leftParam].values, this.params[rp2.paramIndex].values),
|
|
584
|
+
error: okError(),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
constraint: new ParamNotEqualsNode(leftParam, rp2.paramIndex, this.params[leftParam].values, this.params[rp2.paramIndex].values),
|
|
589
|
+
error: okError(),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
// Neither a value nor a parameter -- error
|
|
593
|
+
const resolved = resolveComparison(paramTok.text, valueTok.text, this.params, this.options.caseSensitive);
|
|
594
|
+
return { constraint: null, error: resolved.error };
|
|
595
|
+
}
|
|
596
|
+
if (this.current().type === TokenType.End) {
|
|
597
|
+
return {
|
|
598
|
+
constraint: null,
|
|
599
|
+
error: {
|
|
600
|
+
code: ErrorCode.ConstraintError,
|
|
601
|
+
message: 'Unexpected end of expression',
|
|
602
|
+
detail: "Expected a comparison or '('",
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
constraint: null,
|
|
608
|
+
error: {
|
|
609
|
+
code: ErrorCode.ConstraintError,
|
|
610
|
+
message: `Unexpected token '${this.current().text}' at position ${this.current().position}`,
|
|
611
|
+
detail: "Expected a comparison (e.g. param=value) or '('",
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
parseInExpr(paramTok) {
|
|
616
|
+
this.advance(); // consume IN
|
|
617
|
+
const rp = resolveParam(paramTok.text, this.params, this.options.caseSensitive);
|
|
618
|
+
if (rp.error.code !== ErrorCode.Ok) {
|
|
619
|
+
return { constraint: null, error: rp.error };
|
|
620
|
+
}
|
|
621
|
+
const paramIdx = rp.paramIndex;
|
|
622
|
+
if (!this.match(TokenType.LBrace)) {
|
|
623
|
+
return {
|
|
624
|
+
constraint: null,
|
|
625
|
+
error: {
|
|
626
|
+
code: ErrorCode.ConstraintError,
|
|
627
|
+
message: `Expected '{' after 'IN' at position ${this.current().position}`,
|
|
628
|
+
detail: 'Syntax: parameter IN {value1, value2, ...}',
|
|
629
|
+
},
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
const valueIndices = [];
|
|
633
|
+
// Parse first value
|
|
634
|
+
if (this.current().type !== TokenType.Identifier && this.current().type !== TokenType.Number) {
|
|
635
|
+
return {
|
|
636
|
+
constraint: null,
|
|
637
|
+
error: {
|
|
638
|
+
code: ErrorCode.ConstraintError,
|
|
639
|
+
message: `Expected value in set at position ${this.current().position}`,
|
|
640
|
+
detail: 'Syntax: parameter IN {value1, value2, ...}',
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
{
|
|
645
|
+
const valTok = this.advance();
|
|
646
|
+
const rv = resolveValue(paramIdx, valTok.text, this.params, this.options.caseSensitive);
|
|
647
|
+
if (rv.error.code !== ErrorCode.Ok) {
|
|
648
|
+
return { constraint: null, error: rv.error };
|
|
649
|
+
}
|
|
650
|
+
valueIndices.push(rv.valueIndex);
|
|
651
|
+
}
|
|
652
|
+
// Parse remaining values
|
|
653
|
+
while (this.match(TokenType.Comma)) {
|
|
654
|
+
if (this.current().type !== TokenType.Identifier &&
|
|
655
|
+
this.current().type !== TokenType.Number) {
|
|
656
|
+
return {
|
|
657
|
+
constraint: null,
|
|
658
|
+
error: {
|
|
659
|
+
code: ErrorCode.ConstraintError,
|
|
660
|
+
message: `Expected value after ',' at position ${this.current().position}`,
|
|
661
|
+
detail: 'Syntax: parameter IN {value1, value2, ...}',
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
const valTok = this.advance();
|
|
666
|
+
const rv = resolveValue(paramIdx, valTok.text, this.params, this.options.caseSensitive);
|
|
667
|
+
if (rv.error.code !== ErrorCode.Ok) {
|
|
668
|
+
return { constraint: null, error: rv.error };
|
|
669
|
+
}
|
|
670
|
+
valueIndices.push(rv.valueIndex);
|
|
671
|
+
}
|
|
672
|
+
if (!this.match(TokenType.RBrace)) {
|
|
673
|
+
return {
|
|
674
|
+
constraint: null,
|
|
675
|
+
error: {
|
|
676
|
+
code: ErrorCode.ConstraintError,
|
|
677
|
+
message: `Expected '}' at position ${this.current().position}`,
|
|
678
|
+
detail: 'Syntax: parameter IN {value1, value2, ...}',
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
constraint: new InNode(paramIdx, valueIndices),
|
|
684
|
+
error: okError(),
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
parseLikeExpr(paramTok) {
|
|
688
|
+
this.advance(); // consume LIKE
|
|
689
|
+
const rp = resolveParam(paramTok.text, this.params, this.options.caseSensitive);
|
|
690
|
+
if (rp.error.code !== ErrorCode.Ok) {
|
|
691
|
+
return { constraint: null, error: rp.error };
|
|
692
|
+
}
|
|
693
|
+
const paramIdx = rp.paramIndex;
|
|
694
|
+
if (this.current().type !== TokenType.Identifier && this.current().type !== TokenType.Number) {
|
|
695
|
+
return {
|
|
696
|
+
constraint: null,
|
|
697
|
+
error: {
|
|
698
|
+
code: ErrorCode.ConstraintError,
|
|
699
|
+
message: `Expected pattern after 'LIKE' at position ${this.current().position}`,
|
|
700
|
+
detail: 'Syntax: parameter LIKE pattern (wildcards: * = any string, ? = single char)',
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
const patternTok = this.advance();
|
|
705
|
+
return {
|
|
706
|
+
constraint: new LikeNode(paramIdx, patternTok.text, this.params[paramIdx].values),
|
|
707
|
+
error: okError(),
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
parseRelationalRhs(paramTok, opType) {
|
|
711
|
+
let op;
|
|
712
|
+
switch (opType) {
|
|
713
|
+
case TokenType.Less:
|
|
714
|
+
op = RelOp.Less;
|
|
715
|
+
break;
|
|
716
|
+
case TokenType.LessEqual:
|
|
717
|
+
op = RelOp.LessEqual;
|
|
718
|
+
break;
|
|
719
|
+
case TokenType.Greater:
|
|
720
|
+
op = RelOp.Greater;
|
|
721
|
+
break;
|
|
722
|
+
case TokenType.GreaterEqual:
|
|
723
|
+
op = RelOp.GreaterEqual;
|
|
724
|
+
break;
|
|
725
|
+
default:
|
|
726
|
+
return {
|
|
727
|
+
constraint: null,
|
|
728
|
+
error: {
|
|
729
|
+
code: ErrorCode.ConstraintError,
|
|
730
|
+
message: 'Internal parser error',
|
|
731
|
+
detail: 'Unexpected operator type',
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
const rp = resolveParam(paramTok.text, this.params, this.options.caseSensitive);
|
|
736
|
+
if (rp.error.code !== ErrorCode.Ok) {
|
|
737
|
+
return { constraint: null, error: rp.error };
|
|
738
|
+
}
|
|
739
|
+
const leftParam = rp.paramIndex;
|
|
740
|
+
if (this.current().type === TokenType.Number) {
|
|
741
|
+
const numTok = this.advance();
|
|
742
|
+
const literal = Number.parseFloat(numTok.text);
|
|
743
|
+
return {
|
|
744
|
+
constraint: RelationalNode.fromLiteral(leftParam, op, literal, this.params[leftParam].values),
|
|
745
|
+
error: okError(),
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
if (this.current().type === TokenType.Identifier) {
|
|
749
|
+
const rhsTok = this.advance();
|
|
750
|
+
// Check if RHS is a parameter name
|
|
751
|
+
if (isParameterName(rhsTok.text, this.params, this.options.caseSensitive)) {
|
|
752
|
+
const rp2 = resolveParam(rhsTok.text, this.params, this.options.caseSensitive);
|
|
753
|
+
if (rp2.error.code !== ErrorCode.Ok) {
|
|
754
|
+
return { constraint: null, error: rp2.error };
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
constraint: RelationalNode.fromParams(leftParam, op, rp2.paramIndex, this.params[leftParam].values, this.params[rp2.paramIndex].values),
|
|
758
|
+
error: okError(),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
// Try parsing as a number
|
|
762
|
+
if (isNumericString(rhsTok.text)) {
|
|
763
|
+
const literal = Number.parseFloat(rhsTok.text);
|
|
764
|
+
return {
|
|
765
|
+
constraint: RelationalNode.fromLiteral(leftParam, op, literal, this.params[leftParam].values),
|
|
766
|
+
error: okError(),
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
constraint: null,
|
|
771
|
+
error: {
|
|
772
|
+
code: ErrorCode.ConstraintError,
|
|
773
|
+
message: `Expected number or parameter after relational operator at position ${rhsTok.position}`,
|
|
774
|
+
detail: 'Relational operators (>, >=, <, <=) require numeric values or parameter names',
|
|
775
|
+
},
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
constraint: null,
|
|
780
|
+
error: {
|
|
781
|
+
code: ErrorCode.ConstraintError,
|
|
782
|
+
message: `Expected value after relational operator at position ${this.current().position}`,
|
|
783
|
+
detail: 'Syntax: parameter > number or parameter > parameter',
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Parse a human-readable constraint expression into an AST.
|
|
790
|
+
*
|
|
791
|
+
* Supported syntax examples:
|
|
792
|
+
* "IF os=mac THEN browser!=ie"
|
|
793
|
+
* "IF os=mac THEN browser!=ie ELSE arch!=arm"
|
|
794
|
+
* "NOT (os=win AND browser=safari)"
|
|
795
|
+
* "os=linux IMPLIES arch!=arm"
|
|
796
|
+
* "os=win OR browser=chrome"
|
|
797
|
+
* "NOT os=linux"
|
|
798
|
+
* "version > 3"
|
|
799
|
+
* "env IN {staging, prod}"
|
|
800
|
+
* "browser LIKE chrome*"
|
|
801
|
+
* "start_date < end_date" (parameter-to-parameter comparison)
|
|
802
|
+
*
|
|
803
|
+
* Keywords (case-insensitive): IF, THEN, ELSE, IMPLIES, AND, OR, NOT, IN, LIKE
|
|
804
|
+
* Operators: = != > >= < <=
|
|
805
|
+
* Parentheses: ( )
|
|
806
|
+
* Set literals: { value1, value2, ... }
|
|
807
|
+
*
|
|
808
|
+
* @param expression The constraint string to parse.
|
|
809
|
+
* @param params The parameter definitions (used to resolve names to indices).
|
|
810
|
+
* @param options Parsing options (e.g., case sensitivity). Defaults to case-insensitive.
|
|
811
|
+
* @returns ParseResult with the AST on success, or an error on failure.
|
|
812
|
+
*/
|
|
813
|
+
export function parseConstraint(expression, params, options = { caseSensitive: false }) {
|
|
814
|
+
if (expression.length === 0) {
|
|
815
|
+
return {
|
|
816
|
+
constraint: null,
|
|
817
|
+
error: {
|
|
818
|
+
code: ErrorCode.ConstraintError,
|
|
819
|
+
message: 'Empty constraint expression',
|
|
820
|
+
detail: 'Provide a non-empty constraint string',
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
const tokResult = tokenize(expression);
|
|
825
|
+
if (tokResult.error.code !== ErrorCode.Ok) {
|
|
826
|
+
return { constraint: null, error: tokResult.error };
|
|
827
|
+
}
|
|
828
|
+
const parser = new Parser(tokResult.tokens, params, options);
|
|
829
|
+
return parser.parse();
|
|
830
|
+
}
|
|
831
|
+
//# sourceMappingURL=constraint-parser.js.map
|