def _get_candidates_b_c(rule_a,
                        goal_rule_b,
                        config):
  """Return candidates where `b` can be applied to `c` to form `a`."""

  # Consider subspans in source to replace with a new non-terminal symbol.
  candidate_sources = set()
  for source_nt_start in range(len(rule_a.source)):
    source_nt_end = source_nt_start + len(goal_rule_b.source)

    source_b = rule_a.source[source_nt_start:source_nt_end]

    if not _equal_modulo_nt_idx(source_b, goal_rule_b.source):
      continue

    # Don't allow source_b to occur multiple times in rule_a.source.
    # Otherwise this leads to an ambiguous selection between the occurences,
    # so take the more conservative approach and disallow this.
    if _rhs_count(rule_a.source, source_b) > 1:
      continue

    free_nt = _get_free_nt(rule_a.source[:source_nt_start] +
                           rule_a.source[source_nt_end:])
    source_c = (
        rule_a.source[:source_nt_start] + tuple([free_nt]) +
        rule_a.source[source_nt_end:])

    # Don't allow source_c to only contain a single non-terminal.
    if _is_single_nt(source_c):
      continue

    # Don't allow source_c to contain >k non-terminals.
    if qcfg_rule.get_num_nts(source_c) > config["max_num_nts"]:
      continue

    candidate_sources.add((source_b, source_c))

  # Consider subspans in target to replace with a new non-terminal symbol.
  candidate_targets = set()
  for target_nt_start in range(len(rule_a.target)):
    target_nt_end = target_nt_start + len(goal_rule_b.target)

    target_b = rule_a.target[target_nt_start:target_nt_end]

    if not _equal_modulo_nt_idx(target_b, goal_rule_b.target):
      continue

    # Optionally don't allow target_b to only contain a single non-terminal.
    if not config["allow_single_nt_target"] and _is_single_nt(target_b):
      continue

    # Optionally allow target_b to occur multiple times in rule.target.
    target_b_count = _rhs_count(rule_a.target, target_b)
    if target_b_count > 1:
      if config["allow_repeated_target_nts"]:
        target_a_remove = rule_utils.rhs_remove(rule_a.target, target_b)
        free_nt = _get_free_nt(target_a_remove)
        target_c = rule_utils.rhs_replace(rule_a.target, target_b, free_nt)
        replaced_count = target_c.count(free_nt)
        if replaced_count != target_b_count:
          # This can happen when a subspan is repeated but overlapping.
          continue
      else:
        continue
    else:
      free_nt = _get_free_nt(rule_a.target[:target_nt_start] +
                             rule_a.target[target_nt_end:])
      target_c = (
          rule_a.target[:target_nt_start] + tuple([free_nt]) +
          rule_a.target[target_nt_end:])

    # Optionally don't allow target_c to only contain a single non-terminal.
    if not config["allow_single_nt_target"] and _is_single_nt(target_c):
      continue

    # Don't allow target_c to contain >k non-terminals.
    if qcfg_rule.get_num_nts(target_c) > config["max_num_nts"]:
      continue

    candidate_targets.add((target_b, target_c))

  candidates = set()
  for ((source_b, source_c),
       (target_b, target_c)) in itertools.product(candidate_sources,
                                                  candidate_targets):
    candidate_pair = _maybe_get_candidate_pair(source_c, source_b, target_c,
                                               target_b)

    if candidate_pair:
      rule_c, rule_b = candidate_pair
      if rule_b == goal_rule_b:
        candidates.add(rule_c)
  return candidates
Return candidates where `b` can be applied to `c` to form `a`.
language/compgen/csl/induction/unification_utils.py