@@ -112,6 +112,7 @@ class EventDataWrapper:
112112 def __init__ (self , event_data ):
113113 self .event_data = event_data
114114 self .sendid = event_data .trigger_data .send_id
115+ self .invokeid = event_data .trigger_data .invokeid or ""
115116 if event_data .trigger_data .event is None or event_data .trigger_data .event .internal :
116117 if "error.execution" == event_data .trigger_data .event :
117118 self .type = "platform"
@@ -364,9 +365,60 @@ def raise_action(*args, **kwargs):
364365 return raise_action
365366
366367
368+ def _send_to_parent (machine : "StateChart" , event : str , content : Any , params_values : dict ):
369+ """Route an event to the parent session via #_parent."""
370+ parent_sm = getattr (machine , "_parent_sm" , None )
371+ if parent_sm is not None :
372+ child_invokeid = getattr (machine , "_invokeid" , None )
373+ parent_sm .send (event , * content , invokeid = child_invokeid , ** params_values )
374+ else :
375+ machine .send ("error.communication" , internal = True )
376+
377+
378+ def _send_to_child (machine : "StateChart" , event : str , params_values : dict ):
379+ """Route an event to the first active child via #_child."""
380+ if hasattr (machine , "_engine" ) and hasattr (machine ._engine , "invoke_manager" ):
381+ machine ._engine .invoke_manager .send_to_child (event , ** params_values )
382+ else :
383+ machine .send ("error.communication" , internal = True )
384+
385+
386+ def _send_to_invokeid (
387+ machine : "StateChart" , target : str , event : str , action : SendAction , ** kwargs
388+ ):
389+ """Route an event to a specific child by #_<invokeid>."""
390+ invokeid = target [2 :]
391+ if hasattr (machine , "_engine" ) and hasattr (machine ._engine , "invoke_manager" ):
392+ params_values = {}
393+ for param in action .params :
394+ if param .expr :
395+ params_values [param .name ] = _eval (param .expr , ** kwargs )
396+ machine ._engine .invoke_manager .send_to_invokeid (invokeid , event , ** params_values )
397+ elif target .startswith ("#_scxml_" ):
398+ machine .send ("error.communication" , internal = True )
399+ else :
400+ raise ValueError (f"Invalid target: { target } " )
401+
402+
403+ def _eval_send_params (action : SendAction , ** kwargs ) -> dict :
404+ """Evaluate namelist and <param> into a dict of keyword arguments."""
405+ machine : "StateChart" = kwargs ["machine" ]
406+ names = []
407+ for name in (action .namelist or "" ).strip ().split ():
408+ if not hasattr (machine .model , name ):
409+ raise NameError (f"Namelist variable '{ name } ' not found on model" )
410+ names .append (Param (name = name , expr = name ))
411+ params_values = {}
412+ for param in chain (names , action .params ):
413+ if param .expr is None :
414+ continue
415+ params_values [param .name ] = _eval (param .expr , ** kwargs )
416+ return params_values
417+
418+
367419def create_send_action_callable (action : SendAction ) -> Callable : # noqa: C901
368420 content : Any = ()
369- _valid_targets = (None , "#_internal" , "internal" , "#_parent" , "parent" )
421+ _valid_targets = (None , "#_internal" , "internal" , "#_parent" , "parent" , "#_child" )
370422 if action .content :
371423 try :
372424 content = (eval (action .content , {}, {}),)
@@ -379,22 +431,35 @@ def send_action(*args, **kwargs):
379431 target = action .target if action .target else None
380432
381433 if action .type and action .type != "http://www.w3.org/TR/scxml/#SCXMLEventProcessor" :
382- # Per SCXML spec 6.2.3, unsupported type raises error.execution
383434 raise ValueError (
384435 f"Unsupported send type: { action .type } . "
385436 "Only 'http://www.w3.org/TR/scxml/#SCXMLEventProcessor' is supported"
386437 )
438+
387439 if target not in _valid_targets :
388440 if target and target .startswith ("#_scxml_" ):
389- # Valid SCXML session reference but undispatchable → error.communication
441+ # Valid SCXML session reference but undispatchable -> error.communication
390442 machine .send ("error.communication" , internal = True )
443+ return
444+ elif target and target .startswith ("#_" ):
445+ # Handle #_invokeid target (send to specific child by invoke id)
446+ _send_to_invokeid (machine , target , event , action , ** kwargs )
447+ return
391448 else :
392- # Invalid target expression → error.execution (raised as exception)
393449 raise ValueError (f"Invalid target: { target } . Must be one of { _valid_targets } " )
394450 return
395451
396- internal = target in ( "#_internal" , "internal" )
452+ params_values = _eval_send_params ( action , ** kwargs )
397453
454+ # Handle cross-session targets
455+ if target in ("#_parent" , "parent" ):
456+ _send_to_parent (machine , event , content , params_values )
457+ return
458+ if target == "#_child" :
459+ _send_to_child (machine , event , params_values )
460+ return
461+
462+ internal = target in ("#_internal" , "internal" )
398463 send_id = None
399464 if action .id :
400465 send_id = action .id
@@ -403,20 +468,6 @@ def send_action(*args, **kwargs):
403468 setattr (machine .model , action .idlocation , send_id )
404469
405470 delay = ParseTime .parse_delay (action .delay , action .delayexpr , ** kwargs )
406-
407- # Per SCXML spec, if namelist evaluation causes an error (e.g., variable not found),
408- # the send MUST NOT be dispatched and error.execution is raised.
409- names = []
410- for name in (action .namelist or "" ).strip ().split ():
411- if not hasattr (machine .model , name ):
412- raise NameError (f"Namelist variable '{ name } ' not found on model" )
413- names .append (Param (name = name , expr = name ))
414- params_values = {}
415- for param in chain (names , action .params ):
416- if param .expr is None :
417- continue
418- params_values [param .name ] = _eval (param .expr , ** kwargs )
419-
420471 Event (id = event , name = event , delay = delay , internal = internal , _sm = machine ).put (
421472 * content ,
422473 send_id = send_id ,
0 commit comments