|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +from collections.abc import Iterator |
3 | 4 | from typing import cast |
4 | 5 |
|
5 | 6 | import hyperbase.constants as const |
@@ -153,3 +154,248 @@ def add_argument( |
153 | 154 | new_edge = insert_argrole(edge, argrole, pos) |
154 | 155 | combined = (*tuple(new_edge[: pos + 1]), arg, *tuple(new_edge[pos + 1 :])) |
155 | 156 | return Hyperedge(combined) |
| 157 | + |
| 158 | + |
| 159 | +############# |
| 160 | +# transform # |
| 161 | +############# |
| 162 | + |
| 163 | + |
| 164 | +def transform( |
| 165 | + edge: Hyperedge, |
| 166 | + origin_pattern: Hyperedge | str | list | tuple, |
| 167 | + target_pattern: Hyperedge | str | list | tuple, |
| 168 | + recursive: bool = True, |
| 169 | +) -> Hyperedge: |
| 170 | + """Pattern-driven rewrite of *edge*. |
| 171 | +
|
| 172 | + If ``origin_pattern`` matches *edge*, the edge is rewritten according to |
| 173 | + ``target_pattern`` using the variable bindings extracted from the match. |
| 174 | + If it does not match, the edge is returned unchanged. |
| 175 | +
|
| 176 | + With ``{}`` argroles in the target, un-consumed arguments and argroles |
| 177 | + from the original edge at the matched (top) level are preserved alongside |
| 178 | + the target's. Without ``{}`` only what appears in the target is kept. |
| 179 | +
|
| 180 | + Patterns must contain only named variables; anonymous wildcards |
| 181 | + (``*``, ``.``, ``(*)``, ``...``) and functional patterns (``var``, ``any``, |
| 182 | + ``atoms``, ``lemma``) raise ``ValueError``. The target's variables must be |
| 183 | + a subset of the origin's variables. |
| 184 | +
|
| 185 | + By default the rewrite is applied recursively (depth-first, once per |
| 186 | + level). Pass ``recursive=False`` for a shallow rewrite. |
| 187 | + """ |
| 188 | + origin = hedge(origin_pattern) |
| 189 | + target = hedge(target_pattern) |
| 190 | + _validate_transform_patterns(origin, target) |
| 191 | + return _transform_impl(edge, origin, target, recursive) |
| 192 | + |
| 193 | + |
| 194 | +def _transform_impl( |
| 195 | + edge: Hyperedge, |
| 196 | + origin: Hyperedge, |
| 197 | + target: Hyperedge, |
| 198 | + recursive: bool, |
| 199 | +) -> Hyperedge: |
| 200 | + from hyperbase.patterns import match_pattern |
| 201 | + |
| 202 | + if recursive and edge.not_atom: |
| 203 | + edge = Hyperedge(tuple(_transform_impl(c, origin, target, True) for c in edge)) |
| 204 | + |
| 205 | + matches = match_pattern(edge, origin) |
| 206 | + if matches: |
| 207 | + bindings = matches[0] |
| 208 | + edge = _instantiate(target, origin, edge, bindings, top=True) |
| 209 | + return edge |
| 210 | + |
| 211 | + |
| 212 | +def _validate_transform_patterns(origin: Hyperedge, target: Hyperedge) -> None: |
| 213 | + from hyperbase.patterns.checks import is_variable, is_wildcard |
| 214 | + |
| 215 | + for label, pattern in (("origin_pattern", origin), ("target_pattern", target)): |
| 216 | + if _contains_fun_pattern(pattern): |
| 217 | + raise ValueError( |
| 218 | + f"Functional patterns are not supported in {label}: {pattern}" |
| 219 | + ) |
| 220 | + for atom in _walk_atoms(pattern): |
| 221 | + if is_wildcard(atom) and not is_variable(atom): |
| 222 | + raise ValueError( |
| 223 | + f"Anonymous wildcard '{atom}' not allowed in {label}; " |
| 224 | + f"use a named variable" |
| 225 | + ) |
| 226 | + |
| 227 | + origin_vars = _collect_var_names(origin) |
| 228 | + target_vars = _collect_var_names(target) |
| 229 | + extra = target_vars - origin_vars |
| 230 | + if extra: |
| 231 | + raise ValueError( |
| 232 | + f"target_pattern uses variables not in origin_pattern: {sorted(extra)}" |
| 233 | + ) |
| 234 | + |
| 235 | + |
| 236 | +def _walk_atoms(edge: Hyperedge) -> Iterator[Atom]: |
| 237 | + if edge.atom: |
| 238 | + yield cast(Atom, edge) |
| 239 | + else: |
| 240 | + for child in edge: |
| 241 | + yield from _walk_atoms(child) |
| 242 | + |
| 243 | + |
| 244 | +def _contains_fun_pattern(edge: Hyperedge) -> bool: |
| 245 | + from hyperbase.patterns.checks import is_fun_pattern |
| 246 | + |
| 247 | + if edge.atom: |
| 248 | + return False |
| 249 | + if is_fun_pattern(edge): |
| 250 | + return True |
| 251 | + return any(_contains_fun_pattern(c) for c in edge) |
| 252 | + |
| 253 | + |
| 254 | +def _collect_var_names(pattern: Hyperedge) -> set[str]: |
| 255 | + from hyperbase.patterns.checks import is_variable, variable_name |
| 256 | + |
| 257 | + names: set[str] = set() |
| 258 | + for atom in _walk_atoms(pattern): |
| 259 | + if is_variable(atom): |
| 260 | + names.add(variable_name(atom)) |
| 261 | + return names |
| 262 | + |
| 263 | + |
| 264 | +def _find_var_atom(pattern: Hyperedge, name: str) -> Atom | None: |
| 265 | + from hyperbase.patterns.checks import is_variable, variable_name |
| 266 | + |
| 267 | + for atom in _walk_atoms(pattern): |
| 268 | + if is_variable(atom) and variable_name(atom) == name: |
| 269 | + return atom |
| 270 | + return None |
| 271 | + |
| 272 | + |
| 273 | +def _strip_braces(s: str) -> str: |
| 274 | + if len(s) >= 2 and s[0] == "{" and s[-1] == "}": |
| 275 | + return s[1:-1] |
| 276 | + return s |
| 277 | + |
| 278 | + |
| 279 | +def _atom_decoration_compatible( |
| 280 | + origin_parts: list[str], target_parts: list[str] |
| 281 | +) -> bool: |
| 282 | + """True if origin and target atom decorations differ only in argroles.""" |
| 283 | + o_role = origin_parts[1].split(".") if len(origin_parts) > 1 else [""] |
| 284 | + t_role = target_parts[1].split(".") if len(target_parts) > 1 else [""] |
| 285 | + if o_role[0] != t_role[0]: |
| 286 | + return False |
| 287 | + return origin_parts[2:] == target_parts[2:] |
| 288 | + |
| 289 | + |
| 290 | +def _instantiate( |
| 291 | + target: Hyperedge, |
| 292 | + origin: Hyperedge, |
| 293 | + original: Hyperedge, |
| 294 | + bindings: dict[str, Hyperedge], |
| 295 | + top: bool, |
| 296 | +) -> Hyperedge: |
| 297 | + from hyperbase.patterns.checks import is_variable |
| 298 | + |
| 299 | + if target.atom: |
| 300 | + if is_variable(target): |
| 301 | + return _substitute_var_atom(cast(Atom, target), origin, bindings) |
| 302 | + return target |
| 303 | + |
| 304 | + if top and original.not_atom and target.argroles().startswith("{"): |
| 305 | + return _instantiate_with_preserve(target, origin, original, bindings) |
| 306 | + |
| 307 | + return Hyperedge( |
| 308 | + tuple(_instantiate(c, origin, original, bindings, top=False) for c in target) |
| 309 | + ) |
| 310 | + |
| 311 | + |
| 312 | +def _substitute_var_atom( |
| 313 | + target_atom: Atom, |
| 314 | + origin: Hyperedge, |
| 315 | + bindings: dict[str, Hyperedge], |
| 316 | +) -> Hyperedge: |
| 317 | + from hyperbase.patterns.checks import variable_name |
| 318 | + |
| 319 | + name = variable_name(target_atom) |
| 320 | + if name not in bindings: |
| 321 | + raise ValueError(f"Variable '{name}' has no binding in matched origin pattern") |
| 322 | + binding = bindings[name] |
| 323 | + target_parts = target_atom.parts() |
| 324 | + if len(target_parts) == 1: |
| 325 | + return binding |
| 326 | + |
| 327 | + if binding.atom: |
| 328 | + binding_atom = cast(Atom, binding) |
| 329 | + new_parts = [binding_atom.root(), *target_parts[1:]] |
| 330 | + return Atom("/".join(p for p in new_parts if p)) |
| 331 | + |
| 332 | + origin_atom = _find_var_atom(origin, name) |
| 333 | + origin_parts = origin_atom.parts() if origin_atom else [name] |
| 334 | + if not _atom_decoration_compatible(origin_parts, target_parts): |
| 335 | + raise ValueError( |
| 336 | + f"Cannot change type or namespace on non-atomic binding for " |
| 337 | + f"variable '{name}': origin {origin_parts} vs target {target_parts}" |
| 338 | + ) |
| 339 | + |
| 340 | + t_role = target_parts[1].split(".") if len(target_parts) > 1 else [""] |
| 341 | + target_argroles = t_role[1] if len(t_role) > 1 else "" |
| 342 | + o_role = origin_parts[1].split(".") if len(origin_parts) > 1 else [""] |
| 343 | + origin_argroles = o_role[1] if len(o_role) > 1 else "" |
| 344 | + if target_argroles == origin_argroles: |
| 345 | + return binding |
| 346 | + bare = _strip_braces(target_argroles) |
| 347 | + if bare == "": |
| 348 | + return binding |
| 349 | + return replace_argroles(binding, bare) |
| 350 | + |
| 351 | + |
| 352 | +def _instantiate_with_preserve( |
| 353 | + target: Hyperedge, |
| 354 | + origin: Hyperedge, |
| 355 | + original: Hyperedge, |
| 356 | + bindings: dict[str, Hyperedge], |
| 357 | +) -> Hyperedge: |
| 358 | + consumed = _consumed_arg_indices(original, origin) |
| 359 | + edge_argroles = original.argroles() |
| 360 | + if edge_argroles.startswith("{"): |
| 361 | + edge_argroles = edge_argroles[1:-1] |
| 362 | + |
| 363 | + args = list(original[1:]) |
| 364 | + preserved_args = [args[i] for i in range(len(args)) if i not in consumed] |
| 365 | + preserved_argroles = "".join( |
| 366 | + edge_argroles[i] |
| 367 | + for i in range(min(len(edge_argroles), len(args))) |
| 368 | + if i not in consumed |
| 369 | + ) |
| 370 | + |
| 371 | + new_children = [ |
| 372 | + _instantiate(c, origin, original, bindings, top=False) for c in target |
| 373 | + ] |
| 374 | + final = Hyperedge((new_children[0], *preserved_args, *new_children[1:])) |
| 375 | + |
| 376 | + target_spec = _strip_braces(target.argroles()) |
| 377 | + new_argroles = preserved_argroles + target_spec |
| 378 | + if new_argroles == "": |
| 379 | + return final |
| 380 | + return replace_argroles(final, new_argroles) |
| 381 | + |
| 382 | + |
| 383 | +def _consumed_arg_indices( |
| 384 | + original_edge: Hyperedge, |
| 385 | + origin_pattern: Hyperedge, |
| 386 | +) -> set[int]: |
| 387 | + from hyperbase.patterns import match_pattern |
| 388 | + |
| 389 | + consumed: set[int] = set() |
| 390 | + if origin_pattern.atom or original_edge.atom: |
| 391 | + return consumed |
| 392 | + |
| 393 | + args = list(original_edge[1:]) |
| 394 | + for arg_pattern in origin_pattern[1:]: |
| 395 | + for i, arg in enumerate(args): |
| 396 | + if i in consumed: |
| 397 | + continue |
| 398 | + if match_pattern(arg, arg_pattern): |
| 399 | + consumed.add(i) |
| 400 | + break |
| 401 | + return consumed |
0 commit comments