Przejdź do treści

olski.parse

Parsing: every reading, not the first one.

The parser answers these questions:

  1. Does this sentence have a derivation at all? If not, it is not olski, and the furthest point reached says where the analysis died.
  2. If it has exactly one, that is the reading.
  3. If it has more than one, the readings and the summaries beside them say what the sentence leaves open. Why that is a finding for the author and not a rejection is docs/subset.md#wieloznaczność-jest-znaleziskiem-a-nie-definicją-olskiego.

Distinct readings, not derivations. Two derivations that describe the same structure are one reading. The distinction is not pedantic: it is the mistake recorded in docs/glr-in-practice.md#ambiguity-as-a-confidence-measure, where a system fell silent on lines it had understood perfectly because it counted attempts instead of outcomes.

Implementation. An Earley chart over the segmentation graph builds a forest with shared nodes: one Pozycja per constituent shape, however many derivations stand under it, so six undecided attachments are six positions rather than sixty-four trees. The summaries come off that forest, one method of Las each, and none of them needs another parser. docs/parsowanie.md#werdykt-jest-zapytaniem-o-las-a-nie-listą-czytań owns the argument for asking the forest rather than a list of trees, and docs/parsowanie.md#co-się-pakuje-rozstrzyga-tożsamość-czytania owns the two conditions such a forest has to meet and the measurement behind the second.

Podział na moduły jest tu decyzją i idzie za tym, czym każdy kawałek jest. olski.parse.czytanie definiuje typy, którymi rozbiór mówi o kształcie: pozycję, liść i węzeł. olski.parse.podsumowanie definiuje typy, którymi rozbiór rozmawia z werdyktem: deklarację, którą dostaje od gramatyki, i wynik, który oddaje. olski.parse.tablica rozbiera zdanie tablicą Earleya, olski.parse.las odpowiada na pytania o las, który z tej tablicy wychodzi, a olski.parse.streszczenie składa napis, którym werdykt nazywa jedno czytanie. Nazwa z podkreśleniem przechodzi tu przez granicę modułu, bo znaczy prywatne dla pakietu, a nie dla modułu: poza pakiet wychodzą same nazwy wyliczone w __all__.

Las zostaje jednym modułem, choć jest z nich największy. Każda jego metoda czyta prywatny stan tej samej tablicy i tych samych pamięci podręcznych, więc rozcięcie go dałoby drugi moduł sięgający do wnętrza pierwszego, a taki podział kosztuje więcej niż długość, którą zdejmuje.

Funkcje wejściowe składają tablicę, las i podsumowania w jedną odpowiedź, więc są tutaj, i jest to jedyne miejsce, które je składa.

Tree = Leaf | Node module-attribute

MAX_READINGS = 64 module-attribute

OBOK = ' + ' module-attribute

PRZYŁĄCZONY_DO = ' → ' module-attribute

__all__ = ['ciało_koordynuje', 'Cykl', 'Deklaracja', 'describe', 'Las', 'las', 'Leaf', 'liście', 'MAX_READINGS', 'Node', 'Obsada', 'OBOK', 'parse', 'podsumuj', 'Pozycja', 'Przyłączenie', 'PRZYŁĄCZONY_DO', 'Result', 'Rozbieżność', 'sklej_formy', 'streszczenia', 'streszczone', 'Tree', 'w_zakresie', 'zakresy'] module-attribute

Cykl

Bases: Exception

Pozycja stoi sama pod sobą, więc czytań jest nieskończenie wiele.

Source code in olski/parse/czytanie.py
15
16
class Cykl(Exception):
    """Pozycja stoi sama pod sobą, więc czytań jest nieskończenie wiele."""

Leaf dataclass

Forma pod terminalem i czytania, którymi ją ten kształt bierze.

Source code in olski/parse/czytanie.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@dataclass(frozen=True)
class Leaf:
    """Forma pod terminalem i czytania, którymi ją ten kształt bierze."""

    segment: Segment
    #: Odczytania licencjonujące ten liść w kształcie, w jakim stoi, a nie
    #: dowolne odczytania formy: `lubi` pod orzeczeniem ma tu samo `lubić`, choć
    #: Morfeusz czyta tę formę również jako rzeczownik i jako przymiotnik.
    #: Jest ich kilka tam, gdzie kształtu nie rozstrzygają — `Janek` w podmiocie
    #: stoi i jako `subst:sg:nom:m1`, i jako nazwisko nieodmienne —
    #: i wtedy wybór między nimi nie należy do gramatyki
    #: (:meth:`Node.signature` mówi, dlaczego odczytania kształt nie liczy).
    #: Kolejność jest kolejnością odczytań w segmencie, żeby dwa przebiegi
    #: wypisywały to samo.
    odczytania: tuple[Reading, ...]

    @property
    def reading(self) -> Reading:
        """Pierwsze z odczytań licencjonujących, dla tego, kto pyta o jedno.

        Kto pyta o lemat, tym wyborem wiązać się nie powinien:
        ``olski/skład/rozbiór.py`` pyta o lemat krawędź grafu i mówi, po co.
        """
        return self.odczytania[0]

    @property
    def span(self) -> tuple[int, int]:
        return (self.segment.start, self.segment.end)

    def signature(self):
        """Liść jest swoją rozpiętością i niczym więcej.

        Część mowy zeszła stąd rozmyślnie i nie ma tu wracać przez przeoczenie;
        co ją zdjęło, mówi :meth:`Node.signature`.
        """
        return self.span

    def forms(self) -> list[str]:
        return [self.segment.form]

    def liść_głowy(self) -> Leaf:
        """Głową słowa jest ono samo, i tu schodzenie po głowach się kończy."""
        return self

segment instance-attribute

odczytania instance-attribute

reading property

Pierwsze z odczytań licencjonujących, dla tego, kto pyta o jedno.

Kto pyta o lemat, tym wyborem wiązać się nie powinien: olski/skład/rozbiór.py pyta o lemat krawędź grafu i mówi, po co.

span property

signature()

Liść jest swoją rozpiętością i niczym więcej.

Część mowy zeszła stąd rozmyślnie i nie ma tu wracać przez przeoczenie; co ją zdjęło, mówi Node.signature.

Source code in olski/parse/czytanie.py
73
74
75
76
77
78
79
def signature(self):
    """Liść jest swoją rozpiętością i niczym więcej.

    Część mowy zeszła stąd rozmyślnie i nie ma tu wracać przez przeoczenie;
    co ją zdjęło, mówi :meth:`Node.signature`.
    """
    return self.span

forms()

Source code in olski/parse/czytanie.py
81
82
def forms(self) -> list[str]:
    return [self.segment.form]

liść_głowy()

Głową słowa jest ono samo, i tu schodzenie po głowach się kończy.

Source code in olski/parse/czytanie.py
84
85
86
def liść_głowy(self) -> Leaf:
    """Głową słowa jest ono samo, i tu schodzenie po głowach się kończy."""
    return self

__init__(segment, odczytania)

Node dataclass

Source code in olski/parse/czytanie.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
@dataclass(frozen=True)
class Node:
    label: str
    children: tuple[Leaf | Node, ...]
    #: Skąd dokąd węzeł sięga w grafie segmentów.
    #: Wpisana przy budowaniu,
    #: bo węzeł produkcji o pustym ciele nie ma dzieci, z których dałoby się ją wyliczyć,
    #: a stoi w miejscu, które zna parser.
    #: Takiej produkcji gramatyka olskiego nie ma i nie żąda jej rozwinięcie szyku:
    #: miejsce na okolicznik wychodzi z niego osobnym ciałem, a nie córką o pustej rozpiętości,
    #: bo córka taka stałaby w każdym miejscu każdego zdania i mnożyła wyprowadzenia.
    #: Pustego ciała żąda natomiast luka, i tego żądania nikt nie zaspokoił.
    span: tuple[int, int]
    #: Która z córek jest głową, wzięta z produkcji, która ten węzeł złożyła.
    #: Niesie ją węzeł, a nie odczytuje się jej z gramatyki,
    #: bo streszczenie czytania gramatyki nie dostaje,
    #: a węzeł powstaje tam, gdzie produkcja jest pod ręką.
    głowa: int
    #: Pozycje cennika, którymi płaci produkcja tego węzła (``olski/cennik.py``).
    #: Niesione tu z tego samego powodu co głowa: rachunek czytania składa się nad
    #: drzewem, a drzewo gramatyki już nie widzi.
    koszty: tuple[str, ...]

    def signature(self):
        """Co czyni dwa czytania jednym czytaniem.

        Czytanie jest swoim kształtem i niczym więcej: role stoją w etykietach
        węzłów, przyłączenie w rozpiętościach, a wszystko, o co olski pyta, jest
        pytaniem o drzewo. Wyłączone rozmyślnie: wartości cech, bo zgodność
        wymusiła już unifikacja; lematy, bo polskie formy są homonimiczne
        wszędzie i liczone jako dwa odrzuciłyby prawie całą polszczyznę; części
        mowy, bo tam, gdzie zmieniają strukturę, różni wyprowadzenia już
        kształt — ``do`` jako przyimek i jako nuta dalej są dwoma czytaniami.

        Ostatnią z tych trzech wywodzi docs/subset.md: co ją zdjęło, ile to
        kupuje nad bankiem drzew i czego było warunkiem.

        Pozycja lasu niesie dokładnie ten kształt,
        więc dwa drzewa wyliczone z lasu mają dwie różne sygnatury
        i nie ma tu czego odsiewać.

        Wyliczone drzewo niesie za to więcej niż sygnaturę,
        bo na liściach stoją czytania, a tych kształt nie liczy.
        Klasa czytania zbiera wyprowadzenia różne samą morfologią,
        więc nie rozstrzyga, które z nich w drzewie stoi;
        rozstrzyga, że czytanie spoza niej jest w nim błędem,
        i tyle drzewo dostaje (:meth:`Las._wybierz`).
        """
        return (self.label, tuple(child.signature() for child in self.children))

    def forms(self) -> list[str]:
        return [form for child in self.children for form in child.forms()]

    def liść_głowy(self) -> Leaf:
        """Liść głowy tego konstytuenta: jedno słowo, którym się go nazywa.

        Schodzi po głowach aż do liścia,
        bo głową grupy jest słowo, a nie podgrupa:
        gospodarzem przyłączenia jest ``koszt``, a nie ``koszt szynki``.

        Liściem, a nie samą formą, bo pytający pytają o dwie różne rzeczy o tym
        samym słowie: streszczenie o formę (:meth:`forma_głowy`), a wiersz żądania
        o odczytania, którymi ono w tym kształcie stoi (``olski/werdykt.py``).
        """
        return self.children[self.głowa].liść_głowy()

    def forma_głowy(self) -> str:
        return self.liść_głowy().segment.form

    def find(self, label: str, skip: Sequence[str] = ()) -> list[Node]:
        """Every node with this label, this one included, outermost first.

        A subtree labelled with one of ``skip`` is named where it stands and not
        entered, which is how the summary asks for the roles of the clause it
        summarises rather than for the roles of a clause subordinate to it
        (:attr:`Deklaracja.podrzędne`). Named, because a subordinate clause is
        sometimes a role itself: an adverbial clause is one of the roles readings
        differ in, and its inside is a separate sentence all the same.
        The coverage check passes nothing, because the gold tree marks a role
        wherever a clause has one.
        """
        found = [self] if self.label == label else []
        if self.label in skip:
            return found
        for child in self.children:
            if isinstance(child, Node):
                found.extend(child.find(label, skip))
        return found

label instance-attribute

children instance-attribute

span instance-attribute

głowa instance-attribute

koszty instance-attribute

signature()

Co czyni dwa czytania jednym czytaniem.

Czytanie jest swoim kształtem i niczym więcej: role stoją w etykietach węzłów, przyłączenie w rozpiętościach, a wszystko, o co olski pyta, jest pytaniem o drzewo. Wyłączone rozmyślnie: wartości cech, bo zgodność wymusiła już unifikacja; lematy, bo polskie formy są homonimiczne wszędzie i liczone jako dwa odrzuciłyby prawie całą polszczyznę; części mowy, bo tam, gdzie zmieniają strukturę, różni wyprowadzenia już kształt — do jako przyimek i jako nuta dalej są dwoma czytaniami.

Ostatnią z tych trzech wywodzi docs/subset.md: co ją zdjęło, ile to kupuje nad bankiem drzew i czego było warunkiem.

Pozycja lasu niesie dokładnie ten kształt, więc dwa drzewa wyliczone z lasu mają dwie różne sygnatury i nie ma tu czego odsiewać.

Wyliczone drzewo niesie za to więcej niż sygnaturę, bo na liściach stoją czytania, a tych kształt nie liczy. Klasa czytania zbiera wyprowadzenia różne samą morfologią, więc nie rozstrzyga, które z nich w drzewie stoi; rozstrzyga, że czytanie spoza niej jest w nim błędem, i tyle drzewo dostaje (Las._wybierz).

Source code in olski/parse/czytanie.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def signature(self):
    """Co czyni dwa czytania jednym czytaniem.

    Czytanie jest swoim kształtem i niczym więcej: role stoją w etykietach
    węzłów, przyłączenie w rozpiętościach, a wszystko, o co olski pyta, jest
    pytaniem o drzewo. Wyłączone rozmyślnie: wartości cech, bo zgodność
    wymusiła już unifikacja; lematy, bo polskie formy są homonimiczne
    wszędzie i liczone jako dwa odrzuciłyby prawie całą polszczyznę; części
    mowy, bo tam, gdzie zmieniają strukturę, różni wyprowadzenia już
    kształt — ``do`` jako przyimek i jako nuta dalej są dwoma czytaniami.

    Ostatnią z tych trzech wywodzi docs/subset.md: co ją zdjęło, ile to
    kupuje nad bankiem drzew i czego było warunkiem.

    Pozycja lasu niesie dokładnie ten kształt,
    więc dwa drzewa wyliczone z lasu mają dwie różne sygnatury
    i nie ma tu czego odsiewać.

    Wyliczone drzewo niesie za to więcej niż sygnaturę,
    bo na liściach stoją czytania, a tych kształt nie liczy.
    Klasa czytania zbiera wyprowadzenia różne samą morfologią,
    więc nie rozstrzyga, które z nich w drzewie stoi;
    rozstrzyga, że czytanie spoza niej jest w nim błędem,
    i tyle drzewo dostaje (:meth:`Las._wybierz`).
    """
    return (self.label, tuple(child.signature() for child in self.children))

forms()

Source code in olski/parse/czytanie.py
139
140
def forms(self) -> list[str]:
    return [form for child in self.children for form in child.forms()]

liść_głowy()

Liść głowy tego konstytuenta: jedno słowo, którym się go nazywa.

Schodzi po głowach aż do liścia, bo głową grupy jest słowo, a nie podgrupa: gospodarzem przyłączenia jest koszt, a nie koszt szynki.

Liściem, a nie samą formą, bo pytający pytają o dwie różne rzeczy o tym samym słowie: streszczenie o formę (forma_głowy), a wiersz żądania o odczytania, którymi ono w tym kształcie stoi (olski/werdykt.py).

Source code in olski/parse/czytanie.py
142
143
144
145
146
147
148
149
150
151
152
153
def liść_głowy(self) -> Leaf:
    """Liść głowy tego konstytuenta: jedno słowo, którym się go nazywa.

    Schodzi po głowach aż do liścia,
    bo głową grupy jest słowo, a nie podgrupa:
    gospodarzem przyłączenia jest ``koszt``, a nie ``koszt szynki``.

    Liściem, a nie samą formą, bo pytający pytają o dwie różne rzeczy o tym
    samym słowie: streszczenie o formę (:meth:`forma_głowy`), a wiersz żądania
    o odczytania, którymi ono w tym kształcie stoi (``olski/werdykt.py``).
    """
    return self.children[self.głowa].liść_głowy()

forma_głowy()

Source code in olski/parse/czytanie.py
155
156
def forma_głowy(self) -> str:
    return self.liść_głowy().segment.form

find(label, skip=())

Every node with this label, this one included, outermost first.

A subtree labelled with one of skip is named where it stands and not entered, which is how the summary asks for the roles of the clause it summarises rather than for the roles of a clause subordinate to it (Deklaracja.podrzędne). Named, because a subordinate clause is sometimes a role itself: an adverbial clause is one of the roles readings differ in, and its inside is a separate sentence all the same. The coverage check passes nothing, because the gold tree marks a role wherever a clause has one.

Source code in olski/parse/czytanie.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def find(self, label: str, skip: Sequence[str] = ()) -> list[Node]:
    """Every node with this label, this one included, outermost first.

    A subtree labelled with one of ``skip`` is named where it stands and not
    entered, which is how the summary asks for the roles of the clause it
    summarises rather than for the roles of a clause subordinate to it
    (:attr:`Deklaracja.podrzędne`). Named, because a subordinate clause is
    sometimes a role itself: an adverbial clause is one of the roles readings
    differ in, and its inside is a separate sentence all the same.
    The coverage check passes nothing, because the gold tree marks a role
    wherever a clause has one.
    """
    found = [self] if self.label == label else []
    if self.label in skip:
        return found
    for child in self.children:
        if isinstance(child, Node):
            found.extend(child.find(label, skip))
    return found

__init__(label, children, span, głowa, koszty)

Pozycja dataclass

Miejsce w tablicy: co się wyprowadza i skąd dokąd.

Pakowanie polega na tym, że dwa wyprowadzenia jednego kształtu dostają jedną pozycję. Las rośnie wtedy z długością zdania, a nie z liczbą czytań.

Etykieta i rozpiętość to dokładnie tyle, ile odróżnia jedno czytanie od drugiego, a Node.signature wywodzi dlaczego; pozycja o jeden składnik bogatsza liczyłaby wyprowadzenia zamiast czytań. Ile kosztuje rozdzielanie pozycji po cechach, mierzy docs/parsowanie.md#co-się-pakuje-rozstrzyga-tożsamość-czytania.

Liść etykiety nie ma, bo czytaniem liścia jest sama rozpiętość: dwa czytania jednej formy są jednym liściem.

Source code in olski/parse/czytanie.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@dataclass(frozen=True)
class Pozycja:
    """Miejsce w tablicy: co się wyprowadza i skąd dokąd.

    Pakowanie polega na tym, że dwa wyprowadzenia jednego kształtu dostają jedną pozycję.
    Las rośnie wtedy z długością zdania, a nie z liczbą czytań.

    Etykieta i rozpiętość to dokładnie tyle, ile odróżnia jedno czytanie od drugiego,
    a :meth:`Node.signature` wywodzi dlaczego;
    pozycja o jeden składnik bogatsza liczyłaby wyprowadzenia zamiast czytań.
    Ile kosztuje rozdzielanie pozycji po cechach, mierzy
    docs/parsowanie.md#co-się-pakuje-rozstrzyga-tożsamość-czytania.

    Liść etykiety nie ma, bo czytaniem liścia jest sama rozpiętość:
    dwa czytania jednej formy są jednym liściem.
    """

    label: str | None
    span: tuple[int, int]

    @property
    def liść(self) -> bool:
        return self.label is None

label instance-attribute

span instance-attribute

