Specyfikacja podjęzyka WCCL L2

Język L2 to w miarę autonomiczny podjęzyk reguł i dopasowań.

Są dwie kategorie reguł:
  1. Reguły tagowania — działające na poziomie tokenów. Odpowiadają one regułom starego JOSKIPI używanym przez TaKIPI oraz regułom z Disastera służącym do znakowania chunków.
  2. Reguły dopasowania — działające na całym zdaniu. Jest to mechanizm zaproponowany przez Michała Marcińczuka. Pozwala na wygodniejszy zapis reguł — w sposób bardziej zbliżony do wyrażeń morfosyntaktycznych Chorwatów czy Spejda.
Wyrażenie WCCL L2 to
  • wyrażenie WCCL L1,
  • reguła tagowania lub
  • reguła dopasowania.

Reguły nie są wyrażeniami funkcyjnymi — mogą one mieć skutki uboczne.

TODO składnia całego języka? a może zrobić L3?

Nowy typ danych: wektor dopasowań i 3 typy dopasowań

Typ ten definiujemy rekurencyjnie.

Wektor dopasowań to lista elementów typu dopasowanie (w szczególności może być pusta).

Składnia stałych: MATCH() (pusta), MATCH(EL1, EL2, ..., ELn).
Domyślną wartością tego typu jest pusty wektor. Nie ma żadnego none, nowhere itp.
Składnia zmiennych: $m:Var.

Dopasowanie to nadklasa; mamy 3 typy:
  1. dopasowanie tokenu TOK[pos] (pos to pozycja, pod którą znajduje się token),
  2. dopasowanie anotacji ANN[pos,name] (pos to pierwsza pozycja anotacji (ciągłego kawałka frazy), name to napis—nazwa kanału),
  3. wektor dopasowań @MATCH.

Jak widać, wektor może zawierać wektory (dowolne zagnieżdżenie). Przykładowo: MATCH(TOK[2], TOK[3], MATCH(TOK[4], TOK[5])).

Reguły tagowania

Reguła tagowania określana jest przez nazwę, ciąg akcji do wykonania oraz opcjonalny warunek (jeśli warunek nie zostanie podany, to równoważne jest to warunkowi True). Reguły tagowania odpalane są na bieżącej pozycji w zdaniu. Parser powinien pozwalać na przeparsowanie ciągu reguł i udostępnienie wygodnego mechanizmu, który pozwala na odpalenie całego ciągu reguł na zdaniu w dwóch trybach:
  1. jednorazowo — iterujemy po pozycjach w zdaniu i dla każdej pozycji kolejno próbujemy wszystkich reguł oraz
  2. iteracyjnie — dopóki są zmiany. Reguła zwraca czy dokonała jakichkolwiek zmian w zdaniu.

Akcje reguł są specjalnego typu wyrażeniami, które przypominają predykaty, lecz mają (mogą mieć) skutki uboczne.

Składnia i semantyka reguł

rule(NAME, ACTIONS)
rule(NAME, COND, ACTIONS)

NAME to string określający nazwę reguły (przydatne w diagnozowaniu), COND to dowolny predykat, a ACTIONS to niepusty ciąg akcji oddzielonych przecinkami. Odpalenie reguły zaczyna się od sprawdzenia warunku. Jeśli warunek jest spełniony, akcje uruchamiane są sekwencyjnie. Każda akcja zwraca informację o tym, czy reguła dokonała jakichkolwiek zmian w zdaniu. Reguła zwraca wartość, która określa, czy którakolwiek z akcji dokonała zmian. Uwaga: to nie jest operator and; gdy warunek jest spełniony, to odpalane są wszystkie akcje — niezależnie od tego, czy pierwsza akcja dokonała zmian.

Uwaga: zwracana wartość określa, czy którakolwiek z reguł dokonała zmian w zdaniu. W skrajnym wypadku zwrócona może być wartość True, mimo że efektywnie zmian nie będzie — stać się tak może jeśli druga akcja cofa działanie pierwszej.

Przykładowa reguła zawierająca warunek:

rule("r1",
   in(class[0], {subst,xxx}),
   delete(
      equal(class[0], {xxx})
   )
)

Reguła może być prostsza, np.:

rule("r2",
   delete(
      equal(cas[0], {voc})
   )
)

Ciąg reguł to wyrażenie o następującej składni:

tag_rules(
   RULE; RULE2; ..., RULEn
)

Ciąg reguł nie może być pusty. Ciąg jest parsowany do obiektu pozwalającego na aplikację reguł na podanym zdaniu.

