Coverage for /pythoncovmergedfiles/medio/medio/src/turbohtml/tools/fuzz/dom_lifecycle.py: 28%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

361 statements  

1"""Lifecycle byte programs retain wrappers across callbacks and reentry, then check native tree invariants.""" 

2 

3from __future__ import annotations 

4 

5import gc 

6import threading 

7from collections.abc import Callable 

8from typing import TYPE_CHECKING, Final, NamedTuple, cast 

9 

10from typing_extensions import override 

11 

12from turbohtml import ( 

13 Element, 

14 IncrementalParser, 

15 Node, 

16 NodeFilter, 

17 NodeIterator, 

18 Range, 

19 Text, 

20 Tokenizer, 

21 TreeWalker, 

22 parse, 

23) 

24from turbohtml._html import _tree_verify # ruff: ignore[import-private-name] - the fuzz-only native invariant hook 

25from turbohtml.migration.markupsafe import escape 

26from turbohtml.mutations import MutationObserver 

27from turbohtml.rewrite import rewrite 

28from turbohtml.saxparse import SaxHandler, sax_parse 

29 

30if TYPE_CHECKING: 

31 import random 

32 from collections.abc import Iterable, Iterator, Sequence 

33 

34 from turbohtml.mutations import MutationRecord 

35 from turbohtml.rewrite import Element as RewriteHandle 

36 

37__all__: Final = [ 

38 "LifecycleStep", 

39 "UnsupportedDomLifecycleError", 

40 "dom_lifecycle_check", 

41 "dom_lifecycle_controls", 

42 "dom_lifecycle_generate", 

43 "dom_lifecycle_race", 

44 "dom_lifecycle_seeds", 

45 "run_dom_lifecycle", 

46] 

47 

48# libxml2's api.c bounds content and copies (MAX_CONTENT 100, MAX_COPY_NODES 50) so a short input cannot build an 

49# unbounded tree; these keep the dom-program input bound and cap execution, nesting, registers and created nodes 

50_MAX_BYTES: Final = 1024 

51_MAX_STEPS: Final = 256 

52_MAX_DEPTH: Final = 2 

53_SLOTS: Final = 8 

54_MAX_NODES: Final = 48 

55_TAGS: Final = ("p", "div", "span", "template", "b") 

56_WORDS: Final = ("", "alpha", "beta gamma") 

57_MARKUP: Final = ( 

58 "<p>a<b>c</b></p>", 

59 "<div id=x><span class=y>t</span><!--c--></div>", 

60 "<table><td>x<td>y</table>", 

61 "<template><p>t</p></template><i>z</i>", 

62) 

63_EXPECTED: Final = (ValueError, TypeError, IndexError, RuntimeError) 

64 

65 

66class UnsupportedDomLifecycleError(ValueError): 

67 """Reject unsupported encodings before allocating register state.""" 

68 

69 

70class LifecycleStep(NamedTuple): 

71 """One executed instruction: its nesting depth, operation, outcome and native invariant violations.""" 

72 

73 depth: int 

74 operation: str 

75 outcome: str 

76 violations: tuple[int, ...] 

77 

78 

79def dom_lifecycle_check( 

80 text: str, run: Callable[[bytes], tuple[tuple[LifecycleStep, ...], tuple[str, ...]]] | None = None 

81) -> str | None: 

82 """Require clean native invariants after every step and an identical replay.""" 

83 if len(text) > _MAX_BYTES * 2: 

84 msg: Final = "DOM lifecycle program exceeds the input limit" 

85 raise UnsupportedDomLifecycleError(msg) 

86 try: 

87 program: Final = bytes.fromhex(text) 

88 except ValueError as error: 

89 msg: Final = "DOM lifecycle program must contain hexadecimal bytes" 

90 raise UnsupportedDomLifecycleError(msg) from error 

91 runner: Final = run or run_dom_lifecycle 

92 first: Final = runner(program) 

93 if broken := next((step for step in first[0] if any(step.violations)), None): 

94 return f"tree invariant broken after {broken.operation}" 