liść property

__init__(label, span)

Las

Las ze współdzielonymi węzłami i podsumowania, jakie z niego wychodzą.

Taki las odpowiada na pytanie olskiego pod dwoma warunkami. Jedną pozycję dostaje to, co jest jednym czytaniem, o czym rozstrzyga Pozycja; liczba z jednej pozycji łączy się z liczbą z sąsiedniej tak, jak łączy je unifikacja, o czym rozstrzyga klasy. Wywód obu i pomiar, którym wybrano drugi, mieści docs/parsowanie.md#co-się-pakuje-rozstrzyga-tożsamość-czytania.

Source code in olski/parse/las.py
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
class Las:
    """Las ze współdzielonymi węzłami i podsumowania, jakie z niego wychodzą.

    Taki las odpowiada na pytanie olskiego pod dwoma warunkami.
    Jedną pozycję dostaje to, co jest jednym czytaniem,
    o czym rozstrzyga :class:`Pozycja`;
    liczba z jednej pozycji łączy się z liczbą z sąsiedniej tak, jak łączy je unifikacja,
    o czym rozstrzyga :meth:`klasy`.
    Wywód obu i pomiar, którym wybrano drugi, mieści
    docs/parsowanie.md#co-się-pakuje-rozstrzyga-tożsamość-czytania.
    """

    def __init__(self, tablica: _Tablica) -> None:
        self._tablica = tablica
        self.grammar = tablica.grammar
        self.korzeń = Pozycja(tablica.start, (tablica.początek, tablica.koniec))
        #: Rozpiętość → czytania form, jakie przez nią przechodzą.
        #: Kluczem jest rozpiętość, a nie segment, bo czytaniem liścia jest sama rozpiętość.
        self._czytania_liścia: dict[tuple[int, int], list[tuple[Segment, Reading]]] = {}
        for segment in tablica.segments:
            miejsce = self._czytania_liścia.setdefault((segment.start, segment.end), [])
            miejsce.extend((segment, reading) for reading in segment.readings)
        self._wyprowadzenia: dict[Pozycja, dict[tuple[Pozycja, ...], tuple[Production, ...]]] = {}
        #: Pozycja → koszt najtańszej morfologii pod nią (:meth:`koszt_morfologii`),
        #: wpisywany razem z wyprowadzeniami, bo liczy się go z tego samego przejścia.
        self._koszty: dict[Pozycja, int] = {}
        #: Pozycje, których koszt właśnie się liczy, czyli strażnik cyklu w
        #: gramatyce, gdzie symbol stoi pod sobą o tej samej rozpiętości.
        self._liczone: set[Pozycja] = set()
        self._klasy: dict[Pozycja, dict[Klasa, int]] = {}
        #: (pozycja, klasa) → kombinacja klas córek → produkcje, którymi przeszła.
        #: To jest las już po unifikacji:
        #: kombinacji, której ona nie przepuszcza, nie ma tu wcale,
        #: więc każda gałąź kończy się czytaniem.
        #: Produkcji jest tu kilka, bo dwie o jednym ciele są jednym kształtem,
        #: a różnić je może i terminal, i wypuszczane cechy (:meth:`wyprowadzenia`),
        #: i wyliczenie drzewa wybiera stąd tę, która wypuszcza żądane
        #: (:meth:`_drzewa`).
        #: Odczytania form czyta za to z każdej z nich (:meth:`_wsparte_kształtu`).
        self._krawędzie: dict[tuple[Pozycja, Klasa], dict[tuple, tuple[Production, ...]]] = {}
        self._czynne: set[Pozycja] = set()
        self._żywe_pary: set[tuple[Pozycja, Klasa]] | None = None
        self._rodzice: dict[tuple[Pozycja, Klasa], set[tuple[Pozycja, Klasa]]] | None = None
        self._prefiksy: dict[tuple, frozenset[Env]] = {}
        #: (produkcja, kombinacja, cechy dozwolone, miejsce, środowisko) → czy ciało
        #: domyka się od tego miejsca. Pyta o to raz na odczytanie liścia
        #: (:meth:`_wsparte`), a pary powtarzają się między odczytaniami.
        self._domknięcia: dict[tuple, bool] = {}
        #: (produkcje, kombinacja, cechy dozwolone) → czym każda córka w tym
        #: kształcie być może (:meth:`_wsparte_kształtu`). Ciało pojedyncze drugiego
        #: takiego słownika nie ma: pyta o nie sama ta suma, więc jego klucz nie
        #: powtórzyłby się nigdy.
        self._wsparcia_kształtów: dict[tuple, tuple[frozenset, ...]] = {}
        #: (para, etykieta, symbole mijane) → rozpiętości,
        #: jakie pierwszy węzeł tej etykiety pod nią bierze.
        self._pierwsze_role: dict[
            tuple[tuple[Pozycja, Klasa], str, tuple[str, ...]],
            frozenset[tuple[int, int] | None],
        ] = {}
        #: (para, etykiety, żądane rozdanie) → rozdania, jakie ta para umie złożyć.
        #: Żądane jest w kluczu, bo to ono odsiewa:
        #: rozdania spoza niego nie ma tu wcale (:meth:`_rozdania`).
        self._rozdania_pary: dict[
            tuple[tuple[Pozycja, Klasa], tuple[str, ...], Rozdanie], frozenset[Rozdanie]
        ] = {}
        #: (produkcja, kombinacja, żądane cechy) → czym jest w tym ciele każda córka.
        #: Kluczem jest całe ciało, a nie jedna córka,
        #: bo o czytaniu jednego liścia rozstrzyga unifikacja z pozostałymi.
        self._wybory_ciał: dict[tuple, tuple[Wybór, ...] | None] = {}
        self._przedstawiciele: dict[Pozycja, Node] = {}
        self._najdalszy: int | None = None
        #: Pozycja → ciała, jakimi stoi w czytaniach (:meth:`_ciała_pozycji`).
        self._ciała_pozycji_lasu: dict[Pozycja, set[tuple[Pozycja, ...]]] | None = None
        #: Symbole zdań podrzędnych → pozycje, do których streszczenie zagląda.
        self._widoczne_pozycje: dict[tuple[str, ...], set[Pozycja]] = {}
        #: Symbole zdań składowych → pozycje, które streszczenie streszcza osobno.
        self._składowe_pozycje: dict[tuple[str, ...], set[Pozycja]] = {}
        #: (pozycja, deklaracja) → co pod nią widzą dwa pozostałe podsumowania (:meth:`_pod`).
        self._pod_pozycją: dict[tuple[Pozycja, Deklaracja], tuple[bool, frozenset[int]]] = {}
        #: Deklaracja → wybory przyłączenia, którym werdykt daje wiersz.
        self._przyłączenia_lasu: dict[
            Deklaracja, dict[int, tuple[Pozycja, tuple[str, ...]]]
        ] = {}

    # -- tablica -------------------------------------------------------------#

    def wyprowadzenia(self, pozycja: Pozycja) -> dict[tuple[Pozycja, ...], tuple[Production, ...]]:
        """Wyprowadzenia pod tą pozycją: ciało → produkcje, które je złożyły.

        Kluczem jest ciało, a nie produkcja,
        bo o kształcie rozstrzygają etykiety i rozpiętości córek.
        Dwie produkcje o jednym ciele dają jedno czytanie i wchodzą tu razem,
        choćby brały co innego: nad jedną formą ``człon_imienny`` z rzeczownika i
        ``człon_imienny`` z zaimka są jednym ciałem, bo liść jest swoją rozpiętością.
        Odczytania obu niesie potem liść (:meth:`Las._wsparte_kształtu`).

        Pytana o pozycję, której tablica nie domknęła, oddaje pusty słownik,
        więc jest to zarazem sposób zapytania lasu, czy taki konstytuent w ogóle powstał.
        Odpowiedź kosztuje przy tym całe poddrzewo, bo wycena schodzi po córkach
        (:meth:`koszt_morfologii`), a nie samo domknięcie tej jednej pozycji.

        To jedno miejsce ustala kolejność, w jakiej las wydaje drzewa —
        dziedziczą ją klasy pozycji, krawędzie pod nimi i drzewa z tych krawędzi —
        a rozstrzyga o niej koszt ciała, a pod nim :func:`_cięcie`.
        Nieposortowane szłyby tak, jak ``for_head`` oddaje produkcje,
        czyli w kolejności dopisywania ich do gramatyki (docs/disambiguation.md).
        """
        gotowe = self._wyprowadzenia.get(pozycja)
        if gotowe is not None:
            return gotowe
        znalezione: dict[tuple[Pozycja, ...], list[Production]] = {}
        if not pozycja.liść:
            źródło, k = pozycja.span
            for production in self.grammar.for_head(pozycja.label):
                if not self._tablica.zamknięte(production, źródło, k):
                    continue
                for ciało in self._tablica.ciała(production, len(production.body), źródło, k):
                    znalezione.setdefault(ciało, []).append(production)
        self._liczone.add(pozycja)
        try:
            wyceny = {
                ciało: [
                    (production.koszt, self._morfologia_ciała(production, ciało))
                    for production in produkcje
                ]
                for ciało, produkcje in znalezione.items()
            }
        finally:
            self._liczone.discard(pozycja)
        # Morfologia dodaje się do kosztu tej produkcji, która ją wzięła,
        # bo to jej terminale mówią, którym czytaniem forma tu weszła;
        # ciało kosztuje potem tyle, co najtańsza z produkcji, które je składają.
        koszty = {ciało: min(k + m for k, m in pary) for ciało, pary in wyceny.items()}
        zebrane = {
            ciało: tuple(sorted(znalezione[ciało], key=lambda p: p.koszt))
            for ciało in sorted(znalezione, key=lambda c: (koszty[c], _cięcie(c)))
        }
        self._wyprowadzenia[pozycja] = zebrane
        self._koszty[pozycja] = min((m for pary in wyceny.values() for _k, m in pary), default=0)
        return zebrane

    def _morfologia_ciała(self, production: Production, ciało: tuple[Pozycja, ...]) -> int:
        """Koszt morfologii pod tym ciałem: po córce, a pod liściem po terminalu.

        Koszt morfologii sumuje się po poddrzewie, a koszt produkcji zostaje przy
        swoim ciele, i jest to ten sam warunek czytany dwa razy:
        koszt rozstrzyga między ciałami jednej pozycji,
        więc zostaje tam, gdzie konkurencja jest, a idzie wyżej, gdy jej nie ma.
        Ciała córki rozstrzygnęła sama córka,
        a czytania formy nie rozstrzyga nikt, bo liść ciał nie ma (:class:`Pozycja`).
        Bez tego pchania w górę `Wszystko jest podmiotem.` wydaje przodem czytanie
        z `Wszystko` w okoliczniku, choć opiera się ono na przysłówku,
        który słownik nazywa regionalnym (``olski/rejestr.py``):
        te dwa czytania różnią się dopiero pod `zdanie_składowe`,
        a przysłówek jest trzy pozycje niżej.
        """
        return sum(
            self._tablica.koszt_morfologii(część, dziecko.span)
            if dziecko.liść
            else self.koszt_morfologii(dziecko)
            for część, dziecko in zip(production.body, ciało, strict=True)
        )

    def koszt_morfologii(self, pozycja: Pozycja) -> int:
        """Najtańsza morfologia, na jakiej ten konstytuent się opiera.

        Liść kosztuje zero, bo jego czytania widzi dopiero terminal, który je bierze
        (:meth:`_Tablica.koszt_morfologii`):
        jedna pozycja liścia obsługuje wszystkie terminale, jakie w niej stoją.

        Pozycja stojąca pod sobą kosztuje tutaj zero i cyklu nie zgłasza,
        bo zgłasza go :meth:`klasy`, licząc czytania.
        Tam cykl jest błędem, a tutaj byłby nim porządek postawiony przed
        odpowiedzią na pytanie, czy zdanie ma w ogóle czytania.
        """
        if pozycja.liść or pozycja in self._liczone:
            return 0
        self.wyprowadzenia(pozycja)
        return self._koszty[pozycja]

    # -- unifikacja po lesie ------------------------------------------------ #

    def klasy(self, pozycja: Pozycja) -> dict[Klasa, int]:
        """Ile kształtów stoi pod tą pozycją, w klasach po tym, co wypuszczają.

        Iloczyn liczy się tutaj po parach, które unifikacja przepuszcza,
        a nie po samych pozycjach:
        kombinacja klas córek, której żadna produkcja nie składa,
        nie wnosi ani jednego czytania.

        Klasą jest zbiór cech, a nie jedne cechy,
        bo jeden kształt przechodzi czasem na kilka sposobów:
        ``dla przyjemności`` jest jedną grupą przyimkową w dwóch liczbach.
        Rodzic widzi z córki tylko to, co ona wypuszcza,
        więc grupowanie po tym zbiorze pozwala liczyć kształty zamiast sposobów:
        dwa kształty o jednym zbiorze wpadają do jednej klasy i sumują się,
        a jeden kształt wpada do dokładnie jednej.
        """
        gotowe = self._klasy.get(pozycja)
        if gotowe is not None:
            return gotowe
        if pozycja in self._czynne:
            raise Cykl(
                f"{pozycja.label} na {pozycja.span} stoi samo pod sobą; "
                "czytań jest wtedy nieskończenie wiele"
            )
        self._czynne.add(pozycja)
        klasy: dict[Klasa, int] = {}
        try:
            for ciało, produkcje in self.wyprowadzenia(pozycja).items():
                listy = [
                    [(None, 1)] if dziecko.liść else list(self.klasy(dziecko).items())
                    for dziecko in ciało
                ]
                for kombinacja in product(*listy):
                    wybór = tuple(klasa for klasa, _ile in kombinacja)
                    wypuszczane: set[Cechy] = set()
                    przeszłe = []
                    for production in produkcje:
                        cechy = self._przejdź(production, ciało, wybór)
                        if cechy:
                            wypuszczane |= cechy
                            przeszłe.append(production)
                    if not przeszłe:
                        continue
                    klasa = frozenset(wypuszczane)
                    ile = math.prod(liczba for _klasa, liczba in kombinacja)
                    klasy[klasa] = klasy.get(klasa, 0) + ile
                    self._krawędzie.setdefault((pozycja, klasa), {}).setdefault(
                        tuple(zip(ciało, wybór, strict=True)), tuple(przeszłe)
                    )
        finally:
            self._czynne.discard(pozycja)
        self._klasy[pozycja] = klasy
        return klasy

    def _sposoby(
        self, część: Part, dziecko: Pozycja, cechy: Sequence[Cechy], env: Env
    ) -> Iterator[tuple[int, Wybór, Env]]:
        """Na jakie środowiska ta córka zawęża to jedno, i czym za każdym razem jest.

        Córka wchodzi tu samymi cechami, jakie wypuszcza, bo tyle o niej rodzic wie;
        liść wchodzi czytaniami, bo terminal sprawdza i część mowy, i lemat.
        Wychodzi stąd obok środowiska to, czym córka w tym sposobie była,
        bo wyliczone drzewo pokazuje jeden z tych sposobów,
        a nie czytanie spoza nich (:attr:`Leaf.reading`).
        Numer jest pozycją sposobu w tym, co tu weszło,
        i po nim wybiera :meth:`_wybierz`.

        Unifikacja dotyka lasu tylko w tym jednym miejscu,
        i dlatego to jedna metoda, a nie dwie:
        wołają ją liczenie kształtów, szukanie punktu, na którym odrzucenie stanęło,
        i wyliczanie drzew.
        """
        if isinstance(część, Word):
            for numer, (segment, reading) in enumerate(
                self._czytania_liścia.get(dziecko.span, ())
            ):
                złożone = bierze(
                    część,
                    reading.tag.pos,
                    reading.lemma,
                    segment.lematy,
                    reading.tag.cechy,
                    env,
                )
                if złożone is not None:
                    yield numer, Leaf(segment, (reading,)), złożone
            return
        for numer, wypuszczone in enumerate(cechy):
            złożone = unify(część.constraints, dict(wypuszczone), env)
            if złożone is not None:
                yield numer, wypuszczone, złożone

    def _dołóż(
        self,
        część: Part,
        dziecko: Pozycja,
        cechy: Iterable[Cechy],
        środowiska: Iterable[Env],
    ) -> set[Env]:
        """Środowiska po dołożeniu tej córki do tych, z jakimi ciało doszło przed nią.

        Sposób, którym córka przeszła, tu nie dochodzi,
        bo liczenie kształtów pyta o liczbę, a nie o to, którędy.
        """
        cechy = list(cechy)
        return {
            złożone
            for env in środowiska
            for _numer, _wybór, złożone in self._sposoby(część, dziecko, cechy, env)
        }

    def _przejdź(
        self, production: Production, ciało: tuple[Pozycja, ...], wybór: tuple[Klasa | None, ...]
    ) -> set[Cechy]:
        """Cechy, z jakimi ta produkcja wychodzi nad tymi córkami; pusty zbiór, gdy z żadnymi.

        Środowisko przechodzi ciało od lewej, tak jak przechodzi je wyprowadzenie:
        zmienna wiązana przy pierwszej córce zawęża to, co wolno drugiej.
        """
        środowiska = {EMPTY}
        for część, dziecko, klasa in zip(production.body, ciało, wybór, strict=True):
            środowiska = self._dołóż(część, dziecko, klasa or (), środowiska)
            if not środowiska:
                return set()
        return {frozenset(features_of(production, env).items()) for env in środowiska}

    # -- podsumowania ------------------------------------------------------- #

    def ile_czytań(self) -> int:
        """Ile czytań ma zdanie: suma po klasach korzenia, bez wyliczania drzew."""
        return sum(self.klasy(self.korzeń).values())

    def najdalszy(self) -> int:
        """Dokąd doszła jakakolwiek analiza częściowa, czyli na czym odrzucenie stanęło.

        Liczy się przejście terminalem, bo bloker ma nazwać formę z tego miejsca zdania,
        a czym jest tu analiza częściowa, mówi :meth:`_przed_formą`.
        """
        if self._najdalszy is not None:
            return self._najdalszy
        if self.klasy(self.korzeń):
            # Czytanie sięga przez całe zdanie, więc dalej niż jego koniec nie ma gdzie.
            self._najdalszy = self._tablica.koniec
            return self._najdalszy
        najdalszy = self._tablica.początek
        for k, (production, kropka, źródło) in self._przed_formą():
            terminal = production.body[kropka]
            środowiska = self._prefiks(production, kropka, źródło, k)
            for segment in self._tablica.krawędzie.get(k, ()):
                if segment.end > najdalszy and self._przechodzi(
                    terminal, (k, segment.end), środowiska
                ):
                    najdalszy = segment.end
        self._najdalszy = najdalszy
        return najdalszy

    def _przechodzi(
        self, terminal: Word, rozpiętość: tuple[int, int], środowiska: Iterable[Env]
    ) -> bool:
        """Czy ten terminal przechodzi tę rozpiętość przy którymkolwiek z tych środowisk.

        Pyta o to samo, o co pyta dołożenie córki do ciała,
        więc pyta tym samym: liściem jest tu rozpiętość bez etykiety,
        czyli dokładnie to, czym stoi w ciele.
        """
        return bool(self._dołóż(terminal, Pozycja(None, rozpiętość), (), środowiska))

    def _przed_formą(self) -> Iterator[tuple[int, _Stan]]:
        """Analizy częściowe zatrzymane przed terminalem, pozycja po pozycji od lewej.

        Analizą częściową jest stan pod dwoma warunkami.
        Pierwszy: jego przebyte ciało unifikuje się z czymkolwiek.
        Stan bez ani jednego takiego środowiska w tablicy jest,
        bo ta pyta o cechy dopiero po lesie, a analizą nie jest.
        Drugi: przewidziała go inna analiza częściowa.
        Bez niego wystarczyłoby, że symbolu oczekuje w tym miejscu jakikolwiek stan,
        choćby sam nie był analizą,
        i odrzucenie stawałoby wtedy na formie, do której nie doszedł nikt.

        Przewidywanie ożywia stany tej samej pozycji, w której je czyta,
        więc pozycja przechodzona stan po stanie musiałaby się powtarzać
        do punktu stałego, a każdy stan przechodziłby oba warunki raz na przebieg.
        Kolejka to zdejmuje.
        O żywości rozstrzyga para produkcji i źródła, a nie kropka w ciele,
        więc stany pozycji zebrane są pod taką parą,
        a para wchodzi do kolejki wtedy, kiedy ożywa.
        Symbol ożywia w pozycji wszystkie swoje produkcje naraz,
        a produkcja ma jedną głowę, więc rozwinięcie pilnowane po symbolu
        wpisuje każdą z nich dokładnie raz i sprawdzania duplikatu tu nie ma.
        Pilnowane po produkcji sprawdzałoby go przy każdym kolejnym żądaniu symbolu
        tyle razy, ile ma on produkcji, a ``orzeczenie`` ma ich siedemset;
        tak samo i z tego samego powodu pilnuje tablica (:meth:`_Tablica._przewiduj`).

        Stanu o kropce na zerze tablica nie trzyma (:meth:`_Tablica._rozwiń`),
        a analizą częściową on bywa, bo czeka na pierwszą formę swojego ciała.
        Wychodzi tu więc z pary, a nie z tablicy (:meth:`_zaczyna_się_tu`),
        i pierwszy warunek spełnia zawsze, bo przebyte ciało ma puste.
        """
        żywe = {
            (production, self._tablica.początek)
            for production in self.grammar.for_head(self._tablica.start)
        }
        rozwinięte = {(self._tablica.start, self._tablica.początek)}
        for k in self._tablica.pozycje_grafu:
            kropki: dict[tuple[Production, int], list[int]] = {}
            for production, kropka, źródło in self._tablica.stany[k]:
                if kropka < len(production.body):
                    kropki.setdefault((production, źródło), []).append(kropka)
            kolejka = [para for para in kropki if para in żywe]
            if k == self._tablica.początek:
                # Produkcje symbolu startowego przewiduje początek zdania,
                # a nie stan, więc do kolejki nie wchodzą przez ożywienie.
                kolejka.extend(para for para in żywe if para not in kropki)
            i = 0
            while i < len(kolejka):
                production, źródło = kolejka[i]
                i += 1
                miejsca = kropki.get((production, źródło), ())
                if self._zaczyna_się_tu(production, źródło, k):
                    miejsca = (0, *miejsca)
                for kropka in miejsca:
                    if not self._prefiks(production, kropka, źródło, k):
                        continue
                    część = production.body[kropka]
                    if not isinstance(część, Sym):
                        yield k, (production, kropka, źródło)
                        continue
                    if (część.name, k) in rozwinięte:
                        continue
                    rozwinięte.add((część.name, k))
                    for przewidziana in self.grammar.for_head(część.name):
                        zaczęta = (przewidziana, k)
                        żywe.add(zaczęta)
                        kolejka.append(zaczęta)

    def _zaczyna_się_tu(self, production: Production, źródło: int, k: int) -> bool:
        """Czy ta produkcja czeka w tej pozycji na pierwszą córkę swojego ciała.

        Stanu o kropce na zerze tablica nie trzyma (:meth:`_przed_formą`),
        więc odpowiedź składa się z dwóch pytań o samą produkcję:
        czy zaczyna się w tej pozycji i czy pierwsza część jej ciała
        ma tu od czego się zacząć, czyli czy przechodzi warunek,
        którym tablica odsiewa swoje stany (:meth:`_Tablica.możliwe`).
        """
        return (
            źródło == k
            and bool(production.body)
            and production.body[0] in self._tablica.możliwe(k)
        )

    def _prefiks(
        self, production: Production, kropka: int, źródło: int, k: int
    ) -> frozenset[Env]:
        """Środowiska, z jakimi ta produkcja doszła tu przebytym ciałem.

        To samo pytanie co w :meth:`_przejdź`, zadane o inne miejsce w ciele:
        tam o cechy wychodzące nad ciałem domkniętym,
        a tutaj o środowisko w jego środku,
        bo terminal następujący po córce dostaje jej zawężenie.
        Córka wchodzi tu wszystkimi swoimi klasami naraz,
        bo pytanie nie dotyczy jednego kształtu.
        """
        if kropka == 0:
            return frozenset({EMPTY}) if źródło == k else frozenset()
        klucz = (production, kropka, źródło, k)
        gotowe = self._prefiksy.get(klucz)
        if gotowe is not None:
            return gotowe
        część = production.body[kropka - 1]
        środowiska: set[Env] = set()
        for j, dziecko in self._tablica.stany[k].get((production, kropka, źródło), ()):
            cechy = [] if dziecko.liść else [c for klasa in self.klasy(dziecko) for c in klasa]
            środowiska |= self._dołóż(
                część, dziecko, cechy, self._prefiks(production, kropka - 1, źródło, j)
            )
        self._prefiksy[klucz] = frozenset(środowiska)
        return self._prefiksy[klucz]

    def czytania(self) -> Iterator[Node]:
        """Czytania jako drzewa, po jednym na kształt.

        Kolejność, w jakiej wychodzą, ustala :meth:`_Tablica.ciała`.

        Każda gałąź kończy się czytaniem,
        bo ``klasy`` odsiały już kombinacje, których unifikacja nie przepuszcza.
        Dlatego urwanie po :data:`MAX_READINGS` kosztuje tyle, ile wypisane drzewa,
        i nic ponad to.
        """
        return self._kształty(self.korzeń)

    def _kształty(self, pozycja: Pozycja) -> Iterator[Node]:
        """Drzewa tego konstytuentu, po jednym na kształt.

        Klasa, której żaden rodzic nie przyjmuje, nie wchodzi (:meth:`_żywe`),
        więc drzew wychodzi tyle, ile mówi :meth:`_ile_kształtów`:
        kształty pod taką klasą stoją w tablicy,
        a w żadnym czytaniu zdania nie stoją.
        Korzeń przechodzi przez ten odsiew bez straty, bo jego klasy są żywe wszystkie,
        i dlatego czytania zdania idą tą samą drogą.
        """
        żywe = self._żywe()
        for klasa in self.klasy(pozycja):
            if (pozycja, klasa) in żywe:
                yield from self._drzewa(pozycja, klasa, _jedne(klasa), klasa)

    def _drzewa(
        self, pozycja: Pozycja, klasa: Klasa, wymagane: Cechy, dozwolone: Klasa
    ) -> Iterator[Node]:
        """Drzewa tej pozycji, wypuszczające te cechy: po jednym na kształt pod tą klasą.

        Cechy przychodzą z góry, bo tylko rodzic wie, których żąda:
        klasa zbiera wszystkie, na jakie ten kształt przechodzi,
        a ``szynki`` w pozycji dopełniacza przechodzi tam jednym odczytaniem z dwóch.
        Bez tego żądania drzewo pokazywałoby na liściu odczytanie dowolne,
        więc i takie, którego pozycja nad nim nie licencjonuje.

        Drzew jest tyle, ile kształtów, niezależnie od żądanych cech:
        każda kombinacja z tej klasy wypuszcza każde cechy tej klasy,
        bo klasą jest dokładnie zbiór cech tej kombinacji.
        Dwie produkcje o jednym ciele są jednym kształtem, więc wychodzi z nich jedno drzewo,
        i bierzemy tę, która żądane cechy wypuszcza.

        ``dozwolone`` są cechy, jakie ten kształt wolno tu wypuścić, czyli zwykle
        cała klasa, i idą osobno od żądanych, bo osobno od kształtu liczą się
        odczytania form pod nim (:meth:`_wsparte_kształtu`).
        """
        for kombinacja, produkcje in self._krawędzie[(pozycja, klasa)].items():
            wsparte = self._wsparte_kształtu(produkcje, kombinacja, dozwolone)
            for production in produkcje:
                wybory = self._wybory_ciała(production, kombinacja, wymagane)
                if wybory is None:
                    continue
                yield from self._z_córek(
                    pozycja, production, kombinacja, _z_odczytaniami(wybory, wsparte), wsparte, ()
                )
                break
            else:
                raise AssertionError(
                    f"{pozycja} nie wypuszcza {_klucz_cech(wymagane)} "
                    "ciałem, które stoi w jej klasie"
                )

    def _z_córek(
        self,
        pozycja: Pozycja,
        production: Production,
        kombinacja: tuple,
        wybory: tuple[Wybór, ...],
        wsparte: tuple[frozenset, ...],
        zebrane: tuple,
    ) -> Iterator[Node]:
        """Drzewa, jakie z tych córek wychodzą, budowane od lewej i po jednym.

        Iloczyn kartezjański z biblioteki materializuje swoje wejścia,
        więc granica z :data:`MAX_READINGS` przestałaby cokolwiek ograniczać:
        zdanie o dziesiątkach tysięcy czytań wypisałoby je wszystkie,
        żeby oddać sześćdziesiąt cztery.
        Tutaj każde drzewo kosztuje osobno.
        """
        if len(zebrane) == len(kombinacja):
            yield Node(
                label=pozycja.label or "",
                children=zebrane,
                span=pozycja.span,
                głowa=production.głowa,
                koszty=production.koszty,
            )
            return
        miejsce = len(zebrane)
        dziecko, córka = kombinacja[miejsce]
        wybór = wybory[miejsce]
        córki = (
            [wybór]
            if dziecko.liść
            else self._drzewa(dziecko, córka, wybór, wsparte[miejsce])
        )
        for drzewo in córki:
            yield from self._z_córek(
                pozycja, production, kombinacja, wybory, wsparte, (*zebrane, drzewo)
            )

    def _wybory_ciała(
        self, production: Production, kombinacja: tuple, wymagane: Cechy
    ) -> tuple[Wybór, ...] | None:
        """Czym jest każda córka w ciele, które wypuszcza te cechy; ``None``, gdy w żadnym.

        Wybór jest jeden na całe ciało, a nie jeden na córkę,
        bo córki wiąże unifikacja:
        odczytanie przymiotnika wybrane przy pierwszej z nich
        zawęża odczytania rzeczownika, który się z nim zgadza,
        i zawęża cechy, jakie ciało wypuszcza w górę.

        Wybór mówi o kształcie i tylko o nim.
        Odczytań forma stąd nie dostaje: dokłada je :func:`_z_odczytaniami`
        w :meth:`_drzewa`, bo nie liczy ich ani ten wybór, ani samo to ciało
        (:meth:`_wsparte_kształtu`).
        """
        klucz = (production, kombinacja, wymagane)
        if klucz not in self._wybory_ciał:
            self._wybory_ciał[klucz] = self._wybierz(
                production, kombinacja, wymagane, 0, frozenset({EMPTY})
            )
        return self._wybory_ciał[klucz]

    def _wsparte_kształtu(
        self, produkcje: tuple[Production, ...], kombinacja: tuple, dozwolone: Klasa
    ) -> tuple[frozenset, ...]:
        """Czym każda córka być może w tym kształcie: suma po ciałach, które go budują.

        Odczytaniem jest kształt (:meth:`Node.signature`), a jeden kształt buduje
        w tej gramatyce kilka ciał, więc forma stoi tu każdym odczytaniem, które
        licencjonuje ją w którymkolwiek z nich.
        Ciało wybrane, pytane samo, odpowiada tylko za swoje odczytania:
        ``człon_imienny`` robi grupę imienną z jednej formy trzema ciałami —
        rzeczownikowym, odsłownikowym i zaimkowym — więc bez sumy pod
        dopełnieniem `Znam to polecenie.` wychodzi sam odsłownik `polecieć`,
        a rzeczownik `polecenie` nie wychodzi wcale.

        Zawężenia to nie luzuje: ``dozwolone`` jest tu tym samym, czym w
        :meth:`_wsparte`, więc ciało, które przy tych cechach się nie domyka, nie
        dokłada ani jednego odczytania.
        Że suma nie sięga dalej niż kształt, sprawdza ``tests/test_las.py``,
        zawężając zdanie do odczytań, które liście niosą.

        Zapamiętana, bo pyta o nią każde drzewo tej pozycji, czyli nad zdaniem
        wieloznacznym tyle razy, ile ono ma odczytań.
        """
        klucz = (produkcje, kombinacja, dozwolone)
        gotowe = self._wsparcia_kształtów.get(klucz)
        if gotowe is None:
            wsparcia = [
                self._wsparte(production, kombinacja, dozwolone) for production in produkcje
            ]
            gotowe = tuple(
                frozenset().union(*(wsparte[miejsce] for wsparte in wsparcia))
                for miejsce in range(len(kombinacja))
            )
            self._wsparcia_kształtów[klucz] = gotowe
        return gotowe

    def _wsparte(
        self, production: Production, kombinacja: tuple, dozwolone: Klasa
    ) -> tuple[frozenset, ...]:
        """Czym każda córka w tym ciele być może: wpis na córkę, w porządku ciała.

        Liść dostaje zbiór odczytań formy, a konstytuent zbiór cech, jakie
        wypuszcza, czyli to samo, czym jedno i drugie wchodzi do wyboru
        (:data:`Wybór`).

        Wybór córki liczy się wtedy, gdy przechodzi przy którymś środowisku, do
        jakiego ciało dochodzi z lewej, i gdy po nim ciało domyka się jeszcze
        cechami dozwolonymi (:meth:`_domyka`).
        Sprawdzane są wszystkie środowiska, do jakich ciało dochodzi, a nie te z
        jednego przebytego ciała, bo odczytanie odsiane wyborem sąsiada
        wyglądałoby jak odczytanie, którego gramatyka nie bierze.
        Dozwolona jest przy tym cała klasa, a nie cechy żądane od drzewa:
        kształt wypuszcza każde cechy swojej klasy, więc jedne z nich wybrane
        (:func:`_jedne`) odsiałyby odczytania, którymi forma w tym kształcie stoi.
        """
        córki = [
            (production.body[miejsce], dziecko, sorted(klasa, key=_klucz_cech) if klasa else ())
            for miejsce, (dziecko, klasa) in enumerate(kombinacja)
        ]
        przed = [frozenset({EMPTY})]
        for miejsce, (część, dziecko, cechy) in enumerate(córki):
            przed.append(frozenset(self._dołóż(część, dziecko, cechy, przed[miejsce])))
        wynik = []
        for miejsce, (część, dziecko, cechy) in enumerate(córki):
            zebrane = set()
            for env in przed[miejsce]:
                for _numer, wybór, złożone in self._sposoby(część, dziecko, cechy, env):
                    wartość = wybór.reading if isinstance(wybór, Leaf) else wybór
                    if wartość not in zebrane and self._domyka(
                        production, kombinacja, dozwolone, miejsce + 1, złożone
                    ):
                        zebrane.add(wartość)
            wynik.append(frozenset(zebrane))
        return tuple(wynik)

    def _domyka(
        self,
        production: Production,
        kombinacja: tuple,
        dozwolone: Klasa,
        miejsce: int,
        env: Env,
    ) -> bool:
        """Czy ciało domyka się od tego miejsca cechami dozwolonymi, z tego środowiska.

        Pytanie jest o jedno środowisko, a nie o zbiór, i dlatego odpowiedź da
        się zapamiętać: miejsc w ciele jest kilka, a środowisk tyle, ile
        unifikacja przepuszcza, więc pytanie stawiane raz na odczytanie liścia
        powtarza się nad tymi samymi parami.
        """
        klucz = (production, kombinacja, dozwolone, miejsce, env)
        gotowe = self._domknięcia.get(klucz)
        if gotowe is not None:
            return gotowe
        if miejsce == len(kombinacja):
            odpowiedź = frozenset(features_of(production, env).items()) in dozwolone
        else:
            część = production.body[miejsce]
            dziecko, klasa = kombinacja[miejsce]
            cechy = sorted(klasa, key=_klucz_cech) if klasa else ()
            odpowiedź = any(
                self._domyka(production, kombinacja, dozwolone, miejsce + 1, złożone)
                for _numer, _wybór, złożone in self._sposoby(część, dziecko, cechy, env)
            )
        self._domknięcia[klucz] = odpowiedź
        return odpowiedź

    def _wybierz(
        self,
        production: Production,
        kombinacja: tuple,
        wymagane: Cechy,
        miejsce: int,
        środowiska: frozenset[Env],
    ) -> tuple[Wybór, ...] | None:
        """Sposoby od tego miejsca ciała w prawo; ``None``, gdy przy tych środowiskach żadnych.

        Córka wchodzi w tyle sposobów, w ile ją przepuszcza unifikacja:
        konstytuent w tyle, ile cech wypuszcza, a forma w tyle, ile ma tu czytań.
        Sposób, po którym ciała nie da się domknąć żądanymi cechami,
        oddaje ``None`` i nawrót bierze następny,
        bo o cechach wypuszczanych rozstrzyga całe przebyte ciało, a nie jedna córka.
        """
        if miejsce == len(kombinacja):
            domyka = any(
                frozenset(features_of(production, env).items()) == wymagane
                for env in środowiska
            )
            return () if domyka else None
        część = production.body[miejsce]
        dziecko, klasa = kombinacja[miejsce]
        cechy = sorted(klasa, key=_klucz_cech) if klasa else ()
        sposoby: dict[int, tuple[Wybór, set[Env]]] = {}
        for env in środowiska:
            for numer, wybór, złożone in self._sposoby(część, dziecko, cechy, env):
                sposoby.setdefault(numer, (wybór, set()))[1].add(złożone)
        for numer in sorted(sposoby):
            wybór, dalej = sposoby[numer]
            reszta = self._wybierz(
                production, kombinacja, wymagane, miejsce + 1, frozenset(dalej)
            )
            if reszta is not None:
                return (wybór, *reszta)
        return None

    # -- role, o które czytania się różnią ---------------------------------- #

    def różniące(self, deklaracja: Deklaracja) -> tuple[str, ...]:
        """Te z ról, które nie mają w każdym czytaniu tego samego wypełnienia.

        Pytamy las, a nie streszczenia czytań.
        Streszczeń jest najwyżej :data:`MAX_READINGS`,
        a zdanie ustawy ma czytań dziesiątki tysięcy,
        więc rola różniąca się dopiero za tą granicą nie zostałaby nazwana,
        choć liczba obok niej granicy nie ma.

        Jednym wystąpieniem roli jest to, które nazywa :func:`describe`,
        czyli pierwsze w zdaniu składowym i spoza zdań podrzędnych.
        Etykieta pada w czytaniu kilka razy, bo zdanie współrzędne ma własny podmiot,
        a dwa podmioty stojące obok siebie w jednym czytaniu
        nie mówią nic o różnicy między czytaniami.
        Pytamy więc o zdanie całe i o każde jego składowe osobno.
        Bez pytania o składowe werdykt milczy o roli, którą lista czytań rozdziela:
        czytania różne dopiero w składowym drugim mają w pierwszym to samo.
        Bez pytania o zdanie całe milczy o rozcięciu zdania na dwa,
        bo każde składowe niesie wtedy jedną rozpiętość, stojąc w jednym z czytań.
        Iloczynu po składowych stąd nie ma:
        pytanie zadane każdemu osobno kosztuje tyle, ile ich jest,
        a rozpiętości brane naraz mnożyłyby się jak czytania.

        Porównujemy rozpiętości, a nie formy:
        formy nad jedną rozpiętością są w każdym czytaniu te same,
        a różni je podział na segmenty, którego streszczenie i tak nie pokazuje.
        Rozpiętość ``None`` jest czytaniem bez tej roli,
        tak jak streszczenie bez tego klucza.
        """
        składowe = self._składowe_lasu(deklaracja.składowe)
        return tuple(
            etykieta
            for etykieta in deklaracja.role
            if self._niezgodna(self.korzeń, etykieta, deklaracja.podrzędne)
            or any(
                self._niezgodna(pozycja, etykieta, deklaracja.podrzędne) for pozycja in składowe
            )
        )

    def _niezgodna(self, pozycja: Pozycja, etykieta: str, podrzędne: tuple[str, ...]) -> bool:
        """Czy pierwszy węzeł tej etykiety jest pod tą pozycją w kilku miejscach.

        Klasa martwa nie wchodzi, i z tego samego powodu co w :meth:`_ile_kształtów`:
        niezgoda ma być niezgodą między czytaniami.
        Korzenia to nie dotyczyło, bo jego klasy są żywe wszystkie,
        a pozycja zdania składowego bywa i martwa.
        """
        żywe = self._żywe()
        wystąpienia = {
            rozpiętość
            for klasa in self.klasy(pozycja)
            if (pozycja, klasa) in żywe
            for rozpiętość in self._pierwsza_rola((pozycja, klasa), etykieta, podrzędne)
        }
        return len(wystąpienia) > 1

    def _pierwsza_rola(
        self, para: tuple[Pozycja, Klasa], etykieta: str, podrzędne: tuple[str, ...]
    ) -> frozenset[tuple[int, int] | None]:
        """Czym bywa pierwszy węzeł tej etykiety pod tą parą; ``None``, gdy go nie ma.

        Ciało przechodzi się od lewej i kończy na pierwszej córce,
        która tę rolę niesie w każdym swoim czytaniu:
        dalsze córki są wtedy za pierwszym wystąpieniem i nie nazywają go.
        Wyborów córek nic nie wiąże, więc suma po nich jest tym, co dają czytania,
        a wyników jest tyle, ile rozpiętości, a nie ile drzew.

        Córkę ze zdaniem podrzędnym mijamy tak jak liść,
        bo rola z jej wnętrza jest rolą tamtego zdania (:attr:`Deklaracja.podrzędne`),
        chyba że ta córka sama jest szukaną rolą:
        okolicznik wyrażony zdaniem jest rolą, w której nazywa się całe zdanie,
        a jego wnętrze zostaje mimo to nieotwarte, tak samo jak w :meth:`Node.find`.
        """
        pozycja, _klasa = para
        if pozycja.label == etykieta:
            return frozenset({pozycja.span})
        klucz = (para, etykieta, podrzędne)
        gotowe = self._pierwsze_role.get(klucz)
        if gotowe is not None:
            return gotowe
        znalezione: set[tuple[int, int] | None] = set()
        for kombinacja in self._krawędzie.get(para, {}):
            bez_roli = True
            for dziecko, klasa in kombinacja:
                if dziecko.liść or (dziecko.label in podrzędne and dziecko.label != etykieta):
                    continue
                pod_córką = self._pierwsza_rola((dziecko, klasa), etykieta, podrzędne)
                znalezione |= pod_córką - {None}
                if None not in pod_córką:
                    bez_roli = False
                    break
            if bez_roli:
                znalezione.add(None)
        self._pierwsze_role[klucz] = frozenset(znalezione)
        return self._pierwsze_role[klucz]

    # -- czytanie nazwane rolami z zewnątrz --------------------------------- #

    def numer_czytania(self, role: Mapping[str, frozenset[tuple[int, int]]]) -> int | None:
        """Którym z kolei czytaniem jest to, które przypisuje te role; ``None``, gdy żadnym.

        Pyta ten, kto ma cudze czytanie jednego z tych zdań
        i chce wiedzieć, czy ono w tym lesie ocalało, a jeśli tak, to jak głęboko.
        Numer jest tym, ile odpowiedź „ocalało” jest warta:
        czytanie drugie z dwóch i czytanie tysięczne z dwudziestu ośmiu tysięcy
        ocalały jednakowo, a przeczyta z nich ktoś jedno.

        Rolami, a nie kształtem, bo dwie gramatyki grupują materiał każda po swojemu,
        więc porównanie nawiasów mierzyłoby różnicę między formalizmami.
        Rolę obie orzekają o zdaniu, i tą samą miarą mierzy zgodność
        ``Outcome.agreement`` w ``harness/pomiar.py``, więc obie odpowiedzi mówią o jednym.

        Odpowiedź składa się z dwóch pytań zadanych po kolei i oba są tu potrzebne.
        Czy takie czytanie w lesie jest, mówi las bez wyliczania drzew,
        i po to ta połowa tu jest: lista urywa się na :data:`MAX_READINGS`,
        a zdania wieloznaczne są dokładnie tymi, nad którymi ta granica pada,
        więc czytanie ocalałe za nią wyszłoby z listy przepadłe.
        Którym z kolei jest, mówi dopiero wyliczanie,
        bo numer jest miejscem w kolejności, którą ustala :meth:`_Tablica.ciała`,
        a numer policzony obok byłby tą kolejnością wypisaną drugi raz.

        Wyliczanie rusza więc dopiero po odpowiedzi twierdzącej i na tym czytaniu przystaje,
        czyli kosztuje tyle, ile numer, a nie tyle, ile las ma czytań;
        granica z :data:`MAX_READINGS` nie jest mu przez to potrzebna.
        Ile to kosztuje nad bankiem drzew, mówi
        docs/corpus.md#złote-czytanie-ocalało-w-niemal-każdym-zdaniu-wieloznacznym.

        Zbiór pusty jest żądaniem, a nie jego brakiem:
        etykieta, której pytający nigdzie nie obsadza,
        żąda czytania, które nie obsadza jej również.
        """
        etykiety = tuple(sorted(role))
        żądane: Rozdanie = tuple(frozenset(role[etykieta]) for etykieta in etykiety)
        if not any(
            żądane in self._rozdania((self.korzeń, klasa), etykiety, żądane)
            for klasa in self.klasy(self.korzeń)
        ):
            return None
        for numer, drzewo in enumerate(self.czytania(), 1):
            if _rozdanie_drzewa(drzewo, etykiety) == żądane:
                return numer
        raise AssertionError(
            "las składa to rozdanie ról, a wyliczanie nie wydało drzewa o tym rozdaniu"
        )

    def _rozdania(
        self, para: tuple[Pozycja, Klasa], etykiety: tuple[str, ...], żądane: Rozdanie
    ) -> frozenset[Rozdanie]:
        """Rozdania, jakie czytania tej pary składają, z pominięciem tych ponad żądane.

        Rozdanie pary jest sumą rozdań córek i tego, co para wnosi sama,
        a wnosi rozpiętość wtedy, gdy sama nosi jedną z tych etykiet —
        czyli tyle, ile pod tą parą znajduje :meth:`Node.find`.

        Odsiewamy w trakcie, bo rozdań bywa tyle, ile czytań,
        a po odsianiu najwyżej tyle, ile żądane ma podzbiorów, czyli garść:
        rozdanie z rozpiętością spoza żądanego żądanym już nie zostanie,
        bo suma rozpiętości nie zabiera.
        Odsiew zależy od żądanego, więc żądane wchodzi do klucza spamiętywania.
        """
        klucz = (para, etykiety, żądane)
        gotowe = self._rozdania_pary.get(klucz)
        if gotowe is not None:
            return gotowe
        pozycja, _klasa = para
        własne: Rozdanie = tuple(
            frozenset({pozycja.span}) if pozycja.label == etykieta else frozenset()
            for etykieta in etykiety
        )
        zebrane: set[Rozdanie] = set()
        if not _ponad(własne, żądane):
            for kombinacja in self._krawędzie.get(para, {}):
                złożone = {własne}
                for dziecko, klasa in kombinacja:
                    if dziecko.liść:
                        continue
                    pod = self._rozdania((dziecko, klasa), etykiety, żądane)
                    złożone = _zsumuj(złożone, pod, żądane)
                    if not złożone:
                        break
                zebrane |= złożone
        self._rozdania_pary[klucz] = frozenset(zebrane)
        return self._rozdania_pary[klucz]

    # -- przyłączenia ------------------------------------------------------- #

    def przyłączenia(self, deklaracja: Deklaracja) -> list[Przyłączenie]:
        """Modyfikatory, którym czytania dają więcej niż jednego gospodarza.

        Jeden wpis na wybór, bo tyle wyborów zdanie zostawia.
        Modyfikator występuje w każdym czytaniu raz,
        więc dwóch gospodarzy jednej pozycji to dwa czytania różniące się tym przyłączeniem,
        i zdanie o sześciu wyrażeniach przyimkowych
        daje sześć wpisów wobec sześćdziesięciu czterech czytań.

        Wyborem jest przyimek, a nie pozycja,
        i dlatego pozycje o jednym początku wchodzą tu razem.
        ``w pliku`` i ``w pliku w katalogu`` to dwie pozycje z dwóch różnych czytań,
        a decyzja pod nimi jest jedna: gdzie przyłącza się wyrażenie otwarte przez ``w``.
        Licząc po pozycjach, dostalibyśmy wpis na każdą parę przyimków,
        czyli znów kwadrat zamiast długości zdania.
        """
        wybory = self._nazwane_przyłączenia(deklaracja)
        return [
            Przyłączenie(sklej_formy(self._przedstawiciel(pozycja).forms()), nazwy)
            for _początek, (pozycja, nazwy) in sorted(wybory.items())
        ]

    def _nazwane_przyłączenia(
        self, deklaracja: Deklaracja
    ) -> dict[int, tuple[Pozycja, tuple[str, ...]]]:
        """Początek modyfikatora → jego najkrótsza pozycja i głowy, o które czytania się spierają.

        Osobno od :meth:`przyłączenia`, bo pyta o to samo drugi raz :meth:`rozbieżności`:
        wybór nazwany tutaj jest wyborem, którego ona nie ma nazywać po raz drugi.
        """
        gotowe = self._przyłączenia_lasu.get(deklaracja)
        if gotowe is not None:
            return gotowe
        u_kogo: dict[int, set[Pozycja]] = {}
        najkrótsze: dict[int, Pozycja] = {}
        for pozycja in sorted({para[0] for para in self._żywe()}, key=lambda p: p.span):
            if pozycja.label != deklaracja.rozstrzygany:
                continue
            początek = pozycja.span[0]
            najkrótsze.setdefault(początek, pozycja)
            u_kogo.setdefault(początek, set()).update(
                self._gospodarze(pozycja, deklaracja.gospodarze)
            )
        znalezione: dict[int, tuple[Pozycja, tuple[str, ...]]] = {}
        for początek, pozycja in sorted(najkrótsze.items()):
            # Etykieta rozstrzyga remis: `W skład rady wchodzą radni w liczbie.`
            # daje gospodarzy `grupa_przymiotnikowa` i `grupa_imienna` o jednej rozpiętości,
            # a zbiór ich nie porządkuje.
            gospodarze_pozycji = sorted(u_kogo[początek], key=lambda p: (p.span, p.label))
            if len(gospodarze_pozycji) < 2:
                continue
            # Dwie pozycje o jednej głowie są jednym wyborem,
            # bo grupa imienna dłuższa o inny modyfikator jest tą samą grupą imienną.
            nazwy = list(
                dict.fromkeys(
                    self._przedstawiciel(gospodarz).forma_głowy()
                    for gospodarz in gospodarze_pozycji
                )
            )
            if len(nazwy) < 2:
                continue
            znalezione[początek] = (pozycja, tuple(nazwy))
        self._przyłączenia_lasu[deklaracja] = znalezione
        return znalezione

    def _gospodarze(self, pozycja: Pozycja, gospodarze: Sequence[str]) -> set[Pozycja]:
        """Konstytuenty z ``gospodarze``, w których ten modyfikator stoi w którymś czytaniu.

        Szukamy w górę, bo pytanie dotyczy tego, co modyfikator określa,
        a nie tego, pod czym się znalazł:
        okolicznik zdania sąsiaduje w drzewie z dopełnieniem, którego nie określa.
        Modyfikator bez żadnego z tych konstytuentów nad sobą określa całe czytanie
        i wychodzi stąd korzeniem, tak samo jak w :func:`_host`.
        """
        znalezione: set[Pozycja] = set()
        obejrzane: set[tuple[Pozycja, Klasa]] = set()
        stos = [para for para in self._żywe() if para[0] == pozycja]
        while stos:
            para = stos.pop()
            if para in obejrzane:
                continue
            obejrzane.add(para)
            rodzice = self._rodzicielskie().get(para, set())
            if not rodzice:
                znalezione.add(self.korzeń)
            for rodzic in rodzice:
                if rodzic[0].label in gospodarze:
                    znalezione.add(rodzic[0])
                else:
                    stos.append(rodzic)
        return znalezione

    # -- rozbieżności poza zasięgiem streszczenia ---------------------------- #

    def rozbieżności(self, deklaracja: Deklaracja) -> list[Rozbieżność]:
        """Konstytuenty, którym czytania dają kilka kształtów tam, gdzie streszczenie nie zagląda.

        Jeden wpis na wybór, tak jak w :meth:`przyłączenia`,
        i wyborem jest tu konstytuent o kilku ciałach:
        rozpiętość pozycja ma jedną, więc rozstrzygane jest w takim miejscu to,
        z czego ona się składa, a nie to, gdzie stoi.
        Ciała są po unifikacji, więc wpis dostaje konstytuent,
        który naprawdę czyta się kilkoma sposobami;
        po co werdyktowi ten wiersz, mówi :class:`Rozbieżność`.

        Wykluczenia są trzy, po jednym na wiersz, który werdykt drukuje bez tego
        podsumowania (:meth:`_nazwany_gdzie_indziej`), a po nich zostaje najwęższy
        z konstytuentów: wpis, którego napis obejmuje napis innego wpisu, mówi o tym
        samym słowie i o kilku obok niego, bo wieloznaczność wychodzi w górę.
        ``równych praw kobiet`` czyta się dwoma sposobami przez samo ``równych``,
        a ``równych praw kobiet i mężczyzn`` trzema, i naprawić trzeba jedno słowo.
        """
        kandydaci = [
            pozycja
            for pozycja, ciała in self._ciała_pozycji().items()
            if len(ciała) > 1 and not self._nazwany_gdzie_indziej(pozycja, ciała, deklaracja)
        ]
        wybrani: list[Pozycja] = []
        # Od najkrótszego, żeby każdy kandydat zastał już wybrane wszystko, co
        # obejmuje. Remis rozstrzyga etykieta: dwie pozycje o jednej rozpiętości
        # mówią o tych samych słowach, więc wpis dostaje jedna z nich.
        for pozycja in sorted(kandydaci, key=lambda p: (p.span[1] - p.span[0], p.span, p.label)):
            if not any(_wewnątrz(inny.span, pozycja.span) for inny in wybrani):
                wybrani.append(pozycja)
        return [
            Rozbieżność(
                sklej_formy(self._przedstawiciel(pozycja).forms()),
                self._ile_kształtów(pozycja),
                #  Kształtów wyliczamy tyle, ile czytań wylicza się nad zdaniem,
                #  bo granica jest tu z tego samego powodu: wieloznaczność
                #  konstytuentu mnoży się jak wieloznaczność zdania.
                tuple(streszczenia(islice(self._kształty(pozycja), MAX_READINGS), deklaracja)),
            )
            for pozycja in sorted(wybrani, key=lambda p: (p.span, p.label))
        ]

    def _ile_kształtów(self, pozycja: Pozycja) -> int:
        """Ile czytań ten konstytuent ma w czytaniach zdania.

        Klasa, której żaden rodzic nie przyjmuje, nie wchodzi:
        kształty pod nią stoją w tablicy, a w żadnym czytaniu zdania nie stoją
        (:meth:`_żywe`), i liczba obok konstytuenta ma mówić o czytaniach.
        Klasy żywej to nie dotyczy w środku,
        bo klasą jest zbiór cech wypuszczanych,
        więc rodzic przyjmuje każdy kształt z niej albo żaden.
        """
        żywe = self._żywe()
        return sum(ile for klasa, ile in self.klasy(pozycja).items() if (pozycja, klasa) in żywe)

    def _nazwany_gdzie_indziej(
        self, pozycja: Pozycja, ciała: set[tuple[Pozycja, ...]], deklaracja: Deklaracja
    ) -> bool:
        """Czy o wyborze pod tą pozycją mówi już któryś z pozostałych wierszy werdyktu.

        Ciąg współrzędny mówi go nawiasem w napisie roli,
        więc kryterium jest tu to samo, co w :func:`ciało_koordynuje`.
        Rolę nazywa :meth:`różniące`, a gospodarza modyfikatora :meth:`przyłączenia`,
        i oba widzą dokładnie to, co :meth:`_pod` znajduje w ciałach tej pozycji.
        Modyfikator o jednym gospodarzu wiersza tam nie ma,
        więc wybór nad nim zostaje temu podsumowaniu.
        """
        if pozycja.label in deklaracja.współrzędne and any(
            ciało_koordynuje(pozycja.label, (dziecko.label for dziecko in ciało))
            for ciało in ciała
        ):
            return True
        pod = [self._pod(dziecko, deklaracja) for ciało in ciała for dziecko in ciało]
        if pozycja in self._widoczne(deklaracja.podrzędne) and any(rola for rola, _ in pod):
            return True
        nazwane = set(self._nazwane_przyłączenia(deklaracja))
        return any(przyłączane & nazwane for _rola, przyłączane in pod)

    def _pod(self, pozycja: Pozycja, deklaracja: Deklaracja) -> tuple[bool, frozenset[int]]:
        """Co pod tą pozycją, ją samą licząc, widzą dwa pozostałe podsumowania.

        Pierwsza odpowiedź mówi, czy stoi tu rola, którą nazwie :meth:`różniące`,
        i zejście po nią kończy się na zdaniu podrzędnym, bo tam kończy je tamto
        podsumowanie (:attr:`Deklaracja.podrzędne`).
        Druga wylicza początki modyfikatorów, po których liczy wybory
        :meth:`przyłączenia`, i granicy zdania podrzędnego nie zna, bo tamto też jej nie zna.
        Jedno przejście na dwie odpowiedzi, bo obie pytają o to samo wnętrze,
        a różni je tylko miejsce, w którym się zatrzymują.

        Spamiętywanie jest tu bezpieczne bez straży na cykl:
        pozycja stojąca sama pod sobą przerywa :meth:`klasy` wyjątkiem :class:`Cykl`,
        więc pozycje żywe składają się w graf bez cyklu.
        """
        gotowe = self._pod_pozycją.get((pozycja, deklaracja))
        if gotowe is not None:
            return gotowe
        przyłączane = {pozycja.span[0]} if pozycja.label == deklaracja.rozstrzygany else set()
        rola = pozycja.label in deklaracja.role
        # Liść klas nie ma, więc pętla nad nim się nie wykonuje i liść nie potrzebuje warunku.
        for klasa in self.klasy(pozycja):
            for kombinacja in self._krawędzie.get((pozycja, klasa), {}):
                for dziecko, _klasa in kombinacja:
                    rola_pod, przyłączane_pod = self._pod(dziecko, deklaracja)
                    rola = rola or (rola_pod and pozycja.label not in deklaracja.podrzędne)
                    przyłączane |= przyłączane_pod
        self._pod_pozycją[(pozycja, deklaracja)] = (rola, frozenset(przyłączane))
        return self._pod_pozycją[(pozycja, deklaracja)]

    def _ciała_pozycji(self) -> dict[Pozycja, set[tuple[Pozycja, ...]]]:
        """Pozycja → ciała, jakimi ona w czytaniach stoi, czyli same krotki córek.

        Klasy z ciała schodzą, bo dwa ciała różne samą klasą córki
        są jednym wyborem tej pozycji i różnym wyborem tamtej córki,
        a wpisów ma być tyle, ile wyborów.
        Liścia nie ma tu ani wśród kluczy, ani w ciele:
        czytaniem liścia jest sama rozpiętość, więc etykiety i ciała nie ma (:class:`Pozycja`).
        """
        if self._ciała_pozycji_lasu is not None:
            return self._ciała_pozycji_lasu
        zebrane: dict[Pozycja, set[tuple[Pozycja, ...]]] = {}
        for para in self._żywe():
            for kombinacja in self._krawędzie.get(para, {}):
                ciało = tuple(dziecko for dziecko, _klasa in kombinacja)
                zebrane.setdefault(para[0], set()).add(ciało)
        self._ciała_pozycji_lasu = zebrane
        return zebrane

    def _widoczne(self, podrzędne: tuple[str, ...]) -> set[Pozycja]:
        """Pozycje, do których streszczenie zagląda: od korzenia i bez wchodzenia w podrzędne.

        Tą samą drogą chodzi :meth:`Node.find` po drzewie,
        więc pozycja spoza tego zbioru jest pozycją, o której streszczenie milczy.
        Zdanie podrzędne samo do zbioru wchodzi, bo mijane jest jego wnętrze,
        i nie ma to znaczenia: etykietą roli ono nie jest.
        """
        gotowe = self._widoczne_pozycje.get(podrzędne)
        if gotowe is not None:
            return gotowe
        znalezione: set[Pozycja] = set()
        stos = [self.korzeń]
        while stos:
            pozycja = stos.pop()
            if pozycja in znalezione:
                continue
            znalezione.add(pozycja)
            if pozycja.label in podrzędne:
                continue
            for klasa in self.klasy(pozycja):
                for kombinacja in self._krawędzie.get((pozycja, klasa), {}):
                    stos.extend(
                        dziecko for dziecko, _klasa in kombinacja if not dziecko.liść
                    )
        self._widoczne_pozycje[podrzędne] = znalezione
        return znalezione

    def _składowe_lasu(self, składowe: tuple[str, ...]) -> set[Pozycja]:
        """Pozycje zdań składowych, czyli te, które streszczenie streszcza osobno.

        Od korzenia i bez wchodzenia w składowe już znalezione,
        bo składowym ciągu jest zdanie najwyższe w gałęzi,
        i tą samą drogą chodzi po drzewie :func:`_początki_składowych`.
        Pozycje z różnych czytań stoją tu obok siebie i nie zlewają się:
        zdanie, którego czytania rozcinają je w różnych miejscach,
        daje jedną pozycję na każde takie rozcięcie,
        a pytanie o rolę zadaje się każdej z nich osobno.
        """
        gotowe = self._składowe_pozycje.get(składowe)
        if gotowe is not None:
            return gotowe
        znalezione: set[Pozycja] = set()
        odwiedzone: set[Pozycja] = set()
        stos = [self.korzeń]
        while stos:
            pozycja = stos.pop()
            if pozycja in odwiedzone:
                continue
            odwiedzone.add(pozycja)
            if pozycja.label in składowe:
                znalezione.add(pozycja)
                continue
            for klasa in self.klasy(pozycja):
                for kombinacja in self._krawędzie.get((pozycja, klasa), {}):
                    stos.extend(dziecko for dziecko, _klasa in kombinacja if not dziecko.liść)
        self._składowe_pozycje[składowe] = znalezione
        return znalezione

    def _żywe(self) -> set[tuple[Pozycja, Klasa]]:
        """Pary pozycja–klasa, które stoją w którymś czytaniu.

        Schodzimy od korzenia,
        bo tablica domyka i takie pozycje, których żadne czytanie nie przyjmuje,
        a werdykt ma mówić o czytaniach.
        """
        if self._żywe_pary is not None:
            return self._żywe_pary
        żywe: set[tuple[Pozycja, Klasa]] = set()
        rodzice: dict[tuple[Pozycja, Klasa], set[tuple[Pozycja, Klasa]]] = {}
        stos = [(self.korzeń, klasa) for klasa in self.klasy(self.korzeń)]
        while stos:
            para = stos.pop()
            if para in żywe:
                continue
            żywe.add(para)
            for kombinacja in self._krawędzie.get(para, {}):
                for dziecko, klasa in kombinacja:
                    if dziecko.liść:
                        continue
                    rodzice.setdefault((dziecko, klasa), set()).add(para)
                    stos.append((dziecko, klasa))
        self._żywe_pary = żywe
        self._rodzice = rodzice
        return żywe

    def _rodzicielskie(self) -> dict[tuple[Pozycja, Klasa], set[tuple[Pozycja, Klasa]]]:
        self._żywe()
        assert self._rodzice is not None
        return self._rodzice

    def _przedstawiciel(self, pozycja: Pozycja) -> Node:
        """Jedno z drzew tej pozycji, do nazwania jej.

        Nazwać trzeba konstytuent, a nie czytanie, a formy ma on w każdym swoim
        czytaniu te same; różni je podział na segmenty, którego nazwa i tak nie
        pokazuje. Głowa tak daleko nie sięga: ``dobry kod`` jest raz
        przymiotnikiem przed rzeczownikiem, a raz rzeczownikiem z dopełniaczem
        po nim, więc jedna rozpiętość ma tam dwie głowy, a nazwa bierze tę z
        pierwszego drzewa i tego wyboru nie ogłasza. Co z tym zrobić, jest
        otwarte w todo/.
        """
        gotowe = self._przedstawiciele.get(pozycja)
        if gotowe is not None:
            return gotowe
        for klasa in self.klasy(pozycja):
            for drzewo in self._drzewa(pozycja, klasa, _jedne(klasa), klasa):
                self._przedstawiciele[pozycja] = drzewo
                return drzewo
        raise AssertionError(f"pozycja {pozycja} stoi w lesie bez ani jednego drzewa")

