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,749 @@
1
+ """Postprocessor for the Viv DSL.
2
+
3
+ This module takes in ASTs produced by the Viv parser (with imports honored) and
4
+ postprocesses them to produce compiled content bundles that are ready for validation.
5
+
6
+ The entrypoint function is `postprocess_combined_ast()`, and everything else is only
7
+ meant to be invoked internally, i.e., within this module.
8
+ """
9
+
10
+ __all__ = ["postprocess_combined_ast"]
11
+
12
+ import copy
13
+ import viv_compiler.config
14
+ import viv_compiler.types
15
+ import viv_compiler.utils
16
+ from viv_compiler import __version__
17
+ from typing import Any, cast
18
+ from .validator import validate_join_directives, validate_preliminary_action_definitions
19
+
20
+
21
+ def postprocess_combined_ast(combined_ast: viv_compiler.types.CombinedAST) -> viv_compiler.types.CompiledContentBundle:
22
+ """Postprocess the given combined AST by inserting higher-order metadata and performing other manipulations.
23
+
24
+ Args:
25
+ combined_ast: An abstract syntax tree produced by the Visitor class, integrating the
26
+ respective ASTs of any included files.
27
+
28
+ Returns:
29
+ An AST containing postprocessed action definitions.
30
+ """
31
+ # Retrieve the raw action definitions to be postprocessed
32
+ intermediate_action_definitions: list[viv_compiler.types.RawActionDefinition] = combined_ast["actions"]
33
+ # Handle inheritance between action definitions
34
+ validate_join_directives(raw_action_definitions=intermediate_action_definitions)
35
+ _handle_action_definition_inheritance(raw_action_definitions=intermediate_action_definitions)
36
+ # Add in default values for any optional fields elided in the definitions
37
+ _add_optional_field_defaults(intermediate_action_definitions=intermediate_action_definitions)
38
+ # Wrap preconditions, effects, and reactions with lists containing all roles referenced in the expressions
39
+ intermediate_action_definitions: list[viv_compiler.types.IntermediateActionDefinition] = (
40
+ _wrap_expressions_with_role_references(intermediate_action_definitions=intermediate_action_definitions)
41
+ )
42
+ # Duplicate initiator role definitions in dedicated 'initiator' fields
43
+ _attribute_initiator_role(intermediate_action_definitions=intermediate_action_definitions)
44
+ # Conduct preliminary validation of the intermediate action definitions. This will catch
45
+ # potential major issues that cannot be outstanding by the time we get to the steps below.
46
+ # If validation fails, an error will be thrown at this point.
47
+ validate_preliminary_action_definitions(intermediate_action_definitions=intermediate_action_definitions)
48
+ # Attribute to binding-pool directives whether they are cachable
49
+ _attribute_binding_pool_cachability_values(intermediate_action_definitions=intermediate_action_definitions)
50
+ # Construct a dependency tree that will structure role casting during action targeting. This will
51
+ # be defined across 'parent' and 'children' role fields, which are modified in place by this method.
52
+ for action_definition in intermediate_action_definitions:
53
+ _build_role_casting_dependency_tree(action_definition=action_definition)
54
+ # Convert the 'roles' field into a convenient mapping from role name to role definition
55
+ for action_definition in intermediate_action_definitions:
56
+ action_definition['roles'] = {role['name']: role for role in action_definition['roles']}
57
+ # Convert the 'preconditions' field into a mapping from role name to (only) the preconditions
58
+ # that must be evaluated to cast that role.
59
+ finalized_action_definitions = _attribute_preconditions(
60
+ intermediate_action_definitions=intermediate_action_definitions
61
+ )
62
+ # Isolate the trope definitions
63
+ finalized_trope_definitions = combined_ast["tropes"]
64
+ # Create metadata to be attached to the compiled content bundle
65
+ content_bundle_metadata = _create_metadata(
66
+ action_definitions=finalized_action_definitions,
67
+ trope_definitions=finalized_trope_definitions
68
+ )
69
+ # Package up and return the compiled content bundle
70
+ compiled_content_bundle: viv_compiler.types.CompiledContentBundle = {
71
+ "tropes": {trope["name"]: trope for trope in finalized_trope_definitions},
72
+ "actions": {action["name"]: action for action in finalized_action_definitions},
73
+ "meta": content_bundle_metadata
74
+ }
75
+ return compiled_content_bundle
76
+
77
+
78
+ def _handle_action_definition_inheritance(
79
+ raw_action_definitions: list[viv_compiler.types.RawActionDefinition]
80
+ ) -> None:
81
+ """Modify the given action definitions in place to honor all inheritance declarations.
82
+
83
+ Args:
84
+ raw_action_definitions: Action definitions for which the inheritance postprocessing
85
+ step has not yet been conducted.
86
+
87
+ Returns:
88
+ None (modifies the input definitions are modified in place).
89
+ """
90
+ outstanding_child_action_names = [action['name'] for action in raw_action_definitions if action['parent']]
91
+ while outstanding_child_action_names:
92
+ for i in range(len(raw_action_definitions)):
93
+ child_action_definition = raw_action_definitions[i]
94
+ if child_action_definition['name'] not in outstanding_child_action_names:
95
+ continue
96
+ if child_action_definition['parent'] in outstanding_child_action_names:
97
+ # This action's parent itself has a parent, and so we need to wait for the parent
98
+ # to inherit its material first (and so on if there's further dependencies).
99
+ continue
100
+ try:
101
+ parent_action_definition = next(
102
+ action for action in raw_action_definitions if action['name'] == child_action_definition['parent']
103
+ )
104
+ except StopIteration:
105
+ raise KeyError(
106
+ f"Action '{child_action_definition['name']}' declares undefined parent action "
107
+ f"'{child_action_definition['parent']}'"
108
+ )
109
+ # Handle any 'join' flags
110
+ merged_action_definition = _merge_action_definitions(
111
+ child_action_definition=child_action_definition,
112
+ parent_action_definition=parent_action_definition
113
+ )
114
+ # Handle any role-renaming declarations
115
+ _handle_role_renaming_declarations(merged_action_definition=merged_action_definition)
116
+ # Save the finalized child action
117
+ raw_action_definitions[i] = merged_action_definition
118
+ outstanding_child_action_names.remove(child_action_definition['name'])
119
+
120
+
121
+ def _merge_action_definitions(
122
+ child_action_definition: viv_compiler.types.RawActionDefinition,
123
+ parent_action_definition: viv_compiler.types.RawActionDefinition
124
+ ) -> viv_compiler.types.RawActionDefinition:
125
+ """Clone the given action definitions and return a merged one that honors the author's inheritance declarations.
126
+
127
+ Args:
128
+ child_action_definition: The child action definition that will inherit from the given parent definition.
129
+ parent_action_definition: The parent action definition from which the given child definition will inherit.
130
+
131
+ Returns:
132
+ A merged action definition that honors the author's inheritance declarations.
133
+ """
134
+ new_merged_action_definition = copy.deepcopy(parent_action_definition)
135
+ child_action_definition = cast(dict[str, Any], child_action_definition)
136
+ parent_action_definition = cast(dict[str, Any], parent_action_definition)
137
+ new_merged_action_definition = cast(dict[str, Any], new_merged_action_definition)
138
+ field_name_to_join_flag = {
139
+ "tags": "_join_tags",
140
+ "roles": "_join_roles",
141
+ "preconditions": "_join_preconditions",
142
+ "scratch": "_join_scratch",
143
+ "effects": "_join_effects",
144
+ "reactions": "_join_reactions",
145
+ "saliences": "_join_saliences",
146
+ "associations": "_join_associations",
147
+ "embargoes": "_join_embargoes",
148
+ }
149
+ for key in list(child_action_definition):
150
+ if key.startswith("_join"):
151
+ continue
152
+ if key in field_name_to_join_flag and field_name_to_join_flag[key] in child_action_definition:
153
+ both_dicts = (
154
+ isinstance(new_merged_action_definition[key], dict)
155
+ and isinstance(child_action_definition[key], dict)
156
+ )
157
+ both_lists = (
158
+ isinstance(new_merged_action_definition[key], list) and
159
+ isinstance(child_action_definition[key], list)
160
+ )
161
+ if key not in parent_action_definition: # We can just use the child's value
162
+ new_merged_action_definition[key] = child_action_definition[key]
163
+ elif both_dicts:
164
+ new_merged_action_definition[key].update(child_action_definition[key])
165
+ elif both_lists:
166
+ new_merged_action_definition[key] += child_action_definition[key]
167
+ else:
168
+ raise TypeError(
169
+ f"Cannot join field '{key}' in action '{child_action_definition['name']}': "
170
+ f"expected dict/list, got {type(new_merged_action_definition[key]).__name__} "
171
+ f"and {type(child_action_definition[key]).__name__}"
172
+ )
173
+ del child_action_definition[field_name_to_join_flag[key]]
174
+ else:
175
+ new_merged_action_definition[key] = child_action_definition[key]
176
+ return new_merged_action_definition
177
+
178
+
179
+ def _handle_role_renaming_declarations(
180
+ merged_action_definition: viv_compiler.types.RawActionDefinition
181
+ ) -> None:
182
+ """Modify the given action definition in place to honor all role-renaming declarations.
183
+
184
+ A role-renaming declaration (e.g. `new_name<<old_name`) allows an author to rename a role that
185
+ is inherited from a parent action. This entails not just updating the role's name in the child
186
+ action, but also updating any references to the role in material inherited from the parent.
187
+
188
+ Args:
189
+ merged_action_definition: A merged action definition, meaning one that was produced by merging
190
+ parent content into the definition of a child that inherits from the parent.
191
+
192
+ Returns:
193
+ None (modifies the input definitions are modified in place).
194
+ """
195
+ # Build a mapping from old names to new name
196
+ old_name_to_new_name: dict[viv_compiler.types.RoleName, viv_compiler.types.RoleName] = {}
197
+ for role_renaming_declaration in merged_action_definition["roles"]:
198
+ if "_role_renaming" in role_renaming_declaration:
199
+ source_role_is_defined = any(
200
+ r for r in merged_action_definition["roles"] if r['name'] == role_renaming_declaration["_source_name"]
201
+ )
202
+ if not source_role_is_defined:
203
+ raise ValueError(
204
+ f"Action '{merged_action_definition['name']}' attempts to rename role "
205
+ f"'{role_renaming_declaration['_source_name']}', but no such role is defined "
206
+ f"in the parent action '{merged_action_definition['parent']}'"
207
+ )
208
+ old_name_to_new_name[role_renaming_declaration["_source_name"]] = role_renaming_declaration["_target_name"]
209
+ # If the dictionary is empty, there's no role-renaming declarations to handle and we can return now
210
+ if not old_name_to_new_name:
211
+ return
212
+ # Otherwise, let's proceed. First, filter out the role-renaming directives.
213
+ merged_action_definition["roles"] = [
214
+ role for role in merged_action_definition["roles"] if "_role_renaming" not in role
215
+ ]
216
+ # Next, update the corresponding role definitions
217
+ for role in merged_action_definition["roles"]:
218
+ if role["name"] in old_name_to_new_name:
219
+ role["name"] = old_name_to_new_name[role["name"]]
220
+ # Update any embargo `roles` fields, which contain bare role names (not references or role unpackings)
221
+ for embargo in merged_action_definition["embargoes"]:
222
+ if not embargo["roles"]:
223
+ continue
224
+ updated_roles_field = []
225
+ for role_name in embargo["roles"]:
226
+ updated_role_name = old_name_to_new_name[role_name] if role_name in old_name_to_new_name else role_name
227
+ updated_roles_field.append(updated_role_name)
228
+ embargo["roles"] = updated_roles_field
229
+ # Recursively walk the action definition to update all applicable references and role unpackings
230
+ _rewrite_role_references(ast_chunk=merged_action_definition, old_name_to_new_name=old_name_to_new_name)
231
+
232
+
233
+ def _rewrite_role_references(
234
+ ast_chunk: Any,
235
+ old_name_to_new_name: dict[viv_compiler.types.RoleName, viv_compiler.types.RoleName]
236
+ ) -> Any:
237
+ """Recurse over the given AST chunk to honor any role-renaming declarations captured in the given mapping.
238
+
239
+ This function only updates entity-reference and role-unpacking AST nodes, which it reaches by
240
+ recursively visiting all dictionary values and list elements.
241
+
242
+ Args:
243
+ ast_chunk: The AST chunk to search.
244
+ old_name_to_new_name: A mapping from old role names to new role names, as specified in all the
245
+ role-renaming declarations contained in a given action definition that uses inheritance.
246
+
247
+ Returns:
248
+ The updated AST chunk, which will also be mutated in place. (Note: its shape will never change.)
249
+ """
250
+ # Recurse over a list value
251
+ if isinstance(ast_chunk, list):
252
+ for i in range(len(ast_chunk)):
253
+ ast_chunk[i] = _rewrite_role_references(ast_chunk=ast_chunk[i], old_name_to_new_name=old_name_to_new_name)
254
+ return ast_chunk
255
+ # If it's a dictionary, we may just have a reference or a role unpacking...
256
+ if isinstance(ast_chunk, dict):
257
+ node_type = ast_chunk.get("type")
258
+ # Rename an entity-reference anchor, if applicable, and recurse over its value
259
+ if node_type == viv_compiler.types.ExpressionDiscriminator.ENTITY_REFERENCE:
260
+ value = ast_chunk.get("value")
261
+ name_of_reference_anchor_role = value["anchor"]
262
+ if name_of_reference_anchor_role in old_name_to_new_name:
263
+ value["anchor"] = old_name_to_new_name[name_of_reference_anchor_role]
264
+ for i in range(len(value["path"])):
265
+ value["path"][i] = _rewrite_role_references(
266
+ ast_chunk=value["path"][i],
267
+ old_name_to_new_name=old_name_to_new_name
268
+ )
269
+ return ast_chunk
270
+ # Rename a role-unpacking target, if applicable (no need to recurse)
271
+ if node_type == viv_compiler.types.ExpressionDiscriminator.ROLE_UNPACKING:
272
+ name_of_role_to_unpack = ast_chunk.get("value")
273
+ if name_of_role_to_unpack in old_name_to_new_name:
274
+ ast_chunk["value"] = old_name_to_new_name[name_of_role_to_unpack]
275
+ return ast_chunk
276
+ # Recurse over any other dictionary value
277
+ for key, value in ast_chunk.items():
278
+ ast_chunk[key] = _rewrite_role_references(ast_chunk=value, old_name_to_new_name=old_name_to_new_name)
279
+ return ast_chunk
280
+ # For any other kind of value, there's no need to recurse
281
+ return ast_chunk
282
+
283
+
284
+ def _add_optional_field_defaults(
285
+ intermediate_action_definitions: list[viv_compiler.types.RawActionDefinition],
286
+ ) -> None:
287
+ """Modify the given action definitions in place to add default values for any elided optional fields.
288
+
289
+ Args:
290
+ intermediate_action_definitions: Action definitions for which the inheritance postprocessing
291
+ step has already been conducted.
292
+
293
+ Returns:
294
+ None (modifies the input definitions are modified in place).
295
+ """
296
+ for action_definition in intermediate_action_definitions:
297
+ for key, default in viv_compiler.config.ACTION_DEFINITION_OPTIONAL_FIELD_DEFAULT_VALUES.items():
298
+ action_definition.setdefault(key, copy.deepcopy(default))
299
+
300
+
301
+ def _wrap_expressions_with_role_references(
302
+ intermediate_action_definitions: list[viv_compiler.types.RawActionDefinition],
303
+ ) -> list[viv_compiler.types.IntermediateActionDefinition]:
304
+ """Return modified action definitions in which all preconditions, effects, and reactions
305
+ are wrapped with lists containing all roles referred to in those expressions.
306
+
307
+ Args:
308
+ intermediate_action_definitions: Raw action definitions honoring inheritance declarations and
309
+ containing default values for any elided optional fields.
310
+
311
+ Returns:
312
+ Intermediate action definitions whose preconditions, effects, and reactions are wrapped in
313
+ structures listing all roles referred to in those expressions.
314
+ """
315
+ modified_intermediate_action_definitions: list[viv_compiler.types.IntermediateActionDefinition] = []
316
+ for action_definition in intermediate_action_definitions:
317
+ precondition_wrappers = []
318
+ for precondition in action_definition['preconditions']:
319
+ wrapper = {
320
+ "body": precondition,
321
+ "references": viv_compiler.utils.get_all_referenced_roles(ast_chunk=precondition)
322
+ }
323
+ precondition_wrappers.append(wrapper)
324
+ effect_wrappers = []
325
+ for effect in action_definition['effects']:
326
+ wrapper = {"body": effect, "references": viv_compiler.utils.get_all_referenced_roles(ast_chunk=effect)}
327
+ effect_wrappers.append(wrapper)
328
+ reaction_wrappers = []
329
+ for reaction in action_definition['reactions']:
330
+ wrapper = {
331
+ "body": reaction,
332
+ "references": viv_compiler.utils.get_all_referenced_roles(ast_chunk=reaction)
333
+ }
334
+ reaction_wrappers.append(wrapper)
335
+ modified_intermediate_action_definition: viv_compiler.types.IntermediateActionDefinition = (
336
+ action_definition
337
+ | {
338
+ "preconditions": precondition_wrappers,
339
+ "effects": effect_wrappers,
340
+ "reactions": reaction_wrappers
341
+ }
342
+ )
343
+ modified_intermediate_action_definitions.append(modified_intermediate_action_definition)
344
+ return modified_intermediate_action_definitions
345
+
346
+
347
+ def _attribute_initiator_role(
348
+ intermediate_action_definitions: list[viv_compiler.types.IntermediateActionDefinition],
349
+ ) -> None:
350
+ """Modify the given action definitions in place to attribute an initiator role.
351
+
352
+ Args:
353
+ intermediate_action_definitions: Action definitions that do not yet have the 'initiator' field added in.
354
+
355
+ Returns:
356
+ None (modifies the input definitions in place).
357
+ """
358
+ for action_definition in intermediate_action_definitions:
359
+ if 'roles' not in action_definition:
360
+ raise KeyError(f"Action '{action_definition['name']}' has no 'roles' field (this is required)")
361
+ try:
362
+ initiator_role = next(role for role in action_definition['roles'] if role['initiator'])
363
+ except (StopIteration, KeyError):
364
+ raise KeyError(f"Action '{action_definition['name']}' has no initiator role")
365
+ action_definition['initiator'] = initiator_role
366
+
367
+
368
+ def _attribute_binding_pool_cachability_values(
369
+ intermediate_action_definitions: list[viv_compiler.types.IntermediateActionDefinition],
370
+ ) -> None:
371
+ """Modify the given action definitions in place to mark binding-pool declarations as cachable/uncachable.
372
+
373
+ A role has a cachable binding-pool declaration when it does not reference a non-initiator role.
374
+
375
+ Args:
376
+ intermediate_action_definitions: Action definitions that do have the 'initiator' field added in, but
377
+ have not yet been processed for pool cachability.
378
+
379
+ Returns:
380
+ None (modifies the input definitions in place).
381
+ """
382
+ for action_definition in intermediate_action_definitions:
383
+ for role_definition in action_definition['roles']:
384
+ if not role_definition['pool']:
385
+ continue
386
+ role_definition['pool']['uncachable'] = False
387
+ all_pool_references = viv_compiler.utils.get_all_referenced_roles(role_definition['pool'])
388
+ if any(role for role in all_pool_references if role != action_definition['initiator']['name']):
389
+ role_definition['pool']['uncachable'] = True
390
+
391
+
392
+ def _build_role_casting_dependency_tree(action_definition: viv_compiler.types.IntermediateActionDefinition) -> None:
393
+ """Construct a dependency tree spanning required roles by modifying the given action definition in place.
394
+
395
+ The dependency tree constructed by this method will be defined by setting 'parent' and 'children'
396
+ fields in each of the given action definition's role definitions. This tree will be used at runtime
397
+ to structure role casting during action targeting. Its edges represent dependency from two sources:
398
+ 1) the child role's pool directive being anchored in the parent role, or 2) the child role sharing
399
+ one or more preconditions with the parent role and/or its ancestors in the tree (as constructed to
400
+ that point). During action targeting, role casting will proceed down the dependency tree in a depth-
401
+ first manner, with backtracking working in the inverse direction, upward along the dependency tree.
402
+ This allows for sequential role casting and also greatly reduces the frequency of re-evaluating
403
+ preconditions unnecessarily, since backtracking will not revisit roles without a dependency relation
404
+ to the one for which casting failed. Note that the tree is technically a list of trees, where each
405
+ one is rooted in a role that is either a) the initiator role, b) a role anchored in the initiator
406
+ role, or c) a role with no anchor. Since the subtrees appear in order, we can conceive of it as a
407
+ single tree rooted in an implied single root. Note also that this tree only contains required roles,
408
+ since optional roles are always cast last, in a manner that does not require backtracking.
409
+
410
+ Args:
411
+ action_definition: An intermediate action definition for which the 'initiator' field has been set.
412
+
413
+ Returns:
414
+ None (modifies the given action definition in place).
415
+ """
416
+ # Set up some data that is needed by various helper functions below
417
+ required_roles = [role for role in action_definition["roles"] if role['min'] > 0 and not role['initiator']]
418
+ role_name_to_definition = {role_definition['name']: role_definition for role_definition in required_roles}
419
+ # Record dependency relations that are rooted in binding-pool directives
420
+ _record_binding_pool_dependencies(
421
+ action_definition=action_definition,
422
+ required_roles=required_roles,
423
+ role_name_to_definition=role_name_to_definition
424
+ )
425
+ # Now we will record dependencies rooted in shared preconditions
426
+ role_name_to_role_names_sharing_preconditions = _detect_precondition_dependencies(
427
+ action_definition=action_definition,
428
+ required_roles=required_roles,
429
+ role_name_to_definition=role_name_to_definition
430
+ )
431
+ _record_precondition_dependencies(
432
+ action_definition=action_definition,
433
+ required_roles=required_roles,
434
+ role_name_to_definition=role_name_to_definition,
435
+ role_name_to_role_names_sharing_preconditions=role_name_to_role_names_sharing_preconditions
436
+ )
437
+ # Finally, let's organize the dependency relations into a single tree rooted in the initiator role
438
+ _organize_dependency_tree(
439
+ action_definition=action_definition,
440
+ required_roles=required_roles,
441
+ role_name_to_definition=role_name_to_definition,
442
+ )
443
+
444
+
445
+ def _record_binding_pool_dependencies(
446
+ action_definition: viv_compiler.types.IntermediateActionDefinition,
447
+ required_roles: list[viv_compiler.types.RoleDefinition],
448
+ role_name_to_definition: dict[str, viv_compiler.types.RoleDefinition],
449
+ ) -> None:
450
+ """Modify the given action definition in place to attribute dependency relations among
451
+ its roles that are rooted in their respective binding-pool directives.
452
+
453
+ If a given role R1 references another role R2 in its binding-pool directive, R1 will be
454
+ specified as a child of R2 in the dependency tree.
455
+
456
+ Args:
457
+ action_definition: An intermediate action definition whose 'initiator' field has already been set.
458
+ required_roles: A list containing all required roles in the given action definitions.
459
+ role_name_to_definition: A mapping from role names to role definitions.
460
+
461
+ Returns:
462
+ None (modifies the action definition in place).
463
+ """
464
+ # First, add in any roles that are anchored in the initiator or that have no anchor
465
+ already_included = set() # A set containing role names already added to the tree
466
+ for role in required_roles:
467
+ if role["pool"]:
468
+ all_roles_referenced_in_pools = viv_compiler.utils.get_all_referenced_roles(role['pool'])
469
+ if all_roles_referenced_in_pools:
470
+ parent_name = all_roles_referenced_in_pools[0]
471
+ if parent_name != action_definition['initiator']['name']:
472
+ continue
473
+ already_included.add(role["name"])
474
+ # Next, for each role anchored in another role, attach the former as a child of the latter
475
+ while True:
476
+ try:
477
+ child = next(role for role in required_roles if role['pool'] and role['name'] not in already_included)
478
+ except StopIteration:
479
+ break
480
+ parent_name = viv_compiler.utils.get_all_referenced_roles(child['pool'])[0]
481
+ if parent_name not in already_included:
482
+ # The parent comes later in the 'roles' list. We'll come back around, so `pass` here and move on.
483
+ pass
484
+ child['parent'] = parent_name
485
+ parent = role_name_to_definition[parent_name]
486
+ parent['children'].append(child['name'])
487
+ already_included.add(child['name'])
488
+
489
+
490
+ def _detect_precondition_dependencies(
491
+ action_definition: viv_compiler.types.IntermediateActionDefinition,
492
+ required_roles: list[viv_compiler.types.RoleDefinition],
493
+ role_name_to_definition: dict[str, viv_compiler.types.RoleDefinition],
494
+ ) -> dict[str, set[str]]:
495
+ """Returns a mapping from role name to the names of all other roles that share preconditions.
496
+
497
+ If two roles are referenced in the same precondition, one of them will depend on the other,
498
+ with the direction of the relation depending on other factors. In this function, we merely
499
+ detect such mutual relations, and later on we will make these unidirectional.
500
+
501
+ Args:
502
+ action_definition: An intermediate action definition.
503
+ required_roles: A list containing all required roles in the given action definitions.
504
+ role_name_to_definition: A mapping from role names to role definitions.
505
+
506
+ Returns:
507
+ A mapping from role name to the names of all other roles that share preconditions.
508
+ """
509
+ role_name_to_role_names_sharing_preconditions = {role['name']: set() for role in required_roles}
510
+ for precondition in action_definition["preconditions"]:
511
+ # Skip any precondition that references an optional role
512
+ if any(role_name for role_name in precondition["references"] if role_name not in role_name_to_definition):
513
+ continue
514
+ for precondition_role_name in precondition["references"]:
515
+ for other_precondition_role_name in precondition["references"]:
516
+ if precondition_role_name == other_precondition_role_name:
517
+ continue
518
+ role_name_to_role_names_sharing_preconditions[precondition_role_name].add(other_precondition_role_name)
519
+ return role_name_to_role_names_sharing_preconditions
520
+
521
+
522
+ def _record_precondition_dependencies(
523
+ action_definition: viv_compiler.types.IntermediateActionDefinition,
524
+ required_roles: list[viv_compiler.types.RoleDefinition],
525
+ role_name_to_definition: dict[str, viv_compiler.types.RoleDefinition],
526
+ role_name_to_role_names_sharing_preconditions: dict[str, set[str]]
527
+ ) -> None:
528
+ """Modify the given action definition in place to attribute dependency relations among
529
+ its roles that are rooted in shared preconditions.
530
+
531
+ In the previous step, we detected cases of roles sharing preconditions. In this step,
532
+ we will operate over these to record actual unidirectional dependencies.
533
+
534
+ Args:
535
+ action_definition: An intermediate action definition whose binding-pool dependencies have
536
+ already been attributed.
537
+ required_roles: A list containing all required roles in the given action definitions.
538
+ role_name_to_definition: A mapping from role names to role definitions.
539
+ role_name_to_role_names_sharing_preconditions: A mapping from role name to the names of
540
+ all other roles that share preconditions.
541
+
542
+ Returns:
543
+ None (modifies the action definition in place).
544
+ """
545
+ # For each role, retrieve the role's ancestors and descendants. If a role R shares one or
546
+ # more preconditions with a role S that is not a lineal relative of R, we will shift a subtree
547
+ # containing S to be rooted in R. More on this below.
548
+ for role in required_roles:
549
+ # Search for a role with which this role shares one or more preconditions
550
+ role_ancestors = _get_role_dependency_tree_ancestors(
551
+ action_definition=action_definition, role_name=role['name']
552
+ )
553
+ role_descendants = _get_role_dependency_tree_descendants(
554
+ action_definition=action_definition, role_name=role['name']
555
+ )
556
+ for role_sharing_preconditions in role_name_to_role_names_sharing_preconditions[role['name']]:
557
+ if role_sharing_preconditions not in role_ancestors | role_descendants:
558
+ # We now want to make the role at hand, R, an ancestor of this role with which it shares
559
+ # preconditions, S. (Since sharing preconditions is a mutual dependency, we could just as
560
+ # well work in the opposite direction. Due to our loop construction, R will have been defined
561
+ # prior to S, however, and so we prefer for R to be the ancestor so that we can cast them in
562
+ # the author-defined order -- this is likely a good heuristic for reducing backtracking.) In
563
+ # shifting S to become a descendant of R, we must keep S's existing dependency structure
564
+ # intact, since otherwise we could e.g. separate S from its pool anchor. Naively, we might
565
+ # try to make its primogenitor, P, a child of R, but this will fail in the case where R
566
+ # also descends from P, in particular the case where R's pool is anchored in P. (Since P would
567
+ # then be a child of R, despite R requiring P to be cast first.) In other words, we need to take
568
+ # special care when S is a collateral relative of R. To manage this correctly, we will retrieve
569
+ # the highest-level ancestor of S that is not also an ancestor of R, call it A, and then make
570
+ # A a child of R.
571
+ ancestor_name = role_sharing_preconditions
572
+ while role_name_to_definition[ancestor_name]['parent']:
573
+ if role_name_to_definition[ancestor_name]['parent'] in role_ancestors:
574
+ break
575
+ ancestor_name = role_name_to_definition[ancestor_name]['parent']
576
+ # Leaving its descendant structure intact, make A a child of R
577
+ original_parent = role_name_to_definition[ancestor_name]['parent']
578
+ if original_parent:
579
+ role_name_to_definition[original_parent]['children'].remove(ancestor_name)
580
+ role_name_to_definition[ancestor_name]['parent'] = role['name']
581
+ role['children'].append(ancestor_name)
582
+ role_ancestors.add(role_sharing_preconditions)
583
+
584
+
585
+ def _get_role_dependency_tree_ancestors(
586
+ action_definition: viv_compiler.types.IntermediateActionDefinition,
587
+ role_name: str
588
+ ) -> set[str]:
589
+ """Return all ancestors of the given role in the dependency tree for the given action definition.
590
+
591
+ Args:
592
+ action_definition: An AST postprocessed into an action definition.
593
+ role_name: The role whose dependency-tree ancestors are to be retrieved.
594
+
595
+ Returns:
596
+ A set containing the names of all dependency-tree ancestors of the given role.
597
+ """
598
+ role_ancestors = set()
599
+ if isinstance(action_definition['roles'], list):
600
+ role_definitions = action_definition['roles']
601
+ else:
602
+ role_definitions = action_definition['roles'].values()
603
+ role_definition = next(role for role in role_definitions if role['name'] == role_name)
604
+ parent_name = role_definition['parent']
605
+ if parent_name:
606
+ role_ancestors.add(parent_name)
607
+ role_ancestors |= _get_role_dependency_tree_ancestors(
608
+ action_definition=action_definition, role_name=parent_name
609
+ )
610
+ return role_ancestors
611
+
612
+
613
+ def _get_role_dependency_tree_descendants(
614
+ action_definition: viv_compiler.types.IntermediateActionDefinition,
615
+ role_name: str
616
+ ) -> set[str]:
617
+ """Return all descendants of the given role in the dependency tree for the given action definition.
618
+
619
+ Args:
620
+ action_definition: An AST postprocessed into an action definition.
621
+ role_name: The role whose dependency-tree descendants are to be retrieved.
622
+
623
+ Returns:
624
+ A set containing the names of all dependency-tree descendants of the given role.
625
+ """
626
+ role_descendants = set()
627
+ if isinstance(action_definition['roles'], list):
628
+ role_definitions = action_definition['roles']
629
+ else:
630
+ role_definitions = action_definition['roles'].values()
631
+ role_definition = next(role for role in role_definitions if role['name'] == role_name)
632
+ for child_name in role_definition['children']:
633
+ role_descendants.add(child_name)
634
+ role_descendants |= _get_role_dependency_tree_descendants(
635
+ action_definition=action_definition, role_name=child_name
636
+ )
637
+ return role_descendants
638
+
639
+
640
+ def _organize_dependency_tree(
641
+ action_definition: viv_compiler.types.IntermediateActionDefinition,
642
+ required_roles: list[viv_compiler.types.RoleDefinition],
643
+ role_name_to_definition: dict[str, viv_compiler.types.RoleDefinition],
644
+ ) -> None:
645
+ """Modify the given action definition in place to construct a single dependency tree rooted in its initiator role.
646
+
647
+ Args:
648
+ action_definition: An intermediate action definition whose dependency relations have already been attributed.
649
+ required_roles: A list containing all required roles in the given action definitions.
650
+ role_name_to_definition: A mapping from role names to role definitions.
651
+
652
+ Returns:
653
+ None (modifies the action definition in place).
654
+ """
655
+ # While we've attributed parents and children to all non-initiator required roles, we haven't stored
656
+ # any listing of the subtrees formed thereby. We need this so that we can proceed through them one by
657
+ # one during role casting. An easy solution here is to assign a list of all subtree-root (i.e., top-level)
658
+ # roles as the 'children' value of the initiator role definition for this action. Let's do so now. We'll
659
+ # also set the initiator as 'parent' for these roles, which is needed during precondition attribution.
660
+ top_level_role_names = [role['name'] for role in required_roles if not role['parent']]
661
+ action_definition['initiator']['children'] = top_level_role_names
662
+ for role_name in top_level_role_names:
663
+ role_name_to_definition[role_name]['parent'] = action_definition['initiator']['name']
664
+ # Before we go, let's sort each listing of children such that larger subtrees come first. This heuristic
665
+ # proceeds from an assumption that it's better to reach fail states as soon as possible, and fail states
666
+ # are more likely to occur amid complex dependencies.
667
+ for role_definition in action_definition['roles']:
668
+ role_definition['children'].sort(
669
+ key=lambda subtree_root_name: (
670
+ len(_get_role_dependency_tree_descendants(action_definition, subtree_root_name)),
671
+ ),
672
+ reverse=True
673
+ )
674
+
675
+
676
+ def _attribute_preconditions(
677
+ intermediate_action_definitions: list[viv_compiler.types.IntermediateActionDefinition]
678
+ ) -> list[viv_compiler.types.ActionDefinition]:
679
+ """Modify the given action definitions in place such that the 'preconditions' field maps
680
+ role names to (only) the preconditions that must be evaluated to cast that role.
681
+
682
+ As a minor side effect, this method also sorts precondition references in dependency-stream order.
683
+
684
+ Args:
685
+ intermediate_action_definitions: Action definitions for which role-dependency trees have already been computed.
686
+
687
+ Returns:
688
+ Finalized action definitions, since this is the last postprocessing step.
689
+ """
690
+ final_action_definitions: list[viv_compiler.types.ActionDefinition] = []
691
+ for action_definition in intermediate_action_definitions:
692
+ role_name_to_preconditions = {role_name: [] for role_name in action_definition['roles'].keys()}
693
+ for precondition in action_definition["preconditions"]:
694
+ # If there is no reference, assign it to the initiator. Such preconditions (e.g., chance only)
695
+ # can be evaluated at the beginning of action targeting, prior to casting any additional roles.
696
+ if not precondition['references']:
697
+ role_name_to_preconditions[action_definition['initiator']['name']].append(precondition)
698
+ continue
699
+ # If there's a single reference, assign it to that role
700
+ if len(precondition['references']) == 1:
701
+ role_name_to_preconditions[precondition['references'][0]].append(precondition)
702
+ continue
703
+ # If there's any optional role referenced, assign it to that role
704
+ for role_name in precondition['references']:
705
+ role_definition = next(
706
+ role for role in action_definition['roles'].values() if role['name'] == role_name
707
+ )
708
+ if role_definition['min'] == 0:
709
+ role_name_to_preconditions[role_name].append(precondition)
710
+ continue
711
+ # Otherwise, assign it to the role furthest downstream in the dependency structure. Since all
712
+ # roles sharing a precondition will be situated in a direct line in this structure, we can
713
+ # easily identify the most downstream role as the one with the most ancestors. For clarity,
714
+ # we'll sort the actual precondition 'references' value in upstream-to-downstream order.
715
+ precondition['references'].sort(
716
+ key=lambda role_name: len(_get_role_dependency_tree_ancestors(action_definition, role_name)),
717
+ )
718
+ role_name_to_preconditions[precondition['references'][-1]].append(precondition)
719
+ final_action_definition: viv_compiler.types.ActionDefinition = (
720
+ action_definition | {"preconditions": role_name_to_preconditions}
721
+ )
722
+ final_action_definitions.append(final_action_definition)
723
+ return final_action_definitions
724
+
725
+
726
+ def _create_metadata(
727
+ action_definitions: list[viv_compiler.types.ActionDefinition],
728
+ trope_definitions: list[viv_compiler.types.TropeDefinition],
729
+ ) -> viv_compiler.types.CompiledContentBundleMetadata:
730
+ """Return a package containing metadata for the given compiled content bundle.
731
+
732
+ Args:
733
+ action_definitions: List containing all actions in the content bundle.
734
+ trope_definitions: List containing all tropes in the content bundle.
735
+
736
+ Returns:
737
+ A metadata package for the given content bundle.
738
+ """
739
+ metadata = {
740
+ "vivVersion": __version__,
741
+ "referencedEnums": [],
742
+ "referencedFunctionNames": []
743
+ }
744
+ for ast_chunk in action_definitions + trope_definitions:
745
+ all_referenced_enum_names = set(viv_compiler.utils.get_all_referenced_enum_names(ast_chunk=ast_chunk))
746
+ metadata["referencedEnums"].extend(all_referenced_enum_names)
747
+ all_referenced_function_names = set(viv_compiler.utils.get_all_referenced_enum_names(ast_chunk=ast_chunk))
748
+ metadata["referencedFunctionNames"].extend(all_referenced_function_names)
749
+ return metadata