Solve transitive closure sequentially, updating upper/lower bounds after each step. Transitive closure is represented as a linear graph plus lower/upper bounds for each type variable, see transitive_closure() docstring for details. We solve for type variables that appear in `batch`. If
(
batch: list[TypeVarId], graph: Graph, lowers: Bounds, uppers: Bounds
)
| 189 | |
| 190 | |
| 191 | def solve_iteratively( |
| 192 | batch: list[TypeVarId], graph: Graph, lowers: Bounds, uppers: Bounds |
| 193 | ) -> Solutions: |
| 194 | """Solve transitive closure sequentially, updating upper/lower bounds after each step. |
| 195 | |
| 196 | Transitive closure is represented as a linear graph plus lower/upper bounds for each |
| 197 | type variable, see transitive_closure() docstring for details. |
| 198 | |
| 199 | We solve for type variables that appear in `batch`. If a bound is not constant (i.e. it |
| 200 | looks like T :> F[S, ...]), we substitute solutions found so far in the target F[S, ...] |
| 201 | after solving the batch. |
| 202 | |
| 203 | Importantly, after solving each variable in a batch, we move it from linear graph to |
| 204 | upper/lower bounds, this way we can guarantee consistency of solutions (see comment below |
| 205 | for an example when this is important). |
| 206 | """ |
| 207 | solutions = {} |
| 208 | s_batch = set(batch) |
| 209 | while s_batch: |
| 210 | for tv in sorted(s_batch, key=lambda x: x.raw_id): |
| 211 | if lowers[tv] or uppers[tv]: |
| 212 | solvable_tv = tv |
| 213 | break |
| 214 | else: |
| 215 | break |
| 216 | # Solve each solvable type variable separately. |
| 217 | s_batch.remove(solvable_tv) |
| 218 | result = solve_one(lowers[solvable_tv], uppers[solvable_tv]) |
| 219 | solutions[solvable_tv] = result |
| 220 | if result is None: |
| 221 | # TODO: support backtracking lower/upper bound choices and order within SCCs. |
| 222 | # (will require switching this function from iterative to recursive). |
| 223 | continue |
| 224 | |
| 225 | # Update the (transitive) bounds from graph if there is a solution. |
| 226 | # This is needed to guarantee solutions will never contradict the initial |
| 227 | # constraints. For example, consider {T <: S, T <: A, S :> B} with A :> B. |
| 228 | # If we would not update the uppers/lowers from graph, we would infer T = A, S = B |
| 229 | # which is not correct. |
| 230 | for l, u in graph.copy(): |
| 231 | if l == u: |
| 232 | continue |
| 233 | if l == solvable_tv: |
| 234 | lowers[u].add(result) |
| 235 | graph.remove((l, u)) |
| 236 | if u == solvable_tv: |
| 237 | uppers[l].add(result) |
| 238 | graph.remove((l, u)) |
| 239 | |
| 240 | # We can update uppers/lowers only once after solving the whole SCC, |
| 241 | # since uppers/lowers can't depend on type variables in the SCC |
| 242 | # (and we would reject such SCC as non-linear and therefore not solvable). |
| 243 | subs = {tv: s for (tv, s) in solutions.items() if s is not None} |
| 244 | for tv in lowers: |
| 245 | lowers[tv] = {expand_type(lt, subs) for lt in lowers[tv]} |
| 246 | for tv in uppers: |
| 247 | uppers[tv] = {expand_type(ut, subs) for ut in uppers[tv]} |
| 248 | return solutions |
no test coverage detected
searching dependent graphs…