grammar = tablica.grammar instance-attribute

korzeń = Pozycja(tablica.start, (tablica.początek, tablica.koniec)) instance-attribute

__init__(tablica)

Source code in olski/parse/las.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def __init__(self, tablica: _Tablica) -> None:
    self._tablica = tablica
    self.grammar = tablica.grammar
    self.korzeń = Pozycja(tablica.start, (tablica.początek, tablica.koniec))
    #: Rozpiętość → czytania form, jakie przez nią przechodzą.
    #: Kluczem jest rozpiętość, a nie segment, bo czytaniem liścia jest sama rozpiętość.
    self._czytania_liścia: dict[tuple[int, int], list[tuple[Segment, Reading]]] = {}
    for segment in tablica.segments:
        miejsce = self._czytania_liścia.setdefault((segment.start, segment.end), [])
        miejsce.extend((segment, reading) for reading in segment.readings)
    self._wyprowadzenia: dict[Pozycja, dict[tuple[Pozycja, ...], tuple[Production, ...]]] = {}
    #: Pozycja → koszt najtańszej morfologii pod nią (:meth:`koszt_morfologii`),
    #: wpisywany razem z wyprowadzeniami, bo liczy się go z tego samego przejścia.
    self._koszty: dict[Pozycja, int] = {}
    #: Pozycje, których koszt właśnie się liczy, czyli strażnik cyklu w
    #: gramatyce, gdzie symbol stoi pod sobą o tej samej rozpiętości.
    self._liczone: set[Pozycja] = set()
    self._klasy: dict[Pozycja, dict[Klasa, int]] = {}
    #: (pozycja, klasa) → kombinacja klas córek → produkcje, którymi przeszła.
    #: To jest las już po unifikacji:
    #: kombinacji, której ona nie przepuszcza, nie ma tu wcale,
    #: więc każda gałąź kończy się czytaniem.
    #: Produkcji jest tu kilka, bo dwie o jednym ciele są jednym kształtem,
    #: a różnić je może i terminal, i wypuszczane cechy (:meth:`wyprowadzenia`),
    #: i wyliczenie drzewa wybiera stąd tę, która wypuszcza żądane
    #: (:meth:`_drzewa`).
    #: Odczytania form czyta za to z każdej z nich (:meth:`_wsparte_kształtu`).
    self._krawędzie: dict[tuple[Pozycja, Klasa], dict[tuple, tuple[Production, ...]]] = {}
    self._czynne: set[Pozycja] = set()
    self._żywe_pary: set[tuple[Pozycja, Klasa]] | None = None
    self._rodzice: dict[tuple[Pozycja, Klasa], set[tuple[Pozycja, Klasa]]] | None = None
    self._prefiksy: dict[tuple, frozenset[Env]] = {}
    #: (produkcja, kombinacja, cechy dozwolone, miejsce, środowisko) → czy ciało
    #: domyka się od tego miejsca. Pyta o to raz na odczytanie liścia
    #: (:meth:`_wsparte`), a pary powtarzają się między odczytaniami.
    self._domknięcia: dict[tuple, bool] = {}
    #: (produkcje, kombinacja, cechy dozwolone) → czym każda córka w tym
    #: kształcie być może (:meth:`_wsparte_kształtu`). Ciało pojedyncze drugiego
    #: takiego słownika nie ma: pyta o nie sama ta suma, więc jego klucz nie
    #: powtórzyłby się nigdy.
    self._wsparcia_kształtów: dict[tuple, tuple[frozenset, ...]] = {}
    #: (para, etykieta, symbole mijane) → rozpiętości,
    #: jakie pierwszy węzeł tej etykiety pod nią bierze.
    self._pierwsze_role: dict[
        tuple[tuple[Pozycja, Klasa], str, tuple[str, ...]],
        frozenset[tuple[int, int] | None],
    ] = {}
    #: (para, etykiety, żądane rozdanie) → rozdania, jakie ta para umie złożyć.
    #: Żądane jest w kluczu, bo to ono odsiewa:
    #: rozdania spoza niego nie ma tu wcale (:meth:`_rozdania`).
    self._rozdania_pary: dict[
        tuple[tuple[Pozycja, Klasa], tuple[str, ...], Rozdanie], frozenset[Rozdanie]
    ] = {}
    #: (produkcja, kombinacja, żądane cechy) → czym jest w tym ciele każda córka.
    #: Kluczem jest całe ciało, a nie jedna córka,
    #: bo o czytaniu jednego liścia rozstrzyga unifikacja z pozostałymi.
    self._wybory_ciał: dict[tuple, tuple[Wybór, ...] | None] = {}
    self._przedstawiciele: dict[Pozycja, Node] = {}
    self._najdalszy: int | None = None
    #: Pozycja → ciała, jakimi stoi w czytaniach (:meth:`_ciała_pozycji`).
    self._ciała_pozycji_lasu: dict[Pozycja, set[tuple[Pozycja, ...]]] | None = None
    #: Symbole zdań podrzędnych → pozycje, do których streszczenie zagląda.
    self._widoczne_pozycje: dict[tuple[str, ...], set[Pozycja]] = {}
    #: Symbole zdań składowych → pozycje, które streszczenie streszcza osobno.
    self._składowe_pozycje: dict[tuple[str, ...], set[Pozycja]] = {}
    #: (pozycja, deklaracja) → co pod nią widzą dwa pozostałe podsumowania (:meth:`_pod`).
    self._pod_pozycją: dict[tuple[Pozycja, Deklaracja], tuple[bool, frozenset[int]]] = {}
    #: Deklaracja → wybory przyłączenia, którym werdykt daje wiersz.
    self._przyłączenia_lasu: dict[
        Deklaracja, dict[int, tuple[Pozycja, tuple[str, ...]]]
    ] = {}

