@matter/model 0.12.0-alpha.0-20241229-9d9c99934 → 0.12.0-alpha.0-20241231-9ac20db97

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 (41) hide show
  1. package/dist/cjs/aspects/Constraint.d.ts +24 -15
  2. package/dist/cjs/aspects/Constraint.d.ts.map +1 -1
  3. package/dist/cjs/aspects/Constraint.js +268 -198
  4. package/dist/cjs/aspects/Constraint.js.map +2 -2
  5. package/dist/cjs/common/FieldValue.d.ts +9 -3
  6. package/dist/cjs/common/FieldValue.d.ts.map +1 -1
  7. package/dist/cjs/common/FieldValue.js +1 -1
  8. package/dist/cjs/common/FieldValue.js.map +1 -1
  9. package/dist/cjs/logic/definition-validation/ValueValidator.js +1 -1
  10. package/dist/cjs/logic/definition-validation/ValueValidator.js.map +1 -1
  11. package/dist/cjs/parser/Lexer.d.ts +3 -3
  12. package/dist/cjs/parser/Lexer.d.ts.map +1 -1
  13. package/dist/cjs/parser/Lexer.js +35 -31
  14. package/dist/cjs/parser/Lexer.js.map +1 -1
  15. package/dist/cjs/parser/Token.d.ts +5 -2
  16. package/dist/cjs/parser/Token.d.ts.map +1 -1
  17. package/dist/cjs/parser/TokenStream.js +2 -2
  18. package/dist/esm/aspects/Constraint.d.ts +24 -15
  19. package/dist/esm/aspects/Constraint.d.ts.map +1 -1
  20. package/dist/esm/aspects/Constraint.js +269 -199
  21. package/dist/esm/aspects/Constraint.js.map +2 -2
  22. package/dist/esm/common/FieldValue.d.ts +9 -3
  23. package/dist/esm/common/FieldValue.d.ts.map +1 -1
  24. package/dist/esm/common/FieldValue.js +1 -1
  25. package/dist/esm/common/FieldValue.js.map +1 -1
  26. package/dist/esm/logic/definition-validation/ValueValidator.js +1 -1
  27. package/dist/esm/logic/definition-validation/ValueValidator.js.map +1 -1
  28. package/dist/esm/parser/Lexer.d.ts +3 -3
  29. package/dist/esm/parser/Lexer.d.ts.map +1 -1
  30. package/dist/esm/parser/Lexer.js +35 -31
  31. package/dist/esm/parser/Lexer.js.map +1 -1
  32. package/dist/esm/parser/Token.d.ts +5 -2
  33. package/dist/esm/parser/Token.d.ts.map +1 -1
  34. package/dist/esm/parser/TokenStream.js +2 -2
  35. package/package.json +4 -4
  36. package/src/aspects/Constraint.ts +340 -215
  37. package/src/common/FieldValue.ts +9 -4
  38. package/src/logic/definition-validation/ValueValidator.ts +1 -1
  39. package/src/parser/Lexer.ts +38 -40
  40. package/src/parser/Token.ts +11 -1
  41. package/src/parser/TokenStream.ts +2 -2
@@ -4,7 +4,10 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { camelize, isObject } from "@matter/general";
7
+ import { Lexer } from "#parser/Lexer.js";
8
+ import { BasicToken } from "#parser/Token.js";
9
+ import { TokenStream } from "#parser/TokenStream.js";
10
+ import { camelize } from "@matter/general";
8
11
  import { FieldValue } from "../common/index.js";
9
12
  import { Aspect } from "./Aspect.js";
10
13
 
@@ -12,17 +15,15 @@ import { Aspect } from "./Aspect.js";
12
15
  * An operational view of constraints as defined by the Matter specification.
13
16
  *
14
17
  * A "constraint" limits possible data values.
15
- *
16
- * Formally a constraint is not considered a quality by the specification. It is handled similarly to qualities, though,
17
- * so we keep it in the same section.
18
18
  */
