@@ -385,3 +385,102 @@ def generate_random_dfa(
385385 dfa = DFA .from_nfa (nfa , minify = True )
386386
387387 return dfa
388+
389+
390+ def dfa_string_matching (
391+ regex : str ,
392+ max_length : int = 10 ,
393+ ) -> str :
394+ """
395+ Convert `regex` to a DFA using automata-lib, then randomly generate a string
396+ that the DFA accepts. Returns a string that the DFA accepts.
397+ """
398+
399+ # Step 1: Convert to NFA or directly to DFA
400+ dfa = regex_to_dfa (regex )
401+
402+ # Step 2: Determine for each state if acceptance is possible from that state
403+ # We'll do a BFS backward from each final state to mark reachable states.
404+ can_reach_accept = _compute_accept_reachability (dfa )
405+
406+ # Step 3: Do a random walk
407+ s = _random_walk_dfa (dfa , can_reach_accept , max_length )
408+ if s is None :
409+ raise ValueError ("Failed to generate a string that the DFA accepts." )
410+ return s
411+
412+
413+ def _compute_accept_reachability (dfa : DFA ) -> dict :
414+ """
415+ For each state, store whether it's possible to reach a final state.
416+ Returns a dict: state -> bool
417+ """
418+ # Start from final states and do BFS/DFS backwards:
419+ # We'll create a graph reversed: from each state, we see where it can come from.
420+ reverse_graph = {s : [] for s in dfa .states }
421+ for s in dfa .states :
422+ for sym , t in dfa .transitions [s ].items ():
423+ reverse_graph [t ].append ((s , sym ))
424+
425+ can_reach = {s : False for s in dfa .states }
426+ # Mark final states as can_reach = True
427+ queue = list (dfa .final_states )
428+ for f in queue :
429+ can_reach [f ] = True
430+
431+ # BFS
432+ idx = 0
433+ while idx < len (queue ):
434+ current = queue [idx ]
435+ idx += 1
436+ for prev_state , _symbol in reverse_graph [current ]:
437+ if not can_reach [prev_state ]:
438+ can_reach [prev_state ] = True
439+ queue .append (prev_state )
440+
441+ return can_reach
442+
443+
444+ def _random_walk_dfa (
445+ dfa : DFA , can_reach_accept : dict , max_length : int
446+ ) -> Optional [str ]:
447+ """
448+ Start at dfa.initial_state, randomly choose transitions that lead to states
449+ from which a final state is reachable, until we reach a final or exceed max_length.
450+ Note that max_length is not a hard limit, but rather a wanted length.
451+ Return the accepted string or None if we can't produce one.
452+ """
453+ hard_limit = 100
454+ current_state = dfa .initial_state
455+ out = []
456+ # We'll limit the number of steps to avoid infinite loops
457+ for length_counter in range (hard_limit ):
458+ # If current_state is final, maybe stop or continue?
459+ # We'll do a random 50% chance to stop if final, producing a short string.
460+ if current_state in dfa .final_states :
461+ if length_counter >= max_length or random .random () < 0.5 :
462+ # 50% chance to end early if final
463+ return "" .join (out )
464+ # gather possible transitions that lead to can_reach_accept state
465+ next_options = [
466+ (symbol , dest )
467+ for symbol , dest in dfa .transitions [current_state ].items ()
468+ if can_reach_accept [dest ]
469+ ]
470+
471+ if not next_options :
472+ # no valid transitions, so if we are final we can stop; else give up
473+ if current_state in dfa .final_states :
474+ return "" .join (out )
475+ else :
476+ return None
477+
478+ # choose a random transition
479+ symbol , dest = random .choice (next_options )
480+ out .append (symbol )
481+ current_state = dest
482+
483+ # If we are here, we've reached max_length. Accept if the state is final
484+ if current_state in dfa .final_states :
485+ return "" .join (out )
486+ return None
0 commit comments