wyprowadzenia(pozycja)

Wyprowadzenia pod tą pozycją: ciało → produkcje, które je złożyły.

Kluczem jest ciało, a nie produkcja, bo o kształcie rozstrzygają etykiety i rozpiętości córek. Dwie produkcje o jednym ciele dają jedno czytanie i wchodzą tu razem, choćby brały co innego: nad jedną formą człon_imienny z rzeczownika i człon_imienny z zaimka są jednym ciałem, bo liść jest swoją rozpiętością. Odczytania obu niesie potem liść (Las._wsparte_kształtu).

Pytana o pozycję, której tablica nie domknęła, oddaje pusty słownik, więc jest to zarazem sposób zapytania lasu, czy taki konstytuent w ogóle powstał. Odpowiedź kosztuje przy tym całe poddrzewo, bo wycena schodzi po córkach (koszt_morfologii), a nie samo domknięcie tej jednej pozycji.

To jedno miejsce ustala kolejność, w jakiej las wydaje drzewa — dziedziczą ją klasy pozycji, krawędzie pod nimi i drzewa z tych krawędzi — a rozstrzyga o niej koszt ciała, a pod nim _cięcie. Nieposortowane szłyby tak, jak for_head oddaje produkcje, czyli w kolejności dopisywania ich do gramatyki (docs/disambiguation.md).

