@malloydata/malloy-tag 0.0.335 → 0.0.336

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 (59) hide show
  1. package/CONTEXT.md +83 -9
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +5 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/peggy/dist/peg-tag-parser.d.ts +11 -0
  6. package/dist/peggy/dist/peg-tag-parser.js +3130 -0
  7. package/dist/peggy/dist/peg-tag-parser.js.map +1 -0
  8. package/dist/peggy/index.d.ts +13 -0
  9. package/dist/peggy/index.js +117 -0
  10. package/dist/peggy/index.js.map +1 -0
  11. package/dist/peggy/interpreter.d.ts +32 -0
  12. package/dist/peggy/interpreter.js +208 -0
  13. package/dist/peggy/interpreter.js.map +1 -0
  14. package/dist/peggy/statements.d.ts +51 -0
  15. package/dist/peggy/statements.js +7 -0
  16. package/dist/peggy/statements.js.map +1 -0
  17. package/dist/schema.d.ts +41 -0
  18. package/dist/schema.js +573 -0
  19. package/dist/schema.js.map +1 -0
  20. package/dist/schema.spec.d.ts +1 -0
  21. package/dist/schema.spec.js +980 -0
  22. package/dist/schema.spec.js.map +1 -0
  23. package/dist/tags.d.ts +144 -37
  24. package/dist/tags.js +535 -344
  25. package/dist/tags.js.map +1 -1
  26. package/dist/tags.spec.js +524 -45
  27. package/dist/tags.spec.js.map +1 -1
  28. package/package.json +6 -8
  29. package/src/index.ts +3 -0
  30. package/src/motly-schema.motly +52 -0
  31. package/src/peggy/dist/peg-tag-parser.js +2790 -0
  32. package/src/peggy/index.ts +89 -0
  33. package/src/peggy/interpreter.ts +265 -0
  34. package/src/peggy/malloy-tag.peggy +224 -0
  35. package/src/peggy/statements.ts +49 -0
  36. package/src/schema.spec.ts +1280 -0
  37. package/src/schema.ts +852 -0
  38. package/src/tags.spec.ts +591 -46
  39. package/src/tags.ts +597 -398
  40. package/tsconfig.json +3 -2
  41. package/dist/lib/Malloy/MalloyTagLexer.d.ts +0 -42
  42. package/dist/lib/Malloy/MalloyTagLexer.js +0 -395
  43. package/dist/lib/Malloy/MalloyTagLexer.js.map +0 -1
  44. package/dist/lib/Malloy/MalloyTagParser.d.ts +0 -180
  45. package/dist/lib/Malloy/MalloyTagParser.js +0 -1077
  46. package/dist/lib/Malloy/MalloyTagParser.js.map +0 -1
  47. package/dist/lib/Malloy/MalloyTagVisitor.d.ts +0 -120
  48. package/dist/lib/Malloy/MalloyTagVisitor.js +0 -4
  49. package/dist/lib/Malloy/MalloyTagVisitor.js.map +0 -1
  50. package/scripts/build_parser.js +0 -98
  51. package/src/MalloyTag.g4 +0 -104
  52. package/src/lib/Malloy/MalloyTag.interp +0 -61
  53. package/src/lib/Malloy/MalloyTag.tokens +0 -32
  54. package/src/lib/Malloy/MalloyTagLexer.interp +0 -85
  55. package/src/lib/Malloy/MalloyTagLexer.tokens +0 -32
  56. package/src/lib/Malloy/MalloyTagLexer.ts +0 -386
  57. package/src/lib/Malloy/MalloyTagParser.ts +0 -1065
  58. package/src/lib/Malloy/MalloyTagVisitor.ts +0 -141
  59. package/src/lib/Malloy/_BUILD_DIGEST_ +0 -1
package/dist/tags.js CHANGED
@@ -22,34 +22,48 @@
22
22
  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  */
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.Tag = void 0;
26
- const tree_1 = require("antlr4ts/tree");
27
- const MalloyTagLexer_1 = require("./lib/Malloy/MalloyTagLexer");
28
- const MalloyTagParser_1 = require("./lib/Malloy/MalloyTagParser");
29
- const antlr4ts_1 = require("antlr4ts");
30
- const util_1 = require("./util");
25
+ exports.RefTag = exports.Tag = void 0;
26
+ exports.interfaceFromTag = interfaceFromTag;
27
+ exports.interfaceFromDict = interfaceFromDict;
31
28
  /**
32
29
  * Class for interacting with the parsed output of an annotation
33
30
  * containing the Malloy tag language. Used by the parser to
34
31
  * generate parsed data, and as an API to that data.
35
32
  * ```
36
33
  * tag.text(p?) => string value of tag.p or undefined
37
- * tag.array(p?) => Tag[] value of tag.p or undefined
38
34
  * tag.numeric(p?) => numeric value of tag.p or undefined
39
- * tag.textArray(p ?) => string[] value of elements in tag.p or undefined
40
- * tag.numericArray(p?) => string[] value of elements in tag.p or undefined
41
- * tag.tag(p?) => Tag value of tag.p
42
- * tag.has(p?) => boolean "tag contains tag.p"
43
- * tag.bare(p?) => tag.p exists and has no properties
35
+ * tag.boolean(p?) => boolean value of tag.p or undefined
36
+ * tag.isTrue(p?) => true if tag.p is boolean true
37
+ * tag.isFalse(p?) => true if tag.p is boolean false
38
+ * tag.date(p?) => Date value of tag.p or undefined
39
+ * tag.array(p?) => Tag[] value of tag.p or undefined
40
+ * tag.textArray(p?) => string[] value of elements in tag.p or undefined
41
+ * tag.numericArray(p?) => number[] value of elements in tag.p or undefined
42
+ * tag.tag(p?) => Tag value of tag.p
43
+ * tag.has(p?) => boolean "tag contains tag.p"
44
+ * tag.bare(p?) => tag.p exists and has no properties
44
45
  * tag.dict => Record<string,Tag> of tag properties
46
+ * tag.toObject() => Plain JS object representation
45
47
  * ```
46
48
  */
