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.
- viv_compiler/__init__.py +14 -0
- viv_compiler/__main__.py +3 -0
- viv_compiler/_samples/__init__.py +0 -0
- viv_compiler/_samples/smoke-test.viv +5 -0
- viv_compiler/api.py +58 -0
- viv_compiler/backports/__init__.py +1 -0
- viv_compiler/backports/backports.py +12 -0
- viv_compiler/cli.py +237 -0
- viv_compiler/config/__init__.py +1 -0
- viv_compiler/config/config.py +88 -0
- viv_compiler/core/__init__.py +5 -0
- viv_compiler/core/core.py +185 -0
- viv_compiler/core/importer.py +111 -0
- viv_compiler/core/postprocessor.py +749 -0
- viv_compiler/core/validator.py +915 -0
- viv_compiler/core/visitor.py +1188 -0
- viv_compiler/grammar/__init__.py +0 -0
- viv_compiler/grammar/viv.peg +228 -0
- viv_compiler/py.typed +1 -0
- viv_compiler/types/__init__.py +3 -0
- viv_compiler/types/content_public_schemas.py +420 -0
- viv_compiler/types/dsl_public_schemas.py +566 -0
- viv_compiler/types/internal_types.py +167 -0
- viv_compiler/utils/__init__.py +1 -0
- viv_compiler/utils/_version.py +2 -0
- viv_compiler/utils/utils.py +171 -0
- viv_compiler-0.1.0.dist-info/METADATA +284 -0
- viv_compiler-0.1.0.dist-info/RECORD +32 -0
- viv_compiler-0.1.0.dist-info/WHEEL +5 -0
- viv_compiler-0.1.0.dist-info/entry_points.txt +3 -0
- viv_compiler-0.1.0.dist-info/licenses/LICENSE +21 -0
- viv_compiler-0.1.0.dist-info/top_level.txt +1 -0
@@ -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
|