Source code in olski/parse/las.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def wyprowadzenia(self, pozycja: Pozycja) -> dict[tuple[Pozycja, ...], tuple[Production, ...]]:
    """Wyprowadzenia pod tą pozycją: ciało → produkcje, które je złożyły.

    Kluczem jest ciało, a nie produkcja,
    bo o kształcie rozstrzygają etykiety i rozpiętości córek.
    Dwie produkcje o jednym ciele dają jedno czytanie i wchodzą tu razem,
    choćby brały co innego: nad jedną formą ``człon_imienny`` z rzeczownika i
    ``człon_imienny`` z zaimka są jednym ciałem, bo liść jest swoją rozpiętością.
    Odczytania obu niesie potem liść (:meth:`Las._wsparte_kształtu`).

    Pytana o pozycję, której tablica nie domknęła, oddaje pusty słownik,
    więc jest to zarazem sposób zapytania lasu, czy taki konstytuent w ogóle powstał.
    Odpowiedź kosztuje przy tym całe poddrzewo, bo wycena schodzi po córkach
    (:meth:`koszt_morfologii`), a nie samo domknięcie tej jednej pozycji.

    To jedno miejsce ustala kolejność, w jakiej las wydaje drzewa —
    dziedziczą ją klasy pozycji, krawędzie pod nimi i drzewa z tych krawędzi —
    a rozstrzyga o niej koszt ciała, a pod nim :func:`_cięcie`.
    Nieposortowane szłyby tak, jak ``for_head`` oddaje produkcje,
    czyli w kolejności dopisywania ich do gramatyki (docs/disambiguation.md).
    """
    gotowe = self._wyprowadzenia.get(pozycja)
    if gotowe is not None:
        return gotowe
    znalezione: dict[tuple[Pozycja, ...], list[Production]] = {}
    if not pozycja.liść:
        źródło, k = pozycja.span
        for production in self.grammar.for_head(pozycja.label):
            if not self._tablica.zamknięte(production, źródło, k):
                continue
            for ciało in self._tablica.ciała(production, len(production.body), źródło, k):
                znalezione.setdefault(ciało, []).append(production)
    self._liczone.add(pozycja)
    try:
        wyceny = {
            ciało: [
                (production.koszt, self._morfologia_ciała(production, ciało))
                for production in produkcje
            ]
            for ciało, produkcje in znalezione.items()
        }
    finally:
        self._liczone.discard(pozycja)
    # Morfologia dodaje się do kosztu tej produkcji, która ją wzięła,
    # bo to jej terminale mówią, którym czytaniem forma tu weszła;
    # ciało kosztuje potem tyle, co najtańsza z produkcji, które je składają.
    koszty = {ciało: min(k + m for k, m in pary) for ciało, pary in wyceny.items()}
    zebrane = {
        ciało: tuple(sorted(znalezione[ciało], key=lambda p: p.koszt))
        for ciało in sorted(znalezione, key=lambda c: (koszty[c], _cięcie(c)))
    }
    self._wyprowadzenia[pozycja] = zebrane
    self._koszty[pozycja] = min((m for pary in wyceny.values() for _k, m in pary), default=0)
    return zebrane

koszt_morfologii(pozycja)

Najtańsza morfologia, na jakiej ten konstytuent się opiera.

Liść kosztuje zero, bo jego czytania widzi dopiero terminal, który je bierze (_Tablica.koszt_morfologii): jedna pozycja liścia obsługuje wszystkie terminale, jakie w niej stoją.

Pozycja stojąca pod sobą kosztuje tutaj zero i cyklu nie zgłasza, bo zgłasza go klasy, licząc czytania. Tam cykl jest błędem, a tutaj byłby nim porządek postawiony przed odpowiedzią na pytanie, czy zdanie ma w ogóle czytania.

Source code in olski/parse/las.py
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
def koszt_morfologii(self, pozycja: Pozycja) -> int:
    """Najtańsza morfologia, na jakiej ten konstytuent się opiera.

    Liść kosztuje zero, bo jego czytania widzi dopiero terminal, który je bierze
    (:meth:`_Tablica.koszt_morfologii`):
    jedna pozycja liścia obsługuje wszystkie terminale, jakie w niej stoją.

    Pozycja stojąca pod sobą kosztuje tutaj zero i cyklu nie zgłasza,
    bo zgłasza go :meth:`klasy`, licząc czytania.
    Tam cykl jest błędem, a tutaj byłby nim porządek postawiony przed
    odpowiedzią na pytanie, czy zdanie ma w ogóle czytania.
    """
    if pozycja.liść or pozycja in self._liczone:
        return 0
    self.wyprowadzenia(pozycja)
    return self._koszty[pozycja]

