viv-compiler 0.1.0__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,4 +1,4 @@
1
- """A library of functions to validate preliminary action definitions and compiled content bundles.
1
+ """Module that handles validation of preliminary action definitions and compiled content bundles.
2
2
 
3
3
  The entrypoint functions are as follows:
4
4
  * `validate_join_directives()`: Invoked prior to handling of action inheritance.
@@ -19,31 +19,8 @@ from importlib import import_module
19
19
  from pydantic import TypeAdapter
20
20
 
21
21
 
22
- # todo do we already ensure all references are anchored in a (valid) role name?
23
- # todo warn or error if precondition references scaled enum?
24
- # todo validate time of day to ensure 0-23 and 0-59
25
- # todo technically you could inject major side effects (e.g. reaction queueing) into trope-fit expressions
26
- # todo ensure closing time period is always later than opening time period
27
- # todo ensure embargo is either permanent or has a declared time period
28
- # todo don't allow reaction to precast same entity multiple times
29
- # todo build roles:
30
- # - they can cast exactly one entity, with no chance and no mean
31
- # - no pool declarations
32
- # - can never be precast
33
- # - preconditions cannot reference build roles
34
- # - don't allow them in embargoes (embargo would not be violable)
35
- # todo apparently the grammar allows e.g. multiple roles fields in an action definition
36
- # - test what happens (presumably last in wins)
37
- # todo make sure times like 88:76 aren't allowed (grammar allows them)
38
- # todo role renaming (make sure new name being duplicate of existing one is already caught)
39
- # - just detect duplicate role names right away post-visitor
40
- # todo prohibit entity references anchored in symbol roles
41
- # - not ideal, but interpreter enforces this at runtime currently
42
- # todo assignments only allowed in scratch and effects
43
-
44
-
45
22
  def validate_join_directives(raw_action_definitions: list[viv_compiler.types.RawActionDefinition]) -> None:
46
- """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.
47
24
 
48
25
  If this validation check passes, the given action definitions are ready for inheritance to be handled.
49
26
 
@@ -88,10 +65,10 @@ def validate_preliminary_action_definitions(
88
65
  Exception: At least one action definition did not pass validation.
89
66
  """
90
67
  for action_definition in intermediate_action_definitions:
91
- # Ensure that a 'roles' field is present
68
+ # Make sure that a 'roles' field is present
92
69
  if "roles" not in action_definition:
93
70
  raise KeyError(f"Action '{action_definition['name']}' is missing a 'roles' field, which is required")
94
- # 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,
95
72
  # the 'roles' field is still a list.
96
73
  _detect_wrong_number_of_initiators(action_definition=action_definition)
97
74
  # Detect duplicated role names