47
49
  class Tag {
48
- static tagFrom(from = {}) {
49
- if (from instanceof Tag) {
50
- return from;
50
+ /**
51
+ * Get the parent tag, if this tag is part of a tree.
52
+ */
53
+ get parent() {
54
+ return this._parent;
55
+ }
56
+ /**
57
+ * Get the root tag by traversing up the parent chain.
58
+ * Returns this tag if it has no parent.
59
+ */
60
+ get root() {
61
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
62
+ let current = this;
63
+ while (current._parent !== undefined) {
64
+ current = current._parent;
51
65
  }
52
- return new Tag(from);
66
+ return current;
53
67
  }
54
68
  static id(t) {
55
69
  let thisTagId = Tag.ids.get(t);
@@ -69,62 +83,43 @@ class Tag {
69
83
  return str + `=${this.eq}`;
70
84
  }
71
85
  str += ' {';
72
- if (this.eq) {
73
- if (typeof this.eq === 'string') {
74
- str += `\n${spaces} =: ${this.eq}`;
75
- }
76
- else {
86
+ if (this.eq !== undefined) {
87
+ if (Array.isArray(this.eq)) {
77
88
  str += `\n${spaces} =: [\n${spaces} ${this.eq
78
- .map(el => Tag.tagFrom(el).peek(indent + 4))
89
+ .map(el => el.peek(indent + 4))
79
90
  .join(`\n${spaces} `)}\n${spaces} ]`;
80
91
  }
92
+ else {
93
+ str += `\n${spaces} =: ${this.eq}`;
94
+ }
81
95
  }
82
96
  if (this.properties) {
83
97
  for (const k in this.properties) {
84
- const val = Tag.tagFrom(this.properties[k]);
85
- str += `\n${spaces} ${k}: ${val.peek(indent + 2)}`;
98
+ str += `\n${spaces} ${k}: ${this.properties[k].peek(indent + 2)}`;
86
99
  }
87
100
  }
88
101
  str += `\n${spaces}}`;
89
102
  return str;
90
103
  }
91
- /**
92
- * Parse a line of Malloy tag language into a Tag which can be queried
93
- * @param source -- The source line to be parsed. If the string starts with #, then it skips
94
- * all characters up to the first space.
95
- * @param lineNumber -- A line number to be associated with the parse errors.
96
- * @param extending A tag which this line will be extending
97
- * @param importing Outer "scopes" for $() references
98
- * @returns Something shaped like { tag: Tag, log: ParseErrors[] }
99
- */
100
- static fromTagLine(source, lineNumber = 0, extending, ...importing) {
101
- return parseTagLine(source, extending, importing, lineNumber);
102
- }
103
- /**
104
- * Parse multiple lines of Malloy tag language, merging them into a single Tag
105
- * @param lines -- The source line to be parsed. If the string starts with #, then it skips
106
- * all characters up to the first space.
107
- * @param extending A tag which this line will be extending
108
- * @param importing Outer "scopes" for $() references
109
- * @returns Something shaped like { tag: Tag, log: ParseErrors[] }
110
- */
111
- static fromTagLines(lines, extending, ...importing) {
112
- const allErrs = [];
113
- let current = extending;
114
- for (let i = 0; i < lines.length; i++) {
115
- const text = lines[i];
116
- const noteParse = parseTagLine(text, current, importing, i);
117
- current = noteParse.tag;
118
- allErrs.push(...noteParse.log);
104
+ constructor(from = {}, parent) {
105
+ if (parent !== undefined) {
106
+ this._parent = parent;
119
107
  }
120
- return { tag: current, log: allErrs };
121
- }
122
- constructor(from = {}) {
123
- if (from.eq) {
124
- this.eq = from.eq;
108
+ if (from.eq !== undefined) {
109
+ if (Array.isArray(from.eq)) {
110
+ // Convert array elements to Tags
111
+ this.eq = from.eq.map(el => el instanceof Tag ? el : new Tag(el, this));
112
+ }
113
+ else {
114
+ this.eq = from.eq;
115
+ }
125
116
  }
126
117
  if (from.properties) {
127
- this.properties = from.properties;
118
+ // Convert property values to Tags
119
+ this.properties = {};
120
+ for (const [key, val] of Object.entries(from.properties)) {
121
+ this.properties[key] = val instanceof Tag ? val : new Tag(val, this);
122
+ }
128
123
  }
129
124
  if (from.deleted) {
130
125
  this.deleted = from.deleted;
@@ -141,62 +136,149 @@ class Tag {
141
136
  }
142
137
  text(...at) {
143
138
  var _a;
144
- const str = (_a = this.find(at)) === null || _a === void 0 ? void 0 : _a.eq;
145
- if (typeof str === 'string') {
146
- return str;
139
+ const val = (_a = this.find(at)) === null || _a === void 0 ? void 0 : _a.getEq();
140
+ if (val === undefined || Array.isArray(val)) {
141
+ return undefined;
142
+ }
143
+ if (val instanceof Date) {
144
+ return val.toISOString();
147
145
  }
146
+ return String(val);
148
147
  }
149
148
  numeric(...at) {
150
149
  var _a;
151
- const str = (_a = this.find(at)) === null || _a === void 0 ? void 0 : _a.eq;
152
- if (typeof str === 'string') {
153
- const num = Number.parseFloat(str);
150
+ const val = (_a = this.find(at)) === null || _a === void 0 ? void 0 : _a.getEq();
151
+ if (typeof val === 'number') {
152
+ return val;
153
+ }
154
+ if (typeof val === 'string') {
155
+ const num = Number.parseFloat(val);
154
156
  if (!Number.isNaN(num)) {
155
157
  return num;
156
158
  }
157
159
  }
160
+ return undefined;
161
+ }
162
+ boolean(...at) {
163
+ var _a;
164
+ const val = (_a = this.find(at)) === null || _a === void 0 ? void 0 : _a.getEq();
165
+ if (typeof val === 'boolean') {
166
+ return val;
167
+ }
168
+ return undefined;
169
+ }
170
+ isTrue(...at) {
171
+ var _a;
172
+ return ((_a = this.find(at)) === null || _a === void 0 ? void 0 : _a.getEq()) === true;
173
+ }
174
+ isFalse(...at) {
175
+ var _a;
176
+ return ((_a = this.find(at)) === null || _a === void 0 ? void 0 : _a.getEq()) === false;
177
+ }
178
+ date(...at) {
179
+ var _a;
180
+ const val = (_a = this.find(at)) === null || _a === void 0 ? void 0 : _a.getEq();
181
+ if (val instanceof Date) {
182
+ return val;
183
+ }
184
+ return undefined;
158
185
  }
159
186
  bare(...at) {
160
187
  const p = this.find(at);
161
188
  if (p === undefined) {
162
189
  return;
163
190
  }
164
- return (p.properties === undefined || Object.entries(p.properties).length === 0);
191
+ return !p.hasProperties();
192
+ }
193
+ /** Virtual accessor for eq - overridden in RefTag to resolve */
194
+ getEq() {
195
+ return this.eq;
196
+ }
197
+ /** Virtual accessor for a property - overridden in RefTag to resolve */
198
+ getProperty(name) {
199
+ var _a;
200
+ return (_a = this.properties) === null || _a === void 0 ? void 0 : _a[name];
201
+ }
202
+ /** Virtual accessor for an array element - overridden in RefTag to resolve */
203
+ getArrayElement(index) {
204
+ if (Array.isArray(this.eq) && index < this.eq.length) {
205
+ return this.eq[index];
206
+ }
207
+ return undefined;
165
208
  }
166
209
  get dict() {
167
- const newDict = {};
210
+ var _a;
211
+ return (_a = this.properties) !== null && _a !== void 0 ? _a : {};
212
+ }
213
+ /** Iterate over [name, Tag] pairs for each property */
214
+ *entries() {
215
+ if (this.properties) {
216
+ for (const key in this.properties) {
217
+ yield [key, this.properties[key]];
218
+ }
219
+ }
220
+ }
221
+ /** Iterate over property names */
222
+ *keys() {
168
223
  if (this.properties) {
169
224
  for (const key in this.properties) {
170
- newDict[key] = Tag.tagFrom(this.properties[key]);
225
+ yield key;
171
226
  }
172
227
  }
173
- return newDict;
228
+ }
229
+ /** Check if this tag has any properties */
230
+ hasProperties() {
231
+ return (this.properties !== undefined && Object.keys(this.properties).length > 0);
174
232
  }
175
233
  array(...at) {
176
- var _a;
177
- const array = (_a = this.find(at)) === null || _a === void 0 ? void 0 : _a.eq;
178
- if (array === undefined || typeof array === 'string') {
234
+ const found = this.find(at);
235
+ if (found === undefined) {
236
+ return undefined;
237
+ }
238
+ const arr = found.getEq();
239
+ if (!Array.isArray(arr)) {
179
240
  return undefined;
180
241
  }
181
- return array.map(el => typeof el === 'string' ? new Tag({ eq: el }) : Tag.tagFrom(el));
242
+ return arr.map((_, i) => found.getArrayElement(i));
182
243
  }
183
244
  textArray(...at) {
184
- var _a;
185
- const array = (_a = this.find(at)) === null || _a === void 0 ? void 0 : _a.eq;
186
- if (array === undefined || typeof array === 'string') {
245
+ const found = this.find(at);
246
+ if (found === undefined) {
247
+ return undefined;
248
+ }
249
+ const arr = found.getEq();
250
+ if (!Array.isArray(arr)) {
187
251
  return undefined;
188
252
  }
189
- return array.reduce((allStrs, el) => typeof el.eq === 'string' ? allStrs.concat(el.eq) : allStrs, []);
253
+ return arr.reduce((allStrs, _, i) => {
254
+ const el = found.getArrayElement(i);
255
+ const val = el === null || el === void 0 ? void 0 : el.getEq();
256
+ if (val === undefined || Array.isArray(val)) {
257
+ return allStrs;
258
+ }
259
+ if (val instanceof Date) {
260
+ return allStrs.concat(val.toISOString());
261
+ }
262
+ return allStrs.concat(String(val));
263
+ }, []);
190
264
  }
191
265
  numericArray(...at) {
192
- var _a;
193
- const array = (_a = this.find(at)) === null || _a === void 0 ? void 0 : _a.eq;
194
- if (array === undefined || typeof array === 'string') {
266
+ const found = this.find(at);
267
+ if (found === undefined) {
268
+ return undefined;
269
+ }
270
+ const arr = found.getEq();
271
+ if (!Array.isArray(arr)) {
195
272
  return undefined;
196
273
  }
197
- return array.reduce((allNums, el) => {
198
- if (typeof el.eq === 'string') {
199
- const num = Number.parseFloat(el.eq);
274
+ return arr.reduce((allNums, _, i) => {
275
+ const el = found.getArrayElement(i);
276
+ const val = el === null || el === void 0 ? void 0 : el.getEq();
277
+ if (typeof val === 'number') {
278
+ return allNums.concat(val);
279
+ }
280
+ if (typeof val === 'string') {
281
+ const num = Number.parseFloat(val);
200
282
  if (!Number.isNaN(num)) {
201
283
  return allNums.concat(num);
202
284
  }
@@ -204,15 +286,178 @@ class Tag {
204
286
  return allNums;
205
287
  }, []);
206
288
  }
207
- // Has the sometimes desireable side effect of initalizing properties
289
+ // Has the sometimes desirable side effect of initializing properties
208
290
  getProperties() {
209
291
  if (this.properties === undefined) {
210
292
  this.properties = {};
211
293
  }
212
294
  return this.properties;
213
295
  }
214
- clone() {
215
- return new Tag(structuredClone(this));
296
+ clone(newParent) {
297
+ const cloned = new Tag({}, newParent);
298
+ cloned.prefix = this.prefix;
299
+ cloned.deleted = this.deleted;
300
+ if (this.eq !== undefined) {
301
+ if (Array.isArray(this.eq)) {
302
+ cloned.eq = this.eq.map(el => el.clone(cloned));
303
+ }
304
+ else {
305
+ // Scalar value - copy directly (Date needs special handling)
306
+ cloned.eq = this.eq instanceof Date ? new Date(this.eq) : this.eq;
307
+ }
308
+ }
309
+ if (this.properties) {
310
+ cloned.properties = {};
311
+ for (const [key, val] of Object.entries(this.properties)) {
312
+ cloned.properties[key] = val.clone(cloned);
313
+ }
314
+ }
315
+ return cloned;
316
+ }
317
+ static scalarToObject(val) {
318
+ return val;
319
+ }
320
+ static tagToObject(tag) {
321
+ const hasProps = tag.properties !== undefined && Object.keys(tag.properties).length > 0;
322
+ const hasValue = tag.eq !== undefined;
323
+ // Bare tag (no value, no properties)
324
+ if (!hasValue && !hasProps) {
325
+ return true;
326
+ }
327
+ // Properties only
328
+ if (!hasValue && hasProps) {
329
+ const result = {};
330
+ for (const [key, val] of Object.entries(tag.properties)) {
331
+ if (!val.deleted) {
332
+ result[key] = Tag.tagToObject(val);
333
+ }
334
+ }
335
+ return result;
336
+ }
337
+ // Value only
338
+ if (hasValue && !hasProps) {
339
+ if (Array.isArray(tag.eq)) {
340
+ return tag.eq.map(el => Tag.tagToObject(el));
341
+ }
342
+ return Tag.scalarToObject(tag.eq);
343
+ }
344
+ // Both value and properties
345
+ const result = {};
346
+ if (Array.isArray(tag.eq)) {
347
+ result['='] = tag.eq.map(el => Tag.tagToObject(el));
348
+ }
349
+ else {
350
+ result['='] = Tag.scalarToObject(tag.eq);
351
+ }
352
+ for (const [key, val] of Object.entries(tag.properties)) {
353
+ if (!val.deleted) {
354
+ result[key] = Tag.tagToObject(val);
355
+ }
356
+ }
357
+ return result;
358
+ }
359
+ /**
360
+ * Convert to a plain JS object. References are resolved to actual
361
+ * object pointers (which may be circular in JS - that's fine).
362
+ * @param resolving - RefTags currently being resolved (for cycle detection)
363
+ */
364
+ toObject(resolving = new Set()) {
365
+ const result = {};
366
+ if (this.properties) {
367
+ for (const [key, prop] of Object.entries(this.properties)) {
368
+ if (!prop.deleted) {
369
+ result[key] = prop.toObjectValue(resolving);
370
+ }
371
+ }
372
+ }
373
+ return result;
374
+ }
375
+ /**
376
+ * Convert this tag's value to a plain JS object.
377
+ * Override in RefTag to resolve references.
378
+ * @param resolving - RefTags currently being resolved (for cycle detection)
379
+ */
380
+ toObjectValue(resolving) {
381
+ const hasProps = this.properties !== undefined && Object.keys(this.properties).length > 0;
382
+ const hasValue = this.eq !== undefined;
383
+ // Bare tag (no value, no properties)
384
+ if (!hasValue && !hasProps) {
385
+ return true;
386
+ }
387
+ // Properties only
388
+ if (!hasValue && hasProps) {
389
+ return this.toObject(resolving);
390
+ }
391
+ // Value only
392
+ if (hasValue && !hasProps) {
393
+ if (Array.isArray(this.eq)) {
394
+ return this.eq.map(el => el.toObjectValue(resolving));
395
+ }
396
+ return this.eq;
397
+ }
398
+ // Both value and properties
399
+ const result = this.toObject(resolving);
400
+ if (Array.isArray(this.eq)) {
401
+ result['='] = this.eq.map(el => el.toObjectValue(resolving));
402
+ }
403
+ else {
404
+ result['='] = this.eq;
405
+ }
406
+ return result;
407
+ }
408
+ /**
409
+ * Custom JSON serialization that excludes _parent to avoid circular references.
410
+ * This is called automatically by JSON.stringify().
411
+ */
412
+ toJSON() {
413
+ const result = {};
414
+ if (this.eq !== undefined) {
415
+ if (Array.isArray(this.eq)) {
416
+ result.eq = this.eq.map(el => el.toJSON());
417
+ }
418
+ else {
419
+ result.eq = this.eq;
420
+ }
421
+ }
422
+ if (this.properties !== undefined) {
423
+ result.properties = {};
424
+ for (const [key, val] of Object.entries(this.properties)) {
425
+ result.properties[key] = val.toJSON();
426
+ }
427
+ }
428
+ if (this.deleted) {
429
+ result.deleted = true;
430
+ }
431
+ if (this.prefix) {
432
+ result.prefix = this.prefix;
433
+ }
434
+ return result;
435
+ }
436
+ /**
437
+ * Validate all references in this tag tree.
438
+ * Returns an array of error messages for unresolved references.
439
+ */
440
+ validateReferences() {
441
+ const errors = [];
442
+ this.collectReferenceErrors(errors, []);
443
+ return errors;
444
+ }
445
+ /**
446
+ * Recursively collect reference errors.
447
+ */
448
+ collectReferenceErrors(errors, path) {
449
+ if (this.properties) {
450
+ for (const [key, prop] of Object.entries(this.properties)) {
451
+ if (!prop.deleted) {
452
+ prop.collectReferenceErrors(errors, [...path, key]);
453
+ }
454
+ }
455
+ }
456
+ if (Array.isArray(this.eq)) {
457
+ this.eq.forEach((el, i) => {
458
+ el.collectReferenceErrors(errors, [...path, `[${i}]`]);
459
+ });
460
+ }
216
461
  }
217
462
  static escapeString(str) {
218
463
  return str
@@ -231,6 +476,18 @@ class Tag {
231
476
  // TODO consider choosing the quote character based on which quotes appear in the string
232
477
  return `"${Tag.escapeString(str)}"`;
233
478
  }
479
+ static serializeScalar(val) {
480
+ if (typeof val === 'boolean') {
481
+ return val ? '@true' : '@false';
482
+ }
483
+ if (val instanceof Date) {
484
+ return `@${val.toISOString()}`;
485
+ }
486
+ if (typeof val === 'number') {
487
+ return String(val);
488
+ }
489
+ return Tag.quoteAndEscape(val);
490
+ }
234
491
  toString() {
235
492
  var _a;
236
493
  let annotation = (_a = this.prefix) !== null && _a !== void 0 ? _a : '# ';
@@ -258,7 +515,7 @@ class Tag {
258
515
  annotation += ']';
259
516
  }
260
517
  else {
261
- annotation += Tag.quoteAndEscape(`${child.eq}`);
518
+ annotation += Tag.serializeScalar(child.eq);
262
519
  }
263
520
  }
264
521
  if (child.properties) {
@@ -292,26 +549,20 @@ class Tag {
292
549
  return annotation;
293
550
  }
294
551
  find(path) {
295
- var _a;
296
- let currentTag = Tag.tagFrom(this);
552
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
553
+ let currentTag = this;
297
554
  for (const segment of path) {
555
+ let next;
298
556
  if (typeof segment === 'number') {
299
- if (currentTag.eq === undefined ||
300
- !Array.isArray(currentTag.eq) ||
301
- currentTag.eq.length <= segment) {
302
- return;
303
- }
304
- currentTag = Tag.tagFrom(currentTag.eq[segment]);
557
+ next = currentTag.getArrayElement(segment);
305
558
  }
306
559
  else {
307
- const properties = (_a = currentTag.properties) !== null && _a !== void 0 ? _a : {};
308
- if (segment in properties) {
309
- currentTag = Tag.tagFrom(properties[segment]);
310
- }
311
- else {
312
- return;
313
- }
560
+ next = currentTag.getProperty(segment);
561
+ }
562
+ if (next === undefined) {
563
+ return undefined;
314
564
  }
565
+ currentTag = next;
315
566
  }
316
567
  return currentTag.deleted ? undefined : currentTag;
317
568
  }
@@ -319,16 +570,20 @@ class Tag {
319
570
  return this.find(path) !== undefined;
320
571
  }
321
572
  set(path, value = null) {
322
- const copy = Tag.tagFrom(this);
323
- let currentTag = copy;
573
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
574
+ let currentTag = this;
575
+ let parentTag;
576
+ let lastSegment;
324
577
  for (const segment of path) {
578
+ parentTag = currentTag;
579
+ lastSegment = segment;
325
580
  if (typeof segment === 'number') {
326
581
  if (currentTag.eq === undefined || !Array.isArray(currentTag.eq)) {
327
- currentTag.eq = Array.from({ length: segment + 1 }).map(_ => ({}));
582
+ currentTag.eq = Array.from({ length: segment + 1 }).map(() => new Tag({}, currentTag));
328
583
  }
329
584
  else if (currentTag.eq.length <= segment) {
330
585
  const values = currentTag.eq;
331
- const newVal = Array.from({ length: segment + 1 }).map((_, i) => i < values.length ? values[i] : {});
586
+ const newVal = Array.from({ length: segment + 1 }).map((_, i) => i < values.length ? values[i] : new Tag({}, currentTag));
332
587
  currentTag.eq = newVal;
333
588
  }
334
589
  currentTag = currentTag.eq[segment];
@@ -336,7 +591,7 @@ class Tag {
336
591
  else {
337
592
  const properties = currentTag.properties;
338
593
  if (properties === undefined) {
339
- currentTag.properties = { [segment]: {} };
594
+ currentTag.properties = { [segment]: new Tag({}, currentTag) };
340
595
  currentTag = currentTag.properties[segment];
341
596
  }
342
597
  else if (segment in properties) {
@@ -346,7 +601,7 @@ class Tag {
346
601
  }
347
602
  }
348
603
  else {
349
- properties[segment] = {};
604
+ properties[segment] = new Tag({}, currentTag);
350
605
  currentTag = properties[segment];
351
606
  }
352
607
  }
@@ -354,18 +609,30 @@ class Tag {
354
609
  if (value === null) {
355
610
  currentTag.eq = undefined;
356
611
  }
357
- else if (typeof value === 'string') {
358
- currentTag.eq = value;
612
+ else if (value instanceof Tag) {
613
+ // Clone the tag with correct parent and replace in parent's slot
614
+ const cloned = value.clone(parentTag);
615
+ if (parentTag && lastSegment !== undefined) {
616
+ if (typeof lastSegment === 'number' && Array.isArray(parentTag.eq)) {
617
+ parentTag.eq[lastSegment] = cloned;
618
+ }
619
+ else if (typeof lastSegment === 'string' && parentTag.properties) {
620
+ parentTag.properties[lastSegment] = cloned;
621
+ }
622
+ }
359
623
  }
360
- else if (typeof value === 'number') {
361
- currentTag.eq = value.toString(); // TODO big numbers?
624
+ else if (typeof value === 'string' ||
625
+ typeof value === 'number' ||
626
+ typeof value === 'boolean' ||
627
+ value instanceof Date) {
628
+ currentTag.eq = value;
362
629
  }
363
630
  else if (Array.isArray(value)) {
364
631
  currentTag.eq = value.map((v) => {
365
- return { eq: typeof v === 'string' ? v : v.toString() };
632
+ return new Tag({ eq: v }, currentTag);
366
633
  });
367
634
  }
368
- return copy;
635
+ return this;
369
636
  }
370
637
  delete(...path) {
371
638
  return this.remove(path, false);
@@ -375,20 +642,20 @@ class Tag {
375
642
  }
376
643
  remove(path, hard = false) {
377
644
  var _a;
378
- const origCopy = Tag.tagFrom(this);
379
- let currentTag = origCopy;
645
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
646
+ let currentTag = this;
380
647
  for (const segment of path.slice(0, path.length - 1)) {
381
648
  if (typeof segment === 'number') {
382
649
  if (currentTag.eq === undefined || !Array.isArray(currentTag.eq)) {
383
650
  if (!hard)
384
- return origCopy;
385
- currentTag.eq = Array.from({ length: segment }).map(_ => ({}));
651
+ return this;
652
+ currentTag.eq = Array.from({ length: segment }).map(() => new Tag({}, currentTag));
386
653
  }
387
654
  else if (currentTag.eq.length <= segment) {
388
655
  if (!hard)
389
- return origCopy;
656
+ return this;
390
657
  const values = currentTag.eq;
391
- const newVal = Array.from({ length: segment }).map((_, i) => i < values.length ? values[i] : {});
658
+ const newVal = Array.from({ length: segment }).map((_, i) => i < values.length ? values[i] : new Tag({}, currentTag));
392
659
  currentTag.eq = newVal;
393
660
  }
394
661
  currentTag = currentTag.eq[segment];
@@ -397,8 +664,8 @@ class Tag {
397
664
  const properties = currentTag.properties;
398
665
  if (properties === undefined) {
399
666
  if (!hard)
400
- return origCopy;
401
- currentTag.properties = { [segment]: {} };
667
+ return this;
668
+ currentTag.properties = { [segment]: new Tag({}, currentTag) };
402
669
  currentTag = currentTag.properties[segment];
403
670
  }
404
671
  else if (segment in properties) {
@@ -406,8 +673,8 @@ class Tag {
406
673
  }
407
674
  else {
408
675
  if (!hard)
409
- return origCopy;
410
- properties[segment] = {};
676
+ return this;
677
+ properties[segment] = new Tag({}, currentTag);
411
678
  currentTag = properties[segment];
412
679
  }
413
680
  }
@@ -419,7 +686,7 @@ class Tag {
419
686
  }
420
687
  else if (hard) {
421
688
  (_a = currentTag.properties) !== null && _a !== void 0 ? _a : (currentTag.properties = {});
422
- currentTag.properties[segment] = { deleted: true };
689
+ currentTag.properties[segment] = new Tag({ deleted: true }, currentTag);
423
690
  }
424
691
  }
425
692
  else {
@@ -427,249 +694,173 @@ class Tag {
427
694
  currentTag.eq.splice(segment, 1);
428
695
  }
429
696
  }
430
- return origCopy;
697
+ return this;
431
698
  }
432
699
  }
433
700
  exports.Tag = Tag;
434
701
  // --- Just for debugging ---
435
702
  Tag.ids = new Map();
436
703
  Tag.nextTagId = 1000;
437
- class TagErrorListener {
438
- constructor(line) {
439
- this.line = line;
440
- this.log = [];
441
- }
442
- syntaxError(recognizer, offendingSymbol, line, charPositionInLine, msg, _e) {
443
- const logMsg = {
444
- code: 'tag-parse-syntax-error',
445
- message: msg,
446
- line: this.line,
447
- offset: charPositionInLine,
448
- };
449
- this.log.push(logMsg);
450
- }
451
- semanticError(cx, code, msg) {
452
- const logMsg = {
453
- code,
454
- message: msg,
455
- line: this.line,
456
- offset: cx.start.charPositionInLine,
457
- };
458
- this.log.push(logMsg);
459
- }
460
- }
461
- function getBuildOn(ctx) {
462
- const buildOn = ctx['buildOn'];
463
- if (buildOn instanceof Tag) {
464
- return buildOn;
465
- }
466
- return new Tag();
467
- }
468
704
  /**
469
- * When chasing a path reference, the two interesting gestures are to
470
- * find the path-ed tag so it can be extended, or to find the path tag
471
- * so it can be deleted. This returns the parent and the final tag
472
- * so that the caller can delete the tag with delete parent.tagName
473
- * or assign to it with parent[tagName] = new_value
705
+ * A tag that references another location in the tag tree.
706
+ * When accessed, it dereferences to the target tag.
707
+ *
708
+ * Reference syntax:
709
+ * $path.to.thing - absolute from root
710
+ * $^thing - up one level, then 'thing'
711
+ * $^^thing - up two levels, then 'thing'
712
+ * $items[0].name - with array indexing
474
713
  */
475
- function buildAccessPath(buildOn, path) {
476
- var _a;
477
- var _b;
478
- let parentPropertyObject = buildOn.getProperties();
479
- for (const p of path.slice(0, path.length - 1)) {
480
- let next;
481
- if (parentPropertyObject[p] === undefined) {
482
- next = new Tag({});
483
- parentPropertyObject[p] = next;
484
- }
485
- else {
486
- // The access that we are performing requires that `.properties` be the
487
- // same JS object (not equal, but identical), and `Tag.tagFrom` only copies
488
- // the exact object in if it is actually present.
489
- (_a = (_b = parentPropertyObject[p]).properties) !== null && _a !== void 0 ? _a : (_b.properties = {});
490
- next = Tag.tagFrom(parentPropertyObject[p]);
491
- }
492
- parentPropertyObject = next.getProperties();
493
- }
494
- return [path[path.length - 1], parentPropertyObject];
495
- }
496
- function getString(ctx) {
497
- if (ctx.SQ_STRING() || ctx.DQ_STRING()) {
498
- return (0, util_1.parseString)(ctx.text, ctx.text[0]);
714
+ class RefTag extends Tag {
715
+ constructor(ups, refPath, parent) {
716
+ super({}, parent);
717
+ this.ups = ups;
718
+ this.refPath = refPath;
499
719
  }
500
- return ctx.text;
501
- }
502
- function parseTagLine(source, extending, outerScope, onLine) {
503
- if (source[0] === '#') {
504
- const skipTo = source.indexOf(' ');
505
- if (skipTo > 0) {
506
- source = source.slice(skipTo);
720
+ /**
721
+ * Resolve this reference to the target tag.
722
+ * Returns undefined if the reference cannot be resolved.
723
+ */
724
+ resolve() {
725
+ // Start from the appropriate point based on ups
726
+ let current;
727
+ if (this.ups === 0) {
728
+ // Absolute reference from root
729
+ current = this.root;
507
730
  }
508
731
  else {
509
- source = '';
510
- }
511
- }
512
- const inputStream = antlr4ts_1.CharStreams.fromString(source);
513
- const lexer = new MalloyTagLexer_1.MalloyTagLexer(inputStream);
514
- const tokenStream = new antlr4ts_1.CommonTokenStream(lexer);
515
- const pLog = new TagErrorListener(onLine);
516
- const taglineParser = new MalloyTagParser_1.MalloyTagParser(tokenStream);
517
- taglineParser.removeErrorListeners();
518
- taglineParser.addErrorListener(pLog);
519
- const tagTree = taglineParser.tagLine();
520
- const treeWalker = new TagLineParser(outerScope, pLog);
521
- const tag = treeWalker.tagLineToTag(tagTree, extending);
522
- return { tag, log: pLog.log };
523
- }
524
- class TagLineParser extends tree_1.AbstractParseTreeVisitor {
525
- constructor(outerScopes = [], msgLog) {
526
- super();
527
- this.scopes = [];
528
- this.msgLog = msgLog;
529
- this.scopes.unshift(...outerScopes);
530
- }
531
- defaultResult() {
532
- return new Tag();
533
- }
534
- visitString(ctx) {
535
- return new Tag({ eq: getString(ctx) });
536
- }
537
- getPropName(ctx) {
538
- return ctx
539
- .identifier()
540
- .map(cx => cx.BARE_STRING() ? cx.text : (0, util_1.parseString)(cx.text, cx.text[0]));
541
- }
542
- getTags(tags, tagLine) {
543
- for (const tagSpec of tags) {
544
- // Stash the current state of this tag in the context and then visit it
545
- // visit functions should alter the tagLine
546
- tagSpec['buildOn'] = tagLine;
547
- this.visit(tagSpec);
548
- }
549
- return tagLine;
550
- }
551
- tagLineToTag(ctx, extending) {
552
- extending = (extending === null || extending === void 0 ? void 0 : extending.clone()) || new Tag({});
553
- this.scopes.unshift(extending);
554
- this.getTags(ctx.tagSpec(), extending);
555
- return extending;
556
- }
557
- visitTagLine(_ctx) {
558
- throw new Error('INTERNAL: ERROR: Call tagLineToTag, not vistTagLine');
559
- return this.defaultResult();
560
- }
561
- visitProperties(ctx) {
562
- return this.getTags(ctx.tagSpec(), getBuildOn(ctx));
563
- }
564
- visitArrayValue(ctx) {
565
- return new Tag({ eq: this.getArray(ctx) });
566
- }
567
- getArray(ctx) {
568
- return ctx.arrayElement().map(v => this.visit(v));
569
- }
570
- visitArrayElement(ctx) {
571
- const propCx = ctx.properties();
572
- const properties = propCx ? this.visitProperties(propCx) : undefined;
573
- const strCx = ctx.string();
574
- let value = strCx ? getString(strCx) : undefined;
575
- const arrayCx = ctx.arrayValue();
576
- if (arrayCx) {
577
- value = this.getArray(arrayCx);
578
- }
579
- if (properties) {
580
- if (value !== undefined) {
581
- properties.eq = value;
582
- }
583
- return properties;
584
- }
585
- const refCx = ctx.reference();
586
- if (refCx) {
587
- return this.visitReference(refCx);
588
- }
589
- return new Tag({ eq: value });
590
- }
591
- visitReference(ctx) {
592
- const path = this.getPropName(ctx.propName());
593
- for (const scope of this.scopes) {
594
- // first scope which has the first component gets to resolve the whole path
595
- if (scope.has(path[0])) {
596
- const refTo = scope.tag(...path);
597
- if (refTo) {
598
- return refTo.clone();
599
- }
600
- break;
732
+ // Relative reference - go up 'ups' levels from parent
733
+ // $^ means go up 1 level from the containing scope
734
+ current = this.parent;
735
+ for (let i = 0; i < this.ups && current !== undefined; i++) {
736
+ current = current.parent;
601
737
  }
602
738
  }
603
- this.msgLog.semanticError(ctx, 'tag-property-not-found', `Reference to undefined property ${path.join('.')}`);
604
- return this.defaultResult();
739
+ if (current === undefined) {
740
+ return undefined;
741
+ }
742
+ // Follow the path
743
+ return current.find(this.refPath);
605
744
  }
606
- visitTagEq(ctx) {
607
- const buildOn = getBuildOn(ctx);
608
- const name = this.getPropName(ctx.propName());
609
- const [writeKey, writeInto] = buildAccessPath(buildOn, name);
610
- const eq = this.visit(ctx.eqValue());
611
- const propCx = ctx.properties();
612
- if (propCx) {
613
- // a.b.c { -y } means i want to do -y on
614
- if (propCx.DOTTY() === undefined) {
615
- const properties = this.visitProperties(propCx).dict;
616
- // Add new properties old value
617
- writeInto[writeKey] = { ...eq, properties };
618
- }
619
- else {
620
- // preserve old properties, add new value
621
- writeInto[writeKey] = { ...writeInto[writeKey], ...eq };
745
+ /**
746
+ * Quote an identifier if it contains special characters.
747
+ */
748
+ quoteSegment(seg) {
749
+ // Match bareString from peggy grammar: [0-9A-Za-z_\u00C0-\u024F\u1E00-\u1EFF]+
750
+ if (/^[0-9A-Za-z_\u00C0-\u024F\u1E00-\u1EFF]+$/.test(seg)) {
751
+ return seg;
752
+ }
753
+ // Otherwise, backquote it (escaping backslashes and backquotes)
754
+ return '`' + seg.replace(/\\/g, '\\\\').replace(/`/g, '\\`') + '`';
755
+ }
756
+ /**
757
+ * Convert this reference to its string representation.
758
+ */
759
+ toRefString() {
760
+ const prefix = '$' + '^'.repeat(this.ups);
761
+ const pathStr = this.refPath
762
+ .map((seg, i) => {
763
+ if (typeof seg === 'number') {
764
+ return `[${seg}]`;
622
765
  }
766
+ const quoted = this.quoteSegment(seg);
767
+ return i === 0 ? quoted : `.${quoted}`;
768
+ })
769
+ .join('');
770
+ return prefix + pathStr;
771
+ }
772
+ // Override virtual accessors to resolve the reference
773
+ getEq() {
774
+ var _a;
775
+ return (_a = this.resolve()) === null || _a === void 0 ? void 0 : _a.getEq();
776
+ }
777
+ getProperty(name) {
778
+ var _a;
779
+ return (_a = this.resolve()) === null || _a === void 0 ? void 0 : _a.getProperty(name);
780
+ }
781
+ getArrayElement(index) {
782
+ var _a;
783
+ return (_a = this.resolve()) === null || _a === void 0 ? void 0 : _a.getArrayElement(index);
784
+ }
785
+ hasProperties() {
786
+ var _a, _b;
787
+ return (_b = (_a = this.resolve()) === null || _a === void 0 ? void 0 : _a.hasProperties()) !== null && _b !== void 0 ? _b : false;
788
+ }
789
+ /**
790
+ * For toObject, resolve the reference and return the target's object value.
791
+ * This creates actual object pointers (circular references are allowed).
792
+ * Detects cycles in the reference chain to prevent infinite recursion.
793
+ */
794
+ toObjectValue(resolving) {
795
+ // Check for cycle in reference chain
796
+ if (resolving.has(this)) {
797
+ // We're in a cycle - return undefined to break it
798
+ // (The cycle will be completed when the outer resolution finishes)
799
+ return undefined;
623
800
  }
624
- else {
625
- writeInto[writeKey] = eq;
801
+ const resolved = this.resolve();
802
+ if (resolved === undefined) {
803
+ return undefined;
626
804
  }
627
- return buildOn;
805
+ // Track that we're resolving this RefTag
806
+ resolving.add(this);
807
+ const result = resolved.toObjectValue(resolving);
808
+ resolving.delete(this);
809
+ return result;
628
810
  }
629
- visitTagReplaceProperties(ctx) {
630
- const buildOn = getBuildOn(ctx);
631
- const name = this.getPropName(ctx.propName());
632
- const [writeKey, writeInto] = buildAccessPath(buildOn, name);
633
- const propCx = ctx.properties();
634
- const props = this.visitProperties(propCx);
635
- if (ctx.DOTTY() === undefined) {
636
- // No dots, thropw away the value
637
- writeInto[writeKey] = { properties: props.dict };
811
+ /**
812
+ * For JSON serialization, return a marker object instead of resolving.
813
+ */
814
+ toJSON() {
815
+ return { linkTo: this.toRefString() };
816
+ }
817
+ /**
818
+ * Check if this reference resolves, add error if not.
819
+ */
820
+ collectReferenceErrors(errors, path) {
821
+ if (this.resolve() === undefined) {
822
+ const location = path.length > 0 ? path.join('.') : 'root';
823
+ errors.push(`Unresolved reference at ${location}: ${this.toRefString()}`);
638
824
  }
639
- else {
640
- /// DOTS, just update the properties
641
- writeInto[writeKey].properties = props.dict;
642
- }
643
- return buildOn;
644
- }
645
- visitTagUpdateProperties(ctx) {
646
- const buildOn = getBuildOn(ctx);
647
- const name = this.getPropName(ctx.propName());
648
- const [writeKey, writeInto] = buildAccessPath(buildOn, name);
649
- const propCx = ctx.properties();
650
- propCx['buildOn'] = Tag.tagFrom(writeInto[writeKey]);
651
- const props = this.visitProperties(propCx);
652
- const thisObj = writeInto[writeKey] || new Tag({});
653
- const properties = { ...thisObj.properties, ...props.dict };
654
- writeInto[writeKey] = { ...thisObj, properties };
655
- return buildOn;
656
- }
657
- visitTagDef(ctx) {
658
- const buildOn = getBuildOn(ctx);
659
- const path = this.getPropName(ctx.propName());
660
- const [writeKey, writeInto] = buildAccessPath(buildOn, path);
661
- if (ctx.MINUS()) {
662
- writeInto[writeKey] = { deleted: true };
825
+ }
826
+ /**
827
+ * Clone this RefTag, preserving the reference information.
828
+ */
829
+ clone(newParent) {
830
+ return new RefTag(this.ups, [...this.refPath], newParent);
831
+ }
832
+ }
833
+ exports.RefTag = RefTag;
834
+ /**
835
+ * Convert a Tag to a plain TagInterface without internal fields like _parent.
836
+ * Useful for test comparisons.
837
+ */
838
+ function interfaceFromTag(tag) {
839
+ const result = {};
840
+ if (tag.eq !== undefined) {
841
+ if (Array.isArray(tag.eq)) {
842
+ result.eq = tag.eq.map(el => interfaceFromTag(el));
663
843
  }
664
844
  else {
665
- writeInto[writeKey] = new Tag({});
845
+ result.eq = tag.eq;
666
846
  }
667
- return buildOn;
668
847
  }
669
- visitTagEmpty(ctx) {
670
- const tagList = ctx['buildOn'];
671
- tagList.properties = {};
672
- return tagList;
848
+ if (tag.properties !== undefined) {
849
+ result.properties = interfaceFromDict(tag.properties);
850
+ }
851
+ if (tag.deleted) {
852
+ result.deleted = true;
853
+ }
854
+ return result;
855
+ }
856
+ /**
857
+ * Convert a TagDict to a plain TagDict without internal fields.
858
+ */
859
+ function interfaceFromDict(dict) {
860
+ const result = {};
861
+ for (const [key, val] of Object.entries(dict)) {
862
+ result[key] = interfaceFromTag(val);
673
863
  }
864
+ return result;
674
865
  }
675
866
  //# sourceMappingURL=tags.js.map