Akcje

Część akcji ma dwa warianty: z podaniem pozycji oraz bez. Warianty bez podawania pozycji są równoznaczne z podaniem pozycji 0.

Akcje badające leksem po leksemie

Akcje te działają w specyficzny sposób: wyjmujemy wszystkie leksemy z tokenu na podanej pozycji, po czym każdorazowo wkładamy z powrotem po jednym leksemie i sprawdzamy czy spełniony jest warunek (zależny od danego operatora). Otrzymujemy w ten sposób dwie grupy leksemów — te, które samodzielnie spełniają predykat (dokładniej: gdy jednoleksemowy token spełnia predykat) oraz pozostałe. Wynikiem działania operatora jest wstawienie nowego zbioru leksemów na miejsce starego. Nowy zbiór ustalany jest na podstawie wspomnianych grup oraz specyfiki konkretnego operatora.

delete(pos, pred), delete(pred) — pozostawia jedynie leksemy niespełniające predykatu, no chyba że wszystkie spełniają, to nie wprowadza żadnych zmian

select(pos, pred), select(pred) — pozostawia jedynie leksemy spełniające predykat, no chyba że żaden nie spełnia, to nie wprowadza żadnych zmian

TODO czy corpus2 pozwala zrobić relabel? czy to jest w ogóle bezpieczne i dobre? (używane to było, by traktować subst-ger jako jedną klasę; w Disasterze obsłużone były tylko klasy gramatyczne, może tak zostawić?)

relabel(pos, tset_op, pred), relabel(tset_op, pred) — leksemy niespełniające predykatu pozostają niezmienione, natomiast spełniające poddawane są korekcie tagów; korekta tagów polega na wstawieniu podanych w zbiorze uzyskanym z tset_op wartości w odpowiednie miejsce tagu, czyszcząc poprzednie wartości ustawianych atrybutów, łącznie z klasą gramatyczną (jeśli została podana)

Unifikacja zakresu

Unifikacja jest próbą wymuszenia uzgodnienia na podanym zakresie. W tym wypadku wymuszamy „bardzo słabe uzgodnienie”, tj. odpowiednik słabego uzgodnienia lecz bez szczególnego traktowania krańców zakresu (wszystkie tokeny z zakresu traktowane są jak środek zakresu słabego uzgodnienia). Wymuszenie polega na zostawieniu jedynie tych leksemów, które nie naruszają uzgodnienia. Zwracamy True tylko wtedy, gdy usuniemy przynajmniej jeden leksem.

unify(pos1, pos2, agr_attrs)

Disaster: ops_agr.p_unify

Akcje zmieniające anotacje

Zakresy są najpierw przycinane do granic zdania.

TODO: określić, co dzieje się z relacjami, głowami i opisem anotacji, gdy coś zmieniamy.

TODO: czy nazwy mark, unmark mogą się powtarzać między dwoma typami reguł?

mark(pos1, pos2, phrase) — w kanale o nazwie phrase znakuje anotację zaczynającą się od pos1, kończącą się na pos2; nardzędnik ustawiany jest na pos1.

mark(pos1, pos2, head_pos, phrase) — w kanale o nazwie phrase znakuje anotację zaczynającą się od pos1, kończącą się na pos2; nadrzędnik ustawiany jest na head_pos. Jeśli przekazany head_pos jest poza zakresem, powinno zgłosić wyjątek.

unmark(pos, phrase) — w kanale o nazwie phrase usuwa anotację, która przechodzi przez pozycję pos. Jeśli przez tę pozycję nie przechodzi żadna anotacja, nie dzieje się nic.

Uwaga: jeśli przez podany zakres przebiega jakakolwiek anotacja, żadna akcja nie jest wykonywana.

TODO mark token as head (rm prev head)

TODO mark & rm relation

Reguły dopasowania

UWAGA Oryginalny dokument Michała jest na dole strony Wymagania.

Składnia i semantyka reguł dopasowania

Jedna reguła dopasowania definiowana jest za pomocą słowa kluczowego apply (składnia poniżej).

Ciąg reguł dopasowania to wyrażenie o następującej składni:

match_rules(
   APPLY1; APPLY2; ..., APPLYn
)

Operacje na dopasowaniach

Operator empty zwraca True, jeśli dopasowanie jest pustym wektorem lub wektorem składającym się z samych wektorów pustych.