@@ -160,7 +137,9 @@ def _detect_reference_to_undefined_role(action_definition: viv_compiler.types.In
160
137
  Exception: The action definition did not pass validation.
161
138
  """
162
139
  # Retrieve the names of all defined roles ('roles' is still a list at this point in postprocessing)
163
- 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
+ )
164
143
  # Validate report references
165
144
  if action_definition["report"]:
166
145
  for reference in viv_compiler.utils.get_all_referenced_roles(action_definition["report"]):
@@ -266,11 +245,25 @@ def _validate_trope_definitions(trope_definitions: dict[str, viv_compiler.types.
266
245
  # Detect any reference to an undefined param
267
246
  all_referenced_params = viv_compiler.utils.get_all_referenced_roles(ast_chunk=trope_definition)
268
247
  for referenced_param in all_referenced_params:
269
- if referenced_param not in trope_definition["params"]:
248
+ if referenced_param not in [trope_param["name"] for trope_param in trope_definition["params"]]:
270
249
  raise KeyError(
271
250
  f"Trope '{trope_definition['name']}' references undefined parameter: "
272
251
  f"'{referenced_param}'"
273
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)")
274
267
 
275
268
 
276
269
  def _validate_action_definitions(
@@ -302,7 +295,10 @@ def _validate_action_definitions(
302
295
  _validate_action_effects(action_definition=action_definition)
303
296
  # Validate reactions
304
297
  _validate_action_reactions(action_definition=action_definition, all_action_definitions=action_definitions)
298
+ # Validate saliences
299
+ _validate_action_saliences(action_definition=action_definition)
305
300
  # Validate associations
301
+ _validate_action_associations(action_definition=action_definition)
306
302
  # Validate trope-fit expressions
307
303
  _validate_action_trope_fit_expressions(
308
304
  action_definition=action_definition,
@@ -314,6 +310,8 @@ def _validate_action_definitions(
314
310
  _validate_action_loops(action_definition=action_definition)
315
311
  # Validate assignments
316
312
  _validate_action_assignments(action_definition=action_definition)
313
+ # Validate references to scratch variables
314
+ _validate_scratch_variable_references(action_definition=action_definition)
317
315
  # Validate negated expressions
318
316
  _validate_negated_expressions(action_definition=action_definition)
319
317
  # Validate chance expressions
@@ -353,13 +351,13 @@ def _validate_action_roles(action_definition: viv_compiler.types.ActionDefinitio
353
351
  """
354
352
  # Validate the initiator role
355
353
  _validate_action_initiator_role(action_definition=action_definition)
356
- # Ensure all roles have proper 'min' and 'max' values
354
+ # Make sure all roles have proper 'min' and 'max' values
357
355
  _validate_action_role_min_and_max_values(action_definition=action_definition)
358
- # Ensure all roles have proper 'chance' and 'mean' values
356
+ # Make sure all roles have proper 'chance' and 'mean' values
359
357
  _validate_action_role_chance_and_mean_values(action_definition=action_definition)
360
- # Ensure all roles with binding pools have proper ones
358
+ # Make sure all roles with binding pools have proper ones
361
359
  _validate_action_role_pool_directives(action_definition=action_definition)
362
- # 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
363
361
  _validate_action_role_precast_label_usages(action_definition=action_definition)
364
362
 
365
363
 
@@ -377,13 +375,13 @@ def _validate_action_initiator_role(action_definition: viv_compiler.types.Action
377
375
  """
378
376
  # Retrieve the definition for the action's initiator role
379
377
  initiator_role_definition = action_definition['initiator']
380
- # Ensure that the initiator role has no pool directive
378
+ # Make sure that the initiator role has no pool directive
381
379
  if initiator_role_definition["pool"]:
382
380
  raise ValueError(
383
381
  f"Action '{action_definition['name']}' has initiator role '{initiator_role_definition['name']}' "
384
382
  f"with a pool directive, which is not allowed for initiator roles"
385
383
  )
386
- # Ensure that the initiator role casts exactly one entity
384
+ # Make sure that the initiator role casts exactly one entity
387
385
  if initiator_role_definition['min'] != 1:
388
386
  raise ValueError(
389
387
  f"Action '{action_definition['name']}' has initiator role '{initiator_role_definition['name']}' "
@@ -394,13 +392,13 @@ def _validate_action_initiator_role(action_definition: viv_compiler.types.Action
394
392
  f"Action '{action_definition['name']}' has initiator role '{initiator_role_definition['name']}' "
395
393
  f"with max other than 1 (there must be a single initiator)"
396
394
  )
397
- # Ensure that the initiator role has no specified binding mean
395
+ # Make sure that the initiator role has no specified binding mean
398
396
  if initiator_role_definition['mean'] is not None:
399
397
  raise ValueError(
400
398
  f"Action '{action_definition['name']}' has initiator role '{initiator_role_definition['name']}' "
401
399
  f"with declared casting mean (there must be a single initiator)"
402
400
  )
403
- # Ensure that the initiator role has no specified binding chance
401
+ # Make sure that the initiator role has no specified binding chance
404
402
  if initiator_role_definition['chance'] is not None:
405
403
  raise ValueError(
406
404
  f"Action '{action_definition['name']}' has initiator role '{initiator_role_definition['name']}' "
@@ -502,7 +500,7 @@ def _validate_action_role_pool_directives(action_definition: viv_compiler.types.
502
500
  f"Action '{action_definition['name']}' has role '{role_definition['name']}' "
503
501
  f"that requires a pool declaration but does not have one"
504
502
  )
505
- # 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
506
504
  if role_definition["pool"]:
507
505
  all_pool_references = viv_compiler.utils.get_all_referenced_roles(role_definition["pool"])
508
506
  if len(all_pool_references) > 1:
@@ -627,25 +625,21 @@ def _validate_action_reactions(
627
625
  Raises:
628
626
  Exception: The action definition did not pass validation.
629
627
  """
630
- # Ensure that all reactions are housed in the proper fields
631
- fields_to_check = (
632
- ("preconditions", action_definition["preconditions"]),
633
- ("scratch", action_definition["scratch"]),
634
- ("effects", action_definition["effects"]),
635
- )
636
- for field_name, field_value in fields_to_check:
637
- reactions_in_this_field = viv_compiler.utils.get_all_expressions_of_type(
638
- expression_type="reaction",
639
- ast_chunk=field_value
640
- )
641
- if reactions_in_this_field:
642
- raise ValueError(
643
- f"Action '{action_definition['name']}' has reaction in '{field_name}' field "
644
- 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
645
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
+ )
646
640
  # Collect all reactions
647
641
  all_reactions = viv_compiler.utils.get_all_expressions_of_type(
648
- expression_type="reaction",
642
+ expression_type=ExpressionDiscriminator.REACTION,
649
643
  ast_chunk=action_definition['reactions']
650
644
  )
651
645
  # Validate each reaction in turn
@@ -663,7 +657,7 @@ def _validate_action_reactions(
663
657
  queued_action_initiator_role_name = queued_action_definition['initiator']['name']
664
658
  initiator_is_bound = False
665
659
  for binding in reaction['bindings']:
666
- if binding['value']['role'] == queued_action_initiator_role_name:
660
+ if binding['role'] == queued_action_initiator_role_name:
667
661
  initiator_is_bound = True
668
662
  break
669
663
  if not initiator_is_bound:
@@ -674,12 +668,21 @@ def _validate_action_reactions(
674
668
  # Make sure the reaction references only roles defined in the queued actions
675
669
  all_queued_action_role_names = queued_action_definition['roles'].keys()
676
670
  for binding in reaction['bindings']:
677
- bound_role_name = binding['value']['role']
671
+ bound_role_name = binding['role']
678
672
  if bound_role_name not in all_queued_action_role_names:
679
673
  raise KeyError(
680
674
  f"Action '{action_definition['name']}' has '{queued_action_name}' reaction that "
681
675
  f"references a role that is undefined for '{queued_action_name}': '{bound_role_name}'"
682
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'])
683
686
  # Make sure the reaction precasts all 'precast' roles
684
687
  for role_object in queued_action_definition['roles'].values():
685
688
  if not role_object['precast']:
@@ -687,7 +690,7 @@ def _validate_action_reactions(
687
690
  precast_role_name = role_object['name']
688
691
  role_is_precast = False
689
692
  for binding in reaction['bindings']:
690
- bound_role_name = binding['value']['role']
693
+ bound_role_name = binding['role']
691
694
  if bound_role_name == precast_role_name:
692
695
  role_is_precast = True
693
696
  break
@@ -698,6 +701,50 @@ def _validate_action_reactions(
698
701
  )
699
702
 
700
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
+
701
748
  def _validate_action_trope_fit_expressions(
702
749
  action_definition: viv_compiler.types.ActionDefinition,
703
750
  trope_definitions: dict[str, TropeDefinition]
@@ -715,7 +762,7 @@ def _validate_action_trope_fit_expressions(
715
762
  Exception: The action definition did not pass validation.
716
763
  """
717
764
  all_trope_fit_expressions = viv_compiler.utils.get_all_expressions_of_type(
718
- expression_type="tropeFitExpression",
765
+ expression_type=ExpressionDiscriminator.TROPE_FIT_EXPRESSION,
719
766
  ast_chunk=action_definition
720
767
  )
721
768
  for trope_fit_expression in all_trope_fit_expressions:
@@ -748,21 +795,32 @@ def _validate_action_role_unpackings(action_definition: viv_compiler.types.Actio
748
795
  Raises:
749
796
  Exception: The action definition did not pass validation.
750
797
  """
751
- # Ensure all role unpackings unpack roles that can cast multiple entities
798
+ # Retrieve all unpacked role names
752
799
  all_unpacked_role_names = viv_compiler.utils.get_all_expressions_of_type(
753
- expression_type="roleUnpacking",
800
+ expression_type=ExpressionDiscriminator.ROLE_UNPACKING,
754
801
  ast_chunk=action_definition
755
802
  )
756
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)
757
818
  for role_definition in action_definition['roles'].values():
758
819
  if role_definition['name'] == unpacked_role_name:
759
820
  if role_definition['min'] == role_definition['max'] == 1:
760
- raise ValueError(
761
- f"Action '{action_definition['name']}' unpacks a singleton role (one that is "
762
- f"always bound to a single entity): '{unpacked_role_name}'"
763
- )
821
+ raise ValueError(error_message)
764
822
  break
765
- # 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
766
824
  single_entity_role_references = viv_compiler.utils.get_all_referenced_roles( # I.e., using the '@role' notation
767
825
  ast_chunk=action_definition,
768
826
  ignore_role_unpackings=True
@@ -792,7 +850,10 @@ def _validate_action_loops(action_definition: viv_compiler.types.ActionDefinitio
792
850
  Raises:
793
851
  Exception: The action definition did not pass validation.
794
852
  """
795
- 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
+ )
796
857
  for loop in all_loops:
797
858
  # Detect attempts to loop over single-entity role references (i.e., ones using '@role' notation)
798
859
  if loop['iterable']['type'] == ExpressionDiscriminator.ENTITY_REFERENCE:
@@ -803,10 +864,10 @@ def _validate_action_loops(action_definition: viv_compiler.types.ActionDefinitio
803
864
  f"role: '{role_name}' (perhaps use * instead of @)"
804
865
  )
805
866
  # Detect cases of a loop variable shadowing the name of a role from the same action
806
- 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):
807
868
  raise ValueError(
808
869
  f"Action '{action_definition['name']}' has loop with variable name that "
809
- f"shadows role name '{loop['variable']}' (this is not allowed)"
870
+ f"shadows role name '{loop['variable']['name']}' (this is not allowed)"
810
871
  )
811
872
 
812
873
 
@@ -822,27 +883,48 @@ def _validate_action_assignments(action_definition: viv_compiler.types.ActionDef
822
883
  Raises:
823
884
  Exception: The action definition did not pass validation.
824
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
825
899
  all_assignments = viv_compiler.utils.get_all_expressions_of_type(
826
- expression_type="assignment",
900
+ expression_type=ExpressionDiscriminator.ASSIGNMENT,
827
901
  ast_chunk=action_definition
828
902
  )
903
+ # Validate each assignment in turn
829
904
  for assignment in all_assignments:
830
905
  anchor = assignment['left']['value']['anchor']
831
906
  path = assignment['left']['value']['path']
832
- 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']:
833
916
  # Detect attempts to recast a role via assignment (not allowed)
834
917
  if not path:
835
918
  raise ValueError(
836
919
  f"Assignment expression in action '{action_definition['name']}' recasts "
837
920
  f"role '{anchor}' (this is prohibited)"
838
921
  )
839
- # Detect attempts to set data on the "entity" associated with a symbol role
840
- if anchor != "this":
841
- if action_definition['roles'][anchor]['symbol']:
842
- raise ValueError(
843
- f"Assignment expression in action '{action_definition['name']}' has "
844
- f"symbol role on its left-hand side: '{anchor}'"
845
- )
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
+ )
846
928
  # Detect a trailing eval fail-safe marker, which is bizarre and probably an authoring error
847
929
  if path and path[-1].get('failSafe'):
848
930
  raise ValueError(
@@ -851,8 +933,11 @@ def _validate_action_assignments(action_definition: viv_compiler.types.ActionDef
851
933
  )
852
934
 
853
935
 
854
- def _validate_chance_expressions(action_definition: viv_compiler.types.ActionDefinition) -> None:
855
- """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.
856
941
 
857
942
  Args:
858
943
  action_definition: An action definition from a compiled content bundle.
@@ -863,16 +948,22 @@ def _validate_chance_expressions(action_definition: viv_compiler.types.ActionDef
863
948
  Raises:
864
949
  Exception: The action definition did not pass validation.
865
950
  """
866
- all_chance_expression_values = viv_compiler.utils.get_all_expressions_of_type(
867
- 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(
868
953
  ast_chunk=action_definition
869
- )
870
- for chance_value in all_chance_expression_values:
871
- if chance_value < 0.0 or chance_value > 1.0:
872
- raise ValueError(
873
- f"Chance expression in action '{action_definition['name']}' has "
874
- f"chance value outside of the range [0, 1]: '{chance_value * 100}%'"
875
- )
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
+ )
876
967
 
877
968
 
878
969
  def _validate_negated_expressions(action_definition: viv_compiler.types.ActionDefinition) -> None:
@@ -896,6 +987,30 @@ def _validate_negated_expressions(action_definition: viv_compiler.types.ActionDe
896
987
  )
897
988
 
898
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
+
899
1014
  def _validate_compiled_content_bundle_against_schema(content_bundle: viv_compiler.types.CompiledContentBundle) -> None:
900
1015
  """Validate a compiled content bundle against its public schema.
901
1016