viv-compiler 0.1.1__py3-none-any.whl → 0.1.2__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.
@@ -1,2 +1,2 @@
1
1
  # This is programmatically updated by viv/scripts/bump_version.sh
2
- __version__ = "0.1.0"
2
+ __version__ = "0.1.2"
viv_compiler/cli.py CHANGED
@@ -10,6 +10,8 @@ import traceback
10
10
  import viv_compiler.config
11
11
  from pathlib import Path
12
12
  from importlib import resources
13
+
14
+ from viv_compiler import VivCompileError
13
15
  from .api import compile_from_path, get_version
14
16
  from viv_compiler.types import CompiledContentBundle
15
17
 
@@ -189,10 +191,9 @@ def _invoke_compiler(args: argparse.Namespace) -> CompiledContentBundle:
189
191
  except Exception:
190
192
  pass
191
193
  sys.exit(1)
192
- except Exception as e:
193
- print(f"Error encountered during compilation: {e}", file=sys.stderr)
194
- if args.debug:
195
- traceback.print_exc()
194
+ except VivCompileError as e:
195
+ cause = e.__cause__ or e
196
+ print(f"Error encountered during compilation:\n\n{cause}\n", file=sys.stderr)
196
197
  sys.exit(1)
197
198
 
198
199
 
@@ -25,9 +25,9 @@ NEGATABLE_EXPRESSION_TYPES = {
25
25
  ExpressionDiscriminator.CONJUNCTION,
26
26
  ExpressionDiscriminator.DISJUNCTION,
27
27
  ExpressionDiscriminator.ENTITY_REFERENCE,
28
- ExpressionDiscriminator.LOCAL_VARIABLE_REFERENCE,
29
28
  ExpressionDiscriminator.LOOP,
30
29
  ExpressionDiscriminator.MEMBERSHIP_TEST,
30
+ ExpressionDiscriminator.SYMBOL_REFERENCE,
31
31
  ExpressionDiscriminator.TROPE_FIT_EXPRESSION,
32
32
  }
33
33
 
@@ -56,28 +56,21 @@ REACTION_FIELD_DEFAULT_OPTIONS = {
56
56
  "abandonmentConditions": [],
57
57
  }
58
58
 
59
- # The path to which the global-variable sigil `$` expands. This sigil is really just syntactic sugar for
59
+ # Name for the special action self-reference role, which is always bound to the action itself
60
+ ACTION_SELF_REFERENCE_ROLE_NAME = 'this'
61
+
62
+ # A set containing the special role names that are automatically created by Viv at various points
63
+ SPECIAL_ROLE_NAMES = {'hearer', ACTION_SELF_REFERENCE_ROLE_NAME}
64
+
65
+ # The path to which the scratch-variable sigil `$` expands. This sigil is really just syntactic sugar for
60
66
  # the path `@this.scratch`, which stores a blackboard local to a performed action. For instance, the scratch