19
19
  export class Constraint extends Aspect<Constraint.Definition> implements Constraint.Ast {
20
20
  declare desc?: boolean;
21
- declare value?: FieldValue;
22
- declare min?: FieldValue;
23
- declare max?: FieldValue;
21
+ declare value?: Constraint.Expression;
22
+ declare min?: Constraint.Expression;
23
+ declare max?: Constraint.Expression;
24
24
  declare in?: FieldValue;
25
25
  declare entry?: Constraint;
26
+ declare cpMax?: number;
26
27
  declare parts?: Constraint[];
27
28
 
28
29
  /**
@@ -40,7 +41,7 @@ export class Constraint extends Aspect<Constraint.Definition> implements Constra
40
41
  if (definition.match(/(?:0b[0x ]*x[0x ]*)|(?:0x[0x_]*x[0x_]*)|(?:00[0x]*x)/i)) {
41
42
  break;
42
43
  }
43
- ast = Constraint.parse(this, definition);
44
+ ast = Parser.parse(this, definition);
44
45
  break;
45
46
 
46
47
  case "number":
@@ -77,6 +78,9 @@ export class Constraint extends Aspect<Constraint.Definition> implements Constra
77
78
  if (ast.entry !== undefined) {
78
79
  this.entry = new Constraint(ast.entry);
79
80
  }
81
+ if (ast.cpMax !== undefined) {
82
+ this.cpMax = ast.cpMax;
83
+ }
80
84
  if (ast.parts !== undefined) {
81
85
  this.parts = ast.parts.map(p => new Constraint(p));
82
86
  }
@@ -89,14 +93,40 @@ export class Constraint extends Aspect<Constraint.Definition> implements Constra
89
93
  */
90
94
  test(value: FieldValue, properties?: Record<string, any>): boolean {
91
95
  // Helper that looks up "reference" field values in properties. This is for constraints such as "min FieldName"
92
- function valueOf(value: unknown, raw = false) {
96
+ function valueOf(value: Constraint.Expression | undefined, raw = false): FieldValue | undefined {
93
97
  if (!raw && (typeof value === "string" || Array.isArray(value))) {
94
98
  return value.length;
95
99
  }
96
- if (isObject(value)) {
97
- const { type, name } = value;
98
- if (type === FieldValue.reference && typeof name === "string") {
99
- value = valueOf(properties?.[camelize(name)], raw);
100
+ if (typeof value === "object" && value !== null && "type" in value) {
101
+ const { type } = value;
102
+ switch (type) {
103
+ case FieldValue.reference:
104
+ if (typeof value.name === "string") {
105
+ value = valueOf(properties?.[camelize(value.name)], raw);
106
+ }
107
+ break;
108
+
109
+ case "+":
110
+ {
111
+ const lhs = valueOf(value.lhs);
112
+ const rhs = valueOf(value.rhs);
113
+ if (typeof lhs === "number" && typeof rhs === "number") {
114
+ return lhs + rhs;
115
+ }
116
+ return undefined;
117
+ }
118
+ break;
119
+
120
+ case "-":
121
+ {
122
+ const lhs = valueOf(value.lhs);
123
+ const rhs = valueOf(value.rhs);
124
+ if (typeof lhs === "number" && typeof rhs === "number") {
125
+ return lhs - rhs;
126
+ }
127
+ return undefined;
128
+ }
129
+ break;
100
130
  }
101
131
  }
102
132
 
@@ -110,7 +140,7 @@ export class Constraint extends Aspect<Constraint.Definition> implements Constra
110
140
  if (this.in) {
111
141
  let set = valueOf(this.in, true);
112
142
  if (!Array.isArray(set)) {
113
- set = [set];
143
+ set = set === undefined ? [] : [set];
114
144
  }
115
145
  return (set as unknown[]).indexOf(value) !== -1;
116
146
  }
@@ -126,14 +156,14 @@ export class Constraint extends Aspect<Constraint.Definition> implements Constra
126
156
 
127
157
  if (this.min !== undefined && this.min !== null) {
128
158
  const min = valueOf(this.min);
129
- if (min !== undefined && min !== null && (min as typeof value) > value) {
159
+ if (min !== undefined && min !== null && min > value) {
130
160
  return false;
131
161
  }
132
162
  }
133
163
 
134
164
  if (this.max !== undefined && this.max !== null) {
135
165
  const max = valueOf(this.max);
136
- if (max !== undefined && max !== null && (max as typeof value) < value) {
166
+ if (max !== undefined && max !== null && max < value) {
137
167
  return false;
138
168
  }
139
169
  }
@@ -149,7 +179,7 @@ export class Constraint extends Aspect<Constraint.Definition> implements Constra
149
179
  if (!this.valid && this.definition) {
150
180
  return this.definition.toString();
151
181
  }
152
- return Constraint.serialize(this);
182
+ return Serializer.serialize(this);
153
183
  }
154
184
 
155
185
  protected override freeze() {
@@ -164,7 +194,7 @@ export namespace Constraint {
164
194
  export type NumberOrIdentifier = number | string;
165
195
 
166
196
  /**
167
- * Parsed list structure.
197
+ * Parsed constraint.
168
198
  */
169
199
  export type Ast = {
170
200
  /**
@@ -175,17 +205,17 @@ export namespace Constraint {
175
205
  /**
176
206
  * Constant value.
177
207
  */
178
- value?: FieldValue;
208
+ value?: Expression;
179
209
 
180
210
  /**
181
211
  * Lower bound on value or sequence length.
182
212
  */
183
- min?: FieldValue;
213
+ min?: Expression;
184
214
 
185
215
  /**
186
216
  * Upper bound on value or sequence length.
187
217
  */
188
- max?: FieldValue;
218
+ max?: Expression;
189
219
 
190
220
  /**
191
221
  * Require set membership for the value.
@@ -197,268 +227,363 @@ export namespace Constraint {
197
227
  */
198
228
  entry?: Ast;
199
229
 
230
+ /**
231
+ * Constraint on codepoints in a string.
232
+ */
233
+ cpMax?: number;
234
+
200
235
  /**
201
236
  * List of sub-constraints in a sequence.
202
237
  */
203
238
  parts?: Ast[];
204
239
  };
205
240
 
241
+ /**
242
+ * Parsed binary operator.
243
+ */
244
+ export interface BinaryOperator {
245
+ type: "+" | "-";
246
+
247
+ lhs: Expression;
248
+
249
+ rhs: Expression;
250
+ }
251
+
252
+ /**
253
+ * Parsed expression.
254
+ */
255
+ export type Expression = FieldValue | BinaryOperator;
256
+
206
257
  /**
207
258
  * These are all ways to describe a constraint.
208
259
  */
209
260
  export type Definition = (Ast & { definition?: Definition }) | string | number | undefined;
261
+ }
210
262
 
211
- function parseValue(numOrName: string): FieldValue {
212
- let value;
213
- if (numOrName.match(/^-?0[xb]/)) {
214
- value = Number.parseInt(numOrName.replace(/[_ ]/g, ""));
215
- } else {
216
- value = Number.parseFloat(numOrName);
263
+ namespace Serializer {
264
+ export function serialize(ast: Constraint.Ast): string {
265
+ if (ast.parts) {
266
+ return ast.parts.map(serialize).join(", ");
217
267
  }
218
- if (typeof numOrName === "string") {
219
- const lower = numOrName.toLowerCase();
220
- switch (lower) {
221
- case "true":
222
- return true;
268
+ if (ast.entry) {
269
+ return `${serializeAtom(ast)}[${serialize(ast.entry)}]`;
270
+ }
271
+ if (ast.cpMax) {
272
+ return `${serializeAtom(ast)}{${ast.cpMax}}`;
273
+ }
274
+ return serializeAtom(ast);
275
+ }
223
276
 
224
- case "false":
225
- return false;
226
- }
277
+ function serializeValue(value: Constraint.Expression, inExpr = false): string {
278
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date) {
279
+ return FieldValue.serialize(value);
227
280
  }
228
- if (Number.isNaN(value)) {
229
- return FieldValue.Reference(camelize(numOrName));
281
+
282
+ switch (value.type) {
283
+ case "+":
284
+ case "-":
285
+ const sum = `${serializeValue(value.lhs, true)} ${value.type} ${serializeValue(value.rhs, true)}`;
286
+ if (inExpr) {
287
+ // Ideally only add parenthesis if precedence requires. But nested expressions are not used
288
+ // anywhere as yet (and probably won't be) so don't try to be fancy, just correct
289
+ return `(${sum})`;
290
+ }
291
+ return sum;
292
+
293
+ default:
294
+ return FieldValue.serialize(value);
230
295
  }
231
- if (numOrName.endsWith("%")) {
232
- return FieldValue.Percent(value);
296
+ }
297
+
298
+ function serializeAtom(ast: Constraint.Ast) {
299
+ if (ast.desc) {
300
+ return "desc";
233
301
  }
234
- if (numOrName.endsWith("°C")) {
235
- return FieldValue.Celsius(value);
302
+
303
+ if (ast.value !== undefined && ast.value !== null) {
304
+ return `${serializeValue(ast.value)}`;
236
305
  }
237
- return value;
306
+
307
+ if (ast.min !== undefined && ast.min !== null) {
308
+ if (ast.max === undefined || ast.max === null) {
309
+ return `min ${serializeValue(ast.min)}`;
310
+ }
311
+ return `${serializeValue(ast.min)} to ${serializeValue(ast.max)}`;
312
+ }
313
+
314
+ if (ast.max !== undefined && ast.max !== null) {
315
+ return `max ${serializeValue(ast.max)}`;
316
+ }
317
+
318
+ if (ast.in !== undefined) {
319
+ return `in ${serializeValue(ast.in)}`;
320
+ }
321
+
322
+ return "all";
238
323
  }
324
+ }
239
325
 
240
- function parseAtom(constraint: Constraint, words: string[]): Ast | undefined {
241
- switch (words.length) {
242
- case 0:
243
- return undefined;
326
+ namespace Parser {
327
+ const lexer = new Lexer(["in", "min", "max", "to", "all", "desc", "true", "false"]);
244
328
 
245
- case 1:
246
- switch (words[0].toLowerCase()) {
247
- case "desc":
248
- return { desc: true };
329
+ export function parse(constraint: Constraint, definition: string): Constraint.Ast {
330
+ const tokens = TokenStream(lexer.lex(definition, (code, message) => constraint.error(code, message)));
249
331
 
250
- case "all":
251
- case "any":
252
- return {};
253
- }
254
- const value = parseValue(words[0]);
255
- if (value === undefined || value === null) {
256
- return;
257
- }
258
- return { value };
332
+ const result = parseParts();
259
333
 
260
- case 2:
261
- switch (words[0].toLowerCase()) {
262
- case "min":
263
- const min = parseValue(words[1]);
264
- if (min === undefined || min === null) {
265
- return;
266
- }
267
- return { min: min };
334
+ if (tokens.token && tokens.token?.type !== ",") {
335
+ constraint.error("UNEXPECTED_CONSTRAINT_TOKEN", `Unexpected ${tokens.description}`);
336
+ }
268
337
 
269
- case "max":
270
- const max = parseValue(words[1]);
271
- if (max === undefined || max === null) {
272
- return;
273
- }
274
- return { max: max };
338
+ return result;
275
339
 
276
- case "in":
277
- const ref = parseValue(words[1]);
278
- return { in: ref };
340
+ function parseParts(): Constraint.Ast {
341
+ const parts = Array<Constraint.Ast>();
279
342
 
280
- default:
281
- constraint.error(
282
- "INVALID_CONSTRAINT",
283
- `Two word constraint "${words.join(" ")}" does not start with "min" or "max"`,
284
- );
343
+ while (true) {
344
+ const part = parsePart();
345
+
346
+ if (part !== undefined) {
347
+ parts.push(part);
285
348
  }
286
- return;
287
349
 
288
- case 3:
289
- if (words[1].toLowerCase() === "to") {
290
- function parseBound(name: string, pos: number) {
291
- if (words[pos].toLowerCase() === name) {
292
- return undefined;
293
- }
294
- return parseValue(words[pos]);
295
- }
350
+ if (tokens.done) {
351
+ break;
352
+ }
296
353
 
297
- const ast: Ast = {};
354
+ if (tokens.token?.type !== ",") {
355
+ break;
356
+ }
298
357
 
299
- const min = parseBound("min", 0);
300
- if (min !== undefined && min !== null) {
301
- ast.min = min;
302
- }
358
+ tokens.next();
359
+ }
303
360
 
304
- const max = parseBound("max", 2);
305
- if (max !== undefined && max !== null) {
306
- ast.max = max;
307
- }
361
+ if (!parts.length) {
362
+ return {};
363
+ }
308
364
 
309
- if ((ast.min !== undefined && ast.min !== null) || (ast.max !== undefined && ast.max !== null)) {
310
- return ast;
311
- }
312
- }
313
- return;
365
+ if (parts.length === 1) {
366
+ return parts[0];
367
+ }
368
+
369
+ return { parts };
314
370
  }
315
371
 
316
- constraint.error("INVALID_CONSTRAINT", `Unrecognized value constraint "${words.join(" ")}"`);
317
- }
372
+ function parsePart(): Constraint.Ast | undefined {
373
+ const result = parsePartWithoutSubconstraint();
318
374
 
319
- /**
320
- * Parse constraint DSL. Extremely lenient.
321
- */
322
- export function parse(constraint: Constraint, definition: string): Ast {
323
- let pos = 2;
324
- let current: string | undefined = definition[0];
325
- let peeked: string | undefined = definition[1];
326
-
327
- function next() {
328
- current = peeked;
329
- if (pos === definition.length) {
330
- peeked = undefined;
331
- } else {
332
- peeked = definition[pos];
333
- pos++;
375
+ if (result === undefined) {
376
+ return result;
334
377
  }
335
- }
336
378
 
337
- function scan(depth: number): Ast {
338
- const parts = Array<Ast>();
339
- let words = Array<string>();
340
- let word = "";
379
+ switch (tokens.token?.type) {
380
+ case "[":
381
+ {
382
+ tokens.next();
341
383
 
342
- function parseWords() {
343
- if (word) {
344
- words.push(word);
345
- word = "";
346
- }
384
+ const entry = parseParts();
347
385
 
348
- const atom = parseAtom(constraint, words);
349
- words = Array<string>();
350
- return atom;
351
- }
386
+ if (tokens.token?.type !== ("]" as any)) {
387
+ constraint.error("MISSING_ENTRY_END", 'Entry constraint does not end with "]"');
388
+ }
352
389
 
353
- function emit() {
354
- const atom = parseWords();
355
- if (atom !== undefined) {
356
- parts.push(atom);
357
- }
358
- }
390
+ tokens.next();
359
391
 
360
- while (current !== undefined) {
361
- switch (current) {
362
- case " ":
363
- case "\t":
364
- case "\r":
365
- case "\n":
366
- case "\v":
367
- case "\f":
368
- if (word) {
369
- words.push(word);
370
- word = "";
392
+ if (entry !== undefined) {
393
+ result.entry = entry;
371
394
  }
372
- break;
395
+ }
396
+ break;
373
397
 
374
- case "[":
375
- next();
376
- let ast = parseWords();
377
- const entry = scan(depth + 1);
378
- if (entry) {
379
- if (!ast) {
380
- ast = {};
398
+ case "{":
399
+ {
400
+ tokens.next();
401
+
402
+ if (tokens.token?.type !== ("value" as any)) {
403
+ constraint.error(
404
+ "MISSING_CODEPOINT_MAX",
405
+ "Codepoint constraint does not specify maximum codepoint length",
406
+ );
407
+ if (tokens.peeked?.type === "}") {
408
+ tokens.next();
381
409
  }
382
- ast.entry = entry;
410
+ } else {
411
+ result.cpMax = FieldValue.numericValue(
412
+ (tokens.token as unknown as BasicToken.Number).value,
413
+ );
414
+ tokens.next();
383
415
  }
384
- if (ast) {
385
- parts.push(ast);
386
- }
387
- break;
388
416
 
389
- case "]":
390
- if (!depth) {
391
- constraint.error("INVALID_CONSTRAINT", 'Unexpected "]"');
392
- break;
393
- }
394
- emit();
395
- if (parts.length > 1) {
396
- return { parts: parts };
417
+ if (tokens.token?.type !== ("}" as any)) {
418
+ constraint.error("MISSING_CODEPOINT_END", 'Codepoint constraint does not end with "}"');
397
419
  }
398
- return parts[0];
399
420
 
400
- case ",":
401
- emit();
402
- break;
421
+ tokens.next();
422
+ }
423
+ break;
424
+ }
403
425
 
404
- default:
405
- word += current;
406
- break;
407
- }
426
+ return result;
427
+ }
428
+
429
+ function parsePartWithoutSubconstraint(): Constraint.Ast | undefined {
430
+ const { token } = tokens;
408
431
 
409
- next();
432
+ if (!token) {
433
+ return;
410
434
  }
411
435
 
412
- if (depth) {
413
- constraint.error("INVALID_CONSTRAINT", "Unterminated sub-constraint");
436
+ switch (token.type) {
437
+ case "desc":
438
+ tokens.next();
439
+ return { desc: true };
440
+
441
+ case "all":
442
+ tokens.next();
443
+ return {};
444
+
445
+ case "min":
446
+ case "max":
447
+ tokens.next();
448
+ return parseSingleBound(token.type);
449
+
450
+ case "in":
451
+ tokens.next();
452
+ if (tokens.token?.type === "word") {
453
+ const name = tokens.token.value;
454
+ tokens.next();
455
+ return { in: FieldValue.Reference(name) };
456
+ }
457
+ constraint.error("MISSING_IN_FIELD", 'Expected field name to follow "in"');
458
+ break;
414
459
  }
415
460
 
416
- emit();
461
+ const value = parseExpression();
417
462
 
418
- if (parts.length < 2) {
419
- return parts[0];
463
+ if (value === undefined || tokens.token?.type !== "to") {
464
+ return { value };
420
465
  }
421
466
 
422
- return { parts: parts };
423
- }
467
+ tokens.next();
424
468
 
425
- return scan(0);
426
- }
469
+ const max = parseExpression();
470
+ if (max === undefined) {
471
+ constraint.error("MISSING_UPPER_BOUND", `"to" must be followed by upper boundary value`);
472
+ return;
473
+ }
427
474
 
428
- function serializeAtom(ast: Ast) {
429
- if (ast.desc) {
430
- return "desc";
475
+ return {
476
+ min: value,
477
+ max,
478
+ };
431
479
  }
432
480
 
433
- if (ast.value !== undefined && ast.value !== null) {
434
- return `${FieldValue.serialize(ast.value)}`;
481
+ function parseSingleBound(kind: "min" | "max"): Constraint.Ast | undefined {
482
+ const bound = parseExpression();
483
+ if (bound === undefined) {
484
+ constraint.error("MISSING_SINGLE_BOUND", `"${kind}" must be followed by boundary value`);
485
+ return;
486
+ }
487
+ return { [kind]: bound };
435
488
  }
436
489
 
437
- if (ast.min !== undefined && ast.min !== null) {
438
- if (ast.max === undefined || ast.max === null) {
439
- return `min ${FieldValue.serialize(ast.min)}`;
490
+ function parseExpression(): Constraint.Expression | undefined {
491
+ const value = parseValueExpression();
492
+
493
+ if (value === undefined) {
494
+ return value;
440
495
  }
441
- return `${FieldValue.serialize(ast.min)} to ${FieldValue.serialize(ast.max)}`;
442
- }
443
496
 
444
- if (ast.max !== undefined && ast.max !== null) {
445
- return `max ${FieldValue.serialize(ast.max)}`;
446
- }
497
+ switch (tokens.token?.type) {
498
+ case "+":
499
+ case "-":
500
+ const type = tokens.token.type;
501
+ tokens.next();
502
+ const rhs = parseValueExpression();
503
+ if (rhs === undefined) {
504
+ constraint.error("MISSING_RIGHT_OPERAND", `Missing operand after "${type}"`);
505
+ return;
506
+ }
447
507
 
448
- if (ast.in !== undefined) {
449
- return `in ${FieldValue.serialize(ast.in)}`;
508
+ return {
509
+ type,
510
+ lhs: value,
511
+ rhs,
512
+ };
513
+ }
514
+
515
+ return value;
450
516
  }
451
517
 
452
- return "all";
453
- }
518
+ function parseValueExpression(): Constraint.Expression | undefined {
519
+ const { token } = tokens;
454
520
 
455
- export function serialize(ast: Ast): string {
456
- if (ast.parts) {
457
- return ast.parts.map(serialize).join(", ");
458
- }
459
- if (ast.entry) {
460
- return `${serializeAtom(ast)}[${serialize(ast.entry)}]`;
521
+ if (token === undefined) {
522
+ return;
523
+ }
524
+
525
+ switch (token.type) {
526
+ case "value":
527
+ tokens.next();
528
+ return token.value;
529
+
530
+ case "true":
531
+ tokens.next();
532
+ return true;
533
+
534
+ case "false":
535
+ tokens.next();
536
+ return false;
537
+
538
+ case "word":
539
+ const ref = FieldValue.Reference(camelize(token.value));
540
+ tokens.next();
541
+ return ref;
542
+
543
+ case "-":
544
+ case "+": {
545
+ tokens.next();
546
+
547
+ let number = tokens.token?.type === "value" ? tokens.token.value : undefined;
548
+
549
+ if (number !== undefined) {
550
+ tokens.next();
551
+
552
+ if (token.type === "-") {
553
+ if (typeof number === "number") {
554
+ number *= -1;
555
+ } else if (
556
+ FieldValue.is(number, FieldValue.percent) ||
557
+ FieldValue.is(number, FieldValue.celsius)
558
+ ) {
559
+ (number as FieldValue.Percent | FieldValue.Celsius).value *= -1;
560
+ } else {
561
+ number = undefined;
562
+ }
563
+ }
564
+ }
565
+
566
+ if (number === undefined) {
567
+ constraint.error("MISSING_NUMBER", `Unary "${token.type}" not followed by numeric value`);
568
+ return;
569
+ }
570
+
571
+ return number;
572
+ }
573
+
574
+ case "(": {
575
+ tokens.next();
576
+
577
+ const result = parseExpression();
578
+ if (tokens.token?.type !== ")") {
579
+ constraint.error("MISSING_GROUP_END", 'Group does not end with ")"');
580
+ }
581
+
582
+ tokens.next();
583
+
584
+ return result;
585
+ }
586
+ }
461
587
  }
462
- return serializeAtom(ast);
463
588
  }
464
589
  }