95 return None if runner(program) == first else "DOM lifecycle program replays differently" 

96 

97 

98def dom_lifecycle_generate(rng: random.Random) -> str: 

99 """Seed every register ring before random instructions.""" 

100 return (_PREFIX + bytes(rng.randrange(256) for _ in range(rng.randrange(1, 65) * 4))).hex() 

101 

102 

103def dom_lifecycle_seeds() -> list[str]: 

104 """Run every opcode on each slot with a mutating nested program, then fire the callbacks it registered.""" 

105 return [ 

106 ( 

107 _PREFIX + bytes((opcode, slot, slot + 9, len(body) // 4 | slot % 3 << 2 | slot % 2 << 4)) + body + _TRAILER 

108 ).hex() 

109 for opcode in range(len(_OPERATIONS)) 

110 for slot in range(_SLOTS) 

111 for body in (_BODIES[slot // 3 % len(_BODIES)],) 

112 ] 

113 

114 

115def dom_lifecycle_controls() -> dict[str, bool]: 

116 """Require a reported violation and a drifting replay to fail the registered checker.""" 

117 seed: Final = dom_lifecycle_seeds()[0] 

118 broken: Final = ((LifecycleStep(0, "element", "ok", (1, 0, 0, 0)),), ()) 

119 replays: Final = iter((((), ("<p></p>",)), ((), ("<div></div>",)))) 

120 return { 

121 "missing violation": dom_lifecycle_check(seed, lambda _program: broken) is not None, 

122 "replay drift": dom_lifecycle_check(seed, lambda _program: next(replays)) is not None, 

123 } 

124 

125 

126def run_dom_lifecycle(program: bytes) -> tuple[tuple[LifecycleStep, ...], tuple[str, ...]]: 

127 """Execute the program and return each step plus the final markup of every node register.""" 

128 machine: Final = _Machine(_instructions(program)) 

129 machine.block(range(len(machine.code))) 

130 return tuple(machine.trace), tuple("" if node is None else node.serialize() for node in machine.nodes) 

131 

132 

133def dom_lifecycle_race(seeds: Sequence[str], *, shared: bool = False) -> list[tuple[LifecycleStep, ...]]: 

134 """ 

135 Run every seed in its own thread at once, so the programs contend on the module and interpreter state. 

136 

137 With ``shared`` set, every program reads and writes one set of node registers, so the threads also edit, move and 

138 walk the same trees. 

139 """ 

140 nodes: Final[list[Node | None] | None] = [None] * _SLOTS if shared else None 

141 machines: Final = [_Machine(_instructions(bytes.fromhex(seed)), nodes) for seed in seeds] 

142 start: Final = threading.Barrier(len(machines)) 

143 

144 def run(machine: _Machine) -> None: 

145 start.wait() 

146 machine.block(range(len(machine.code))) 

147 

148 threads: Final = [threading.Thread(target=run, args=(machine,)) for machine in machines] 

149 for thread in threads: 

150 thread.start() 

151 for thread in threads: 

152 thread.join() 

153 return [tuple(machine.trace) for machine in machines] 

154 

155 

156def _instructions(program: bytes) -> list[tuple[int, int, int, int]]: 

157 bounded: Final = program[:_MAX_BYTES] 

158 return [ 

159 (bounded[index], bounded[index + 1], bounded[index + 2], bounded[index + 3]) 

160 for index in range(0, len(bounded) - len(bounded) % 4, 4) 

161 ] 

162 

163 

164class _UnwindError(Exception): 

165 """Unwind a nested program through the native caller that invoked its callback.""" 

166 

167 

168class _Machine: 

169 """Typed register rings over one program; operands pick slots, so overwriting a slot drops its reference.""" 

170 

171 def __init__(self, code: list[tuple[int, int, int, int]], nodes: list[Node | None] | None = None) -> None: 

172 self.code: Final = code 

173 self.nodes: Final[list[Node | None]] = [None] * _SLOTS if nodes is None else nodes 

174 self.walkers: Final[list[TreeWalker | NodeIterator | Iterator[object] | None]] = [None] * _SLOTS 

175 self.parsers: Final[list[Tokenizer | IncrementalParser | None]] = [None] * _SLOTS 

176 self.observers: Final[list[MutationObserver | None]] = [None] * _SLOTS 

177 self.trace: Final[list[LifecycleStep]] = [] 

178 self.depth = 0 

179 self.created = 0 

180 

181 def block(self, span: range) -> None: 

182 index = span.start 

183 while index < span.stop and len(self.trace) < _MAX_STEPS: 

184 opcode, first, second, third = self.code[index] 

185 name, operation, nested = _OPERATIONS[opcode % len(_OPERATIONS)] 

186 body = range(index + 1, min(index + 1 + third % 4, span.stop)) if nested else range(index + 1, index + 1) 

187 if operation is None and self.depth: 

188 raise _UnwindError 

189 try: 

190 outcome = "abort" if operation is None else operation(self, first % _SLOTS, second, third, body) 

191 except (*_EXPECTED, _UnwindError) as error: 

192 outcome = type(error).__name__ 

193 self.trace.append(LifecycleStep(self.depth, name, outcome, self.verify())) 

194 index = body.stop 

195 

196 def verify(self) -> tuple[int, ...]: 

197 roots: Final = [ 

198 *(node for node in self.nodes if node is not None), 

199 *(walker.root for walker in self.walkers if isinstance(walker, (TreeWalker, NodeIterator))), 

200 ] 

201 return tuple(map(sum, zip((0, 0, 0, 0), *(_tree_verify(node) for node in roots), strict=True))) 

202 

203 def nested(self, body: range) -> None: 

204 """Run a callback's program one level deeper, so reentry stays bounded by depth instead of the stack.""" 

205 if self.depth < _MAX_DEPTH: 

206 self.depth += 1 

207 try: 

208 self.block(body) 

209 finally: 

210 self.depth -= 1 

211 

212 

213def _element(machine: _Machine, slot: int, second: int, _third: int, _body: range) -> str: 

214 if machine.created >= _MAX_NODES: 

215 return "cap" 

216 machine.created += 1 

217 machine.nodes[slot] = Element(_TAGS[second % len(_TAGS)]) 

218 return "ok" 

219 

220 

221def _text(machine: _Machine, slot: int, second: int, _third: int, _body: range) -> str: 

222 if machine.created >= _MAX_NODES: 

223 return "cap" 

224 machine.created += 1 

225 machine.nodes[slot] = Text(_WORDS[second % len(_WORDS)]) 

226 return "ok" 

227 

228 

229def _parse(machine: _Machine, slot: int, second: int, _third: int, _body: range) -> str: 

230 if machine.created >= _MAX_NODES: 

231 return "cap" 

232 machine.created += 8 

233 machine.nodes[slot] = parse(_MARKUP[second % len(_MARKUP)]) 

234 return "ok" 

235 

236 

237def _navigate(machine: _Machine, slot: int, second: int, third: int, _body: range) -> str: 

238 if (source := machine.nodes[second % _SLOTS]) is None: 

239 return "empty" 

240 children: Final = source.children 

241 relatives: Final = ( 

242 source.parent, 

243 children[0] if children else None, 

244 children[-1] if children else None, 

245 source.next_sibling, 

246 source.previous_sibling, 

247 ) 

248 machine.nodes[slot] = relatives[third % len(relatives)] 

249 return "ok" 

250 

251 

252def _append(machine: _Machine, slot: int, second: int, _third: int, _body: range) -> str: 

253 target: Final = machine.nodes[slot] 

254 if not isinstance(target, Element) or (child := machine.nodes[second % _SLOTS]) is None: 

255 return "empty" 

256 target.append(child) 

257 return "ok" 

258 

259 

260def _insert(machine: _Machine, slot: int, second: int, third: int, body: range) -> str: 

261 target: Final = machine.nodes[slot] 

262 if not isinstance(target, Element) or (child := machine.nodes[second % _SLOTS]) is None: 

263 return "empty" 

264 target.insert(cast("int", _Index(machine, body, third, second % 3)), child) 

265 return "ok" 

266 

267 

268def _extract(machine: _Machine, slot: int, _second: int, _third: int, _body: range) -> str: 

269 if (node := machine.nodes[slot]) is None: 

270 return "empty" 

271 node.extract() 

272 return "ok" 

273 

274 

275def _decompose(machine: _Machine, slot: int, _second: int, _third: int, _body: range) -> str: 

276 if (node := machine.nodes[slot]) is None: 

277 return "empty" 

278 node.decompose() 

279 return "ok" 

280 

281 

282def _unwrap(machine: _Machine, slot: int, _second: int, _third: int, _body: range) -> str: 

283 if (node := machine.nodes[slot]) is None: 

284 return "empty" 

285 node.unwrap() 

286 return "ok" 

287 

288 

289def _replace(machine: _Machine, slot: int, second: int, _third: int, _body: range) -> str: 

290 if (node := machine.nodes[slot]) is None or (other := machine.nodes[second % _SLOTS]) is None: 

291 return "empty" 

292 node.replace_with(other) 

293 return "ok" 

294 

295 

296def _normalize(machine: _Machine, slot: int, _second: int, _third: int, _body: range) -> str: 

297 if not isinstance(node := machine.nodes[slot], Element): 

298 return "empty" 

299 node.normalize() 

300 return "ok" 

301 

302 

303def _range_extract(machine: _Machine, slot: int, second: int, _third: int, _body: range) -> str: 

304 if (node := machine.nodes[slot]) is None: 

305 return "empty" 

306 selection: Final = Range(node) 

307 selection.select_node_contents(node) 

308 machine.nodes[second % _SLOTS] = selection.extract_contents() 

309 return "ok" 

310 

311 

312def _collect(machine: _Machine, slot: int, second: int, _third: int, _body: range) -> str: 

313 if second % 4 == 0: 

314 machine.nodes[slot] = None 

315 elif second % 4 == 1: 

316 machine.walkers[slot] = None 

317 elif second % 4 == 2: 

318 machine.parsers[slot] = None 

319 else: 

320 machine.observers[slot] = None 

321 gc.collect() 

322 return "ok" 

323 

324 

325def _tree_walker(machine: _Machine, slot: int, second: int, third: int, body: range) -> str: 

326 if (root := machine.nodes[second % _SLOTS]) is None: 

327 return "empty" 

328 machine.walkers[slot] = TreeWalker(root, filter=_filter(machine, body, third)) 

329 return "ok" 

330 

331 

332def _node_iterator(machine: _Machine, slot: int, second: int, third: int, body: range) -> str: 

333 if (root := machine.nodes[second % _SLOTS]) is None: 

334 return "empty" 

335 machine.walkers[slot] = NodeIterator(root, filter=_filter(machine, body, third)) 

336 return "ok" 

337 

338 

339def _filter(machine: _Machine, body: range, third: int) -> Callable[[Node], int]: 

340 verdicts: Final = (NodeFilter.FILTER_ACCEPT, NodeFilter.FILTER_REJECT, NodeFilter.FILTER_SKIP) 

341 

342 def verdict(_node: Node) -> int: 

343 machine.nested(body) 

344 # the walker converts the verdict inside its step, so an __index__ program runs mid-traversal 

345 return ( 

346 cast("int", _Index(machine, body, third, (third >> 2) % 3)) 

347 if third >> 4 & 1 

348 else verdicts[(third >> 2) % 3] 

349 ) 

350 

351 return verdict 

352 

353 

354def _iterate(machine: _Machine, slot: int, second: int, third: int, _body: range) -> str: 

355 if (node := machine.nodes[second % _SLOTS]) is None: 

356 return "empty" 

357 iterators: Final = (node.descendants, node.iter_elements(), node.serialize_iter()) 

358 machine.walkers[slot] = iter(iterators[third % len(iterators)]) 

359 return "ok" 

360 

361 

362def _step(machine: _Machine, slot: int, second: int, third: int, _body: range) -> str: 

363 walker: Final = machine.walkers[slot] 

364 if isinstance(walker, TreeWalker): 

365 moves: Final = ( 

366 walker.next_node, 

367 walker.previous_node, 

368 walker.parent_node, 

369 walker.first_child, 

370 walker.last_child, 

371 walker.next_sibling, 

372 walker.previous_sibling, 

373 ) 

374 found: object = moves[second % len(moves)]() 

375 elif isinstance(walker, NodeIterator): 

376 found = walker.next_node() if second % 2 else walker.previous_node() 

377 elif walker is None: 

378 return "empty" 

379 else: 

380 found = next(walker, None) 

381 if isinstance(found, Node): 

382 machine.nodes[third % _SLOTS] = found 

383 return "ok" if found is not None else "end" 

384 

385 

386def _observe(machine: _Machine, slot: int, second: int, third: int, body: range) -> str: 

387 if (target := machine.nodes[second % _SLOTS]) is None: 

388 return "empty" 

389 

390 def callback(_records: list[MutationRecord], _observer: MutationObserver) -> None: 

391 machine.nested(body) 

392 

393 observer: Final = MutationObserver(callback) 

394 machine.observers[slot] = observer 

395 observer.observe( 

396 target, 

397 child_list=True, 

398 attributes=True, 

399 character_data=True, 

400 subtree=True, 

401 attribute_filter=cast("Sequence[str]", _Items(machine, body, third, second % 3, ["id"])), 

402 ) 

403 return "ok" 

404 

405 

406def _deliver(machine: _Machine, slot: int, second: int, _third: int, _body: range) -> str: 

407 if (observer := machine.observers[slot]) is None: 

408 return "empty" 

409 actions: Final = (observer.deliver, observer.take_records, observer.disconnect) 

410 actions[second % len(actions)]() 

411 return "ok" 

412 

413 

414def _rewrite(machine: _Machine, slot: int, second: int, _third: int, body: range) -> str: 

415 def handler(handle: RewriteHandle) -> None: 

416 machine.nested(body) 

417 # the handle is callback-scoped, so the program edits it here and never stores it in a register 

418 if second % 3 == 0: 

419 handle.remove() 

420 elif second % 3 == 1: 

421 handle.after("<i>z</i>", html=True) 

422 else: 

423 handle.before("y") 

424 

425 rewrite(_MARKUP[slot % len(_MARKUP)], elements=[("*", handler)], text=handler, comments=handler) 

426 return "ok" 

427 

428 

429def _sax(machine: _Machine, slot: int, _second: int, _third: int, body: range) -> str: 

430 sax_parse(_MARKUP[slot % len(_MARKUP)], _Sax(machine, body)) 

431 return "ok" 

432 

433 

434def _tokenizer(machine: _Machine, slot: int, _second: int, _third: int, _body: range) -> str: 

435 machine.parsers[slot] = Tokenizer() 

436 return "ok" 

437 

438 

439def _incremental(machine: _Machine, slot: int, _second: int, _third: int, _body: range) -> str: 

440 machine.parsers[slot] = IncrementalParser() 

441 return "ok" 

442 

443 

444def _parser_call(machine: _Machine, slot: int, second: int, third: int, body: range) -> str: 

445 parser: Final = machine.parsers[slot] 

446 markup: Final = _MARKUP[third % len(_MARKUP)] 

447 if isinstance(parser, Tokenizer): 

448 if second % 4 == 0: 

449 machine.walkers[third % _SLOTS] = parser.feed(markup) 

450 elif second % 4 == 1: 

451 machine.walkers[third % _SLOTS] = parser.close() 

452 elif second % 4 == 2: 

453 parser.reset() 

454 else: 

455 # a handler whose program feeds, closes or resets this tokenizer reenters it mid-dispatch 

456 parser.dispatch(_Dispatch(machine, body)) 

457 elif isinstance(parser, IncrementalParser): 

458 if second % 2: 

459 parser.feed(markup) 

460 else: 

461 machine.nodes[third % _SLOTS] = parser.close() 

462 else: 

463 return "empty" 

464 return "ok" 

465 

466 

467def _attribute(machine: _Machine, slot: int, second: int, third: int, body: range) -> str: 

468 if not isinstance(node := machine.nodes[slot], Element): 

469 return "empty" 

470 if second % 3 == 0: 

471 node.attrs["id"] = _WORDS[third % len(_WORDS)] 

472 elif second % 3 == 1: 

473 node.attrs.pop("id", None) 

474 else: 

475 # dict(mapping) hashes each key and the comparison calls __eq__, both from inside the attrs view 

476 return str(node.attrs == _Items(machine, body, third, second // 3 % 3, ["id"])) 

477 return "ok" 

478 

479 

480def _text_conversion(machine: _Machine, slot: int, second: int, third: int, body: range) -> str: 

481 if not isinstance(node := machine.nodes[slot], Element): 

482 return "empty" 

483 node.text = escape(_Text(machine, body, third, second % 3)) 

484 return "ok" 

485 

486 

487def _extend(machine: _Machine, slot: int, second: int, third: int, body: range) -> str: 

488 if not isinstance(node := machine.nodes[slot], Element) or (child := machine.nodes[second % _SLOTS]) is None: 

489 return "empty" 

490 node.extend(cast("Iterable[Node]", _Items(machine, body, third, second % 3, [child]))) 

491 return "ok" 

492 

493 

494def _compare(machine: _Machine, slot: int, second: int, third: int, body: range) -> str: 

495 if (node := machine.nodes[slot]) is None: 

496 return "empty" 

497 # a Node compares only with a Node, so Python falls back to the reflected __eq__ of the other operand 

498 return str(node == _Key(machine, body, third, second % 3)) 

499 

500 

501def _select(machine: _Machine, slot: int, second: int, _third: int, _body: range) -> str: 

502 if not isinstance(node := machine.nodes[slot], Element): 

503 return "empty" 

504 # both populate per-handle caches stamped with tree versions, which the verifier then cross-checks 

505 return str(len(node.select("b, span")) if second % 2 else node.css_path()) 

506 

507 

508class _Conversion: 

509 """An argument whose conversion method runs a nested program, then answers, raises or behaves plainly.""" 

510 

511 def __init__(self, machine: _Machine, body: range, third: int, mode: int) -> None: 

512 self.machine: Final = machine 

513 self.body: Final = body 

514 self.third: Final = third 

515 self.mode: Final = mode 

516 

517 def convert(self) -> None: 

518 if self.mode: 

519 self.machine.nested(self.body) 

520 if self.mode == 2: 

521 raise _UnwindError 

522 

523 

524class _Index(_Conversion): 

525 def __index__(self) -> int: 

526 self.convert() 

527 return self.third 

528 

529 

530class _Text(_Conversion): 

531 def __str__(self) -> str: 

532 self.convert() 

533 return _WORDS[self.third % len(_WORDS)] 

534 

535 

536class _Key(_Conversion): 

537 def __hash__(self) -> int: 

538 self.convert() 

539 return hash("id") 

540 

541 def __eq__(self, other: object) -> bool: 

542 self.convert() 

543 return other == "id" 

544 

545 

546class _Items(_Conversion): 

547 """Serve as an iterable of fixed items, and as a mapping keyed by a conversion key.""" 

548 

549 def __init__(self, machine: _Machine, body: range, third: int, mode: int, items: list[Node] | list[str]) -> None: 

550 super().__init__(machine, body, third, mode) 

551 self.items: Final = items 

552 

553 def __iter__(self) -> Iterator[Node | str]: 

554 self.convert() 

555 return iter(self.items) 

556 

557 def keys(self) -> list[_Key]: 

558 return [_Key(self.machine, self.body, self.third, self.mode)] 

559 

560 def __getitem__(self, _key: object) -> str: 

561 return _WORDS[self.third % len(_WORDS)] 

562 

563 

564class _Sax(SaxHandler): 

565 """A sax_parse handler whose start and text events run the nested program.""" 

566 

567 def __init__(self, machine: _Machine, body: range) -> None: 

568 self.machine: Final = machine 

569 self.body: Final = body 

570 

571 @override 

572 def start_element(self, tag: str, attrs: tuple[tuple[str, str | None], ...]) -> None: 

573 self.machine.nested(self.body) 

574 

575 @override 

576 def characters(self, data: str) -> None: 

577 self.machine.nested(self.body) 

578 

579 

580class _Dispatch: 

581 """A Tokenizer.dispatch handler whose every html.parser event runs the nested program.""" 

582 

583 def __init__(self, machine: _Machine, body: range) -> None: 

584 self.machine: Final = machine 

585 self.body: Final = body 

586 

587 def __getattr__(self, _name: str) -> Callable[..., None]: 

588 return lambda *_args: self.machine.nested(self.body) 

589 

590 

591_Operation = Callable[[_Machine, int, int, int, range], str] 

592_OPERATIONS: Final[tuple[tuple[str, _Operation | None, bool], ...]] = ( 

593 ("element", _element, False), 

594 ("text", _text, False), 

595 ("parse", _parse, False), 

596 ("navigate", _navigate, False), 

597 ("append", _append, False), 

598 ("insert", _insert, True), 

599 ("extract", _extract, False), 

600 ("decompose", _decompose, False), 

601 ("unwrap", _unwrap, False), 

602 ("replace", _replace, False), 

603 ("collect", _collect, False), 

604 ("tree-walker", _tree_walker, True), 

605 ("node-iterator", _node_iterator, True), 

606 ("iterate", _iterate, False), 

607 ("step", _step, False), 

608 ("observe", _observe, True), 

609 ("deliver", _deliver, False), 

610 ("rewrite", _rewrite, True), 

611 ("sax", _sax, True), 

612 ("tokenizer", _tokenizer, False), 

613 ("incremental", _incremental, False), 

614 ("parser-call", _parser_call, True), 

615 ("attribute", _attribute, True), 

616 ("text-conversion", _text_conversion, True), 

617 ("extend", _extend, True), 

618 ("compare", _compare, True), 

619 ("select", _select, False), 

620 ("raise", None, False), 

621 ("normalize", _normalize, False), 

622 ("range-extract", _range_extract, False), 

623) 

624 

625# element p, element div, text, parsed document, then html/body/p/b of that document, a tokenizer and a fed parser 

626_PREFIX: Final = bytes(( 

627 *(0, 0, 0, 0), 

628 *(0, 1, 1, 0), 

629 *(1, 2, 1, 0), 

630 *(2, 3, 0, 0), 

631 *(4, 0, 1, 0), 

632 *(4, 0, 2, 0), 

633 *(3, 4, 3, 1), 

634 *(3, 5, 4, 2), 

635 *(3, 6, 5, 1), 

636 *(3, 7, 6, 2), 

637 *(19, 0, 0, 0), 

638 *(20, 1, 0, 0), 

639 *(21, 1, 1, 0), 

640)) 

641# a nested program edits across trees, reenters the tokenizer and incremental parser, or runs a program of its own 

642_BODIES: Final = ( 

643 bytes((*(6, 1, 0, 0), *(4, 6, 1, 0), *(7, 7, 0, 0))), 

644 bytes((*(21, 0, 0, 0), *(21, 0, 2, 0), *(21, 1, 0, 2))), 

645 bytes((*(18, 0, 0, 2), *(7, 7, 0, 0), *(8, 6, 0, 0))), 

646) 

647# step every walker, mutate the observed trees, deliver every observer, then dispatch the tokenizer into a program 

648# that feeds and resets it 

649_TRAILER: Final = bytes(( 

650 *(byte for slot in range(_SLOTS) for byte in (14, slot, slot, slot)), 

651 *(22, 0, 0, 1), 

652 *(5, 6, 2, 0), 

653 *(byte for slot in range(_SLOTS) for byte in (16, slot, 0, 0)), 

654 *(21, 0, 3, 3), 

655 *_BODIES[1], 

656))