Definiujemy dwa pomocnicze operatory: first i last. Operatory te biorą dowolne dopasowanie i zwracają pozycję wskazującą na pierwszy i ostatni (odpowiednio) token należący do dopasowania. Jeśli dopasowaniem jest wektor, to przy ustalaniu pozycji pomijamy puste wektory w nim zawarte. Jeśli empty(vec), to first(vec) i last(vec) zwrócą nowhere.

Operator wyłuskujący element wektora. W każdej sytuacji, gdy operacji nie da się wykonać, zgłaszany jest wyjątek.

Składnia: vec -> int (indeksujemy od 1).

Składnia skrócona. Wszystkie operatory podjęzyka dopasowań, które oczekują elementu typu dopasowanie, mogą przyjąć składnię skróconą: M oznacza $m:_M,

Operator match i operatory z niego korzystające

Podstawowym elementem podjęzyka dopasowań jest operator match. Operator ten nie jest używany samodzielnie, lecz w ramach innych operatorów (jeśli zdecydujemy się na cukier składniowy, to w niektórych kontekstach użycia słowo match można będzie pominąć, a mimo to efektywnie operator będzie używany).

match(conditions)

Operator match próbuje dopasować podane w nim warunki (conditions) do kolejnych tokenów. Operator iteruje po kolejnych tokenach, zwiększając bieżącą pozycję w zdaniu. Iteracja wykonywana jest w ramach realizacji kolejnych kroków dopasowań. Dzięki temu, operatory sprawdzające kolejne warunki dopasowania mogą się odwołać do sprawdzanego właśnie tokenu za pomocą pozycji 0. Operator korzysta też ze zmiennej o nazwie $m:_M, gdzie trzymany jest wektor dopasowań1.
  1. match zawsze zaczyna od bieżącej pozycji w zdaniu; za jej zerowanie odpowiedzialny jest kod/operator korzystający z match
  2. match wykonuje dwie akcje: z każdym dopasowanym tokenem zwiększa wartość bieżącej pozycji w zdaniu oraz gromadzi kolejne elementy wektora (wskazujące dopasowania kolejnych kawałków reguły) — wektor ten zostanie zwrócony w przypadku powodzenia
  3. warunki (elementy listy conditions) rozpatrywane są sekwencyjnie; każdy spełniony warunek owocuje dodaniem do wektora dopasowań jednego elementu
  4. jeśli któryś z warunków nie jest spełniony, dopasowanie jest przerywane, a bieżąca pozycja w zdaniu ustawiana jest na wartość początkową + 1; zwracany jest wtedy „martwy” obiekt match (działający obiekt match uzyskamy tylko, gdy wszystkie warunki będą spełnione — wtedy bieżąca pozycja ustawiona będzie na pierwszym tokenie za dopasowaniem)

Warunki operatora match

Warunki operatora match to osobna klasa operatorów. Każdy warunek zwraca dwie rzeczy: (1) True/False oraz (2) element typu dopasowanie.

Operator korzystający z danego warunku decyduje, co zrobić ze zwróconym przez warunek dopasowaniem — np. w przypadku True dodać zwrócony element do własnego wektora dopasowań.

Typ warunek operatora match dzieli się na trzy podtypy:
  1. opakowanie na predykaty L0; zwraca True/False zgodnie z tym, co zwrócił dany predykat; wtedy operator może zwrócić TOK(pos) — wskazanie na sprawdzaną pozycję;
  2. operator zwracający True/False oraz ANN[pos, name]
  3. operator zwracający True/False oraz wektor dopasowań (może być pusty, nie musi to znaczyć, że się nie powiodło)

Pierwszy typ warunków to opakowane predykaty CCL — wtedy dopasowywany jest pojedynczy token i zwracana jego pozycja opakowana w dopasowanie. Warunki mogą być także specjalnymi operatorami określającymi dopasowania. Każdy taki operator w zależności od typu, może zwracać opakowanie na anotację lub wektor dopasowań.

repeat (zwraca wektor wektorów)
  • choć w jego składni nie oczekujemy słowa match, działa, jakby przyjmował zawsze operator match jako argument (warunki tego matcha zawarte są luzem w repeat)
  • tworzy pusty wektor dopasowań, po czym szukamy kolejnych powtórzeń matcha
  • z każdego spełnionego matcha brany jest wynik (wektor) i wstawiany do wektora zewnętrznego
  • jeśli ani razu nie uda się odpalić, zwracana jest wartość False oraz pusty wektor
  • w razie sukcesu zwraca więc wektor (listę powtórzeń) wektorów (odpowiadających kolejnym krokom dopasowań)