61
- # operation `$foo.bar = 99` is syntactic sugar for the expression `@this.scratch.foo.bar = 99`.
62
- GLOBAL_VARIABLE_REFERENCE_ANCHOR = "this"
63
- GLOBAL_VARIABLE_REFERENCE_PATH_PREFIX = [{
67
+ # operation `$@foo.bar = 99` is syntactic sugar for the expression `@this.scratch.foo.bar = 99`.
68
+ SCRATCH_VARIABLE_REFERENCE_ANCHOR = ACTION_SELF_REFERENCE_ROLE_NAME
69
+ SCRATCH_VARIABLE_REFERENCE_PATH_PREFIX = [{
64
70
  "type": ReferencePathComponentDiscriminator.REFERENCE_PATH_COMPONENT_PROPERTY_NAME,
65
71
  "name": "scratch",
66
72
  }]
67
73
 
68
- # The path to which the local-variable sigil `$$` expands. This sigil is really just a property lookup in
69
- # the special `__locals__` field of an evaluation context, which is a temporary store for scoped local
70
- # variables. For instance, the local-variable reference `$$c` defines an attempt to access `__locals__.c`
71
- # in an evaluation context. Unlike the `$` sigil, this is not syntactic sugar, since the Viv author has
72
- # no other way to reference local variables.
73
- LOCAL_VARIABLE_REFERENCE_PATH = ["__locals__"]
74
-
75
- # Name for the variable to which each character is set when computing their salience for an action
76
- SALIENCES_VARIABLE_NAME = "c"
77
-
78
- # Name for the variable to which each character is set when computing their associations for an action
79
- ASSOCIATIONS_VARIABLE_NAME = "c"
80
-
81
74
  # A default salience value, to be used when the API caller does not provide one
82
75
  DEFAULT_SALIENCE_VALUE = 1.0
83
76
 
@@ -86,3 +79,9 @@ DEFAULT_ASSOCIATIONS_VALUE = []
86
79
 
87
80
  # A default reaction priority value, to be used when the API caller does not provide one
88
81
  DEFAULT_REACTION_PRIORITY_VALUE = 1.0
82
+
83
+ # A list containing the names of action fields in which assignments are permitted
84
+ ACTION_FIELDS_PERMITTING_ASSIGNMENTS = ("scratch", "effects",)
85
+
86
+ # A list containing the names of action fields in which reactions are permitted
87
+ ACTION_FIELDS_PERMITTING_REACTIONS = ("reactions",)
viv_compiler/core/core.py CHANGED
@@ -106,7 +106,7 @@ def _honor_user_supplied_config_parameters(
106
106
  """
107
107
  viv_compiler.config.ACTION_DEFINITION_OPTIONAL_FIELD_DEFAULT_VALUES["saliences"] = {
108
108
  "default": {'type': 'float', 'value': default_salience},
109
- "variable": viv_compiler.config.SALIENCES_VARIABLE_NAME,
109
+ "variable": None,
110
110
  "body": [],
111
111
  }
112
112
  default_associations_expression = {
@@ -115,7 +115,7 @@ def _honor_user_supplied_config_parameters(
115
115
  }
116
116
  viv_compiler.config.ACTION_DEFINITION_OPTIONAL_FIELD_DEFAULT_VALUES["associations"] = {
117
117
  "default": default_associations_expression,
118
- "variable": viv_compiler.config.ASSOCIATIONS_VARIABLE_NAME,
118
+ "variable": None,
119
119
  "body": [],
120
120
  }
121
121
  viv_compiler.config.REACTION_FIELD_DEFAULT_OPTIONS["priority"] = {
@@ -33,15 +33,17 @@ def create_metadata(
33
33
  "buildRoles": [],
34
34
  "timeOfDayConstrainedReactions": []
35
35
  }
36
+ all_referenced_enum_names = []
37
+ all_referenced_function_names = []
36
38
  for ast_chunk in action_definitions + trope_definitions:
37
39
  # Compile all referenced enums
38
- all_referenced_enum_names = set(viv_compiler.utils.get_all_referenced_enum_names(ast_chunk=ast_chunk))
39
- metadata["referencedEnums"].extend(all_referenced_enum_names)
40
+ all_referenced_enum_names.extend(viv_compiler.utils.get_all_referenced_enum_names(ast_chunk=ast_chunk))
40
41
  # Compile all referenced adapter functions
41
- all_referenced_function_names = set(
42
- viv_compiler.utils.get_all_referenced_adapter_function_names(ast_chunk=ast_chunk)
43
- )
44
- metadata["referencedFunctionNames"].extend(all_referenced_function_names)
42
+ all_referenced_function_names.extend(viv_compiler.utils.get_all_referenced_adapter_function_names(
43
+ ast_chunk=ast_chunk
44
+ ))
45
+ metadata["referencedEnums"].extend(sorted(set(all_referenced_enum_names)))
46
+ metadata["referencedFunctionNames"].extend(sorted(set(all_referenced_function_names)))
45
47
  for action_definition in action_definitions:
46
48
  # Compile all roles carrying 'item' and 'build' labels
47
49
  for role_definition in action_definition["roles"].values():
@@ -20,7 +20,7 @@ from pydantic import TypeAdapter
20
20
 
21
21
 
22
22
  def validate_join_directives(raw_action_definitions: list[viv_compiler.types.RawActionDefinition]) -> None:
23
- """Ensure that the given action definition makes proper use of `join` directives.
23
+ """Make sure that the given action definition makes proper use of `join` directives.
24
24
 
25
25
  If this validation check passes, the given action definitions are ready for inheritance to be handled.
26
26
 
@@ -65,10 +65,10 @@ def validate_preliminary_action_definitions(
65
65
  Exception: At least one action definition did not pass validation.
66
66
  """
67
67
  for action_definition in intermediate_action_definitions:
68
- # Ensure that a 'roles' field is present
68
+ # Make sure that a 'roles' field is present
69
69
  if "roles" not in action_definition:
70
70
  raise KeyError(f"Action '{action_definition['name']}' is missing a 'roles' field, which is required")
71
- # Ensure that there is a single initiator role. Note that during initial validation,
71
+ # Make sure that there is a single initiator role. Note that during initial validation,
72
72
  # the 'roles' field is still a list.
73
73
  _detect_wrong_number_of_initiators(action_definition=action_definition)
74
74
  # Detect duplicated role names
@@ -137,7 +137,9 @@ def _detect_reference_to_undefined_role(action_definition: viv_compiler.types.In
137
137
  Exception: The action definition did not pass validation.
138
138
  """
139
139
  # Retrieve the names of all defined roles ('roles' is still a list at this point in postprocessing)
140
- all_defined_role_names = {role['name'] for role in action_definition['roles']} | {'hearer', 'this', 'default'}
140
+ all_defined_role_names = (
141
+ {role['name'] for role in action_definition['roles']} | viv_compiler.config.SPECIAL_ROLE_NAMES
142
+ )
141
143
  # Validate report references
142
144
  if action_definition["report"]:
143
145
  for reference in viv_compiler.utils.get_all_referenced_roles(action_definition["report"]):
@@ -243,11 +245,25 @@ def _validate_trope_definitions(trope_definitions: dict[str, viv_compiler.types.
243
245
  # Detect any reference to an undefined param
244
246
  all_referenced_params = viv_compiler.utils.get_all_referenced_roles(ast_chunk=trope_definition)
245
247
  for referenced_param in all_referenced_params:
246
- if referenced_param not in trope_definition["params"]:
248
+ if referenced_param not in [trope_param["name"] for trope_param in trope_definition["params"]]:
247
249
  raise KeyError(
248
250
  f"Trope '{trope_definition['name']}' references undefined parameter: "
249
251
  f"'{referenced_param}'"
250
252
  )
253
+ # Detect assignments, which are not allowed in trope bodies
254
+ assignments_in_trope_body = viv_compiler.utils.get_all_expressions_of_type(
255
+ expression_type=ExpressionDiscriminator.ASSIGNMENT,
256
+ ast_chunk=trope_definition
257
+ )
258
+ if assignments_in_trope_body:
259
+ raise ValueError(f"Trope '{trope_definition['name']}' has assignment in body (not allowed)")
260
+ # Detect reactions, which are not allowed in trope bodies
261
+ reactions_in_trope_body = viv_compiler.utils.get_all_expressions_of_type(
262
+ expression_type=ExpressionDiscriminator.REACTION,
263
+ ast_chunk=trope_definition
264
+ )
265
+ if reactions_in_trope_body:
266
+ raise ValueError(f"Trope '{trope_definition['name']}' has reaction in body (not allowed)")
251
267
 
252
268
 
253
269
  def _validate_action_definitions(
@@ -279,7 +295,10 @@ def _validate_action_definitions(
279
295
  _validate_action_effects(action_definition=action_definition)
280
296
  # Validate reactions
281
297
  _validate_action_reactions(action_definition=action_definition, all_action_definitions=action_definitions)
298
+ # Validate saliences
299
+ _validate_action_saliences(action_definition=action_definition)
282
300
  # Validate associations
301
+ _validate_action_associations(action_definition=action_definition)
283
302
  # Validate trope-fit expressions
284
303
  _validate_action_trope_fit_expressions(
285
304
  action_definition=action_definition,
@@ -291,6 +310,8 @@ def _validate_action_definitions(
291
310
  _validate_action_loops(action_definition=action_definition)
292
311
  # Validate assignments
293
312
  _validate_action_assignments(action_definition=action_definition)
313
+ # Validate references to scratch variables
314
+ _validate_scratch_variable_references(action_definition=action_definition)
294
315
  # Validate negated expressions
295
316
  _validate_negated_expressions(action_definition=action_definition)
296
317
  # Validate chance expressions
@@ -330,13 +351,13 @@ def _validate_action_roles(action_definition: viv_compiler.types.ActionDefinitio
330
351
  """
331
352
  # Validate the initiator role
332
353
  _validate_action_initiator_role(action_definition=action_definition)
333
- # Ensure all roles have proper 'min' and 'max' values
354
+ # Make sure all roles have proper 'min' and 'max' values
334
355
  _validate_action_role_min_and_max_values(action_definition=action_definition)
335
- # Ensure all roles have proper 'chance' and 'mean' values
356
+ # Make sure all roles have proper 'chance' and 'mean' values
336
357
  _validate_action_role_chance_and_mean_values(action_definition=action_definition)
337
- # Ensure all roles with binding pools have proper ones
358
+ # Make sure all roles with binding pools have proper ones
338
359
  _validate_action_role_pool_directives(action_definition=action_definition)
339
- # Ensure that the 'precast' label is only used if this is a special action
360
+ # Make sure that the 'precast' label is only used if this is a special action
340
361
  _validate_action_role_precast_label_usages(action_definition=action_definition)
341
362
 
342
363
 
@@ -354,13 +375,13 @@ def _validate_action_initiator_role(action_definition: viv_compiler.types.Action
354
375
  """
355
376
  # Retrieve the definition for the action's initiator role
356
377
  initiator_role_definition = action_definition['initiator']
357
- # Ensure that the initiator role has no pool directive
378
+ # Make sure that the initiator role has no pool directive
358
379
  if initiator_role_definition["pool"]:
359
380
  raise ValueError(
360
381
  f"Action '{action_definition['name']}' has initiator role '{initiator_role_definition['name']}' "
361
382
  f"with a pool directive, which is not allowed for initiator roles"
362
383
  )
363
- # Ensure that the initiator role casts exactly one entity
384
+ # Make sure that the initiator role casts exactly one entity
364
385
  if initiator_role_definition['min'] != 1:
365
386
  raise ValueError(
366
387
  f"Action '{action_definition['name']}' has initiator role '{initiator_role_definition['name']}' "
@@ -371,13 +392,13 @@ def _validate_action_initiator_role(action_definition: viv_compiler.types.Action
371
392
  f"Action '{action_definition['name']}' has initiator role '{initiator_role_definition['name']}' "
372
393
  f"with max other than 1 (there must be a single initiator)"
373
394
  )
374
- # Ensure that the initiator role has no specified binding mean
395
+ # Make sure that the initiator role has no specified binding mean
375
396
  if initiator_role_definition['mean'] is not None:
376
397
  raise ValueError(
377
398
  f"Action '{action_definition['name']}' has initiator role '{initiator_role_definition['name']}' "
378
399
  f"with declared casting mean (there must be a single initiator)"
379
400
  )
380
- # Ensure that the initiator role has no specified binding chance
401
+ # Make sure that the initiator role has no specified binding chance
381
402
  if initiator_role_definition['chance'] is not None:
382
403
  raise ValueError(
383
404
  f"Action '{action_definition['name']}' has initiator role '{initiator_role_definition['name']}' "
@@ -479,7 +500,7 @@ def _validate_action_role_pool_directives(action_definition: viv_compiler.types.
479
500
  f"Action '{action_definition['name']}' has role '{role_definition['name']}' "
480
501
  f"that requires a pool declaration but does not have one"
481
502
  )
482
- # Ensure that all pool directives reference at most one other (valid) role
503
+ # Make sure that all pool directives reference at most one other (valid) role
483
504
  if role_definition["pool"]:
484
505
  all_pool_references = viv_compiler.utils.get_all_referenced_roles(role_definition["pool"])
485
506
  if len(all_pool_references) > 1:
@@ -604,25 +625,21 @@ def _validate_action_reactions(
604
625
  Raises:
605
626
  Exception: The action definition did not pass validation.
606
627
  """
607
- # Ensure that all reactions are housed in the proper fields
608
- fields_to_check = (
609
- ("preconditions", action_definition["preconditions"]),
610
- ("scratch", action_definition["scratch"]),
611
- ("effects", action_definition["effects"]),
612
- )
613
- for field_name, field_value in fields_to_check:
614
- reactions_in_this_field = viv_compiler.utils.get_all_expressions_of_type(
615
- expression_type="reaction",
616
- ast_chunk=field_value
617
- )
618
- if reactions_in_this_field:
619
- raise ValueError(
620
- f"Action '{action_definition['name']}' has reaction in '{field_name}' field "
621
- f"(only allowed in 'reactions' field)"
628
+ # Make sure that all reactions are housed in the proper fields
629
+ for field_name, field_value in action_definition.items():
630
+ if field_name not in viv_compiler.config.ACTION_FIELDS_PERMITTING_REACTIONS:
631
+ reactions_in_this_field = viv_compiler.utils.get_all_expressions_of_type(
632
+ expression_type=ExpressionDiscriminator.REACTION,
633
+ ast_chunk=field_value
622
634
  )
635
+ if reactions_in_this_field:
636
+ raise ValueError(
637
+ f"Action '{action_definition['name']}' has reaction in '{field_name}' field "
638
+ f"(only allowed in 'reactions' field)"
639
+ )
623
640
  # Collect all reactions
624
641
  all_reactions = viv_compiler.utils.get_all_expressions_of_type(
625
- expression_type="reaction",
642
+ expression_type=ExpressionDiscriminator.REACTION,
626
643
  ast_chunk=action_definition['reactions']
627
644
  )
628
645
  # Validate each reaction in turn
@@ -640,7 +657,7 @@ def _validate_action_reactions(
640
657
  queued_action_initiator_role_name = queued_action_definition['initiator']['name']
641
658
  initiator_is_bound = False
642
659
  for binding in reaction['bindings']:
643
- if binding['value']['role'] == queued_action_initiator_role_name:
660
+ if binding['role'] == queued_action_initiator_role_name:
644
661
  initiator_is_bound = True
645
662
  break
646
663
  if not initiator_is_bound:
@@ -651,12 +668,21 @@ def _validate_action_reactions(
651
668
  # Make sure the reaction references only roles defined in the queued actions
652
669
  all_queued_action_role_names = queued_action_definition['roles'].keys()
653
670
  for binding in reaction['bindings']:
654
- bound_role_name = binding['value']['role']
671
+ bound_role_name = binding['role']
655
672
  if bound_role_name not in all_queued_action_role_names:
656
673
  raise KeyError(
657
674
  f"Action '{action_definition['name']}' has '{queued_action_name}' reaction that "
658
675
  f"references a role that is undefined for '{queued_action_name}': '{bound_role_name}'"
659
676
  )
677
+ # Make sure that no role appears multiple times in the bindings
678
+ roles_already_precast = set()
679
+ for bindings in reaction['bindings']:
680
+ if bindings['role'] in roles_already_precast:
681
+ raise ValueError(
682
+ f"Action '{action_definition['name']}' has '{queued_action_name}' reaction that "
683
+ f"includes role '{queued_action_initiator_role_name}' in bindings more than once"
684
+ )
685
+ roles_already_precast.add(bindings['role'])
660
686
  # Make sure the reaction precasts all 'precast' roles
661
687
  for role_object in queued_action_definition['roles'].values():
662
688
  if not role_object['precast']:
@@ -664,7 +690,7 @@ def _validate_action_reactions(
664
690
  precast_role_name = role_object['name']
665
691
  role_is_precast = False
666
692
  for binding in reaction['bindings']:
667
- bound_role_name = binding['value']['role']
693
+ bound_role_name = binding['role']
668
694
  if bound_role_name == precast_role_name:
669
695
  role_is_precast = True
670
696
  break
@@ -675,6 +701,50 @@ def _validate_action_reactions(
675
701
  )
676
702
 
677
703
 
704
+ def _validate_action_saliences(action_definition: viv_compiler.types.ActionDefinition) -> None:
705
+ """Validate the given action definition's 'saliences' field.
706
+
707
+ Args:
708
+ action_definition: An action definition from a compiled content bundle.
709
+
710
+ Returns:
711
+ None.
712
+
713
+ Raises:
714
+ Exception: The action definition did not pass validation.
715
+ """
716
+ # Detect cases of a saliences variable shadowing the name of a role from the same action
717
+ all_role_names = viv_compiler.utils.get_all_role_names(action_definition=action_definition)
718
+ if action_definition['saliences']['variable']:
719
+ if action_definition['saliences']['variable']['name'] in all_role_names:
720
+ raise ValueError(
721
+ f"Action '{action_definition['name']}' has 'saliences' variable name that shadows role name "
722
+ f"'{action_definition['saliences']['variable']['name']}' (this is not allowed)"
723
+ )
724
+
725
+
726
+ def _validate_action_associations(action_definition: viv_compiler.types.ActionDefinition) -> None:
727
+ """Validate the given action definition's 'associations' field.
728
+
729
+ Args:
730
+ action_definition: An action definition from a compiled content bundle.
731
+
732
+ Returns:
733
+ None.
734
+
735
+ Raises:
736
+ Exception: The action definition did not pass validation.
737
+ """
738
+ # Detect cases of an associations variable shadowing the name of a role from the same action
739
+ all_role_names = viv_compiler.utils.get_all_role_names(action_definition=action_definition)
740
+ if action_definition['associations']['variable']:
741
+ if action_definition['associations']['variable']['name'] in all_role_names:
742
+ raise ValueError(
743
+ f"Action '{action_definition['name']}' has 'associations' variable name that shadows role name "
744
+ f"'{action_definition['associations']['variable']['name']}' (this is not allowed)"
745
+ )
746
+
747
+
678
748
  def _validate_action_trope_fit_expressions(
679
749
  action_definition: viv_compiler.types.ActionDefinition,
680
750
  trope_definitions: dict[str, TropeDefinition]
@@ -692,7 +762,7 @@ def _validate_action_trope_fit_expressions(
692
762
  Exception: The action definition did not pass validation.
693
763
  """
694
764
  all_trope_fit_expressions = viv_compiler.utils.get_all_expressions_of_type(
695
- expression_type="tropeFitExpression",
765
+ expression_type=ExpressionDiscriminator.TROPE_FIT_EXPRESSION,
696
766
  ast_chunk=action_definition
697
767
  )
698
768
  for trope_fit_expression in all_trope_fit_expressions:
@@ -725,21 +795,32 @@ def _validate_action_role_unpackings(action_definition: viv_compiler.types.Actio
725
795
  Raises:
726
796
  Exception: The action definition did not pass validation.
727
797
  """
728
- # Ensure all role unpackings unpack roles that can cast multiple entities
798
+ # Retrieve all unpacked role names
729
799
  all_unpacked_role_names = viv_compiler.utils.get_all_expressions_of_type(
730
- expression_type="roleUnpacking",
800
+ expression_type=ExpressionDiscriminator.ROLE_UNPACKING,
731
801
  ast_chunk=action_definition
732
802
  )
733
803
  for unpacked_role_name in all_unpacked_role_names:
804
+ if unpacked_role_name in viv_compiler.config.SPECIAL_ROLE_NAMES:
805
+ raise ValueError(
806
+ f"Action '{action_definition['name']}' unpacks a singleton role (one that is "
807
+ f"always bound to a single entity): '{unpacked_role_name}'"
808
+ )
809
+ # Make sure all role unpackings unpack roles that can cast multiple entities. Here, we'll also
810
+ # include all our special roles, since these are always singletons.
811
+ for unpacked_role_name in all_unpacked_role_names:
812
+ error_message = (
813
+ f"Action '{action_definition['name']}' unpacks a singleton role (one that is "
814
+ f"always bound to a single entity): '{unpacked_role_name}'"
815
+ )
816
+ if unpacked_role_name in viv_compiler.config.SPECIAL_ROLE_NAMES:
817
+ raise ValueError(error_message)
734
818
  for role_definition in action_definition['roles'].values():
735
819
  if role_definition['name'] == unpacked_role_name:
736
820
  if role_definition['min'] == role_definition['max'] == 1:
737
- raise ValueError(
738
- f"Action '{action_definition['name']}' unpacks a singleton role (one that is "
739
- f"always bound to a single entity): '{unpacked_role_name}'"
740
- )
821
+ raise ValueError(error_message)
741
822
  break
742
- # Ensure that all other references to roles that can cast multiple entities use role unpackings
823
+ # Make sure that all other references to roles that can cast multiple entities use role unpackings
743
824
  single_entity_role_references = viv_compiler.utils.get_all_referenced_roles( # I.e., using the '@role' notation
744
825
  ast_chunk=action_definition,
745
826
  ignore_role_unpackings=True
@@ -769,7 +850,10 @@ def _validate_action_loops(action_definition: viv_compiler.types.ActionDefinitio
769
850
  Raises:
770
851
  Exception: The action definition did not pass validation.
771
852
  """
772
- all_loops = viv_compiler.utils.get_all_expressions_of_type(expression_type="loop", ast_chunk=action_definition)
853
+ all_loops = viv_compiler.utils.get_all_expressions_of_type(
854
+ expression_type=ExpressionDiscriminator.LOOP,
855
+ ast_chunk=action_definition
856
+ )
773
857
  for loop in all_loops:
774
858
  # Detect attempts to loop over single-entity role references (i.e., ones using '@role' notation)
775
859
  if loop['iterable']['type'] == ExpressionDiscriminator.ENTITY_REFERENCE:
@@ -780,10 +864,10 @@ def _validate_action_loops(action_definition: viv_compiler.types.ActionDefinitio
780
864
  f"role: '{role_name}' (perhaps use * instead of @)"
781
865
  )
782
866
  # Detect cases of a loop variable shadowing the name of a role from the same action
783
- if loop['variable'] in viv_compiler.utils.get_all_role_names(action_definition=action_definition):
867
+ if loop['variable']['name'] in viv_compiler.utils.get_all_role_names(action_definition=action_definition):
784
868
  raise ValueError(
785
869
  f"Action '{action_definition['name']}' has loop with variable name that "
786
- f"shadows role name '{loop['variable']}' (this is not allowed)"
870
+ f"shadows role name '{loop['variable']['name']}' (this is not allowed)"
787
871
  )
788
872
 
789
873
 
@@ -799,27 +883,48 @@ def _validate_action_assignments(action_definition: viv_compiler.types.ActionDef
799
883
  Raises:
800
884
  Exception: The action definition did not pass validation.
801
885
  """
886
+ # Make sure that all assignments are housed in the proper fields
887
+ for field_name, field_value in action_definition.items():
888
+ if field_name not in viv_compiler.config.ACTION_FIELDS_PERMITTING_ASSIGNMENTS:
889
+ assignments_in_field = viv_compiler.utils.get_all_expressions_of_type(
890
+ expression_type=ExpressionDiscriminator.ASSIGNMENT,
891
+ ast_chunk=field_value
892
+ )
893
+ if assignments_in_field:
894
+ raise ValueError(
895
+ f"Action '{action_definition['name']}' has assignment in '{field_name}' field "
896
+ f"(only allowed in {', '.join(viv_compiler.config.ACTION_FIELDS_PERMITTING_ASSIGNMENTS)})"
897
+ )
898
+ # Collect all assignments
802
899
  all_assignments = viv_compiler.utils.get_all_expressions_of_type(
803
- expression_type="assignment",
900
+ expression_type=ExpressionDiscriminator.ASSIGNMENT,
804
901
  ast_chunk=action_definition
805
902
  )
903
+ # Validate each assignment in turn
806
904
  for assignment in all_assignments:
807
905
  anchor = assignment['left']['value']['anchor']
808
906
  path = assignment['left']['value']['path']
809
- if assignment['left']['type'] == ExpressionDiscriminator.ENTITY_REFERENCE:
907
+ # Detect attempts to set a local variable. Note that we do allow assignments that are anchored
908
+ # in local variables and also have a path, since this enables the common pattern of looping
909
+ # over a role unpacking to execute effects for each entity cast in the group role.
910
+ if assignment['left']['value']['local'] and not path:
911
+ raise ValueError(
912
+ f"Assignment expression in action '{action_definition['name']}' sets local variable '{anchor}', "
913
+ f"but local variables can only be set in loops, 'saliences' headers, and 'associations' headers"
914
+ )
915
+ if anchor != viv_compiler.config.ACTION_SELF_REFERENCE_ROLE_NAME and anchor in action_definition['roles']:
810
916
  # Detect attempts to recast a role via assignment (not allowed)
811
917
  if not path:
812
918
  raise ValueError(
813
919
  f"Assignment expression in action '{action_definition['name']}' recasts "
814
920
  f"role '{anchor}' (this is prohibited)"
815
921
  )
816
- # Detect attempts to set data on the "entity" associated with a symbol role
817
- if anchor != "this":
818
- if action_definition['roles'][anchor]['symbol']:
819
- raise ValueError(
820
- f"Assignment expression in action '{action_definition['name']}' has "
821
- f"symbol role on its left-hand side: '{anchor}'"
822
- )
922
+ # Detect attempts to set data on a complex symbol role (currently prohibited)
923
+ if action_definition['roles'][anchor]['symbol']:
924
+ raise ValueError(
925
+ f"Assignment expression in action '{action_definition['name']}' has "
926
+ f"symbol role on its left-hand side (this is currently prohibited): '{anchor}'"
927
+ )
823
928
  # Detect a trailing eval fail-safe marker, which is bizarre and probably an authoring error
824
929
  if path and path[-1].get('failSafe'):
825
930
  raise ValueError(
@@ -828,8 +933,11 @@ def _validate_action_assignments(action_definition: viv_compiler.types.ActionDef
828
933
  )
829
934
 
830
935
 
831
- def _validate_chance_expressions(action_definition: viv_compiler.types.ActionDefinition) -> None:
832
- """Validate the given action definition's usage of chance expressions.
936
+ def _validate_scratch_variable_references(action_definition: viv_compiler.types.ActionDefinition) -> None:
937
+ """Validate the given action definition's references to scratch variables.
938
+
939
+ This procedure ensures that any scratch variable that is referenced is assigned *somewhere* in
940
+ the action definition, but it does not ensure that the variable is assigned before it's used.
833
941
 
834
942
  Args:
835
943
  action_definition: An action definition from a compiled content bundle.
@@ -840,16 +948,22 @@ def _validate_chance_expressions(action_definition: viv_compiler.types.ActionDef
840
948
  Raises:
841
949
  Exception: The action definition did not pass validation.
842
950
  """
843
- all_chance_expression_values = viv_compiler.utils.get_all_expressions_of_type(
844
- expression_type="chanceExpression",
951
+ # Retrieve the names of all scratch variables that are set anywhere in the action definition
952
+ all_assigned_scratch_variable_names = set(viv_compiler.utils.get_all_assigned_scratch_variable_names(
845
953
  ast_chunk=action_definition
846
- )
847
- for chance_value in all_chance_expression_values:
848
- if chance_value < 0.0 or chance_value > 1.0:
849
- raise ValueError(
850
- f"Chance expression in action '{action_definition['name']}' has "
851
- f"chance value outside of the range [0, 1]: '{chance_value * 100}%'"
852
- )
954
+ ))
955
+ # Retrieve the names of all scratch variables that are referenced anywhere in the action definition
956
+ all_referenced_scratch_variable_names = set(viv_compiler.utils.get_all_referenced_scratch_variable_names(
957
+ ast_chunk=action_definition
958
+ ))
959
+ # Flag any cases of a scratch variable being referenced without being assigned anywhere
960
+ referenced_but_not_assigned = all_referenced_scratch_variable_names - all_assigned_scratch_variable_names
961
+ if referenced_but_not_assigned:
962
+ snippet = "'" + "', '".join(sorted(referenced_but_not_assigned)) + "'"
963
+ raise ValueError(
964
+ f"Action '{action_definition['name']}' references the following scratch variables without ever "
965
+ f"assigning them: {snippet}"
966
+ )
853
967
 
854
968
 
855
969
  def _validate_negated_expressions(action_definition: viv_compiler.types.ActionDefinition) -> None:
@@ -873,6 +987,30 @@ def _validate_negated_expressions(action_definition: viv_compiler.types.ActionDe
873
987
  )
874
988
 
875
989
 
990
+ def _validate_chance_expressions(action_definition: viv_compiler.types.ActionDefinition) -> None:
991
+ """Validate the given action definition's usage of chance expressions.
992
+
993
+ Args:
994
+ action_definition: An action definition from a compiled content bundle.
995
+
996
+ Returns:
997
+ None.
998
+
999
+ Raises:
1000
+ Exception: The action definition did not pass validation.
1001
+ """
1002
+ all_chance_expression_values = viv_compiler.utils.get_all_expressions_of_type(
1003
+ expression_type=ExpressionDiscriminator.CHANCE_EXPRESSION,
1004
+ ast_chunk=action_definition
1005
+ )
1006
+ for chance_value in all_chance_expression_values:
1007
+ if chance_value < 0.0 or chance_value > 1.0:
1008
+ raise ValueError(
1009
+ f"Chance expression in action '{action_definition['name']}' has "
1010
+ f"chance value outside of the range [0, 1]: '{chance_value * 100}%'"
1011
+ )
1012
+
1013
+
876
1014
  def _validate_compiled_content_bundle_against_schema(content_bundle: viv_compiler.types.CompiledContentBundle) -> None:
877
1015
  """Validate a compiled content bundle against its public schema.
878
1016