viv-compiler 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,1188 @@
1
+ """Visitor component of the Viv DSL compiler.
2
+
3
+ Defines a single `Visitor` class that traverses and modifies a Viv AST, in
4
+ accordance with the structure prescribed by Arpeggio, our PEG parser solution.
5
+ """
6
+
7
+ import math
8
+ import copy
9
+ import arpeggio
10
+ import viv_compiler.config
11
+ import viv_compiler.types
12
+ from viv_compiler.types import ExpressionDiscriminator, ReferencePathComponentDiscriminator
13
+ from typing import Any, Optional, TypeGuard
14
+
15
+
16
+ class Visitor(arpeggio.PTNodeVisitor):
17
+ """A visitor for Viv abstract syntax trees (AST), following the visitor pattern in parsing."""
18
+
19
+ @staticmethod
20
+ def visit_file(_, children: Any) -> viv_compiler.types.AST:
21
+ """Visit the <file> node, i.e., the root of the parse tree."""
22
+ ast = {"_includes": [], "tropes": [], "actions": []}
23
+ for child in children:
24
+ if child["type"] == 'include':
25
+ ast['_includes'].append(child['value'])
26
+ elif child["type"] == 'action':
27
+ ast['actions'].append(child['value'])
28
+ elif child["type"] == 'trope':
29
+ ast['tropes'].append(child['value'])
30
+ return ast
31
+
32
+ @staticmethod
33
+ def visit_include(_, children: Any) -> dict[str, Any]:
34
+ """Visit an <include> node."""
35
+ return {"type": "include", "value": children[0]}
36
+
37
+ @staticmethod
38
+ def visit_filename(_, children: Any) -> str:
39
+ """Visit a <filename> node."""
40
+ return ''.join(children)
41
+
42
+ @staticmethod
43
+ def visit_action(_, children: Any) -> dict[str, Any]:
44
+ """Visit an <action> node."""
45
+ action_definition = {"special": False, "parent": None}
46
+ header, body = children
47
+ action_definition.update(header)
48
+ action_definition.update(body)
49
+ return {"type": "action", "value": action_definition}
50
+
51
+ @staticmethod
52
+ def visit_action_header(_, children: Any) -> dict[str, Any]:
53
+ """Visit a <action_header> node."""
54
+ component = {"special": False}
55
+ for child in children:
56
+ if isinstance(child, str):
57
+ component['name'] = child
58
+ else:
59
+ component.update(child)
60
+ return component
61
+
62
+ @staticmethod
63
+ def visit_special_action_tag(_, __: Any) -> dict[str, bool]:
64
+ """Visit a <special_action_tag> node."""
65
+ return {"special": True}
66
+
67
+ @staticmethod
68
+ def visit_parent_action_declaration(_, children: Any) -> dict[str, str]:
69
+ """Visit a <parent_action_declaration> node."""
70
+ return {"parent": children[0]}
71
+
72
+ @staticmethod
73
+ def visit_action_body(_, children: Any) -> dict[str, Any]:
74
+ """Visit a <action_body> node."""
75
+ # Prepare with base data including optional fields
76
+ component = {}
77
+ for child in children:
78
+ component.update(child)
79
+ return component
80
+
81
+ @staticmethod
82
+ def visit_gloss(
83
+ _, children: Any
84
+ ) -> dict[str, viv_compiler.types.TemplateStringField | viv_compiler.types.StringField]:
85
+ """Visit a <gloss> node."""
86
+ gloss = children[0]
87
+ return {"gloss": gloss}
88
+
89
+ @staticmethod
90
+ def visit_report(_, children: Any) -> dict[str, viv_compiler.types.TemplateStringField]:
91
+ """Visit a <report> node."""
92
+ report = children[0]
93
+ return {"report": report}
94
+
95
+ @staticmethod
96
+ def visit_templated_text(_, children: Any) -> viv_compiler.types.TemplateStringField:
97
+ """Visit a <templated_text> node."""
98
+ template = []
99
+ for child in children:
100
+ if isinstance(child, dict):
101
+ template.append(child)
102
+ else:
103
+ template.append({"type": "string", "value": child})
104
+ component = {"type": ExpressionDiscriminator.TEMPLATE_STRING, "value": template}
105
+ return component
106
+
107
+ @staticmethod
108
+ def visit_template_frame_component_double_quote(node: Any, _) -> str:
109
+ """Visit a <template_frame_component_double_quote> node."""
110
+ return str(node)
111
+
112
+ @staticmethod
113
+ def visit_template_frame_component_single_quote(node: Any, _) -> str:
114
+ """Visit a <template_frame_component_single_quote> node."""
115
+ return str(node)
116
+
117
+ @staticmethod
118
+ def visit_template_gap(_, children: Any) -> viv_compiler.types.Expression:
119
+ """Visit a <template_gap> node."""
120
+ return children[0]
121
+
122
+ @staticmethod
123
+ def visit_tags_field(_, children: Any) -> dict[str, Any]:
124
+ """Visit a <tags_field> node."""
125
+ if len(children) == 2:
126
+ component = {"tags": children[1], "_join_tags": True}
127
+ else:
128
+ component = {"tags": children[0]}
129
+ return component
130
+
131
+ @staticmethod
132
+ def visit_tags(_, children: Any) -> viv_compiler.types.ListField:
133
+ """Visit a <tags> node."""
134
+ component = {"type": ExpressionDiscriminator.LIST, "value": children}
135
+ return component
136
+
137
+ @staticmethod
138
+ def visit_roles(_, children: Any) -> dict[str, Any]:
139
+ """Visit a <roles> node."""
140
+ if children[0] == "join":
141
+ component = {"roles": children[1:], "_join_roles": True}
142
+ else:
143
+ component = {"roles": children}
144
+ return component
145
+
146
+ @staticmethod
147
+ def visit_role(_, children: Any) -> viv_compiler.types.RoleDefinition:
148
+ """Visit a <role> node."""
149
+ if "_role_renaming" in children[0]:
150
+ return children[0]
151
+ component = {
152
+ "min": 1,
153
+ "max": 1,
154
+ "chance": None,
155
+ "mean": None,
156
+ "sd": None,
157
+ "pool": None,
158
+ "parent": None,
159
+ "children": [],
160
+ "character": False,
161
+ "item": False,
162
+ "action": False,
163
+ "location": False,
164
+ "symbol": False,
165
+ "initiator": False,
166
+ "partner": False,
167
+ "recipient": False,
168
+ "bystander": False,
169
+ "subject": False,
170
+ "absent": False,
171
+ "precast": False,
172
+ "build": False,
173
+ }
174
+ for child in children:
175
+ if 'min' in child:
176
+ component['min'] = child['min']['value']
177
+ component['max'] = child['max']['value']
178
+ else:
179
+ component.update(child)
180
+ if not (component['item'] or component['action'] or component['location'] or component['symbol']):
181
+ component['character'] = True
182
+ if component['mean'] is not None:
183
+ # Some initial experimentation has shown that taking the log of max - min produces a solid
184
+ # standard deviation (for a normal distribution centered on the author-supplied mean), but
185
+ # only for smaller values. Once the span eclipses 20 or so, we need a bigger SD to allow
186
+ # for the tails to get better coverage (since otherwise the min and max are effectively
187
+ # ignored). Empirically, it appears that dividing the span by 7 works for bigger spans.
188
+ span = component['max'] - component['min']
189
+ if span != 0:
190
+ sd = max(math.log(span), span / 7)
191
+ else:
192
+ sd = 0
193
+ component['sd'] = round(sd, 2)
194
+ return component
195
+
196
+ @staticmethod
197
+ def visit_role_renaming(_, children: Any) -> dict[str, Any]:
198
+ """Visit a <role_renaming> node."""
199
+ component = {"_role_renaming": True, "_source_name": children[1]['name'], "_target_name": children[0]['name']}
200
+ return component
201
+
202
+ @staticmethod
203
+ def visit_number_range(_, children: Any) -> dict[str, Any]:
204
+ """Visit a <number_range> node."""
205
+ if len(children) == 1:
206
+ return {"min": children[0], "max": children[0]}
207
+ return {"min": children[0], "max": children[1]}
208
+
209
+ @staticmethod
210
+ def visit_role_name(_, children: Any) -> dict[str, str]:
211
+ """Visit a <role_name> node."""
212
+ name = children[0]
213
+ return {"name": name}
214
+
215
+ @staticmethod
216
+ def visit_binding_pool_directive(_, children: Any) -> dict[str, Any]:
217
+ """Visit a <binding_pool_directive> node."""
218
+ return children[0]
219
+
220
+ @staticmethod
221
+ def visit_binding_pool_from_directive(_, children: Any) -> dict[str, Any]:
222
+ """Visit a <binding_pool_from_directive> node."""
223
+ component = {"pool": {"body": children[0]}}
224
+ return component
225
+
226
+ @staticmethod
227
+ def visit_binding_pool_is_directive(_, children: Any) -> dict[str, Any]:
228
+ """Visit a <binding_pool_is_directive> node."""
229
+ binding_pool = {"type": ExpressionDiscriminator.LIST, "value": [children[0]]}
230
+ component = {"pool": {"body": binding_pool}}
231
+ return component
232
+
233
+ @staticmethod
234
+ def visit_role_labels(_, children: Any) -> dict[str, Any]:
235
+ """Visit a <role_labels> node."""
236
+ role_labels = []
237
+ entity_recipe = None
238
+ for role_label in children:
239
+ if isinstance(role_label, dict): # Build directive
240
+ role_labels.append("build")
241
+ entity_recipe = role_label["build"]
242
+ else:
243
+ role_labels.append(role_label)
244
+ role_labels_component = {tag: True for tag in role_labels}
245
+ if entity_recipe:
246
+ role_labels_component["entityRecipe"] = entity_recipe
247
+ return role_labels_component
248
+
249
+ @staticmethod
250
+ def visit_build_directive(_, children: Any) -> dict[str, Any]:
251
+ """Visit a <build_directive> node."""
252
+ return {"build": children[0]}
253
+
254
+ @staticmethod
255
+ def visit_build_directive_entity_recipe(_, children: Any) -> Any:
256
+ """Visit a <build_directive_entity_recipe> node."""
257
+ return children[0]
258
+
259
+ @staticmethod
260
+ def visit_binding_rate_directive(_, children: Any) -> Any:
261
+ """Visit a <binding_rate_directive> node."""
262
+ return children[0]
263
+
264
+ @staticmethod
265
+ def visit_chance_directive(_, children: Any) -> dict[str, float]:
266
+ """Visit a <chance_directive> node."""
267
+ # Children are the numeric tokens only (the `%` is a literal in the grammar)
268
+ number = ''.join(str(child) for child in children)
269
+ # Guard against empty or degenerate number strings
270
+ if number in ('', '-', '.', '-.'):
271
+ chance = -1.0 # There's no number preceding the `%`, so just trigger an error reliably
272
+ else:
273
+ try:
274
+ chance = float(number) / 100.0
275
+ except ValueError:
276
+ chance = -1.0 # Same idea here
277
+ # Preserve any negative we just created for the validator, and otherwise clamp tiny positives for stability
278
+ if chance > 0:
279
+ chance = max(round(chance, 3), 0.001)
280
+ return {"chance": chance}
281
+
282
+ @staticmethod
283
+ def visit_mean_directive(_, children: Any) -> dict[str, float]:
284
+ """Visit a <mean_directive> node."""
285
+ # Children are the numeric tokens only (the `~` is a literal in the grammar)
286
+ number = ''.join(str(child) for child in children)
287
+ # Guard against empty or degenerate number strings
288
+ if number in ('', '-', '.', '-.'):
289
+ mean = -1.0 # There's no number succeeding the `~`, so just trigger an error reliably
290
+ else:
291
+ try:
292
+ mean = float(number)
293
+ except ValueError:
294
+ mean = -1.0 # Same idea here
295
+ mean = round(mean, 2)
296
+ return {"mean": mean}
297
+
298
+ @staticmethod
299
+ def visit_preconditions(_, children: Any) -> dict[str, Any]:
300
+ """Visit a <preconditions> node."""
301
+ if len(children) == 2:
302
+ component = {"preconditions": children[1], "_join_preconditions": True}
303
+ else:
304
+ component = {"preconditions": children[0]}
305
+ return component
306
+
307
+ @staticmethod
308
+ def visit_child_join_operator(node: Any, __: Any) -> Any:
309
+ """Visit a <child_join_operator> node."""
310
+ return node
311
+
312
+ @staticmethod
313
+ def visit_scratch(_, children: Any) -> dict[str, Any]:
314
+ """Visit a <scratch> node."""
315
+ if len(children) == 2:
316
+ component = {"scratch": children[1], "_join_scratch": True}
317
+ else:
318
+ component = {"scratch": children[0]}
319
+ return component
320
+
321
+ @staticmethod
322
+ def visit_effects(_, children: Any) -> dict[str, Any]:
323
+ """Visit a <effects> node."""
324
+ if len(children) == 2:
325
+ component = {"effects": children[1], "_join_effects": True}
326
+ else:
327
+ component = {"effects": children[0]}
328
+ return component
329
+
330
+ @staticmethod
331
+ def visit_reactions(_, children: Any) -> dict[str, Any]:
332
+ """Visit a <reactions> node."""
333
+ if children[0] == "join":
334
+ component = {"reactions": children[1:], "_join_reactions": True}
335
+ else:
336
+ component = {"reactions": children}
337
+ return component
338
+
339
+ @staticmethod
340
+ def visit_reaction(_, children: Any):
341
+ """Visit a <reaction> node."""
342
+ # Prepare default reaction settings
343
+ reaction_object = {
344
+ "options": copy.deepcopy(viv_compiler.config.REACTION_FIELD_DEFAULT_OPTIONS),
345
+ }
346
+ for child in children:
347
+ reaction_object.update(child)
348
+ # Move the 'bindings' field from the options component to the top level of the reaction value
349
+ reaction_object['bindings'] = reaction_object['options']['bindings']
350
+ del reaction_object['options']['bindings']
351
+ # Validate the shape of the reaction value
352
+ def _is_reaction_value(obj) -> "TypeGuard[viv_compiler.types.ReactionValue]":
353
+ if not isinstance(obj, dict):
354
+ return False
355
+ if not {"actionName", "bindings", "options"} <= set(obj):
356
+ return False
357
+ if not isinstance(obj.get("actionName"), str):
358
+ return False
359
+ bindings = obj.get("bindings")
360
+ if not isinstance(bindings, list):
361
+ return False
362
+ for b in bindings:
363
+ if not (isinstance(b, dict) and b.get("type") == "binding" and isinstance(b.get("value"), dict)):
364
+ return False
365
+ v = b["value"]
366
+ if not (isinstance(v.get("role"), str) and "entity" in v):
367
+ return False
368
+ options = obj.get("options")
369
+ if not isinstance(options, dict):
370
+ return False
371
+ if "bindings" in options:
372
+ return False
373
+ return True
374
+ if not _is_reaction_value(reaction_object):
375
+ raise ValueError(f"Malformed reaction generated by Visitor: {reaction_object!r}")
376
+ # Package up the component and return it
377
+ reaction_value: viv_compiler.types.ReactionValue = reaction_object # type: ignore[assignment]
378
+ component: viv_compiler.types.Reaction = {"type": ExpressionDiscriminator.REACTION, "value": reaction_value}
379
+ return component
380
+
381
+ @staticmethod
382
+ def visit_reaction_action_name(_, children: Any) -> dict[str, Any]:
383
+ """Visit a <reaction_action_name> node."""
384
+ return {"actionName": children[0]}
385
+
386
+ @staticmethod
387
+ def visit_bindings(_, children: Any) -> dict[str, Any]:
388
+ """Visit a <bindings> node."""
389
+ return {"bindings": children}
390
+
391
+ @staticmethod
392
+ def visit_binding(_, children: Any) -> viv_compiler.types.ReactionBinding | viv_compiler.types.Loop:
393
+ """Visit a <binding> node."""
394
+ # Support both (name ":" reference) and the 'loop' alternative per the grammar
395
+ if len(children) == 1:
396
+ if isinstance(children[0], dict) and children[0].get("type") == ExpressionDiscriminator.LOOP:
397
+ return children[0]
398
+ role_name, entity_to_bind = children
399
+ component = {"type": ExpressionDiscriminator.BINDING, "value": {"role": role_name, "entity": entity_to_bind}}
400
+ return component
401
+
402
+ @staticmethod
403
+ def visit_reaction_options(_, children: Any) -> dict[str, Any]:
404
+ """Visit a <reaction_options> node."""
405
+ component = {"options": copy.deepcopy(viv_compiler.config.REACTION_FIELD_DEFAULT_OPTIONS)}
406
+ for child in children:
407
+ component['options'].update(child)
408
+ return component
409
+
410
+ @staticmethod
411
+ def visit_urgent(_, children: Any) -> dict[str, Any]:
412
+ """Visit a <urgent> node."""
413
+ component = {"urgent": children[0]}
414
+ return component
415
+
416
+ @staticmethod
417
+ def visit_priority(_, children: Any) -> dict[str, Any]:
418
+ """Visit a <priority> node."""
419
+ component = {"priority": children[0]}
420
+ return component
421
+
422
+ @staticmethod
423
+ def visit_kill_code(_, children: Any) -> dict[str, Any]:
424
+ """Visit a <kill_code> node."""
425
+ component = {"killCode": children[0]}
426
+ return component
427
+
428
+ @staticmethod
429
+ def visit_where(_, children: Any) -> dict[str, Any]:
430
+ """Visit a <where> node."""
431
+ component = {"where": children[0]}
432
+ return component
433
+
434
+ @staticmethod
435
+ def visit_when(_, children: Any) -> dict[str, viv_compiler.types.TemporalConstraints]:
436
+ """Visit a <when> node."""
437
+ when_value = {}
438
+ for child in children:
439
+ when_value.update(child)
440
+ component = {"when": when_value}
441
+ return component
442
+
443
+ @staticmethod
444
+ def visit_when_action_timestamp_anchor(_, __: Any) -> dict[str, bool]:
445
+ """Visit a <when_action_timestamp_anchor> node."""
446
+ component = {"useActionTimestamp": True}
447
+ return component
448
+
449
+ @staticmethod
450
+ def visit_when_hearing_timestamp_anchor(_, __: Any) -> dict[str, bool]:
451
+ """Visit a <when_hearing_timestamp_anchor> node."""
452
+ component = {"useActionTimestamp": False}
453
+ return component
454
+
455
+ @staticmethod
456
+ def visit_time_expression(_, children: Any) -> viv_compiler.types.TemporalConstraints:
457
+ """Visit a <time_expression> node."""
458
+ time_constraints: dict[str, Optional[dict[str, Any]]] = {"timePeriod": None, "timeOfDay": None}
459
+ for child in children:
460
+ if child["operator"] == "between":
461
+ if child["open"]["type"] == "timePeriod":
462
+ time_constraints["timePeriod"] = {
463
+ "open": {"amount": child["open"]["amount"], "unit": child["open"]["unit"]},
464
+ "close": {"amount": child["close"]["amount"], "unit": child["close"]["unit"]},
465
+ }
466
+ else: # child["open"]["type"] == "time"
467
+ time_constraints["timeOfDay"] = {
468
+ "open": {"hour": child["open"]["hour"], "minute": child["open"]["minute"]},
469
+ "close": {"hour": child["close"]["hour"], "minute": child["close"]["minute"]},
470
+ }
471
+ elif child["operator"] == "before":
472
+ anchor = child["anchor"]
473
+ if anchor["type"] == "timePeriod":
474
+ time_constraints["timePeriod"] = {
475
+ "open": None,
476
+ "close": {"amount": anchor["amount"], "unit": anchor["unit"]},
477
+ }
478
+ else: # anchor["type"] == "time"
479
+ time_constraints["timeOfDay"] = {
480
+ "open": None,
481
+ "close": {"hour": anchor["hour"], "minute": anchor["minute"]}
482
+ }
483
+ else: # operator == "after":
484
+ anchor = child["anchor"]
485
+ if anchor["type"] == "timePeriod":
486
+ time_constraints["timePeriod"] = {
487
+ "open": {"amount": anchor["amount"], "unit": anchor["unit"]},
488
+ "close": None,
489
+ }
490
+ else: # anchor["type"] == "time"
491
+ time_constraints["timeOfDay"] = {
492
+ "open": {"hour": anchor["hour"], "minute": anchor["minute"]},
493
+ "close": None,
494
+ }
495
+ return time_constraints
496
+
497
+ @staticmethod
498
+ def visit_time_statement(_, children: Any) -> dict[str, Any]:
499
+ """Visit a <time_statement> node."""
500
+ return children[0]
501
+
502
+ @staticmethod
503
+ def visit_before_time_statement(_, children: Any) -> dict[str, Any]:
504
+ """Visit a <before_time_statement> node."""
505
+ return {"operator": "before", "anchor": children[0]}
506
+
507
+ @staticmethod
508
+ def visit_after_time_statement(_, children: Any) -> dict[str, Any]:
509
+ """Visit a <after_time_statement> node."""
510
+ return {"operator": "after", "anchor": children[0]}
511
+
512
+ @staticmethod
513
+ def visit_between_time_statement(_, children: Any) -> dict[str, Any]:
514
+ """Visit a <between_time_statement> node."""
515
+ return {"operator": "between", "open": children[0], "close": children[1]}
516
+
517
+ @staticmethod
518
+ def visit_time_period(_, children: Any) -> dict[str, Any]:
519
+ """Visit a <time_period> node."""
520
+ number = children[0]['value']
521
+ unit = children[1] + 's' if children[1][-1] != 's' else children[1]
522
+ component = {"type": "timePeriod", "amount": number, "unit": unit}
523
+ return component
524
+
525
+ @staticmethod
526
+ def visit_time(_, children: Any) -> dict[str, Any]:
527
+ """Visit a <time> node."""
528
+ period = children[-1] # AM or PM
529
+ raw_hour = int(children[0])
530
+ hour = (raw_hour % 12) + (12 if period.upper() == 'PM' else 0)
531
+ if len(children) == 2: # Only the hour was provided
532
+ component = {
533
+ "type": "time",
534
+ "hour": hour,
535
+ "minute": 0
536
+ }
537
+ else:
538
+ component = {
539
+ "type": "time",
540
+ "hour": hour,
541
+ "minute": int(children[1])
542
+ }
543
+ return component
544
+
545
+ @staticmethod
546
+ def visit_digits(_, children: Any) -> str:
547
+ """Visit a <digits> node."""
548
+ return ''.join(children)
549
+
550
+ @staticmethod
551
+ def visit_abandonment_conditions(_, children: Any) -> dict[str, list[viv_compiler.types.Expression]]:
552
+ """Visit a <abandonment_conditions> node."""
553
+ component = {"abandonmentConditions": children}
554
+ return component
555
+
556
+ @staticmethod
557
+ def visit_saliences(_, children: Any) -> dict[str, Any]:
558
+ """Visit a <saliences> node."""
559
+ join_saliences = False
560
+ if children[0] == "join":
561
+ join_saliences = True
562
+ children = children[1:]
563
+ default_value_expression = children[0]
564
+ body = children[1] if len(children) > 1 else []
565
+ component = {
566
+ "saliences": {
567
+ "default": default_value_expression,
568
+ "variable": viv_compiler.config.SALIENCES_VARIABLE_NAME,
569
+ "body": body,
570
+ }
571
+ }
572
+ if join_saliences:
573
+ component["_join_saliences"] = True
574
+ return component
575
+
576
+ @staticmethod
577
+ def visit_saliences_default_value(_, children: Any) -> viv_compiler.types.Expression:
578
+ """Visit a <saliences_default_value> node."""
579
+ return children[0]
580
+
581
+ @staticmethod
582
+ def visit_saliences_body(_, children: Any) -> list[viv_compiler.types.Expression]:
583
+ """Visit a <saliences_body> node."""
584
+ return children
585
+
586
+ @staticmethod
587
+ def visit_associations(_, children: Any) -> dict[str, Any]:
588
+ """Visit an <associations> node."""
589
+ join_associations = False
590
+ if children[0] == "join":
591
+ join_associations = True
592
+ children = children[1:]
593
+ default_value_expression = children[0]
594
+ body = children[1] if len(children) > 1 else []
595
+ component = {
596
+ "associations": {
597
+ "default": default_value_expression,
598
+ "variable": viv_compiler.config.ASSOCIATIONS_VARIABLE_NAME,
599
+ "body": body,
600
+ }
601
+ }
602
+ if join_associations:
603
+ component["_join_associations"] = True
604
+ return component
605
+
606
+ @staticmethod
607
+ def visit_associations_default_value(_, children: Any) -> viv_compiler.types.Expression:
608
+ """Visit a <associations_default_value> node."""
609
+ return children[0]
610
+
611
+ @staticmethod
612
+ def visit_associations_body(_, children: Any) -> list[viv_compiler.types.Expression]:
613
+ """Visit a <associations_body> node."""
614
+ return children[0]
615
+
616
+ @staticmethod
617
+ def visit_associations_statements(_, children: Any) -> list[viv_compiler.types.Expression]:
618
+ """Visit a <associations_statements> node."""
619
+ return children
620
+
621
+ @staticmethod
622
+ def visit_associations_statement(_, children: Any) -> list[viv_compiler.types.Expression]:
623
+ """Visit a <associations_statement> node."""
624
+ return children[0]
625
+
626
+ @staticmethod
627
+ def visit_associations_loop(_, children: Any) -> viv_compiler.types.Loop:
628
+ """Visit a <associations_loop> node."""
629
+ iterable_reference, variable_name, loop_body = children
630
+ component = {
631
+ "type": ExpressionDiscriminator.LOOP,
632
+ "value": {"iterable": iterable_reference, "variable": variable_name['name'], "body": loop_body}
633
+ }
634
+ return component
635
+
636
+ @staticmethod
637
+ def visit_associations_conditional(_, children: Any) -> viv_compiler.types.Conditional:
638
+ """Visit a <associations_conditional> node."""
639
+ conditional_object = {"type": ExpressionDiscriminator.CONDITIONAL, "value": {}}
640
+ for child in children:
641
+ conditional_object['value'].update(child)
642
+ return conditional_object
643
+
644
+ @staticmethod
645
+ def visit_associations_conditional_consequent(_, children: Any) -> dict[str, list[viv_compiler.types.Expression]]:
646
+ """Visit a <associations_conditional_consequent> node."""
647
+ return {"consequent": children[0]}
648
+
649
+ @staticmethod
650
+ def visit_associations_conditional_alternative(_, children: Any) -> dict[str, list[viv_compiler.types.Expression]]:
651
+ """Visit a <associations_conditional_alternative> node."""
652
+ return {"alternative": children[0]}
653
+
654
+ @staticmethod
655
+ def visit_associations_scoped_statements(_, children: Any) -> list[viv_compiler.types.Expression]:
656
+ """Visit a <associations_scoped_statements> node."""
657
+ return children
658
+
659
+ @staticmethod
660
+ def visit_associations_scoped_statement(_, children: Any) -> list[viv_compiler.types.Expression]:
661
+ """Visit a <associations_scoped_statement> node."""
662
+ return children[0]
663
+
664
+ @staticmethod
665
+ def visit_embargoes(_, children: Any) -> dict[str, Any]:
666
+ """Visit an <embargoes> node."""
667
+ if children[0] == "join":
668
+ component = {"embargoes": children[1:], "_join_embargoes": True}
669
+ else:
670
+ component = {"embargoes": children}
671
+ return component
672
+
673
+ @staticmethod
674
+ def visit_embargo(_, children: Any) -> dict[str, Any]:
675
+ """Visit an <embargo> node."""
676
+ # Prepare default values that we'll override below or in post-processing
677
+ component = {"roles": None, "permanent": False, "period": None, "here": False}
678
+ for child in children:
679
+ component.update(child)
680
+ return component
681
+
682
+ @staticmethod
683
+ def visit_embargo_roles(_, children: Any) -> dict[str, list[str]]:
684
+ """Visit an <embargo_roles> node."""
685
+ component = {"roles": [child['name'] for child in children]}
686
+ return component
687
+
688
+ @staticmethod
689
+ def visit_embargo_time_period(_, children: Any) -> dict[str, Any]:
690
+ """Visit an <embargo_time_period> node."""
691
+ if children == ["forever"]:
692
+ component = {"permanent": True, "period": None}
693
+ else:
694
+ component = {
695
+ "permanent": False,
696
+ "period": {
697
+ "amount": children[0]["amount"],
698
+ "unit": children[0]["unit"]
699
+ }
700
+ }
701
+ return component
702
+
703
+ @staticmethod
704
+ def visit_embargo_location(_, __: Any) -> dict[str, bool]:
705
+ """Visit an <embargo_location> node."""
706
+ component = {"here": True}
707
+ return component
708
+
709
+ @staticmethod
710
+ def visit_conditional(_, children: Any) -> viv_compiler.types.Conditional:
711
+ """Visit a <conditional> node."""
712
+ conditional_object = {"type": ExpressionDiscriminator.CONDITIONAL, "value": {}}
713
+ for child in children:
714
+ conditional_object['value'].update(child)
715
+ return conditional_object
716
+
717
+ @staticmethod
718
+ def visit_condition(_, children: Any) -> dict[str, viv_compiler.types.Expression]:
719
+ """Visit a <condition> node."""
720
+ return {"condition": children[0]}
721
+
722
+ @staticmethod
723
+ def visit_consequent(_, children: Any) -> dict[str, list[viv_compiler.types.Expression]]:
724
+ """Visit a <condition> node."""
725
+ return {"consequent": children[0]}
726
+
727
+ @staticmethod
728
+ def visit_alternative(_, children: Any) -> dict[str, list[viv_compiler.types.Expression]]:
729
+ """Visit a <condition> node."""
730
+ return {"alternative": children[0]}
731
+
732
+ @staticmethod
733
+ def visit_loop(_, children: Any) -> viv_compiler.types.Loop:
734
+ """Visit a <loop> node."""
735
+ iterable_reference, variable_name, loop_body = children
736
+ component = {
737
+ "type": ExpressionDiscriminator.LOOP,
738
+ "value": {"iterable": iterable_reference, "variable": variable_name['name'], "body": loop_body}
739
+ }
740
+ return component
741
+
742
+ @staticmethod
743
+ def visit_statements(_, children: Any) -> list[viv_compiler.types.Expression]:
744
+ """Visit a <statement> node."""
745
+ return children
746
+
747
+ @staticmethod
748
+ def visit_statement(_, children: Any) -> viv_compiler.types.Expression:
749
+ """Visit a <statement> node."""
750
+ return children[0]
751
+
752
+ @staticmethod
753
+ def visit_scoped_statements(_, children: Any) -> list[viv_compiler.types.Expression]:
754
+ """Visit a <scoped_statements> node."""
755
+ return children
756
+
757
+ @staticmethod
758
+ def visit_expression(_, children: Any) -> viv_compiler.types.Expression:
759
+ """Visit an <expression> node."""
760
+ return children[0]
761
+
762
+ @staticmethod
763
+ def visit_logical_expression(_, children: Any) -> viv_compiler.types.Expression:
764
+ """Visit a <logical_expression> node."""
765
+ # The <logical_expression> nonterminal is just a wrapper around disjunction,
766
+ # and in any event all the work to package up the expression will have already
767
+ # been done by the time it gets to here, so we simply return the sole child.
768
+ return children[0]
769
+
770
+ @staticmethod
771
+ def visit_disjunction(_, children: Any) -> viv_compiler.types.Expression:
772
+ """Visit a <disjunction> node."""
773
+ # The elimination of left recursion in the grammar allows for a child expression to
774
+ # work its way up to here, in which case we just want to return that, since it's
775
+ # not a true disjunction.
776
+ if len(children) == 1:
777
+ return children[0]
778
+ negated = False
779
+ if children[0] == "!":
780
+ negated = True
781
+ children = children[1:]
782
+ component = {"type": ExpressionDiscriminator.DISJUNCTION, "value": {"operands": children}}
783
+ if negated:
784
+ component["negated"] = True
785
+ return component
786
+
787
+ @staticmethod
788
+ def visit_conjunction(_, children: Any) -> viv_compiler.types.Expression:
789
+ """Visit a <conjunction> node."""
790
+ # The elimination of left recursion in the grammar allows for a child expression to
791
+ # work its way up to here, in which case we just want to return that, since it's
792
+ # not a true conjunction.
793
+ if len(children) == 1:
794
+ return children[0]
795
+ negated = False
796
+ if children[0] == "!":
797
+ negated = True
798
+ children = children[1:]
799
+ component = {"type": ExpressionDiscriminator.CONJUNCTION, "value": {"operands": children}}
800
+ if negated:
801
+ component["negated"] = True
802
+ return component
803
+
804
+ @staticmethod
805
+ def visit_relational_expression(_, children: Any) -> viv_compiler.types.Expression:
806
+ """Visit a <relational_expression> node."""
807
+ # The elimination of left recursion in the grammar allows for a child expression to
808
+ # work its way up to here, in which case we just want to return that, since it's
809
+ # not a true comparison.
810
+ if len(children) == 1:
811
+ return children[0]
812
+ negated = False
813
+ if children[0] == "!":
814
+ negated = True
815
+ children = children[1:]
816
+ left, operator, right = children
817
+ component = {
818
+ "type": ExpressionDiscriminator.MEMBERSHIP_TEST if operator == "in" else ExpressionDiscriminator.COMPARISON,
819
+ "value": {
820
+ "left": left,
821
+ "operator": operator,
822
+ "right": right
823
+ }
824
+ }
825
+ if negated:
826
+ component["negated"] = True
827
+ return component
828
+
829
+ @staticmethod
830
+ def visit_assignment_expression(_, children: Any) -> viv_compiler.types.Assignment:
831
+ """Visit an <assignment_expression> node."""
832
+ left, operator, right = children
833
+ component = {
834
+ "type": ExpressionDiscriminator.ASSIGNMENT,
835
+ "value": {
836
+ "left": left,
837
+ "operator": operator,
838
+ "right": right
839
+ }
840
+ }
841
+ return component
842
+
843
+ @staticmethod
844
+ def visit_assignment_lvalue(_, children: Any) -> dict[str, Any]:
845
+ """Visit an <assignment_lvalue> node."""
846
+ return children[0]
847
+
848
+ @staticmethod
849
+ def visit_arithmetic_expression(_, children: Any) -> viv_compiler.types.ArithmeticExpression:
850
+ """Visit an <arithmetic_expression> node."""
851
+ left, operator, right = children
852
+ component = {
853
+ "type": ExpressionDiscriminator.ARITHMETIC_EXPRESSION,
854
+ "value": {
855
+ "left": left,
856
+ "operator": operator,
857
+ "right": right
858
+ }
859
+ }
860
+ return component
861
+
862
+ @staticmethod
863
+ def visit_unary_expression(_, children: Any) -> viv_compiler.types.Expression:
864
+ """Visit a <unary_expression> node."""
865
+ component = children[-1]
866
+ if len(children) > 1:
867
+ component['negated'] = True
868
+ return component
869
+
870
+ @staticmethod
871
+ def visit_object(_, children: Any) -> viv_compiler.types.ObjectField:
872
+ """Visit a <object> node."""
873
+ component = {"type": ExpressionDiscriminator.OBJECT, "value": {}}
874
+ for child in children:
875
+ component['value'].update(child)
876
+ return component
877
+
878
+ @staticmethod
879
+ def visit_key_value_pair(_, children: Any) -> dict[str, viv_compiler.types.Expression]:
880
+ """Visit a <key_value_pair> node."""
881
+ key, value = children
882
+ if isinstance(key, dict): # The author formatted the key as a string
883
+ key = key['value']
884
+ return {key: value}
885
+
886
+ @staticmethod
887
+ def visit_adapter_function_call(_, children: Any) -> viv_compiler.types.AdapterFunctionCall:
888
+ """Visit a <adapter_function_call> node."""
889
+ function_result_fail_safe = False
890
+ if children[-1] == {"type": ExpressionDiscriminator.EVAL_FAIL_SAFE}:
891
+ function_result_fail_safe = True
892
+ children = children[:-1]
893
+ try:
894
+ function_name, function_args = children
895
+ except ValueError: # No arguments passed in the function call, which is syntactically fine
896
+ function_name = children[0]
897
+ function_args = []
898
+ function_call_object = {
899
+ "type": ExpressionDiscriminator.ADAPTER_FUNCTION_CALL,
900
+ "value": {
901
+ "name": function_name,
902
+ "args": function_args,
903
+ }
904
+ }
905
+ if function_result_fail_safe:
906
+ function_call_object["value"]["resultFailSafe"] = True
907
+ return function_call_object
908
+
909
+ @staticmethod
910
+ def visit_args(_, children: Any) -> list[viv_compiler.types.Expression]:
911
+ """Visit a <args> node."""
912
+ return children
913
+
914
+ @staticmethod
915
+ def visit_reference(
916
+ _, children: Any
917
+ ) -> viv_compiler.types.EntityReference | viv_compiler.types.LocalVariableReference:
918
+ return children[0]
919
+
920
+ @staticmethod
921
+ def visit_role_anchored_reference(_, children: Any) -> viv_compiler.types.EntityReference:
922
+ """Visit a <role_anchored_reference> node."""
923
+ # Determine the anchor role name
924
+ if "globalVariable" in children[0]:
925
+ # If the reference is anchored in a global variable, expand the `$` anchor. `$` is really
926
+ # just syntactic sugar for `@this.scratch.`, which means any reference anchored in a global
927
+ # variable is in fact an entity reference anchored in a role name.
928
+ anchor = viv_compiler.config.GLOBAL_VARIABLE_REFERENCE_ANCHOR
929
+ path = viv_compiler.config.GLOBAL_VARIABLE_REFERENCE_PATH_PREFIX
930
+ # Add in the global variable itself, as a property name
931
+ path.append({
932
+ "type": ReferencePathComponentDiscriminator.REFERENCE_PATH_COMPONENT_PROPERTY_NAME,
933
+ "propertyName": children[0]["name"],
934
+ })
935
+ path += children[1] if len(children) > 1 else []
936
+ else:
937
+ anchor = children[0]["name"]
938
+ path = children[1] if len(children) > 1 else []
939
+ component = {
940
+ "type": ExpressionDiscriminator.ENTITY_REFERENCE,
941
+ "value": {
942
+ "anchor": anchor, # The name of the role anchoring this entity reference
943
+ "path": path, # Sequence of components constituting a property path
944
+ }
945
+ }
946
+ return component
947
+
948
+ @staticmethod
949
+ def visit_local_variable_anchored_reference(_, children: Any) -> viv_compiler.types.LocalVariableReference:
950
+ """Visit a <local_variable_anchored_reference> node."""
951
+ anchor = children[0]["name"]
952
+ path = children[1] if len(children) > 1 else []
953
+ component = {
954
+ "type": ExpressionDiscriminator.LOCAL_VARIABLE_REFERENCE,
955
+ "value": {
956
+ "anchor": anchor, # The name of the local variable anchoring this reference
957
+ "path": path, # Sequence of components constituting a property path
958
+ }
959
+ }
960
+ return component
961
+
962
+ @staticmethod
963
+ def visit_role_reference(_, children: Any) -> dict[str, str]:
964
+ """Visit a <role_reference> node."""
965
+ name = children[0]
966
+ return {"name": name}
967
+
968
+ @staticmethod
969
+ def visit_local_variable_reference(_, children: Any):
970
+ """Visit a <local_variable_reference> node."""
971
+ return {"name": children[0]}
972
+
973
+ @staticmethod
974
+ def visit_global_variable_reference(_, children: Any):
975
+ """Visit a <global_variable_reference> node."""
976
+ return {"globalVariable": True, "name": children[0]}
977
+
978
+ @staticmethod
979
+ def visit_reference_path(_, children: Any) -> list[viv_compiler.types.ReferencePathComponent]:
980
+ """Visit a <reference_path> node."""
981
+ return children
982
+
983
+ @staticmethod
984
+ def visit_reference_path_property_name(_, children: Any) -> viv_compiler.types.ReferencePathComponentPropertyName:
985
+ """Visit a <reference_path_property_name> node."""
986
+ property_name = children[0]
987
+ component = {
988
+ "type": ReferencePathComponentDiscriminator.REFERENCE_PATH_COMPONENT_PROPERTY_NAME,
989
+ "name": property_name,
990
+ }
991
+ if len(children) > 1:
992
+ component["failSafe"] = True
993
+ return component
994
+
995
+ @staticmethod
996
+ def visit_reference_path_pointer(_, children: Any) -> viv_compiler.types.ReferencePathComponentPointer:
997
+ """Visit a <reference_path_pointer> node."""
998
+ property_name = children[0]
999
+ component = {
1000
+ "type": ReferencePathComponentDiscriminator.REFERENCE_PATH_COMPONENT_POINTER,
1001
+ "propertyName": property_name,
1002
+ }
1003
+ if len(children) > 1:
1004
+ component["failSafe"] = True
1005
+ return component
1006
+
1007
+ @staticmethod
1008
+ def visit_reference_path_lookup(_, children: Any) -> viv_compiler.types.ReferencePathComponentLookup:
1009
+ """Visit a <reference_path_lookup> node."""
1010
+ key = children[0]
1011
+ component = {
1012
+ "type": ReferencePathComponentDiscriminator.REFERENCE_PATH_COMPONENT_LOOKUP,
1013
+ "key": key,
1014
+ }
1015
+ if len(children) > 1:
1016
+ component["failSafe"] = True
1017
+ return component
1018
+
1019
+ @staticmethod
1020
+ def visit_role_unpacking(_, children: Any) -> viv_compiler.types.RoleUnpacking:
1021
+ """Visit a <role_unpacking> node."""
1022
+ component = {"type": ExpressionDiscriminator.ROLE_UNPACKING, "value": children[0]}
1023
+ return component
1024
+
1025
+ @staticmethod
1026
+ def visit_relational_operator(_, children: Any) -> str:
1027
+ """Visit a <relational_operator> node."""
1028
+ return children[0]
1029
+
1030
+ @staticmethod
1031
+ def visit_assignment_operator(_, children: Any) -> str:
1032
+ """Visit a <assignment_operator> node."""
1033
+ return children[0]
1034
+
1035
+ @staticmethod
1036
+ def visit_negation(_, __: Any) -> str:
1037
+ """Visit a <negation> node."""
1038
+ return "!"
1039
+
1040
+ @staticmethod
1041
+ def visit_list(_, children: Any) -> viv_compiler.types.ListField:
1042
+ """Visit a <list> node."""
1043
+ component = {"type": ExpressionDiscriminator.LIST, "value": children}
1044
+ return component
1045
+
1046
+ @staticmethod
1047
+ def visit_chance_expression(_, children: Any) -> viv_compiler.types.ChanceExpression:
1048
+ """Visit a <chance_expression> node."""
1049
+ # Children may include a leading '-' and the numeric fragments; '%' is a literal in the grammar
1050
+ chance_str = ''.join(str(tok) for tok in children if tok != '%')
1051
+ if not chance_str or chance_str == '-':
1052
+ chance = -1 # signal for later validation
1053
+ else:
1054
+ chance = float(chance_str) / 100
1055
+ component = {"type": ExpressionDiscriminator.CHANCE_EXPRESSION, "value": chance}
1056
+ return component
1057
+
1058
+ @staticmethod
1059
+ def visit_literal(_, children: Any) -> viv_compiler.types.Expression:
1060
+ """Visit a <literal> node."""
1061
+ return children[0]
1062
+
1063
+ @staticmethod
1064
+ def visit_enum(_, children: Any) -> viv_compiler.types.Enum:
1065
+ """Visit an <enum> node."""
1066
+ scaled = "#" in children # As opposed to ##, which yields an unscaled enum
1067
+ additive_inverse_present = "-" in children
1068
+ component = {
1069
+ "type": ExpressionDiscriminator.ENUM,
1070
+ "value": {
1071
+ "name": children[-1],
1072
+ "scaled": scaled,
1073
+ "minus": additive_inverse_present
1074
+ }
1075
+ }
1076
+ return component
1077
+
1078
+ @staticmethod
1079
+ def visit_string(_, children: Any) -> viv_compiler.types.TemplateStringField | viv_compiler.types.StringField:
1080
+ """Visit a <string> node."""
1081
+ string = [] # A mix of strings and string-producing references
1082
+ injected_string = False
1083
+ partial_string = ""
1084
+ for child in children:
1085
+ if isinstance(child, str):
1086
+ partial_string += child
1087
+ else: # A reference, making this an injected string
1088
+ injected_string = True
1089
+ if partial_string:
1090
+ string.append(partial_string)
1091
+ partial_string = " " # Needed because the parser gobbles whitespace between nonterminals
1092
+ string.append(child)
1093
+ if partial_string.strip():
1094
+ string.append(partial_string)
1095
+ if injected_string:
1096
+ component = {"type": ExpressionDiscriminator.TEMPLATE_STRING, "value": string}
1097
+ else:
1098
+ component = {"type": ExpressionDiscriminator.STRING, "value": partial_string}
1099
+ return component
1100
+
1101
+ @staticmethod
1102
+ def visit_character(node: Any, _) -> str:
1103
+ """Visit a <character> node."""
1104
+ return str(node)
1105
+
1106
+ @staticmethod
1107
+ def visit_space(node: Any, _) -> str:
1108
+ """Visit a <space> node."""
1109
+ return str(node)
1110
+
1111
+ @staticmethod
1112
+ def visit_number(_, children: Any) -> viv_compiler.types.FloatField | viv_compiler.types.IntField:
1113
+ """Visit a <number> node."""
1114
+ number_str = ''.join(children)
1115
+ if any(child for child in children if child == '.'):
1116
+ component = {"type": ExpressionDiscriminator.FLOAT, "value": float(number_str)}
1117
+ else:
1118
+ component = {"type": ExpressionDiscriminator.INT, "value": int(number_str)}
1119
+ return component
1120
+
1121
+ @staticmethod
1122
+ def visit_number_word(_, children: Any) -> viv_compiler.types.IntField:
1123
+ """Visit a <number_word> node."""
1124
+ number_word_to_value = {
1125
+ "one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6, "seven": 7,
1126
+ "eight": 8, "nine": 9, "ten": 10, "eleven": 11, "twelve": 12,
1127
+ }
1128
+ component = {"type": ExpressionDiscriminator.INT, "value": number_word_to_value[children[0]]}
1129
+ return component
1130
+
1131
+ @staticmethod
1132
+ def visit_boolean(node: Any, __: Any) -> viv_compiler.types.BoolField:
1133
+ """Visit a <boolean> node."""
1134
+ component = {"type": ExpressionDiscriminator.BOOL, "value": True if node.value == "true" else False}
1135
+ return component
1136
+
1137
+ @staticmethod
1138
+ def visit_null_type(_, __: Any) -> viv_compiler.types.NullField:
1139
+ """Visit a <null_type> node."""
1140
+ component = {"type": ExpressionDiscriminator.NULL_TYPE, "value": None}
1141
+ return component
1142
+
1143
+ @staticmethod
1144
+ def visit_tag(_, children: Any) -> viv_compiler.types.StringField:
1145
+ """Visit a <tag> node."""
1146
+ component = {"type": ExpressionDiscriminator.STRING, "value": children[0]}
1147
+ return component
1148
+
1149
+ @staticmethod
1150
+ def visit_eval_fail_safe_marker(_, __: Any) -> viv_compiler.types.EvalFailSafeField:
1151
+ """Visit an <eval_fail_safe_marker> node."""
1152
+ component = {"type": ExpressionDiscriminator.EVAL_FAIL_SAFE}
1153
+ return component
1154
+
1155
+ @staticmethod
1156
+ def visit_trope(_, children: Any) -> dict[str, Any]:
1157
+ """Visit a <trope> node."""
1158
+ if len(children) == 3:
1159
+ name, role_names, conditions = children
1160
+ else:
1161
+ name, conditions = children
1162
+ role_names = []
1163
+ component_value = {"name": name, "params": role_names, "conditions": conditions}
1164
+ component = {"type": "trope", "value": component_value}
1165
+ return component
1166
+
1167
+ @staticmethod
1168
+ def visit_trope_role_names(_, children: Any) -> list[str]:
1169
+ """Visit a <trope_role_names> node."""
1170
+ return children
1171
+
1172
+ @staticmethod
1173
+ def visit_trope_fit_expression(_, children: Any) -> viv_compiler.types.TropeFitExpression:
1174
+ """Visit a <trope_fit_expression> node."""
1175
+ arguments, trope_name = children
1176
+ component = {
1177
+ "type": ExpressionDiscriminator.TROPE_FIT_EXPRESSION,
1178
+ "value": {
1179
+ "tropeName": trope_name,
1180
+ "args": arguments
1181
+ }
1182
+ }
1183
+ return component
1184
+
1185
+ @staticmethod
1186
+ def visit_trope_fit_expression_args(_, children: Any) -> list[viv_compiler.types.Expression]:
1187
+ """Visit a <trope_fit_expression_args> node."""
1188
+ return children