match($vec, <A>, repeat(match($vecinner, <B, C>)))

$vec[0] = MATCH ( A )
$vec[1] = MATCH ( MATCH (B, C), MATCH(B,C) )
optional
  • analogicznie do repeat, ale dopasuje się raz lub wcale
  • zawsze zwraca True (TODO no chyba, że da się wymyślić jakieś skrajnie niespójne wywołanie)
  • zwraca wektor dopasowań, który ma jeden element lub jest pusty (jeśli jednoelementowy, element jest wektorem)
text
  • operator przejmuje kontrolę nad iteracją, poczynając od obecnie przetwarzanego tokenu aż do skończenia podanego tekstu, bądź porażki (działanie podobne do repeat)
  • dokleja kolejne tokeny uwzględniając informację o spacji między tokenami i z każdym tokenem sprawdza, czy się nie pokrywa ze stringiem przekazanym jako argument operatora oraz czy nie zaszedł już za daleko (może zdarzyć się, że podany tekst mógłby się dopasować jedynie gdyby zmienić podział na tokeny — w takich sytuacjach przerywamy próbę gdy tylko wykryjemy, że już dalej dopasować się nie da)
  • jeśli przekazany string nie chce się dopasować (napotkamy niezgodność, bądź zdanie się urwie), cofamy bieżącą pozycję do wartości pocz. + 1 i zwracamy False
  • operator zwraca wektor zawierający kolejne tokeny
is
  • podobnie jak text, operator przejmuje kontrolę nad iteracją — tym razem aż do końca frazy bądź porażki
  • operator dopasowuje się do anotacji o podanej nazwie,
  • musi być ustawiony na początku tej anotacji, by ją rozpoznać
  • zwraca dopasowanie na anotację, bądź „martwy” match, gdy się nie uda
oneof(variant(v1), variant(v2), … )
  • operator sprawdza kolejno warianty będące de facto dopasowaniami;
  • pierwsze udane dopasowanie przerywa działanie i zwracany jest jego wynik;
  • jeśli się nie uda dopasować, to zwracane to, co zwróciłby nieudany match
longest(variant(v1), variant(v2), … )
  • operator działa podobnie do oneof,
  • różnica: wszystkie dopasowania są wykonywane i wybierane jest dopasowanie najdłuższe (w tokenach)

TODO czy da się językiem dopasowań zapisać coś takiego: przeiteruj po anotacjach danego typu, dla każdej z nich znajdź pierwszy lub ostatni token spełniający ograniczenia i zrób z nim coś (oznakuj jako głowę)

TODO macro defs, decsn classes, amb classes, ac seq,
TODO co z lex, trzeba by najpierw określić mechanizm przechowywania leksykonów; "lex" LPAREN s_strings = es_any [scope] COMMA lex_name: STRING RPAREN

Użycie dopasowań: apply

Operator apply iteruje po całym zdaniu i uruchamia MATCH. Dla każdego dopasowania sprawdzane są najpierw warunki (o ile zostały podane jakiekolwiek), po czym uruchamiane są akcje (w przypadku, gdy warunki spełnione).

apply(MATCH, CONDITIONS, ACTIONS)

Dopuszczalna składnia (dwa warianty):

apply(
  match( dopasowania ),
  actions( akcje )
)
apply(
  match( dopasowania ),
  cond( warunki ),
  actions( akcje )
)
Gdzie:
  • dopasowania to lista operatorów dopasowujących elementy w zdaniu (is, equal, text, itd.),
  • warunki to lista operatorów logicznych (np. equal, ann, annsub),
  • akcje to lista akcji (mark, unmark).

Token obecnie dopasowywany zawsze znajduje się pod pozycją $0$ — mogą z tego korzystać elementy operatora match.

Iteracja działa w następujący sposób:
  1. Bieżąca pozycja jest ustawiana na zero. Z tego miejsca zaczynamy iterację zewnętrzną.
  2. Dla każdej pozycji bieżącej wykonujemy następującą operację:
    1. zapamiętujemy wartość bieżącej pozycji w zdaniu
    2. jednorazowo uruchamiamy MATCH, sprawdzamy CONDITIONS i jeśli spełnione, wstawiamy MATCH( wynik dopasowania ) (tj. wektor jednoelementowy) w zmienną $m:_M, po czym kolejno odpalamy akcje
  3. Jeśli się udało dopasować, wartość bieżącej pozycji pozostaje ustawiona na pierwszym tokenie za dopasowaniem (to działanie zapewnione jest przez operator match)
  4. Jeśli się nie udało, przywracamy wartość równą zapamiętanej wartości początkowej + 1 (czyli w wyniku nieudanej próby dopasowania pomijamy tylko jeden token)