klasy(pozycja)

Ile kształtów stoi pod tą pozycją, w klasach po tym, co wypuszczają.

Iloczyn liczy się tutaj po parach, które unifikacja przepuszcza, a nie po samych pozycjach: kombinacja klas córek, której żadna produkcja nie składa, nie wnosi ani jednego czytania.

Klasą jest zbiór cech, a nie jedne cechy, bo jeden kształt przechodzi czasem na kilka sposobów: dla przyjemności jest jedną grupą przyimkową w dwóch liczbach. Rodzic widzi z córki tylko to, co ona wypuszcza, więc grupowanie po tym zbiorze pozwala liczyć kształty zamiast sposobów: dwa kształty o jednym zbiorze wpadają do jednej klasy i sumują się, a jeden kształt wpada do dokładnie jednej.

Source code in olski/parse/las.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
def klasy(self, pozycja: Pozycja) -> dict[Klasa, int]:
    """Ile kształtów stoi pod tą pozycją, w klasach po tym, co wypuszczają.

    Iloczyn liczy się tutaj po parach, które unifikacja przepuszcza,
    a nie po samych pozycjach:
    kombinacja klas córek, której żadna produkcja nie składa,
    nie wnosi ani jednego czytania.

    Klasą jest zbiór cech, a nie jedne cechy,
    bo jeden kształt przechodzi czasem na kilka sposobów:
    ``dla przyjemności`` jest jedną grupą przyimkową w dwóch liczbach.
    Rodzic widzi z córki tylko to, co ona wypuszcza,
    więc grupowanie po tym zbiorze pozwala liczyć kształty zamiast sposobów:
    dwa kształty o jednym zbiorze wpadają do jednej klasy i sumują się,
    a jeden kształt wpada do dokładnie jednej.
    """
    gotowe = self._klasy.get(pozycja)
    if gotowe is not None:
        return gotowe
    if pozycja in self._czynne:
        raise Cykl(
            f"{pozycja.label} na {pozycja.span} stoi samo pod sobą; "
            "czytań jest wtedy nieskończenie wiele"
        )
    self._czynne.add(pozycja)
    klasy: dict[Klasa, int] = {}
    try:
        for ciało, produkcje in self.wyprowadzenia(pozycja).items():
            listy = [
                [(None, 1)] if dziecko.liść else list(self.klasy(dziecko).items())
                for dziecko in ciało
            ]
            for kombinacja in product(*listy):
                wybór = tuple(klasa for klasa, _ile in kombinacja)
                wypuszczane: set[Cechy] = set()
                przeszłe = []
                for production in produkcje:
                    cechy = self._przejdź(production, ciało, wybór)
                    if cechy:
                        wypuszczane |= cechy
                        przeszłe.append(production)
                if not przeszłe:
                    continue
                klasa = frozenset(wypuszczane)
                ile = math.prod(liczba for _klasa, liczba in kombinacja)
                klasy[klasa] = klasy.get(klasa, 0) + ile
                self._krawędzie.setdefault((pozycja, klasa), {}).setdefault(
                    tuple(zip(ciało, wybór, strict=True)), tuple(przeszłe)
                )
    finally:
        self._czynne.discard(pozycja)
    self._klasy[pozycja] = klasy
    return klasy

ile_czytań()

Ile czytań ma zdanie: suma po klasach korzenia, bez wyliczania drzew.

Source code in olski/parse/las.py
455
456
457
def ile_czytań(self) -> int:
    """Ile czytań ma zdanie: suma po klasach korzenia, bez wyliczania drzew."""
    return sum(self.klasy(self.korzeń).values())

najdalszy()

Dokąd doszła jakakolwiek analiza częściowa, czyli na czym odrzucenie stanęło.

Liczy się przejście terminalem, bo bloker ma nazwać formę z tego miejsca zdania, a czym jest tu analiza częściowa, mówi _przed_formą.

Source code in olski/parse/las.py
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
def najdalszy(self) -> int:
    """Dokąd doszła jakakolwiek analiza częściowa, czyli na czym odrzucenie stanęło.

    Liczy się przejście terminalem, bo bloker ma nazwać formę z tego miejsca zdania,
    a czym jest tu analiza częściowa, mówi :meth:`_przed_formą`.
    """
    if self._najdalszy is not None:
        return self._najdalszy
    if self.klasy(self.korzeń):
        # Czytanie sięga przez całe zdanie, więc dalej niż jego koniec nie ma gdzie.
        self._najdalszy = self._tablica.koniec
        return self._najdalszy
    najdalszy = self._tablica.początek
    for k, (production, kropka, źródło) in self._przed_formą():
        terminal = production.body[kropka]
        środowiska = self._prefiks(production, kropka, źródło, k)
        for segment in self._tablica.krawędzie.get(k, ()):
            if segment.end > najdalszy and self._przechodzi(
                terminal, (k, segment.end), środowiska
            ):
                najdalszy = segment.end
    self._najdalszy = najdalszy
    return najdalszy

czytania()

Czytania jako drzewa, po jednym na kształt.

Kolejność, w jakiej wychodzą, ustala _Tablica.ciała.

Każda gałąź kończy się czytaniem, bo klasy odsiały już kombinacje, których unifikacja nie przepuszcza. Dlatego urwanie po MAX_READINGS kosztuje tyle, ile wypisane drzewa, i nic ponad to.

Source code in olski/parse/las.py
605
606
607
608
609
610
611
612
613
614
615
def czytania(self) -> Iterator[Node]:
    """Czytania jako drzewa, po jednym na kształt.

    Kolejność, w jakiej wychodzą, ustala :meth:`_Tablica.ciała`.

    Każda gałąź kończy się czytaniem,
    bo ``klasy`` odsiały już kombinacje, których unifikacja nie przepuszcza.
    Dlatego urwanie po :data:`MAX_READINGS` kosztuje tyle, ile wypisane drzewa,
    i nic ponad to.
    """
    return self._kształty(self.korzeń)

różniące(deklaracja)

Te z ról, które nie mają w każdym czytaniu tego samego wypełnienia.

Pytamy las, a nie streszczenia czytań. Streszczeń jest najwyżej MAX_READINGS, a zdanie ustawy ma czytań dziesiątki tysięcy, więc rola różniąca się dopiero za tą granicą nie zostałaby nazwana, choć liczba obok niej granicy nie ma.

Jednym wystąpieniem roli jest to, które nazywa describe, czyli pierwsze w zdaniu składowym i spoza zdań podrzędnych. Etykieta pada w czytaniu kilka razy, bo zdanie współrzędne ma własny podmiot, a dwa podmioty stojące obok siebie w jednym czytaniu nie mówią nic o różnicy między czytaniami. Pytamy więc o zdanie całe i o każde jego składowe osobno. Bez pytania o składowe werdykt milczy o roli, którą lista czytań rozdziela: czytania różne dopiero w składowym drugim mają w pierwszym to samo. Bez pytania o zdanie całe milczy o rozcięciu zdania na dwa, bo każde składowe niesie wtedy jedną rozpiętość, stojąc w jednym z czytań. Iloczynu po składowych stąd nie ma: pytanie zadane każdemu osobno kosztuje tyle, ile ich jest, a rozpiętości brane naraz mnożyłyby się jak czytania.

Porównujemy rozpiętości, a nie formy: formy nad jedną rozpiętością są w każdym czytaniu te same, a różni je podział na segmenty, którego streszczenie i tak nie pokazuje. Rozpiętość None jest czytaniem bez tej roli, tak jak streszczenie bez tego klucza.

Source code in olski/parse/las.py
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
def różniące(self, deklaracja: Deklaracja) -> tuple[str, ...]:
    """Te z ról, które nie mają w każdym czytaniu tego samego wypełnienia.

    Pytamy las, a nie streszczenia czytań.
    Streszczeń jest najwyżej :data:`MAX_READINGS`,
    a zdanie ustawy ma czytań dziesiątki tysięcy,
    więc rola różniąca się dopiero za tą granicą nie zostałaby nazwana,
    choć liczba obok niej granicy nie ma.

    Jednym wystąpieniem roli jest to, które nazywa :func:`describe`,
    czyli pierwsze w zdaniu składowym i spoza zdań podrzędnych.
    Etykieta pada w czytaniu kilka razy, bo zdanie współrzędne ma własny podmiot,
    a dwa podmioty stojące obok siebie w jednym czytaniu
    nie mówią nic o różnicy między czytaniami.
    Pytamy więc o zdanie całe i o każde jego składowe osobno.
    Bez pytania o składowe werdykt milczy o roli, którą lista czytań rozdziela:
    czytania różne dopiero w składowym drugim mają w pierwszym to samo.
    Bez pytania o zdanie całe milczy o rozcięciu zdania na dwa,
    bo każde składowe niesie wtedy jedną rozpiętość, stojąc w jednym z czytań.
    Iloczynu po składowych stąd nie ma:
    pytanie zadane każdemu osobno kosztuje tyle, ile ich jest,
    a rozpiętości brane naraz mnożyłyby się jak czytania.

    Porównujemy rozpiętości, a nie formy:
    formy nad jedną rozpiętością są w każdym czytaniu te same,
    a różni je podział na segmenty, którego streszczenie i tak nie pokazuje.
    Rozpiętość ``None`` jest czytaniem bez tej roli,
    tak jak streszczenie bez tego klucza.
    """
    składowe = self._składowe_lasu(deklaracja.składowe)
    return tuple(
        etykieta
        for etykieta in deklaracja.role
        if self._niezgodna(self.korzeń, etykieta, deklaracja.podrzędne)
        or any(
            self._niezgodna(pozycja, etykieta, deklaracja.podrzędne) for pozycja in składowe
        )
    )

numer_czytania(role)

Którym z kolei czytaniem jest to, które przypisuje te role; None, gdy żadnym.

Pyta ten, kto ma cudze czytanie jednego z tych zdań i chce wiedzieć, czy ono w tym lesie ocalało, a jeśli tak, to jak głęboko. Numer jest tym, ile odpowiedź „ocalało” jest warta: czytanie drugie z dwóch i czytanie tysięczne z dwudziestu ośmiu tysięcy ocalały jednakowo, a przeczyta z nich ktoś jedno.

Rolami, a nie kształtem, bo dwie gramatyki grupują materiał każda po swojemu, więc porównanie nawiasów mierzyłoby różnicę między formalizmami. Rolę obie orzekają o zdaniu, i tą samą miarą mierzy zgodność Outcome.agreement w harness/pomiar.py, więc obie odpowiedzi mówią o jednym.

Odpowiedź składa się z dwóch pytań zadanych po kolei i oba są tu potrzebne. Czy takie czytanie w lesie jest, mówi las bez wyliczania drzew, i po to ta połowa tu jest: lista urywa się na MAX_READINGS, a zdania wieloznaczne są dokładnie tymi, nad którymi ta granica pada, więc czytanie ocalałe za nią wyszłoby z listy przepadłe. Którym z kolei jest, mówi dopiero wyliczanie, bo numer jest miejscem w kolejności, którą ustala _Tablica.ciała, a numer policzony obok byłby tą kolejnością wypisaną drugi raz.

Wyliczanie rusza więc dopiero po odpowiedzi twierdzącej i na tym czytaniu przystaje, czyli kosztuje tyle, ile numer, a nie tyle, ile las ma czytań; granica z MAX_READINGS nie jest mu przez to potrzebna. Ile to kosztuje nad bankiem drzew, mówi docs/corpus.md#złote-czytanie-ocalało-w-niemal-każdym-zdaniu-wieloznacznym.

Zbiór pusty jest żądaniem, a nie jego brakiem: etykieta, której pytający nigdzie nie obsadza, żąda czytania, które nie obsadza jej również.

Source code in olski/parse/las.py
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
def numer_czytania(self, role: Mapping[str, frozenset[tuple[int, int]]]) -> int | None:
    """Którym z kolei czytaniem jest to, które przypisuje te role; ``None``, gdy żadnym.

    Pyta ten, kto ma cudze czytanie jednego z tych zdań
    i chce wiedzieć, czy ono w tym lesie ocalało, a jeśli tak, to jak głęboko.
    Numer jest tym, ile odpowiedź „ocalało” jest warta:
    czytanie drugie z dwóch i czytanie tysięczne z dwudziestu ośmiu tysięcy
    ocalały jednakowo, a przeczyta z nich ktoś jedno.

    Rolami, a nie kształtem, bo dwie gramatyki grupują materiał każda po swojemu,
    więc porównanie nawiasów mierzyłoby różnicę między formalizmami.
    Rolę obie orzekają o zdaniu, i tą samą miarą mierzy zgodność
    ``Outcome.agreement`` w ``harness/pomiar.py``, więc obie odpowiedzi mówią o jednym.

    Odpowiedź składa się z dwóch pytań zadanych po kolei i oba są tu potrzebne.
    Czy takie czytanie w lesie jest, mówi las bez wyliczania drzew,
    i po to ta połowa tu jest: lista urywa się na :data:`MAX_READINGS`,
    a zdania wieloznaczne są dokładnie tymi, nad którymi ta granica pada,
    więc czytanie ocalałe za nią wyszłoby z listy przepadłe.
    Którym z kolei jest, mówi dopiero wyliczanie,
    bo numer jest miejscem w kolejności, którą ustala :meth:`_Tablica.ciała`,
    a numer policzony obok byłby tą kolejnością wypisaną drugi raz.

    Wyliczanie rusza więc dopiero po odpowiedzi twierdzącej i na tym czytaniu przystaje,
    czyli kosztuje tyle, ile numer, a nie tyle, ile las ma czytań;
    granica z :data:`MAX_READINGS` nie jest mu przez to potrzebna.
    Ile to kosztuje nad bankiem drzew, mówi
    docs/corpus.md#złote-czytanie-ocalało-w-niemal-każdym-zdaniu-wieloznacznym.

    Zbiór pusty jest żądaniem, a nie jego brakiem:
    etykieta, której pytający nigdzie nie obsadza,
    żąda czytania, które nie obsadza jej również.
    """
    etykiety = tuple(sorted(role))
    żądane: Rozdanie = tuple(frozenset(role[etykieta]) for etykieta in etykiety)
    if not any(
        żądane in self._rozdania((self.korzeń, klasa), etykiety, żądane)
        for klasa in self.klasy(self.korzeń)
    ):
        return None
    for numer, drzewo in enumerate(self.czytania(), 1):
        if _rozdanie_drzewa(drzewo, etykiety) == żądane:
            return numer
    raise AssertionError(
        "las składa to rozdanie ról, a wyliczanie nie wydało drzewa o tym rozdaniu"
    )

przyłączenia(deklaracja)

Modyfikatory, którym czytania dają więcej niż jednego gospodarza.

Jeden wpis na wybór, bo tyle wyborów zdanie zostawia. Modyfikator występuje w każdym czytaniu raz, więc dwóch gospodarzy jednej pozycji to dwa czytania różniące się tym przyłączeniem, i zdanie o sześciu wyrażeniach przyimkowych daje sześć wpisów wobec sześćdziesięciu czterech czytań.

Wyborem jest przyimek, a nie pozycja, i dlatego pozycje o jednym początku wchodzą tu razem. w pliku i w pliku w katalogu to dwie pozycje z dwóch różnych czytań, a decyzja pod nimi jest jedna: gdzie przyłącza się wyrażenie otwarte przez w. Licząc po pozycjach, dostalibyśmy wpis na każdą parę przyimków, czyli znów kwadrat zamiast długości zdania.

Source code in olski/parse/las.py
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
def przyłączenia(self, deklaracja: Deklaracja) -> list[Przyłączenie]:
    """Modyfikatory, którym czytania dają więcej niż jednego gospodarza.

    Jeden wpis na wybór, bo tyle wyborów zdanie zostawia.
    Modyfikator występuje w każdym czytaniu raz,
    więc dwóch gospodarzy jednej pozycji to dwa czytania różniące się tym przyłączeniem,
    i zdanie o sześciu wyrażeniach przyimkowych
    daje sześć wpisów wobec sześćdziesięciu czterech czytań.

    Wyborem jest przyimek, a nie pozycja,
    i dlatego pozycje o jednym początku wchodzą tu razem.
    ``w pliku`` i ``w pliku w katalogu`` to dwie pozycje z dwóch różnych czytań,
    a decyzja pod nimi jest jedna: gdzie przyłącza się wyrażenie otwarte przez ``w``.
    Licząc po pozycjach, dostalibyśmy wpis na każdą parę przyimków,
    czyli znów kwadrat zamiast długości zdania.
    """
    wybory = self._nazwane_przyłączenia(deklaracja)
    return [
        Przyłączenie(sklej_formy(self._przedstawiciel(pozycja).forms()), nazwy)
        for _początek, (pozycja, nazwy) in sorted(wybory.items())
    ]

rozbieżności(deklaracja)

Konstytuenty, którym czytania dają kilka kształtów tam, gdzie streszczenie nie zagląda.

Jeden wpis na wybór, tak jak w przyłączenia, i wyborem jest tu konstytuent o kilku ciałach: rozpiętość pozycja ma jedną, więc rozstrzygane jest w takim miejscu to, z czego ona się składa, a nie to, gdzie stoi. Ciała są po unifikacji, więc wpis dostaje konstytuent, który naprawdę czyta się kilkoma sposobami; po co werdyktowi ten wiersz, mówi Rozbieżność.

Wykluczenia są trzy, po jednym na wiersz, który werdykt drukuje bez tego podsumowania (_nazwany_gdzie_indziej), a po nich zostaje najwęższy z konstytuentów: wpis, którego napis obejmuje napis innego wpisu, mówi o tym samym słowie i o kilku obok niego, bo wieloznaczność wychodzi w górę. równych praw kobiet czyta się dwoma sposobami przez samo równych, a równych praw kobiet i mężczyzn trzema, i naprawić trzeba jedno słowo.

Source code in olski/parse/las.py
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
def rozbieżności(self, deklaracja: Deklaracja) -> list[Rozbieżność]:
    """Konstytuenty, którym czytania dają kilka kształtów tam, gdzie streszczenie nie zagląda.

    Jeden wpis na wybór, tak jak w :meth:`przyłączenia`,
    i wyborem jest tu konstytuent o kilku ciałach:
    rozpiętość pozycja ma jedną, więc rozstrzygane jest w takim miejscu to,
    z czego ona się składa, a nie to, gdzie stoi.
    Ciała są po unifikacji, więc wpis dostaje konstytuent,
    który naprawdę czyta się kilkoma sposobami;
    po co werdyktowi ten wiersz, mówi :class:`Rozbieżność`.

    Wykluczenia są trzy, po jednym na wiersz, który werdykt drukuje bez tego
    podsumowania (:meth:`_nazwany_gdzie_indziej`), a po nich zostaje najwęższy
    z konstytuentów: wpis, którego napis obejmuje napis innego wpisu, mówi o tym
    samym słowie i o kilku obok niego, bo wieloznaczność wychodzi w górę.
    ``równych praw kobiet`` czyta się dwoma sposobami przez samo ``równych``,
    a ``równych praw kobiet i mężczyzn`` trzema, i naprawić trzeba jedno słowo.
    """
    kandydaci = [
        pozycja
        for pozycja, ciała in self._ciała_pozycji().items()
        if len(ciała) > 1 and not self._nazwany_gdzie_indziej(pozycja, ciała, deklaracja)
    ]
    wybrani: list[Pozycja] = []
    # Od najkrótszego, żeby każdy kandydat zastał już wybrane wszystko, co
    # obejmuje. Remis rozstrzyga etykieta: dwie pozycje o jednej rozpiętości
    # mówią o tych samych słowach, więc wpis dostaje jedna z nich.
    for pozycja in sorted(kandydaci, key=lambda p: (p.span[1] - p.span[0], p.span, p.label)):
        if not any(_wewnątrz(inny.span, pozycja.span) for inny in wybrani):
            wybrani.append(pozycja)
    return [
        Rozbieżność(
            sklej_formy(self._przedstawiciel(pozycja).forms()),
            self._ile_kształtów(pozycja),
            #  Kształtów wyliczamy tyle, ile czytań wylicza się nad zdaniem,
            #  bo granica jest tu z tego samego powodu: wieloznaczność
            #  konstytuentu mnoży się jak wieloznaczność zdania.
            tuple(streszczenia(islice(self._kształty(pozycja), MAX_READINGS), deklaracja)),
        )
        for pozycja in sorted(wybrani, key=lambda p: (p.span, p.label))
    ]

