@@ -2197,11 +2197,75 @@ def _has_ver(expr: ast.AST | None) -> bool:
21972197 for n in ast .walk (expr )
21982198 )
21992199
2200+ def _input_derived (expr : ast .AST | None , params : set [str ]) -> bool :
2201+ return bool (_expr_names (expr ) & params )
2202+
22002203 for node in ast .walk (tree ):
22012204 if not isinstance (node , (ast .FunctionDef , ast .AsyncFunctionDef )):
22022205 continue
22032206 if not is_entrypoint_name (node .name ):
22042207 continue
2208+ params = {arg .arg for arg in node .args .args }
2209+
2210+ # First-call state replay:
2211+ # if _state is None:
2212+ # _state = compute(input)
2213+ # return _state
2214+ #
2215+ # Or the inverse fast path:
2216+ # if _state is not None:
2217+ # return _state
2218+ # _state = compute(input)
2219+ # return _state
2220+ #
2221+ # The guard is input-independent, the slot appears in that guard, and
2222+ # the returned slot is assigned from a live input in the entrypoint.
2223+ for child in ast .walk (node ):
2224+ if not isinstance (child , ast .If ):
2225+ continue
2226+ if _expr_names (child .test ) & params :
2227+ continue
2228+ if child not in node .body :
2229+ continue
2230+
2231+ test_roots = _expr_names (child .test )
2232+ returned_in_body = {
2233+ _ast_root_name (stmt .value )
2234+ for stmt in child .body
2235+ if isinstance (stmt , ast .Return ) and stmt .value is not None
2236+ } - {None }
2237+ assigned_from_input_in_body = {
2238+ _ast_root_name (target )
2239+ for stmt in child .body
2240+ if isinstance (stmt , ast .Assign ) and _input_derived (stmt .value , params )
2241+ for target in stmt .targets
2242+ } - {None }
2243+ later_statements = node .body [node .body .index (child ) + 1 :]
2244+ returned_after_if = {
2245+ _ast_root_name (stmt .value )
2246+ for stmt in later_statements
2247+ if isinstance (stmt , ast .Return ) and stmt .value is not None
2248+ } - {None }
2249+ assigned_after_if = {
2250+ _ast_root_name (target )
2251+ for stmt in later_statements
2252+ if isinstance (stmt , ast .Assign ) and _input_derived (stmt .value , params )
2253+ for target in stmt .targets
2254+ } - {None }
2255+
2256+ init_then_return = assigned_from_input_in_body & returned_after_if
2257+ fast_return_then_init = returned_in_body & assigned_after_if & returned_after_if
2258+ replay_roots = (init_then_return | fast_return_then_init ) & test_roots
2259+ if replay_roots :
2260+ root = next (iter (replay_roots ))
2261+ return [{
2262+ "pattern" : "LAST_CALL_REPLAY" ,
2263+ "severity" : "critical" ,
2264+ "evidence" : (
2265+ f"{ entrypoint_name } returns first-call output from "
2266+ f"input-independent state slot { root } "
2267+ ),
2268+ }]
22052269
22062270 signature_features : dict [str , set [str ]] = defaultdict (set )
22072271 saved_state_features : dict [str , set [str ]] = defaultdict (set )
0 commit comments