Ustawienie wartości zmiennej $m:_M to zabieg pozwalający na późniejsze odwołania do wektora. Wynik matcha opakowujemy w wektor jednoelementowy, ponieważ istnieje też drugi wariant, który zakłada obecność dwóch matchów — a w przyszłości być może będziemy chcieli mieć ich więcej. Uwaga: na poziomie parsera należy zapewnić mechanizm, by tłumaczyć wyrażenia typu :1, 1:1 w składni operatorów pobierających elementy tablicy na odwołanie do zmiennej $m:_M.

Uwaga: apply ahead jest mniej istotny niż apply.

apply_ahead(MATCH, MATCH_AHEAD, ACTIONS) TODO uaktualnić składnię
apply_ahead(MATCH, MATCH_AHEAD, CONDITION, ACTIONS)

  1. Iteracja zewnętrzna (bieżącą pozycją w zdaniu) działa jak w zwykłym apply
  2. Gdy uda się dopasować,
    1. zapamiętujemy bieżącą pozycję w zdaniu,
    2. odpalamy tymczasowo drugą iterację, sprawdzając drugie dopasowanie (MATCH_AHEAD),
    3. tutaj idziemy tylko do pierwszego dopasowania (lub porażki); jeśli się uda dopasować, wstawiamy (ARRAY(m1, m2) w zmienną $m:_M, gdzie m1 i m2 to wyniki matchów; sprawdzamy warunki i odpalamy akcje
  3. Jeśli udało się dopasować MATCH (na MATCH_AHEAD nie nakładamy żadnych warunków), to pomijamy długość MATCH
  4. Jeśli nie, pomijamy jeden token
  5. Operator zwraca wektor zawierającą jeszcze jeden poziom zagłębienia; zewnętrzny wektor zawiera bowiem dwa wektory, pierwszy odpowiadający części MATCH, drugi — części MATCH_AHEAD.

Warunki operatora apply (post-conditions)

Operator apply opcjonalnie przyjmuje listę warunków. Warunek taki jest specjalnym typem operatora.

Warunek operatora apply to:
  1. opakowany predykat CCL L0 (składnia identyczna jak w L0)
W ramach L2 dodajemy dwa nowe predykaty:
  1. operator ann(m1, name) bądź ann(m1, m2, name) — sprawdza czy między granice dopasowania (dopasowań) pokrywają się z granicami frazy o nazwie name (wystarczy, by pokrywały się z ciągłym kawawłkiem nieciągłej frazy)
  2. operator annsub(m1, name) bądź annsub(m1, m2, name) — sprawdza czy między granicami dopasowania (dopasowań) rozciąga się kawałek frazy (podciąg) podanego typu (technicznie, cały ten zakres musi być wypełniony jednym niezerowym numerkiem)

Oba z nich zakładają, że przedział musi być niepusty.

Uwaga: operatory ann i annsub mogą korzystać ze skrótów składniowych dla dopasowań (np. :2).

Akcje

Operator mark znakuje anotację o podanej nazwie.

mark(match_from, match_to, annotation_name) — anotacja rozciąga się od tokenu first(match_from) do last(match_to) włącznie, gdzie rmatch_from i match_to są dopasowaniami (jednym z 3 podtypów)
mark(match, annotation_name) — od tokenu first(match) do last(match) włącznie, gdzie match jest dopasowaniem

Powyższe warianty znakują anotację, której pierwszy token uznawany jest za nadrzędnik. Poniższy wariant pozwala na określenie nadrzędnika; jego pozycja określana jest poprzez podanie dopasowania — nadrzędnik umieszczony zostanie na pozycji first(match_head)

mark(match_from, match_to, match_head, annotation_name)

  1. trzeba zrobić sztuczkę — sprawdzić czy na podanych popzycjach nie ma pustych wektorów
  2. jeśli są, idziemy albo do przodu, albo do tyłu (odpowiednio)

Uwaga: jeśli przez podany zakres w podanym kanale przechodzi jakakolwiek anotacja, działanie przerywane jest wyjątkiem.

Operator unmark usuwa anotację o podanej nazwie. Jeśli w podanym miejscu nie ma anotacji, działanie przerywane jest wyjątkiem.

unmark(match, annotation_name)

TODO dodać akcje, które znakują relacje