Deklaracja dataclass

Co gramatyka mówi o sobie podsumowaniom werdyktu.

Które symbole są rolami i gdzie szukać przyłączenia, wie gramatyka, a nie rozbiór, więc każde podsumowanie bierze to jedną wartością — parse, describe i obie metody Las pod nimi — a podsumowanie następne dokłada tutaj pole i nie rusza żadnej z tych sygnatur. Wypełnia ją gramatyka, a typ definiuje rozbiór, bo formalizm z olski/grammar.py niesie produkcje i o werdykcie nic nie wie.

Source code in olski/parse/podsumowanie.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
@dataclass(frozen=True)
class Deklaracja:
    """Co gramatyka mówi o sobie podsumowaniom werdyktu.

    Które symbole są rolami i gdzie szukać przyłączenia, wie gramatyka, a nie rozbiór,
    więc każde podsumowanie bierze to jedną wartością —
    :func:`parse`, :func:`describe` i obie metody :class:`Las` pod nimi —
    a podsumowanie następne dokłada tutaj pole i nie rusza żadnej z tych sygnatur.
    Wypełnia ją gramatyka, a typ definiuje rozbiór,
    bo formalizm z ``olski/grammar.py`` niesie produkcje i o werdykcie nic nie wie.
    """

    #: Role, którymi streszcza się czytanie i o które czytania mogą się różnić.
    role: tuple[str, ...]
    #: Role, które się przyłączają,
    #: czyli te, przy których streszczenie nazywa jeszcze gospodarza.
    przyłączane: tuple[str, ...]
    #: Symbole konstytuentów, w których produkcjach przyłączenie stoi.
    gospodarze: tuple[str, ...]
    #: Ta z ról przyłączanych, której nierozstrzygniętego gospodarza werdykt liczy
    #: osobnym wierszem (:meth:`Las.przyłączenia`),
    #: a warstwa za parserem zgaduje (``olski/rozstrzyganie.py``).
    #: Jest nią jedna, bo tabela skłonności i leksykon walencyjny
    #: mówią o wyrażeniu przyimkowym, a nie o każdym okoliczniku;
    #: czy wiersz werdyktu ma być szerszy od warstwy, trzyma ``todo/``.
    rozstrzygany: str
    #: Symbole, których produkcje koordynują, czyli te, po których streszczenie
    #: nawiasuje człon ciągu współrzędnego.
    współrzędne: tuple[str, ...]
    #: Symbole zdań składowych, czyli członów ciągu zdań współrzędnych.
    #: Streszczeń jest tyle, ile zdanie ma składowych, po jednym na składowe,
    #: więc widać w nich całe zdanie współrzędne (:func:`describe`).
    składowe: tuple[str, ...]
    #: Symbole zdań podrzędnych, czyli tych, których wnętrze jest osobnym zdaniem.
    #: Streszczenie i :meth:`Las.różniące` zatrzymują się na nich,
    #: bo rola z wnętrza takiego zdania jest jego rolą, a nie rolą zdania nad nim.
    #: Zatrzymują się na nich, a nie przed nimi: symbol stojący i tutaj, i w
    #: :attr:`role` nazywa się w streszczeniu całym sobą i wnętrza nie otwiera,
    #: czym jest okolicznik wyrażony zdaniem.
    #: Zatrzymać się muszą oba naraz, inaczej wiersz ``differing in``
    #: nazywa rolę, której lista czytań pod nim nie nazywa.
    #: Wywód, przykład i cenę trzyma
    #: docs/parsowanie.md#werdykt-jest-zapytaniem-o-las-a-nie-listą-czytań.
    podrzędne: tuple[str, ...]
    #: Które role obsadzają pozycje ramy czasownika (:class:`Obsada`).
    obsada: Obsada
    #: Symbol konstytuenta, którym tekst nazywa rzecz.
    #: Pyta o niego warstwa szukająca tego, na co zaimek wskazuje
    #: (``olski/odniesienia.py``): kandydatem jest głowa takiego konstytuenta
    #: wziętego najszerzej, bo `duże pole maków` nazywa pole, a nie maki.
    #: Rolą ten symbol nie jest, bo rzecz nazwana stoi w każdej z ról imiennych
    #: i pod przyimkiem także, a pytanie jest o nią samą, a nie o jej pozycję.
    grupa_imienna: str

role instance-attribute

przyłączane instance-attribute

gospodarze instance-attribute

rozstrzygany instance-attribute

współrzędne instance-attribute

składowe instance-attribute

podrzędne instance-attribute

obsada instance-attribute

grupa_imienna instance-attribute

__init__(role, przyłączane, gospodarze, rozstrzygany, współrzędne, składowe, podrzędne, obsada, grupa_imienna)

Obsada dataclass

Które role czytania obsadzają pozycje ramy czasownika.

Czyta to warstwa nad plikiem żądań (olski/żądania.py): żeby powiedzieć, czego czasownik żąda od słowa stojącego w jego pozycji, trzeba wiedzieć, która rola niesie ten czasownik, a która jego pozycję wypełnia. Rolami, bo warstwa ta ogląda gotowe czytanie, a nie gramatykę, a w czytaniu pozycja ramy jest właśnie etykietą roli.

Source code in olski/parse/podsumowanie.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
@dataclass(frozen=True)
class Obsada:
    """Które role czytania obsadzają pozycje ramy czasownika.

    Czyta to warstwa nad plikiem żądań (``olski/żądania.py``):
    żeby powiedzieć, czego czasownik żąda od słowa stojącego w jego pozycji,
    trzeba wiedzieć, która rola niesie ten czasownik, a która jego pozycję wypełnia.
    Rolami, bo warstwa ta ogląda gotowe czytanie, a nie gramatykę,
    a w czytaniu pozycja ramy jest właśnie etykietą roli.
    """

    #: Role, których głowa rządzi ramą zdania składowego.
    #: Jest ich kilka, bo orzeka też forma nieosobowa i predykatyw
    #: (``ORZECZENIE_BEZOSOBOWE`` w ``olski/subset/deklaracja.py``);
    #: w jednym zdaniu składowym stoi jedna z nich.
    orzeczenia: tuple[str, ...]
    #: Rola stojąca w pozycji podmiotu.
    podmiot: str
    #: Role, których pozycję nazywa przypadek wypełnienia,
    #: bo jedna nazwa roli pokrywa kilka pozycji ramy:
    #: `dopełnienie` nie mówi, w którym przypadku stoi.
    przypadkowe: tuple[str, ...]
    #: Symbole, których wnętrze obsadza ramę własnego czasownika:
    #: `dokument` w `Autor zamierzył edytować dokument.` jest dopełnieniem
    #: bezokolicznika, a nie formy osobowej nad nim.
    #: Zejście po role staje na nich, więc wiersz o tym dopełnieniu nie powstaje
    #: wcale, zamiast powstać z żądaniem cudzego czasownika.
    własna_rama: tuple[str, ...]

orzeczenia instance-attribute

podmiot instance-attribute

przypadkowe instance-attribute

własna_rama instance-attribute

__init__(orzeczenia, podmiot, przypadkowe, własna_rama)

Przyłączenie dataclass

Modyfikator, którego przyłączenie zostaje nierozstrzygnięte, i jego gospodarze.

Werdykt nad zdaniem o kilku takich przyłączeniach ma powiedzieć autorowi, co poprawić, a lista czytań mówi to iloczynem: sześć niezależnych wyborów wychodzi z niej sześćdziesięcioma czterema wierszami. Stąd ta postać, czyli jeden wpis na wybór: wpisów jest tyle, ile decyzji, a nie ile czytań.

Source code in olski/parse/podsumowanie.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@dataclass(frozen=True)
class Przyłączenie:
    """Modyfikator, którego przyłączenie zostaje nierozstrzygnięte, i jego gospodarze.

    Werdykt nad zdaniem o kilku takich przyłączeniach ma powiedzieć autorowi, co poprawić,
    a lista czytań mówi to iloczynem:
    sześć niezależnych wyborów wychodzi z niej sześćdziesięcioma czterema wierszami.
    Stąd ta postać, czyli jeden wpis na wybór:
    wpisów jest tyle, ile decyzji, a nie ile czytań.
    """

    #: Formy modyfikatora, czyli to, co autor ma przestawić.
    modyfikator: str
    #: Konstytuenty, do których modyfikator w czytaniach dochodzi,
    #: nazwane swoją głową i ustawione tak jak w zdaniu.
    gospodarze: tuple[str, ...]

modyfikator instance-attribute

gospodarze instance-attribute

__init__(modyfikator, gospodarze)

Result dataclass

What the parser concluded about one sentence.

Source code in olski/parse/podsumowanie.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
@dataclass
class Result:
    """What the parser concluded about one sentence."""

    #: Ile czytań zdanie ma, policzone po lesie i bez granicy, jakiej podlega
    #: :attr:`readings`.
    #: Liczba jest osobno od listy,
    #: bo werdykt nad zdaniem o sześciu nierozstrzygniętych przyłączeniach
    #: jest liczbą, której nikt nie chce zobaczyć wypisanej drzewo po drzewie.
    ile: int = 0
    readings: list[Node] = field(default_factory=list)
    #: The furthest graph node any partial analysis reached, which is where a
    #: rejected sentence stopped making sense.
    #: Over a sentence that has a reading it is the sentence's last node.
    #: ``None`` says that nobody asked (:func:`podsumuj`), not that no analysis
    #: reached anywhere: the first graph node is an answer, since a sentence
    #: that stops on its own first form stops there.
    furthest: int | None = None
    #: Czy wyliczanie stanęło na :data:`MAX_READINGS`,
    #: czyli czy lista czytań jest krótsza niż :attr:`ile`.
    truncated: bool = False
    #: Role, o które czytania się różnią, o ile :func:`parse` dostał :class:`Deklaracja`.
    #: Wzięte z lasu, a nie ze streszczeń, których jest najwyżej :data:`MAX_READINGS`;
    #: dlaczego, mówi :meth:`Las.różniące`.
    różniące: tuple[str, ...] = ()
    #: Przyłączenia, których czytania nie rozstrzygają,
    #: z tej samej deklaracji co :attr:`różniące`.
    #: Wpisane tu, a nie odpytywane z lasu:
    #: las jednego zdania waży tyle, ile jego tablica,
    #: a werdyktów trzyma się naraz tyle, ile dokument ma zdań.
    przyłączenia: tuple[Przyłączenie, ...] = ()
    #: Konstytuenty, w których czytania się różnią poza zasięgiem streszczenia,
    #: z tej samej deklaracji co :attr:`różniące`.
    rozbieżności: tuple[Rozbieżność, ...] = ()

    @property
    def valid(self) -> bool:
        return self.ile == 1

    @property
    def ambiguous(self) -> bool:
        return self.ile > 1

    @property
    def rejected(self) -> bool:
        return self.ile == 0

    @property
    def status(self) -> str:
        """Which of the three the sentence is, as the verdict a reader is shown."""
        if self.valid:
            return "valid"
        return "ambiguous" if self.ambiguous else "rejected"

ile = 0 class-attribute instance-attribute

readings = field(default_factory=list) class-attribute instance-attribute

furthest = None class-attribute instance-attribute

truncated = False class-attribute instance-attribute

różniące = () class-attribute instance-attribute

przyłączenia = () class-attribute instance-attribute

rozbieżności = () class-attribute instance-attribute

valid property

ambiguous property

rejected property

status property

Which of the three the sentence is, as the verdict a reader is shown.

__init__(ile=0, readings=list(), furthest=None, truncated=False, różniące=(), przyłączenia=(), rozbieżności=())

Rozbieżność dataclass

Konstytuent, który czyta się kilkoma sposobami tam, gdzie streszczenie nie zagląda.

Streszczenie nazywa wypełnienie roli i gospodarza przyłączenia, więc dwa czytania różne czymkolwiek innym wychodzą z niego jednym napisem, a werdykt mówi wtedy samą liczbę czytań i czyta się jak usterka narzędzia. Poza zasięgiem streszczenia zostają dwa miejsca: wnętrze wypełnienia jednej roli i wnętrze zdania podrzędnego (Deklaracja.podrzędne). zainteresowana rada gminy jest raz przymiotnikiem przed rzeczownikiem, a raz rzeczownikiem z dopełniaczem po nim, i podmiotem jest w obu ten sam napis; że organ gminy wydaje przepis różni podmiot i dopełnienie, tyle że tamtego zdania, a nie tego.

Lematu wpis nie nazywa, choć różnica bywa właśnie lematem. Nazwałby to, czego liczba czytań obok niego nie liczy: część mowy i lemat są z tożsamości czytania wyłączone rozmyślnie (Node.signature), więc dwa czytania różne samym lematem są jednym.

Streszczenia niesie wpis dlatego, że streszczenie zdania ich nie niesie: rola z wnętrza zdania podrzędnego jest rolą tego zdania, a nie tego nad nim, więc dopiero streszczone osobno mówi, czym te czytania się różnią — w Ustawa mówi, że organ gminy wydaje przepis. podmiotem jest raz organ gminy, a raz przepis. Grupa imienna roli zdania nie nosi, więc oba jej kształty streszczają się pustym słownikiem i po odsianiu powtórzeń zostaje z nich jedno streszczenie: różnicę niesie tam głowa, której streszczenie nie nazywa (todo/).

Source code in olski/parse/podsumowanie.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@dataclass(frozen=True)
class Rozbieżność:
    """Konstytuent, który czyta się kilkoma sposobami tam, gdzie streszczenie nie zagląda.

    Streszczenie nazywa wypełnienie roli i gospodarza przyłączenia,
    więc dwa czytania różne czymkolwiek innym wychodzą z niego jednym napisem,
    a werdykt mówi wtedy samą liczbę czytań i czyta się jak usterka narzędzia.
    Poza zasięgiem streszczenia zostają dwa miejsca:
    wnętrze wypełnienia jednej roli i wnętrze zdania podrzędnego
    (:attr:`Deklaracja.podrzędne`).
    ``zainteresowana rada gminy`` jest raz przymiotnikiem przed rzeczownikiem,
    a raz rzeczownikiem z dopełniaczem po nim, i podmiotem jest w obu ten sam napis;
    ``że organ gminy wydaje przepis`` różni podmiot i dopełnienie,
    tyle że tamtego zdania, a nie tego.

    Lematu wpis nie nazywa, choć różnica bywa właśnie lematem.
    Nazwałby to, czego liczba czytań obok niego nie liczy:
    część mowy i lemat są z tożsamości czytania wyłączone rozmyślnie
    (:meth:`Node.signature`), więc dwa czytania różne samym lematem są jednym.

    Streszczenia niesie wpis dlatego, że streszczenie zdania ich nie niesie:
    rola z wnętrza zdania podrzędnego jest rolą tego zdania, a nie tego nad nim,
    więc dopiero streszczone osobno mówi, czym te czytania się różnią —
    w ``Ustawa mówi, że organ gminy wydaje przepis.``
    podmiotem jest raz ``organ gminy``, a raz ``przepis``.
    Grupa imienna roli zdania nie nosi, więc oba jej kształty streszczają się
    pustym słownikiem i po odsianiu powtórzeń zostaje z nich jedno streszczenie:
    różnicę niesie tam głowa, której streszczenie nie nazywa (``todo/``).
    """

    #: Formy konstytuenta, czyli to, co autor ma przepisać.
    konstytuent: str
    #: Ile czytań ten konstytuent ma, liczone tak jak :attr:`Result.ile` liczy zdanie.
    ile: int
    #: Streszczenia tych czytań, każde raz (:func:`streszczenia`).
    #: Pola bez wartości domyślnej, bo jedno streszczenie jest tu twierdzeniem
    #: o konstytuencie: znaczy, że streszczenie tej różnicy nie widzi.
    czytania: tuple[tuple[dict[str, str], ...], ...]

konstytuent instance-attribute

ile instance-attribute

czytania instance-attribute

__init__(konstytuent, ile, czytania)

ciało_koordynuje(etykieta, córki)

Czy ciało o tych córkach koordynuje: etykieta powtórzona wśród nich, a znak obok.

Ciąg współrzędny jest resztą ciągu po odjęciu członu, więc symbol koordynacji stoi wśród własnych córek. Liczba córek-konstytuentów tego nie mówi: grupa_imienna → człon_imienny zdanie_względne ma je dwie i koordynacją nie jest. Samo powtórzenie symbolu też go nie mówi, bo nad ciągiem stoi jeszcze okolicznik zdaniowy dochodzący do całego ciągu, który powtarza go tak samo. Rozdziela je znak: koordynacja spina członów słowem, a przecinek okolicznika należy do konstytuentu, który spójnik tworzy, więc słowem w tym ciele nie stoi. Znak wchodzi tu pustą nazwą, bo Pozycja liścia etykiety nie ma, i po tym samym poznają go pozostałe dwa wejścia.

Pytają o to kryterium trzy miejsca: nawias w napisie roli (_koordynuje), wybór przemilczany wśród rozbieżności (Las._nazwany_gdzie_indziej) i pomiar różnicowy (koordynuje w harness/ruch.py). Stoi w jednym, bo rozejście tych trzech widać dopiero w liczbach, a niezmiennik, na którym ono stoi, pilnuje tests/test_subset.py.

