Specyfikacja podjęzyka WCCL L2
Język L2 to w miarę autonomiczny podjęzyk reguł i dopasowań.
Są dwie kategorie reguł:- 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.
- 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 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 tokenu
TOK[pos]
(pos
to pozycja, pod którą znajduje się token), - dopasowanie anotacji
ANN[pos,name]
(pos
to pierwsza pozycja anotacji (ciągłego kawałka frazy),name
to napis—nazwa kanału), - 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 warunkowiTrue
). 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:
- jednorazowo — iterujemy po pozycjach w zdaniu i dla każdej pozycji kolejno próbujemy wszystkich reguł oraz
- 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)
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.
- match zawsze zaczyna od bieżącej pozycji w zdaniu; za jej zerowanie odpowiedzialny jest kod/operator korzystający z
match
- 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
- warunki (elementy listy
conditions
) rozpatrywane są sekwencyjnie; każdy spełniony warunek owocuje dodaniem do wektora dopasowań jednego elementu - 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ń.
- opakowanie na predykaty
L0
; zwracaTrue
/False
zgodnie z tym, co zwrócił dany predykat; wtedy operator może zwrócićTOK(pos)
— wskazanie na sprawdzaną pozycję; - operator zwracający
True
/False
orazANN[pos, name]
- 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
.
- Bieżąca pozycja jest ustawiana na zero. Z tego miejsca zaczynamy iterację zewnętrzną.
- Dla każdej pozycji bieżącej wykonujemy następującą operację:
- zapamiętujemy wartość bieżącej pozycji w zdaniu
- jednorazowo uruchamiamy
MATCH
, sprawdzamyCONDITIONS
i jeśli spełnione, wstawiamyMATCH(
wynik dopasowania)
(tj. wektor jednoelementowy) w zmienną$m:_M
, po czym kolejno odpalamy akcje
- 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
) - 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)
- Iteracja zewnętrzna (bieżącą pozycją w zdaniu) działa jak w zwykłym
apply
- Gdy uda się dopasować,
- zapamiętujemy bieżącą pozycję w zdaniu,
- odpalamy tymczasowo drugą iterację, sprawdzając drugie dopasowanie (
MATCH_AHEAD
), - tutaj idziemy tylko do pierwszego dopasowania (lub porażki); jeśli się uda dopasować, wstawiamy (
ARRAY(m1, m2)
w zmienną$m:_M
, gdziem1
im2
to wyniki matchów; sprawdzamy warunki i odpalamy akcje
- Jeśli udało się dopasować
MATCH
(naMATCH_AHEAD
nie nakładamy żadnych warunków), to pomijamy długość MATCH - Jeśli nie, pomijamy jeden token
- 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ęściMATCH_AHEAD
.
Warunki operatora apply (post-conditions)¶
Operator apply
opcjonalnie przyjmuje listę warunków. Warunek taki jest specjalnym typem operatora.
- opakowany predykat CCL L0 (składnia identyczna jak w L0)
- operator
ann(m1, name)
bądźann(m1, m2, name)
— sprawdza czy między granice dopasowania (dopasowań) pokrywają się z granicami frazy o nazwiename
(wystarczy, by pokrywały się z ciągłym kawawłkiem nieciągłej frazy) - 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)
- trzeba zrobić sztuczkę — sprawdzić czy na podanych popzycjach nie ma pustych wektorów
- 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