3030from aiida .engine .processes .graphs .ir import (
3131 CONDITION_PORT ,
3232 BranchControl ,
33+ Comparison ,
3334 Dependency ,
3435 Endpoint ,
3536 GraphIR ,
@@ -503,10 +504,14 @@ def _lower_branch(state: _LoweringState, statement: ast.If) -> None:
503504 _reject (state , statement , 'condition conflicts with a branch input; rename one of them' )
504505 instance = f'branch_{ len (state .tasks ) + 1 } '
505506 given : dict [str , CapturedValue ] = {}
506- _wire (state , instance , CONDITION_PORT , _lower_value (state , test ), given )
507+ comparison = _lower_comparison (state , instance , test , given )
508+ if comparison is None :
509+ _wire (state , instance , CONDITION_PORT , _lower_value (state , test ), given )
507510 for name in sorted ({path .split ('.' )[0 ] for side in (first , second ) for path in side .child .inputs }):
508511 _wire (state , instance , name , state .names [name ], given )
509- state .tasks .append (BranchControl (name = instance , inputs = given , body = first .body , otherwise = second .body ))
512+ state .tasks .append (
513+ BranchControl (name = instance , inputs = given , body = first .body , otherwise = second .body , comparison = comparison )
514+ )
510515 selected = first .child .names [first .name ]
511516 state .names [first .name ] = _Reference (instance , selected .port , selected .shape )
512517
@@ -521,9 +526,11 @@ def _lower_loop(state: _LoweringState, statement: ast.While) -> None:
521526 ast .copy_location (update .targets [0 ], statement .test )
522527 body .append (update )
523528 test : ast .expr = bound if bound is not None else statement .test
524- if statement .orelse or not isinstance (test , ast .Name ) or test .id not in state .names :
525- _reject (state , statement , 'while requires a bound condition name and no else' )
526- condition = test .id
529+ if statement .orelse or (
530+ not isinstance (test , ast .Compare ) and (not isinstance (test , ast .Name ) or test .id not in state .names )
531+ ):
532+ _reject (state , statement , 'while requires a bound condition or comparison and no else' )
533+ condition = test .id if isinstance (test , ast .Name ) else CONDITION_PORT
527534 child = _collect_region_inputs (state , body )
528535 # Loop state is read from the previous iteration, including values rebound in the body.
529536 assigned : set [str ] = {
@@ -536,7 +543,7 @@ def _lower_loop(state: _LoweringState, statement: ast.While) -> None:
536543 _reject (state , statement , f'loop state { name !r} must be initialized before the loop' )
537544 child .inputs .setdefault (name , [])
538545 child .names [name ] = _Reference (None , name , state .names [name ].shape )
539- if condition not in assigned :
546+ if not isinstance ( test , ast . Compare ) and condition not in assigned :
540547 _reject (state , statement , 'while body must update its condition; rebind the condition name in the body' )
541548 for node in body :
542549 _lower_assignment (child , node , rebind = True )
@@ -545,7 +552,12 @@ def _lower_loop(state: _LoweringState, statement: ast.While) -> None:
545552 instance = f'loop_{ len (state .tasks ) + 1 } '
546553 given : dict [str , CapturedValue ] = {}
547554 _route_region (state , child , instance , given )
548- state .tasks .append (LoopControl (name = instance , inputs = given , body = body_spec , condition_port = condition ))
555+ comparison = _lower_comparison (state , instance , test , given ) if isinstance (test , ast .Compare ) else None
556+ if comparison is None and isinstance (test , ast .Compare ):
557+ _reject (state , test , _unsupported_reason (test ))
558+ state .tasks .append (
559+ LoopControl (name = instance , inputs = given , body = body_spec , condition_port = condition , comparison = comparison )
560+ )
549561 for name in assigned :
550562 state .names [name ] = _Reference (instance , name , child .names [name ].shape )
551563
@@ -611,6 +623,38 @@ def _bind_condition(state: _LoweringState, test: ast.expr) -> ast.Name | None:
611623 return ast .Name (id = name , ctx = ast .Load ())
612624
613625
626+ _COMPARISON_OPERATORS : dict [type [ast .cmpop ], str ] = {
627+ ast .Eq : '==' ,
628+ ast .NotEq : '!=' ,
629+ ast .Lt : '<' ,
630+ ast .LtE : '<=' ,
631+ ast .Gt : '>' ,
632+ ast .GtE : '>=' ,
633+ }
634+
635+
636+ def _lower_comparison (
637+ state : _LoweringState , instance : str , test : ast .expr , given : dict [str , CapturedValue ]
638+ ) -> Comparison | None :
639+ """Lower one comparison over names and literals, or decline anything richer."""
640+ if not isinstance (test , ast .Compare ) or len (test .ops ) != 1 or type (test .ops [0 ]) not in _COMPARISON_OPERATORS :
641+ return None
642+ operands : list [str | int | float | bool | None ] = []
643+ for side in (test .left , * test .comparators ):
644+ if isinstance (side , ast .Constant ) and (side .value is None or isinstance (side .value , (int , float , str , bool ))):
645+ operands .append (side .value )
646+ elif isinstance (side , ast .Name ):
647+ lowered = _lower_value (state , side )
648+ if not isinstance (lowered , _Reference ):
649+ return None
650+ _wire (state , instance , side .id , lowered , given )
651+ operands .append (side .id )
652+ else :
653+ return None
654+ left , right = operands
655+ return Comparison (operator = _COMPARISON_OPERATORS [type (test .ops [0 ])], left = left , right = right )
656+
657+
614658def _snapshot_namespace_boundary (state : _LoweringState ) -> dict [str , t .Any ] | None :
615659 """Snapshot region boundaries only when routing a declared namespace field."""
616660 if not any ('.' in path for path in state .inputs ):
0 commit comments