Source code in olski/parse/streszczenie.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
def ciało_koordynuje(etykieta: str | None, córki: Iterable[str | None]) -> bool:
    """Czy ciało o tych córkach koordynuje: etykieta powtórzona wśród nich, a znak obok.

    Ciąg współrzędny jest resztą ciągu po odjęciu członu,
    więc symbol koordynacji stoi wśród własnych córek.
    Liczba córek-konstytuentów tego nie mówi:
    `grupa_imienna → człon_imienny zdanie_względne` ma je dwie i koordynacją nie jest.
    Samo powtórzenie symbolu też go nie mówi,
    bo nad ciągiem stoi jeszcze okolicznik zdaniowy dochodzący do całego ciągu,
    który powtarza go tak samo.
    Rozdziela je znak: koordynacja spina członów słowem,
    a przecinek okolicznika należy do konstytuentu, który spójnik tworzy,
    więc słowem w tym ciele nie stoi.
    Znak wchodzi tu pustą nazwą, bo :class:`Pozycja` liścia etykiety nie ma,
    i po tym samym poznają go pozostałe dwa wejścia.

    Pytają o to kryterium trzy miejsca:
    nawias w napisie roli (:func:`_koordynuje`),
    wybór przemilczany wśród rozbieżności (:meth:`Las._nazwany_gdzie_indziej`)
    i pomiar różnicowy (``koordynuje`` w ``harness/ruch.py``).
    Stoi w jednym, bo rozejście tych trzech widać dopiero w liczbach,
    a niezmiennik, na którym ono stoi, pilnuje ``tests/test_subset.py``.
    """
    nazwy = list(córki)
    return etykieta in nazwy and None in nazwy

describe(node, deklaracja)

Streszczenie czytania: co stoi w której roli i do czego doszedł modyfikator.

Streszczeń jest tyle, ile zdanie ma zdań składowych, po jednym na składowe, bo każde z nich obsadza role własnym materiałem (zakresy dzieli między nie zdanie), i widać w nich przez to całe zdanie współrzędne. Czemu nie jedno na zdanie i co ten podział kosztuje, mówi docs/parsowanie.md#werdykt-jest-zapytaniem-o-las-a-nie-listą-czytań.

Dwa czytania jednego zdania gdzieś się różnią, a streszczenie pokazuje tę różnicę temu, kto ma zdanie poprawić. Same role tego nie pokazują, bo grupa przyimkowa dochodzi raz do jednej głowy, a raz do drugiej, i formy stojące nad nią zostają wtedy te same: koszt szynki z dodatkami jest tym samym dopełnieniem niezależnie od tego, czy z dodatkami doszło do koszt, czy do szynki. Rola przyłączana dostaje więc obok wypełnienia to, co modyfikator określa.

Drugim takim miejscem jest granica członu w ciągu współrzędnym, i tam odpowiada nawias, a nie nazwa obok: granica biegnie wewnątrz wypełnienia, więc widać ją tylko w samym napisie. Co dostaje nawias, a co nie, mówi _nawiasuj.

Żądają tego role przyłączane, bo ich gospodarza gramatyka zostawia nierozstrzygniętego rozmyślnie: podmiot i dopełnienie rozstrzyga przypadek, a pozycje przyłączeniowe stoją po to, żeby dać oba czytania (docs/subset.md#przyjąć-koszt-to-znaczy-dać-oba-czytania-wszędzie). Dopisane jest wypełnienie, a nie pozycja obok niego, więc Deklaracja.role zostaje listą ról.

Rola przyłączana jest nazwana pierwszym wystąpieniem w składowym (_streszcz), więc dwa czytania różne miejscem drugiego okolicznika tej samej roli wychodzą stąd jednym napisem. Streszczeń wychodzi przez to nie więcej niż czytań, bo powtórzone na listę nie wchodzi (Verdict.readings w olski/werdykt.py); zdanie, którego to nie rozstrzyga, rozstrzyga Las.przyłączenia, gdzie wpisów jest tyle, ile nierozstrzygniętych wyborów.

Zdanie podrzędne jest z tego wyszukiwania wyjęte (Deklaracja.podrzędne), bo streszczane jest zdanie zewnętrzne. Zdanie współrzędne wyjęte nie jest, bo jego role są rolami tego samego zdania; osobne jest tylko streszczenie, w którym one stoją.

Source code in olski/parse/streszczenie.py
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def describe(node: Node, deklaracja: Deklaracja) -> tuple[dict[str, str], ...]:
    """Streszczenie czytania: co stoi w której roli i do czego doszedł modyfikator.

    Streszczeń jest tyle, ile zdanie ma zdań składowych, po jednym na składowe,
    bo każde z nich obsadza role własnym materiałem
    (:func:`zakresy` dzieli między nie zdanie),
    i widać w nich przez to całe zdanie współrzędne.
    Czemu nie jedno na zdanie i co ten podział kosztuje, mówi
    docs/parsowanie.md#werdykt-jest-zapytaniem-o-las-a-nie-listą-czytań.

    Dwa czytania jednego zdania gdzieś się różnią,
    a streszczenie pokazuje tę różnicę temu, kto ma zdanie poprawić.
    Same role tego nie pokazują,
    bo grupa przyimkowa dochodzi raz do jednej głowy, a raz do drugiej,
    i formy stojące nad nią zostają wtedy te same:
    ``koszt szynki z dodatkami`` jest tym samym dopełnieniem niezależnie od tego,
    czy ``z dodatkami`` doszło do ``koszt``, czy do ``szynki``.
    Rola przyłączana dostaje więc obok wypełnienia to, co modyfikator określa.

    Drugim takim miejscem jest granica członu w ciągu współrzędnym,
    i tam odpowiada nawias, a nie nazwa obok:
    granica biegnie wewnątrz wypełnienia, więc widać ją tylko w samym napisie.
    Co dostaje nawias, a co nie, mówi :func:`_nawiasuj`.

    Żądają tego role przyłączane,
    bo ich gospodarza gramatyka zostawia nierozstrzygniętego rozmyślnie:
    podmiot i dopełnienie rozstrzyga przypadek,
    a pozycje przyłączeniowe stoją po to, żeby dać oba czytania
    (docs/subset.md#przyjąć-koszt-to-znaczy-dać-oba-czytania-wszędzie).
    Dopisane jest wypełnienie, a nie pozycja obok niego,
    więc :attr:`Deklaracja.role` zostaje listą ról.

    Rola przyłączana jest nazwana pierwszym wystąpieniem w składowym
    (:func:`_streszcz`), więc dwa czytania różne miejscem drugiego okolicznika
    tej samej roli wychodzą stąd jednym napisem.
    Streszczeń wychodzi przez to nie więcej niż czytań, bo powtórzone na listę
    nie wchodzi (``Verdict.readings`` w ``olski/werdykt.py``);
    zdanie, którego to nie rozstrzyga, rozstrzyga :meth:`Las.przyłączenia`,
    gdzie wpisów jest tyle, ile nierozstrzygniętych wyborów.

    Zdanie podrzędne jest z tego wyszukiwania wyjęte
    (:attr:`Deklaracja.podrzędne`), bo streszczane jest zdanie zewnętrzne.
    Zdanie współrzędne wyjęte nie jest, bo jego role są rolami tego samego
    zdania; osobne jest tylko streszczenie, w którym one stoją.
    """
    return tuple(
        _streszcz(node, deklaracja, zakres) for zakres in zakresy(node, deklaracja.składowe)
    )

liście(drzewo)

Liście tego drzewa, w porządku zdania.

Pyta o nie werdykt, bo odczytania formy niesie liść (Leaf.odczytania), a nie węzeł nad nim.

Source code in olski/parse/streszczenie.py
31
32
33
34
35
36
37
38
39
40
41
def liście(drzewo: Tree) -> Iterator[Leaf]:
    """Liście tego drzewa, w porządku zdania.

    Pyta o nie werdykt, bo odczytania formy niesie liść
    (:attr:`Leaf.odczytania`), a nie węzeł nad nim.
    """
    if isinstance(drzewo, Leaf):
        yield drzewo
        return
    for dziecko in drzewo.children:
        yield from liście(dziecko)

sklej_formy(formy)

Formy jako jeden napis, tak jak stoją w zdaniu.

Przecinek jest osobnym segmentem, więc sklejenie przez sam odstęp daje wolni , równi, czego autor w swoim zdaniu nie napisał.

Source code in olski/parse/streszczenie.py
44
45
46
47
48
49
50
51
52
53
54
55
def sklej_formy(formy: Iterable[str]) -> str:
    """Formy jako jeden napis, tak jak stoją w zdaniu.

    Przecinek jest osobnym segmentem, więc sklejenie przez sam odstęp
    daje ``wolni , równi``, czego autor w swoim zdaniu nie napisał.
    """
    napis = ""
    for forma in formy:
        if napis and forma not in PRZYLEGAJĄCE:
            napis += " "
        napis += forma
    return napis

streszczenia(drzewa, deklaracja)

Streszczenia tych drzew, każde raz i w kolejności pierwszego wystąpienia.

Dwa drzewa różne poza zasięgiem describe wychodzą z niego jednym napisem, a napis wypisany drugi raz nie mówi nic ponad ten nad sobą. Wołają to dwa miejsca — odczytania zdania i kształty konstytuentu — i pierwsze z nich pokazuje, ile powtórzeń bywa: zdanie o siedmiu wyrażeniach przyimkowych ma odczytań ponad sto, a napisów różnych kilka.

Source code in olski/parse/streszczenie.py
137
138
139
140
141
142
143
144
145
146
147
148
def streszczenia(
    drzewa: Iterable[Node], deklaracja: Deklaracja
) -> list[tuple[dict[str, str], ...]]:
    """Streszczenia tych drzew, każde raz i w kolejności pierwszego wystąpienia.

    Dwa drzewa różne poza zasięgiem :func:`describe` wychodzą z niego jednym
    napisem, a napis wypisany drugi raz nie mówi nic ponad ten nad sobą.
    Wołają to dwa miejsca — odczytania zdania i kształty konstytuentu — i pierwsze
    z nich pokazuje, ile powtórzeń bywa: zdanie o siedmiu wyrażeniach
    przyimkowych ma odczytań ponad sto, a napisów różnych kilka.
    """
    return [streszczenie for streszczenie, _drzewa in streszczone(drzewa, deklaracja)]

streszczone(drzewa, deklaracja)

Te drzewa pogrupowane po streszczeniu, w kolejności pierwszego wystąpienia.

Odsiew ze streszczenia jest tu jeden na oba pytania, bo werdykt pyta o jedno i drugie: o same streszczenia i o to, czym forma stoi pod każdym z nich (Verdict.morfologia w olski/werdykt.py). Napisany dwa razy rozjechałby się po cichu i wtedy morfologia opisywałaby inne streszczenie niż to, które nad nią wypisano.

Grupa ma pod sobą drzewa, a nie jedno z nich, bo streszczenie zbiera czasem kilka kształtów, a odczytania formy nie muszą być w nich te same.

Source code in olski/parse/streszczenie.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def streszczone(
    drzewa: Iterable[Node], deklaracja: Deklaracja
) -> list[tuple[tuple[dict[str, str], ...], list[Node]]]:
    """Te drzewa pogrupowane po streszczeniu, w kolejności pierwszego wystąpienia.

    Odsiew ze :func:`streszczenia` jest tu jeden na oba pytania, bo werdykt pyta
    o jedno i drugie: o same streszczenia i o to, czym forma stoi pod każdym z
    nich (``Verdict.morfologia`` w ``olski/werdykt.py``). Napisany dwa razy
    rozjechałby się po cichu i wtedy morfologia opisywałaby inne streszczenie
    niż to, które nad nią wypisano.

    Grupa ma pod sobą drzewa, a nie jedno z nich, bo streszczenie zbiera czasem
    kilka kształtów, a odczytania formy nie muszą być w nich te same.
    """
    wynik: list[tuple[tuple[dict[str, str], ...], list[Node]]] = []
    for drzewo in drzewa:
        streszczenie = describe(drzewo, deklaracja)
        for gotowe, pod_nim in wynik:
            if gotowe == streszczenie:
                pod_nim.append(drzewo)
                break
        else:
            wynik.append((streszczenie, [drzewo]))
    return wynik

w_zakresie(node, rola, podrzędne, zakres)

Węzły tej roli, które należą do tej części zdania, w porządku zdania.

Rolę przypisuje zakresowi jej początek, a nie cała rozpiętość, i mówi to _streszcz. Pytają o to dwa podsumowania — streszczenie oraz wiersz żądania (olski/werdykt.py) — więc kryterium jest jedno: oba mówią o tym samym zdaniu składowym i wiersz jednego ma stać obok wiersza drugiego.

Source code in olski/parse/streszczenie.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def w_zakresie(
    node: Node, rola: str, podrzędne: Sequence[str], zakres: tuple[int, int]
) -> list[Node]:
    """Węzły tej roli, które należą do tej części zdania, w porządku zdania.

    Rolę przypisuje zakresowi jej początek, a nie cała rozpiętość, i mówi to
    :func:`_streszcz`. Pytają o to dwa podsumowania — streszczenie oraz wiersz
    żądania (``olski/werdykt.py``) — więc kryterium jest jedno: oba mówią o tym
    samym zdaniu składowym i wiersz jednego ma stać obok wiersza drugiego.
    """
    return [
        węzeł
        for węzeł in node.find(rola, podrzędne)
        if zakres[0] <= węzeł.span[0] < zakres[1]
    ]

zakresy(node, symbole)

Zdanie podzielone na tyle części, ile ma zdań składowych, po jednej na składowe.

Granicą jest początek składowego następnego, a nie koniec poprzedniego, więc każde słowo zdania wpada dokładnie do jednej części i nie ginie z niej to, co między składowymi stoi: spójnik, przecinek, a za ostatnim składowym dopowiedzenie i kropka. Zdanie o jednym składowym wychodzi stąd całe i jedną częścią, tak samo jak konstytuent, który składowego nie ma pod sobą wcale.

Source code in olski/parse/streszczenie.py
194
195
196
197
198
199
200
201
202
203
204
205
206
def zakresy(node: Node, symbole: Sequence[str]) -> list[tuple[int, int]]:
    """Zdanie podzielone na tyle części, ile ma zdań składowych, po jednej na składowe.

    Granicą jest początek składowego następnego, a nie koniec poprzedniego,
    więc każde słowo zdania wpada dokładnie do jednej części
    i nie ginie z niej to, co między składowymi stoi: spójnik, przecinek,
    a za ostatnim składowym dopowiedzenie i kropka.
    Zdanie o jednym składowym wychodzi stąd całe i jedną częścią,
    tak samo jak konstytuent, który składowego nie ma pod sobą wcale.
    """
    początki = _początki_składowych(node, symbole)
    granice = [node.span[0], *początki[1:], node.span[1]]
    return [(granice[i], granice[i + 1]) for i in range(len(granice) - 1)]

parse(grammar, segments, start=None, deklaracja=None, zatrzymanie=True)

Rozbierz zdanie i zapytaj las, ile czytań ma, które pokazać i co zostawia otwarte.

Source code in olski/parse/__init__.py
88
89
90
91
92
93
94
95
96
def parse(
    grammar: Grammar,
    segments: list[Segment],
    start: str | None = None,
    deklaracja: Deklaracja | None = None,
    zatrzymanie: bool = True,
) -> Result:
    """Rozbierz zdanie i zapytaj las, ile czytań ma, które pokazać i co zostawia otwarte."""
    return podsumuj(las(grammar, segments, start), deklaracja, zatrzymanie=zatrzymanie)

podsumuj(zbudowany, deklaracja=None, zatrzymanie=True)

Podsumowania, jakie werdykt bierze z gotowego lasu.

Osobno od parse, bo pomiar buduje las sam i pyta go jeszcze o coś, czego werdykt nie niesie; bez tego rozbierałby zdanie drugi raz.

Bez deklaracji werdykt jest samą liczbą i listą czytań; co ona niesie i czemu jest jedna, mówi Deklaracja.

O to, dokąd analiza doszła, pyta się na żądanie, bo nad zdaniem odrzuconym jest to najdroższe z podsumowań, jakie ta funkcja bierze: Las.najdalszy przechodzi wtedy tablicę drugi raz, i to drożej, niż kosztowało samo jej zbudowanie: przejście drugie unifikuje przebyte ciała, czego budowanie nie robi wcale. Nad zdaniem, które ma czytanie, oddaje koniec zdania bez przejścia. Czyta tę odpowiedź odrzucenie mówiące, gdzie stanęło (explain w olski/werdykt.py) oraz ranking blokerów (olski/pokrycie.py); przebieg, który liczy same werdykty, nie czyta jej wcale. Kto nie pyta, dostaje w Result.furthest stan „nikt nie pytał”.

Warunku na samo zdanie tutaj nie ma, bo zmierzony nic nie kupił. Werdykt nad zdaniem, którego forma nie ma licencji, nazywa tę formę i zatrzymania nie czyta, a takich zdań jest większość odrzuconych, tyle że każde umiera wcześnie i jego tablica jest mała. Cena rośnie w zdaniach, które dochodzą daleko i nie domykają się, a tych warunek na licencję nie dotyka.

Source code in olski/parse/__init__.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def podsumuj(
    zbudowany: Las, deklaracja: Deklaracja | None = None, zatrzymanie: bool = True
) -> Result:
    """Podsumowania, jakie werdykt bierze z gotowego lasu.

    Osobno od :func:`parse`, bo pomiar buduje las sam i pyta go jeszcze o coś,
    czego werdykt nie niesie; bez tego rozbierałby zdanie drugi raz.

    Bez deklaracji werdykt jest samą liczbą i listą czytań;
    co ona niesie i czemu jest jedna, mówi :class:`Deklaracja`.

    O to, dokąd analiza doszła, pyta się na żądanie,
    bo nad zdaniem odrzuconym jest to najdroższe z podsumowań, jakie ta funkcja bierze:
    :meth:`Las.najdalszy` przechodzi wtedy tablicę drugi raz, i to drożej,
    niż kosztowało samo jej zbudowanie: przejście drugie unifikuje przebyte ciała,
    czego budowanie nie robi wcale.
    Nad zdaniem, które ma czytanie, oddaje koniec zdania bez przejścia.
    Czyta tę odpowiedź odrzucenie mówiące, gdzie stanęło
    (``explain`` w ``olski/werdykt.py``) oraz ranking blokerów (``olski/pokrycie.py``);
    przebieg, który liczy same werdykty, nie czyta jej wcale.
    Kto nie pyta, dostaje w ``Result.furthest`` stan „nikt nie pytał”.

    Warunku na samo zdanie tutaj nie ma, bo zmierzony nic nie kupił.
    Werdykt nad zdaniem, którego forma nie ma licencji, nazywa tę formę
    i zatrzymania nie czyta, a takich zdań jest większość odrzuconych,
    tyle że każde umiera wcześnie i jego tablica jest mała.
    Cena rośnie w zdaniach, które dochodzą daleko i nie domykają się,
    a tych warunek na licencję nie dotyka.
    """
    ile = zbudowany.ile_czytań()
    readings: list[Node] = []
    for tree in zbudowany.czytania():
        readings.append(tree)
        if len(readings) >= MAX_READINGS:
            break
    różniące, przyłączenia, rozbieżności = (
        ((), (), ())
        if deklaracja is None
        else (
            zbudowany.różniące(deklaracja),
            tuple(zbudowany.przyłączenia(deklaracja)),
            tuple(zbudowany.rozbieżności(deklaracja)),
        )
    )
    return Result(
        ile,
        readings,
        zbudowany.najdalszy() if zatrzymanie else None,
        truncated=ile > len(readings),
        różniące=różniące,
        przyłączenia=przyłączenia,
        rozbieżności=rozbieżności,
    )