diff --git a/src/reynir/Greynir.grammar b/src/reynir/Greynir.grammar index 31fe92b..7dbe6c2 100644 --- a/src/reynir/Greynir.grammar +++ b/src/reynir/Greynir.grammar @@ -112,7 +112,7 @@ /tala = et ft # Person (1st, 2nd, 3rd) variant -/pers = p1 p2 p3 +/pers = p1 p2 p3 sp # sp = spurnarmynd með viðskeyttu frumlagi (ertu, viltu) # Hacks to generate single variants only /hk = hk @@ -155,6 +155,17 @@ MgrInnihald → # "Allt er klárt," segir Jón og bætir við: "Komum þessu í gang." Staðhæfing → Sagt Segjandi ÁframSagt? Lokatákn? + # Spurning eða upphrópun í beinni ræðu, án kommu á undan segjanda: + # "Komst hann ekki í hópinn?" sagði hann. / "Við erum að falla á tíma!" skrifar Limbourg. + | Forskeyti? SagtSpurning ","? SegirKjarni SegirÁframSagt? Lokatákn? + +# og bætti við: "Þetta er mjög gott." +SegirÁframSagt → + SegirÁfram ":"? Sagt? + +SagtSpurning → + Upphrópun* Spurnarsetning + | Yfirsetning StLiður* "!" ÁframSagt → Segir SegirÁfram Sagt @@ -197,11 +208,16 @@ StrikEðaKomma → "-:em" | "," $score(-6) Forskeyti # Ekki freistast í forskeyti ef annað er í boði Heimild_nf/tala/pers → - Nl_nf/tala/pers | Sérnafn + Nl_nf/tala/pers Heimild_þgf/tala/pers → - Nl_þgf/tala/pers | Sérnafn + Nl_þgf/tala/pers Heimild_ef/tala/pers → - Nl_ef/tala/pers | Sérnafn + Nl_ef/tala/pers + +# Sérnafn sem heimild er alltaf 3. persóna eintölu: segir Limbourg +Heimild_nf_et_p3 → Sérnafn +Heimild_þgf_et_p3 → Sérnafn +Heimild_ef_et_p3 → Sérnafn # Koma í veg fyrir að þessir hnútar séu bestaðir burt $tag(keep) Heimild_nf/tala/pers @@ -211,12 +227,30 @@ $tag(keep) Heimild_ef/tala/pers Sagt → Forskeyti? Yfirsetning StLiður* Segir → - # , segir/fullyrðir Jón Ásmundsson, fulltrúi samtakanna, í dag í útvarpinu - "," SögnSegja/tala/pers Heimild_nf/tala/pers AtvFsAuka? + "," SegirKjarni + +SegirKjarni → + # segir/fullyrðir Jón Ásmundsson, fulltrúi samtakanna, í dag í útvarpinu + SögnSegja/tala/pers Heimild_nf/tala/pers AtvFsAuka? + # bætir Jón við + | SögnBætaVið/tala/pers + +# bætir Jón við / bætir hún við í færslu sinni +SögnBætaVið/tala/pers → + 'bæta:so'_0_gm_fh/tala/pers Heimild_nf/tala/pers "við:ao" AtvFsAuka? SegirÁfram → "og:st" SögnSegja/tala/pers SegirLíka? | "og:st" SögnSegjaÁfram/tala/pers + | "og:st" SögnSegjaLátbragð/tala/pers # spurði Þorgerður og hló + +# Látbragð sem fylgir tilvitnun: segir KK og hlær / brosir / andvarpar +SögnSegjaLátbragð/tala/pers → + 'hlæja:so'_0_gm_fh/tala/pers + | 'brosa:so'_0_gm_fh/tala/pers + | 'andvarpa:so'_0_gm_fh/tala/pers + | 'kinka:so'_0_gm_fh/tala/pers 'kollur:kk'_þgf_et + | 'hrista:so'_0_gm_fh/tala/pers 'höfuð:hk'_þf_et_gr $score(+999) SegirÁfram @@ -232,6 +266,13 @@ SögnSegja/tala/pers → | 'ítreka:so'_0_gm_fh/tala/pers | 'viðurkenna:so'_0_gm_fh/tala/pers | 'álíta:so'_0_gm_fh/tala/pers + | 'spyrja:so'_0_gm_fh/tala/pers # "Er þetta satt?" spyr Jón + | 'svara:so'_0_gm_fh/tala/pers + | 'hrópa:so'_0_gm_fh/tala/pers + | 'kalla:so'_0_gm_fh/tala/pers + | 'tísta:so'_0_gm_fh/tala/pers + | 'hugsa:so'_0_gm_fh/tala/pers + | 'útskýra:so'_0_gm_fh/tala/pers SögnSegjaÁfram/tala/pers → 'bæta:so'_0_gm_fh/tala/pers "við:ao" @@ -401,8 +442,7 @@ Spurnarsetning → | SpurnarForsetningarliður BeygingarliðurSögnFremst? Spurningarmerki # Í hvaða fötum er barnið? | SpurnarAtviksorð BeygingarliðurSögnFremst? Spurningarmerki # Hvernig missti tröllið lífið? | SpurnarFrumlag/tala/kyn BlTagl/tala/p3/kyn? Spurningarmerki # Hvað er í pokanum? - | BeygingarliðurSögnFremst Spurningarmerki # Getur þú hlaupið hratt? - #| BeygingarliðurSpurnarsníkill Spurningarmerki # Ferðu á ballið? + | BeygingarliðurSögnFremst Spurningarmerki # Getur þú hlaupið hratt? / Ferðu á ballið? # Spurnarorðið er frumlag setningarinnar, stendur utan beygingarliðar BlTagl/tala/pers/kyn → @@ -829,6 +869,17 @@ $tag(begin_prep_scope) SagnRunaKnöpp/tala/pers/kyn # Sagnir innan undirtrés geta ekki orsakað bónus á forsetningar utan þess $tag(purge_verb) SagnRuna/tala/pers/kyn +# Sagnögn sem stendur á milli sagnar og andlags: +# 'bjó til hús', 'kastaði upp boltanum', 'sagði upp störfum'. +# Reducer gefur plús ef sögnin hefur ögnina í Verbs.conf (*til, *upp...), +# en annars mínus, þannig að forsetningarliður sé tekinn fram yfir ögn +SagnÖgn → + "upp:ao" | "niður:ao" | "inn:ao" | "út:ao" | "fram:ao" | "aftur:ao" + | "til:ao" | "af:ao" | "frá:ao" | "undan:ao" | "yfir:ao" | "um:ao" + | "á:ao" | "við:ao" | "saman:ao" | "burt:ao" + +$tag(verb_particle) SagnÖgn + SagnInnskot → FsAtv | ÁttFs # suður götuna @@ -880,6 +931,18 @@ SagnHlutiMegiSkuli → # [Ætíð] skal fara með skjölin sem trúnaðarmál HjSögnMegiSkuli_et_p3 FsAtv? EinSögn_nh_et_hk SagnInnskot? +# Sögn með viðskeyttu frumlagi ('spurnarmynd'): Ertu búinn? Viltu koma? +# (Hvernig) komstu í kynni við hana? (Þá) viltu það ekki +# Sagnmyndin sjálf inniheldur frumlagið 'þú', sem því vantar í setninguna +SagnHluti_et_p2/kyn → + SagnliðurSníkill/kyn SagnInnskot? SagnViðhengi_et_p2/kyn? + +SagnliðurSníkill/kyn → + Sagnliður_et_sp/kyn + +# Kjósum fremur venjulega greiningu ef hún er tæk +$score(-8) SagnliðurSníkill/kyn + $tag(begin_prep_scope) SagnHluti/tala/pers/kyn # Nýtt undirtré fyrir tengingu sagnar og forsetningar FrumlagsInnskot/tala/pers/kyn → @@ -1967,6 +2030,12 @@ SögnErLoBotn/tala/kyn → | SögnErLoMM # (Hún er/var/hefur verið) talin/álitin/sögð vera lykillinn [að því að málið leystist] | SögnLhÞtVera_nf/tala/kyn HjSögnVera NlSagnfylling_nf + # (Maðurinn er/var) sagður/talinn [ekki] hafa stolið bílnum / hafa verið vopnaður + | SögnLhÞtVera_nf/tala/kyn FsAtv? 'hafa:so'_nh HreinSögn_sagnb/tala/kyn + # (Maðurinn er) sagður/talinn [ekki] vera í haldi lögreglu / búa í Reykjavík + | SögnLhÞtVera_nf/tala/kyn FsAtv? HreinSögn_nh/tala/kyn + # (Maðurinn er) sagður/talinn [ekki] vera/hafa verið vopnaður + | SögnLhÞtVera_nf/tala/kyn FsAtv? HjSögnEkkiVera so_lhþt_sb_nf/tala/kyn SögnErLoMM → FsAtv? so_0_mm_sagnb @@ -2565,19 +2634,23 @@ Sögn_1_sagnb → # Jón getur verið [af náttúrunnar hendi] hálfgerður asni So_1_sagnb_nf FsRunaEftirSögn? NlSagnfylling_nf # Þingmenn geta/hafa lagt [fyrir Alþingi] frumvörp (til laga) - | So_1_sagnb/aukafall FsRunaEftirSögn? NlBeintAndlag/aukafall + | So_1_sagnb/aukafall SagnÖgn? FsRunaEftirSögn? NlBeintAndlag/aukafall | NlEnginnAndlag/fall So_1_sagnb/fall # (Þó getur hann) enga samninga gert +$tag(apply_particle_bonus) Sögn_1_sagnb + NlEnginnAndlag/fall → 'enginn:fn'/fall/tala/kyn Nl/fall/tala/kyn # Sögn í nafnhætti með einu viðfangi Sögn_1_nh → - So_1_nh/fall FsRunaInnskot? NlBeintAndlag/fall + So_1_nh/fall SagnÖgn? FsRunaInnskot? NlBeintAndlag/fall # sjá/láta/'horfa á' Pál breyta [í snarheitum] sér (í kött) | So_1_þf_nh Nl_þf So_1_nh/fall FsRunaInnskot? Nl/fall +$tag(apply_particle_bonus) Sögn_1_nh + # Gefa forsetningarliðum milli sagnar og andlags mínus # enda eru þær klasturslegt mál ('Hann mun sjá í snatri fílinn') FsRunaInnskot → @@ -2591,7 +2664,9 @@ Sögn_1_bh → # Vertu [í alvörunni] þú sjálfur So_1_bh_nf FsRunaEftirSögn? NlSagnfylling_nf # Bakaðu [í snarheitum] kökuna - | So_1_bh/aukafall FsRunaEftirSögn? NlBeintAndlag/aukafall + | So_1_bh/aukafall SagnÖgn? FsRunaEftirSögn? NlBeintAndlag/aukafall + +$tag(apply_particle_bonus) Sögn_1_bh # Sagnbót með tveimur viðföngum @@ -2680,8 +2755,9 @@ Sögn_1/tala/pers/kyn → # (Hún) sagði lafmóð/sjálf söguna so_1_nf/tala/pers OgSögn_1_nf/tala/pers* FsRunaEftirSögn? LoTengtSögn_nf/tala/kyn? NlSagnfylling_nf NhFylling? + # (Hann) bjó til hús / (Hann) kastaði upp boltanum | so_1/aukafall/tala/pers OgSögn_1/aukafall/tala/pers* - FsRunaEftirSögn? LoTengtSögn_nf/tala/kyn? NlBeintAndlag/aukafall NhFylling? + SagnÖgn? FsRunaEftirSögn? LoTengtSögn_nf/tala/kyn? NlBeintAndlag/aukafall NhFylling? # (Ég tel/segi/álít) Pál vera dagfarsprúðan mann | So_1_þf_nh/tala/pers OgSögn_1_þf_nh/tala/pers* SagnarBotn @@ -2701,6 +2777,8 @@ Sögn_1/tala/pers/kyn → # Ég læt þess getið að Jón er frábær | 'láta:so'_gm/tala/pers SögnÞessGetið/tala/kyn +$tag(apply_particle_bonus) Sögn_1/tala/pers/kyn + FáAndlag_þf → # Páll fékk bílinn strax endurgreiddan NlBeintAndlag_þf/tala/pers/kyn AtvFs? so_lhþt_sb_þf/tala/kyn @@ -3474,9 +3552,9 @@ $score(+3) NhSagnInnskot NhSögn → so_0_nh so_0_gm_vh_p3_et? OgNhSögn* # að kjósa [þurfi] og telja (upp á nýtt) | so_1_nf_nh so_0_gm_vh_p3_et? OgNhSögn_nf* FsRunaEftirSögn? NlSagnfylling_nf # að hrjósa [muni] hugur - | so_1_þf_nh so_0_gm_vh_p3_et? OgNhSögn_þf* FsRunaEftirSögn? NlBeintAndlag_þf # að selja [skyldi] í snatri hestinn - | so_1_þgf_nh so_0_gm_vh_p3_et? OgNhSögn_þgf* FsRunaEftirSögn? NlBeintAndlag_þgf # að hætta [ætti] baráttunni - | so_1_ef_nh so_0_gm_vh_p3_et? OgNhSögn_ef* FsRunaEftirSögn? NlBeintAndlag_ef # að freista [eigi] gæfunnar + | so_1_þf_nh so_0_gm_vh_p3_et? OgNhSögn_þf* SagnÖgn? FsRunaEftirSögn? NlBeintAndlag_þf # að selja [skyldi] í snatri hestinn / að búa til mat + | so_1_þgf_nh so_0_gm_vh_p3_et? OgNhSögn_þgf* SagnÖgn? FsRunaEftirSögn? NlBeintAndlag_þgf # að hætta [ætti] baráttunni / að skila inn skýrslunni + | so_1_ef_nh so_0_gm_vh_p3_et? OgNhSögn_ef* SagnÖgn? FsRunaEftirSögn? NlBeintAndlag_ef # að freista [eigi] gæfunnar | so_2_þf_þf_nh so_0_gm_vh_p3_et? OgNhSögn_þf_þf* FsRunaEftirSögn? NlÓbeintAndlag_þf NlBeintAndlag_þf # að lýsa auðlindir þjóðareign | so_2_þf_þgf_nh so_0_gm_vh_p3_et? OgNhSögn_þf_þgf* FsRunaEftirSögn? NlÓbeintAndlag_þf NlBeintAndlag_þgf # að kalla [ætti] Pál illum nöfnum | so_2_þf_ef_nh so_0_gm_vh_p3_et? OgNhSögn_þf_ef* FsRunaEftirSögn? NlÓbeintAndlag_þf NlBeintAndlag_ef # að spyrja og krefja Pál svars @@ -3506,6 +3584,8 @@ NhSögn → # (Hann tók á öllu sínu til að) verða ekki fúll | NhSögnLo +$tag(apply_particle_bonus) NhSögn + NhSögnLo → HjSögnVeraVerða SögnErLoBotn/tala/kyn @@ -3904,6 +3984,11 @@ NlStak_p2_ft/fall/kyn → NlStak_ft_p2/fall/kyn → NlStak_p2_ft/fall/kyn FsRunaEftirNl? +# Persónan 'sp' (spurnarmynd sagnar með viðskeyttu frumlagi, 'ertu') +# er í raun 2. persóna eintölu; nafnliðir hennar eru þeir sömu +NlStak_et_sp/fall/kyn → NlStak_et_p2/fall/kyn +NlStak_ft_sp/fall/kyn → NlStak_ft_p2/fall/kyn + # !!! ATH: reglurnar hér fyrir ofan leyfa 'ég og þú fóruð til Frakklands' # Sameiginlegar reglur fyrir 3p. eintölu og fleirtölu @@ -4132,6 +4217,15 @@ $score(+24) GreinirMeðLo/tala/fall/kyn NlStak_p3/tala/fall/kyn → AtviksliðurEinkunn* LoSemNafnliður/tala/fall/kyn +# Lýsingarorð með lausum greini sem stendur sem nafnliður: +# 'Hið sama á við um starfsfólkið', 'Hið rétta er að ...', +# 'hið opinbera greiðir', 'björgunarsveitir óttast hið versta' +NlStak_p3/tala/fall/kyn → + NlGreinirLo/tala/fall/kyn + +NlGreinirLo/tala/fall/kyn → + Greinir/tala/fall/kyn LoLiður/tala/fall/kyn + # 'Fleiri/færri komu í veisluna en ég bjóst við' # 'Fleirum fannst kvikmyndin leiðinleg' NlStak_p3_ft/fall/kyn → @@ -6354,6 +6448,22 @@ FsUndirÁÍ/fall → # Atviksliður +# Atviksliður úr lausum greini og efsta stigi lýsingarorðs: +# (leysa) hið fyrsta / (yfirgefa) hið snarasta / (koma) hið allra fyrsta +HiðFyrsta → + 'hinn:gr'_et_þf_hk "allra:ao"? HiðFyrstaLo + +HiðFyrstaLo → + 'fyrstur:lo'_vb_et_þf_hk # hið fyrsta + | 'snar:lo'_vb_et_þf_hk # hið snarasta + | 'bráður:lo'_vb_et_þf_hk # hið bráðasta + | 'fljótur:lo'_vb_et_þf_hk # hið fljótasta + | 'skjótur:lo'_vb_et_þf_hk # hið skjótasta + | 'lítill:lo'_vb_et_þf_hk # hið minnsta + | 'mikill:lo'_vb_et_þf_hk # hið mesta + +$score(+16) HiðFyrsta + Al → EinnAl+ EinnAl → @@ -6369,6 +6479,7 @@ EinnAl → | AlHvortSemUmErAðRæða # Hvort sem um er að ræða X eða Y | AðNýju | FjöldiSaman # Tugþúsundum saman + | HiðFyrsta # hið fyrsta, hið snarasta, hið allra bráðasta # Fjölyrtir atviksliðir sem ekki er unnt að búa til fasta frasa úr diff --git a/src/reynir/config/Phrases.conf b/src/reynir/config/Phrases.conf index deb91f6..0df66e5 100644 --- a/src/reynir/config/Phrases.conf +++ b/src/reynir/config/Phrases.conf @@ -1365,7 +1365,7 @@ meaning = ao frasi - # "þess efnis", "fahee nhee", "sá efni" #"þess utan", "fphee ae", "það utan" #"þess vegna", "fphee ae", "það vegna" -#"þess í stað", "fphee ao nkeo", "það í staður" +"þess í stað", "fphee ao nkeo", "það í staður" "þvert á móti", "lhensf aa aa", "þver á móti" "því miður", "fpheþ aam", "það lítt" "fyrir slysni", "ao nveo", "fyrir slysni" @@ -1677,9 +1677,9 @@ meaning = fs frasi - "lokið er við" so so fs/ao "eiga* afturkvæmt" so ao "nýta* til uppbyggingar" so fs kvk -"sama gilti" fn so -"sama gildi" fn so -"sama gildir" fn so +"sama gilti" fn/lo so +"sama gildi" fn/lo so +"sama gildir" fn/lo so "nefna* sem dæmi" so fs hk "taka* sem dæmi" so fs hk "vera* boðið upp á" so so ao fs diff --git a/src/reynir/config/Prefs.conf b/src/reynir/config/Prefs.conf index cce1ba3..70c7204 100644 --- a/src/reynir/config/Prefs.conf +++ b/src/reynir/config/Prefs.conf @@ -251,7 +251,7 @@ aukinn no < so ári so < no ára so < no át no << so -átta so < töl +átta so <<< töl áttu no < so banka so < no bankar so < no diff --git a/src/reynir/config/Verbs.conf b/src/reynir/config/Verbs.conf index 37bc1b1..546a62a 100644 --- a/src/reynir/config/Verbs.conf +++ b/src/reynir/config/Verbs.conf @@ -2252,6 +2252,7 @@ búa /um sig *upp búa /yfir illur_hk_et_þgf búa /að þgf /með þgf *vel búa /við þf # vel, illa +búa |þf *til # Hún bjó til mat #búa *afskekkt # afskekkt, rúmt, þröngt búa *um búa *saman diff --git a/src/reynir/reducer.py b/src/reynir/reducer.py index 6704401..989a74f 100644 --- a/src/reynir/reducer.py +++ b/src/reynir/reducer.py @@ -92,7 +92,17 @@ """ -from typing import Dict, DefaultDict, List, Set, Tuple, Optional, Any, cast +from typing import ( + Dict, + DefaultDict, + Iterable, + List, + Set, + Tuple, + Optional, + Any, + cast, +) from typing_extensions import TypedDict, Required from collections import defaultdict @@ -119,6 +129,10 @@ class ResultDict(TypedDict, total=False): # Verb scope information so: VerbList sl: VerbList + # Text of an adverb matched by a literal terminal (only at token nodes) + ao: str + # Text of a verb particle (SagnÖgn), passed to the enclosing verb phrase + pcl: str VerbStack = List[Optional[VerbList]] @@ -138,10 +152,25 @@ class ResultDict(TypedDict, total=False): _VERB_PREP_BONUS = 7 # Give 7 extra points for a verb/preposition match _VERB_PREP_PENALTY = -2 # Subtract 2 points for a non-match +# Bonus for a verb particle (SagnÖgn) preceding the object, e.g. 'bjó til hús', +# if the particle is listed for the verb in Verbs.conf (*til); otherwise +# a penalty, which must outweigh _VERB_PREP_PENALTY so that a preposition +# reading is preferred for verbs that do not take the particle +_VERB_PARTICLE_BONUS = 7 +_VERB_PARTICLE_PENALTY = -6 +# Penalty for a verb terminal whose argument cases are only licensed by +# frames with a reflexive pronoun ('eiga sér |þf' -> eiga_þgf_þf), so that +# a reading with arbitrary objects in those cases is discouraged +_VERB_WEAK_ONLY_PENALTY = -6 _LENGTH_BONUS_FACTOR = 10 # For length bonus, multiply number of tokens by this factor _CASES_SET = BIN_Token.CASES_SET +# Penalties for verb forms with a cliticized subject ('ertu', 'viltu') +# when the token also has an adjective reading ('næstu', 'mestu') or +# a regular verb reading ('áttu', 'yrðu') +_CLITIC_ADJECTIVE_PENALTY = -40 +_CLITIC_VERB_PENALTY = -6 _CONTAINED_VERBS_SET = frozenset(("begin_prep_scope", "purge_verb")) # BÍN categories ('fl') of person and entity names @@ -155,14 +184,17 @@ class _ReductionScope: """Class to accumulate information about a nonterminal and its child productions during reduction""" - __slots__ = ("reducer", "sc", "pushed_prep_bonus", "start_verb") + __slots__ = ("reducer", "sc", "keys", "pushed_prep_bonus", "start_verb", "nt") def __init__(self, reducer: "ParseForestReducer", node: Node) -> None: self.reducer = reducer # Child tree scores self.sc: ChildDict = defaultdict(lambda: {"sc": 0}) + # Content-based tie-break keys, by family index + self.keys: Dict[int, Any] = {} # We are only interested in completed nonterminals nt = node.nonterminal if node.is_completed else None + self.nt = nt # Verb/preposition matching stuff self.pushed_prep_bonus = False verb = reducer.get_current_verb() @@ -180,11 +212,24 @@ def __init__(self, reducer: "ParseForestReducer", node: Node) -> None: reducer.push_current_verb(verb) self.start_verb = verb - def start_family(self, ix: int, prod: Production) -> None: + def start_family( + self, ix: int, prod: Production, children: Iterable[Optional[Node]] + ) -> None: """Start the processing of a production (numbered ix) of a nonterminal""" # Initialize the score of this family of children, so that productions # with higher priorities (more negative prio values) get a starting bonus self.sc[ix]["sc"] = -10 * prod.priority + # Remember a content-based key for this family, used to break + # ties between equally scored families deterministically. + # The family index itself reflects the order in which the Earley + # parser completed the derivations, which shifts with unrelated + # grammar edits. Instead, the production that appears first in + # the grammar wins, and between derivations of the same + # production, the one with the longest leading children wins. + self.keys[ix] = ( + -prod.index, + tuple((ch.start, ch.end) for ch in children if ch is not None), + ) self.reducer.set_current_verb(self.start_verb) def add_child(self, ix: int, rd: ResultDict) -> None: @@ -192,6 +237,13 @@ def add_child(self, ix: int, rd: ResultDict) -> None: where the parent family has index ix (0..n)""" d = self.sc[ix] d["sc"] += rd.get("sc", 0) + if "ao" in rd and self.nt is not None and self.nt.has_tag("verb_particle"): + # A verb particle (SagnÖgn): note its text so that the enclosing + # verb phrase can check it against the verb's frames + d["pcl"] = rd["ao"] + elif "pcl" in rd: + # Carry the particle up to the enclosing verb phrase + d["pcl"] = rd["pcl"] # Carry information about contained verbs ("so") up the tree for key in ("so", "sl"): if key in rd: @@ -218,14 +270,26 @@ def process(self, node: Node) -> Tuple[ResultDict, int]: nt = node.nonterminal if node.is_completed else None + if nt is not None and nt.has_tag("apply_particle_bonus"): + # Sögn_1 & co.: adjust the score of each family containing + # a verb particle (SagnÖgn) by whether the verb(s) take it + for d in csc.values(): + pcl = d.pop("pcl", None) + if pcl is not None: + d["sc"] += self.reducer.verb_particle_bonus(pcl, d.get("so")) + if len(csc) == 1: # Not ambiguous: only one result, do a shortcut # Will raise an exception if not exactly one item [(ix, sc)] = csc.items() else: - # Find the best scoring family, using the lowest - # family index as a tie-breaker for determinism - ix, sc = max(csc.items(), key=lambda x: (x[1]["sc"], -x[0])) + # Find the best scoring family, breaking ties by the + # content-based family key (see start_family()) and + # only as a last resort by the family index + keys = self.keys + ix, sc = max( + csc.items(), key=lambda x: (x[1]["sc"], keys[x[0]], -x[0]) + ) if nt is not None: # Adjust the winning family's score. Note that sc is this @@ -261,6 +325,7 @@ def process(self, node: Node) -> Tuple[ResultDict, int]: # and Setning have this tag sc.pop("so", None) # Simpler than if "so" in sc: del sc["so"] sc.pop("sl", None) + sc.pop("pcl", None) return sc, ix @@ -345,12 +410,31 @@ def verb_prep_bonus( # If no match, discourage return _VERB_PREP_PENALTY + def verb_particle_bonus(self, particle: str, verbs: Optional[VerbList]) -> int: + """Return a bonus if any of the given verbs takes the particle + according to its frames in Verbs.conf, otherwise a penalty""" + if verbs: + for verb_terminal, verb_token in verbs: + m = verb_token.match_with_meaning(verb_terminal) + assert isinstance(m, BIN_Tuple) + verb = m.stofn + if "MM" in m.beyging: + verb = BIN_Token.mm_verb_stem(verb) + verb_with_cases = verb + verb_terminal.verb_cases + if VerbFrame.matches_particle(verb_with_cases, "*" + particle): + return _VERB_PARTICLE_BONUS + return _VERB_PARTICLE_PENALTY + def visit_token(self, node: Node) -> ResultDict: """At token node""" # Return the score of this token/terminal match d: ResultDict = {"sc": 0} nt = cast(BIN_Terminal, node.terminal) sc = self._scores[node.start][nt] + if nt.matches_category("ao") and nt.is_literal: + # Adverb matched by a literal terminal such as "upp:ao": + # note its text, in case it is a verb particle (SagnÖgn) + d["ao"] = cast(BIN_Token, node.token).lower if nt.matches_category("fs"): # Preposition terminal - this is either a normal fs_case terminal # or a literal terminal such as "á:fs" @@ -456,7 +540,7 @@ def calc_score(w: Node, sig: Any) -> ResultDict: fam_children: List[List[Tuple[Node, Any]]] = [] # Go through each family and calculate its score for family_ix, (prod, children) in enumerate(w._families): - scope.start_family(family_ix, prod) + scope.start_family(family_ix, prod, children) this_fam: List[Tuple[Node, Any]] = [] for ch in children: if ch is not None: @@ -694,6 +778,11 @@ def _calc_terminal_scores(self, w: Node) -> ScoreDict: elif txt == "á" and t.has_variant("þgf"): # Larger bonus for á + þgf to resolve conflict with verb 'eiga' sc[t] += 4 + elif txt == "í" and t.has_variant("þgf"): + # Slightly larger bonus for í + þgf (location) than + # í + þf (direction), to resolve ties where the noun + # form is the same in both cases ("í umdæmi") + sc[t] += 3 else: # Else, give a bonus for each matched preposition sc[t] += 2 @@ -721,6 +810,8 @@ def _calc_terminal_scores(self, w: Node) -> ScoreDict: # In the (rare) cases where there are conflicting scores, # apply the most positive adjustment adjmax: Optional[int] = None + # Is the terminal only licensed by weak frames? + weak_only: Optional[bool] = None for m in token.meanings: if m.ordfl == "so": key = m.stofn + t.verb_cases @@ -730,7 +821,26 @@ def _calc_terminal_scores(self, w: Node) -> ScoreDict: adjmax = score else: adjmax = max(adjmax, score) + if numcases > 0 and VerbFrame.matches_arguments(key): + wo = VerbFrame.weak_only(key) + weak_only = wo if weak_only is None else (weak_only and wo) sc[t] += adj + (adjmax or 0) + if weak_only: + sc[t] += _VERB_WEAK_ONLY_PENALTY + if t.has_variant("sp"): + # Interrogative form with a cliticized subject + # ('ertu', 'viltu'). BÍN lists such forms for many + # verbs whose clitic form coincides with a common + # adjective ('næstu', 'mestu', 'elstu') or with a + # regular verb form ('áttu', 'yrðu', 'máttu'): in + # those cases, discourage the clitic reading + if any(m.ordfl == "lo" for m in token.meanings): + sc[t] += _CLITIC_ADJECTIVE_PENALTY + elif any( + m.ordfl == "so" and "SP" not in m.beyging + for m in token.meanings + ): + sc[t] += _CLITIC_VERB_PENALTY if t.is_bh: # Discourage 'boðháttur' sc[t] -= 4 diff --git a/src/reynir/simpletree.py b/src/reynir/simpletree.py index a63100e..79c7623 100644 --- a/src/reynir/simpletree.py +++ b/src/reynir/simpletree.py @@ -138,6 +138,7 @@ class SimpleTreeNode(CanonicalTokenDict, total=False): "Afleiðing": "S-CONS", "Spurnarsetning": "S-QUE", "Sagt": "CP-QUOTE", + "SagtSpurning": "CP-QUOTE", # "Segjandi": "CP-SOURCE", "Forskeyti": "S-PREFIX", "Tíðarsetning": "CP-ADV-TEMP", @@ -244,6 +245,7 @@ class SimpleTreeNode(CanonicalTokenDict, total=False): # "NhSögn": "VP", "NhEinfaldur": "VP", "SagnliðurÁnF": "VP", + "SagnliðurSníkill": "VP", "ÖfugurSagnliður": "VP", "SagnliðurVh": "VP", "HjSögnLhÞt": "VP", # Auxiliary verb, hjálparsögn diff --git a/src/reynir/verbframe.py b/src/reynir/verbframe.py index 98d93c2..50f8060 100644 --- a/src/reynir/verbframe.py +++ b/src/reynir/verbframe.py @@ -235,8 +235,14 @@ def __init__( preps: Iterable[Tuple[str, str]], particle: Optional[str], score: Optional[int], + weak: bool = False, ) -> None: self.verb = verb + # True if the frame contains a reflexive pronoun (sig, sér, sín) + # as an object or within a prepositional phrase ('átta |sig /á þgf', + # 'tryggja sér |þf'); such a frame is registered under its object + # cases but is only a weak license for arbitrary objects in those cases + self.weak = weak # All arguments self.args = args self.obj = obj @@ -277,6 +283,7 @@ def create_from_config(cls, s: str) -> None: # Format: verb [arg1] [arg2] [/preposition arg]... [*particle] [$pragma(txt)] complex = False + weak = False def get_score(s: str) -> Tuple[str, Optional[int]]: """Handle the $score() pragma, if present""" @@ -364,7 +371,7 @@ def get_indirect_object(s: str) -> Tuple[str, str]: raise ConfigError("Verb '{0}' is not a valid word".format(verb)) case = "" if len(a) > 1: - case = get_case_and_kind((" ".join(a[1:]))) + case = get_case_and_kind(" ".join(a[1:])) return verb, case def get_case_and_kind(w: str) -> str: @@ -384,16 +391,17 @@ def get_case_and_kind(w: str) -> str: case = "" # kind: int = 1 # Default value refl = REFLPRN_CASE.get(w, "") - nonlocal complex + nonlocal complex, weak if w in ALL_CASES: # Case 1 case = w # kind = 1 elif refl and refl in ALL_CASES: - # Case 2 + # Case 2: a reflexive pronoun is registered under its case, + # so that e.g. 'átta |sig /á þgf' yields the frame 'átta_þf' case = refl # kind = 2 - complex = True + weak = True elif w in SUBCLAUSES: # Cases 5-8 case = w @@ -432,7 +440,10 @@ def get_case_and_kind(w: str) -> str: args.append(iobj) if obj: args.append(obj) - # Add frame to verb database, if this construct is supported (not 'complex') + # Add frame to verb database, if this construct is supported. + # Frames with subclause arguments (nh, nhx, falls, spurns, ...) + # or fixed phrases are not yet supported; frames with reflexive + # pronouns are registered under the pronoun's case, marked as weak. if complex: return vf = cls( @@ -443,6 +454,7 @@ def get_case_and_kind(w: str) -> str: score=score, obj=obj, iobj=iobj, + weak=weak, ) case_key = vf.case_key if error: @@ -501,6 +513,16 @@ def matches_particle(cls, verb_with_cases: str, particle: str) -> bool: # Check the frames one by one and return True if any matches are found return any(vf.matches_pcl(particle) for vf in verb_frames) + @classmethod + @lru_cache(maxsize=1024) + def weak_only(cls, verb_with_cases: str) -> bool: + """Return True if the given key, e.g. 'tryggja_þgf_þf', is only + licensed by frames containing a reflexive pronoun ('tryggja sér |þf')""" + verb_frames = cls.CASE_FRAMES.get(verb_with_cases) + if not verb_frames: + return False + return all(vf.weak for vf in verb_frames) + @classmethod @lru_cache(maxsize=1024) def verb_score(cls, verb_with_cases: str) -> Optional[int]: @@ -510,7 +532,11 @@ def verb_score(cls, verb_with_cases: str) -> Optional[int]: # No such verb frame, hence no score return None # Return the highest score associated with a verb frame with the - # given arguments, or None if no score was given + # given arguments, or None if no score was given. Weak (reflexive) + # frames only contribute if no strong frame exists for the key, + # so that an idiom's $score() does not apply to arbitrary objects + if any(not vf.weak for vf in verb_frames): + verb_frames = [vf for vf in verb_frames if not vf.weak] score = None for vf in verb_frames: if vf.score is not None: diff --git a/test/test_parse.py b/test/test_parse.py index 4d41284..77ec5a6 100644 --- a/test/test_parse.py +++ b/test/test_parse.py @@ -249,9 +249,8 @@ def test_parse(r: Greynir, verbose: bool = False) -> None: "borða", ] assert results[12].tree.verbs == ["horfa", "borða"] - assert (results[32].tree.verbs == ["finna", "segja", "gefa", "hafa", "deyja"]) or ( - results[32].tree.verbs == ["finna", "gefa", "hafa", "deyja"] - ) + # 'er sögð gefa í skyn' is a raising construction: vera + sögð + gefa + assert results[32].tree.verbs == ["finna", "vera", "segja", "gefa", "hafa", "deyja"] # Test that the parser finds the correct word lemmas assert results[0].tree.lemmas == [ "hér", @@ -2481,12 +2480,13 @@ def test_rel_clause_pp_attachment(r) -> None: s.tree.flat_with_all_variants == "S0 S-MAIN IP NP-SUBJ pfn_et_hk_nf_p3 /NP-SUBJ VP VP " "so_2_þgf_þf_et_fh_gm_p3_þt /VP NP-IOBJ abfn_þgf /NP-IOBJ " - "NP-OBJ no_et_kk_þf PP P fs_þf /P NP no_et_hk_þf " - "NP-POSS no_ef_et_kvk to_ft_hk_nf /NP-POSS /NP /PP p " + "NP-OBJ no_et_kk_þf PP P fs_þgf /P NP no_et_hk_þgf " + "NP-POSS no_ef_et_kvk to_ft_hk_nf p " "CP-REL C stt /C IP VP VP so_0_et_fh_gm_nt_p3 /VP " "PP P fs_þf /P NP no_et_kk_þf C st /C no_et_kk_þf /NP /PP " - "/VP /IP /CP-REL /NP-OBJ /VP /IP /S-MAIN p /S0" + "/VP /IP /CP-REL /NP-POSS /NP /PP /NP-OBJ /VP /IP /S-MAIN p /S0" ) + # The relative clause attaches to the nearest noun (lögreglustöðvar) # A preposition that matches the main verb (fresta e-u vegna e-s) # must still attach to the verb phrase, not the object noun phrase s = r.parse_single("Dómarinn frestaði mótinu vegna veðurs.") @@ -2499,6 +2499,287 @@ def test_rel_clause_pp_attachment(r) -> None: ) +def test_reflexive_verb_frames(r) -> None: + """Verb frames in Verbs.conf whose object is a reflexive pronoun + ('átta |sig /á þgf', 'tryggja sér |þf') used to be dropped entirely, + leaving verbs such as 'átta' unknown to the parser. They are now + registered under the pronoun's case, so that 1st and 2nd person + objects ('áttaði mig') as well as non-reflexive objects in the same + frame ('tryggði gestunum sigur') parse.""" + for sentence, verb in ( + ("Ég áttaði mig á þessu.", "átta"), + ("Hann áttaði sig á villu síns vegar.", "átta"), + ("Við áttuðum okkur ekki á því hvað þetta var erfitt.", "átta"), + ("Liðið tryggði gestunum sigur.", "tryggja"), + ("Hún ímyndaði sér þetta.", "ímynda"), + ("Hún furðaði sig á þessu.", "furða"), + ("Hann notfærði sér aðstöðuna.", "notfæra"), + ("Hann faðmaði hana.", "faðma"), + ("Þau minnka losun.", "minnka"), + ("Þau trúlofuðu sig árið 2011 og gengu í hjónaband árið 2013.", "trúlofa"), + ): + s = r.parse_single(sentence) + assert s is not None and s.tree is not None, sentence + assert verb in s.tree.verbs, sentence + # A verb reading that is only licensed by a reflexive frame + # ('eiga sér |þf') must not displace a preposition reading + s = r.parse_single("Eðlisfræðingurinn lést í dag, á pí-deginum.") + assert s is not None and s.tree is not None + assert s.tree.verbs == ["láta"] + + +def test_verb_particle_before_object(r) -> None: + """A verb particle may precede the object ('bjó til hús', + 'kastaði upp boltanum'); the particle is accepted if Verbs.conf lists + it for the verb (*til, *upp), and the object is parsed as a direct + object rather than as the object of a preposition.""" + for sentence, pattern in ( + ("Hann bjó til hús.", "VP so_1_þf_et_p3 /VP ao NP-OBJ no_et_þf_hk /NP-OBJ"), + ("Hann tók upp bókina.", "VP so_1_þf_et_p3 /VP ao NP-OBJ no_et_þf_kvk /NP-OBJ"), + ("Hann kastaði upp boltanum.", "VP so_1_þgf_et_p3 /VP ao NP-OBJ no_et_þgf_kk /NP-OBJ"), + ("Hann skilaði inn skýrslunni.", "VP so_1_þgf_et_p3 /VP ao NP-OBJ no_et_þgf_kvk /NP-OBJ"), + ("Hann sagði upp störfum.", "VP so_1_þgf_et_p3 /VP ao NP-OBJ no_ft_þgf_hk /NP-OBJ"), + ("Hún hefur búið til mat.", "VP so_1_þf_sagnb /VP ao NP-OBJ no_et_þf_kk /NP-OBJ"), + ("Hún ætlar að búa til mat.", "VP so_1_þf_nh /VP ao NP-OBJ no_et_þf_kk /NP-OBJ"), + ("Hann ætlar að skila inn skýrslunni.", "VP so_1_þgf_nh /VP ao NP-OBJ no_et_þgf_kvk /NP-OBJ"), + ("Búðu til mat!", "VP so_1_þf_bh /VP ao NP-OBJ no_et_þf_kk /NP-OBJ"), + ): + s = r.parse_single(sentence) + assert s is not None and s.tree is not None, sentence + assert pattern in s.tree.flat, (sentence, s.tree.flat) + # Prepositional phrases are still preferred where the verb + # does not take the particle + for sentence in ( + "Hann bjó í húsinu.", + "Hann kastaði upp á þakið.", + "Hann ætlar að kasta upp á þakið.", + ): + s = r.parse_single(sentence) + assert s is not None and s.tree is not None, sentence + assert " PP " in s.tree.flat and "NP-OBJ" not in s.tree.flat, (sentence, s.tree.flat) + + +def test_deterministic_tie_break() -> None: + """Equally scored families of children are resolved by the position + of the production in the grammar (first alternative wins) and then + by the spans of the child nodes, never by the order in which the + parser happened to complete the derivations.""" + from reynir.reducer import _ReductionScope + + class FakeReducer: + """Stand-in for ParseForestReducer with no verb context""" + + def get_current_verb(self): + return None + + def set_current_verb(self, verb): + pass + + def push_current_verb(self, verb): + pass + + def pop_current_verb(self): + pass + + class FakeNode: + is_completed = False + nonterminal = None + start = 0 + end = 3 + + def __init__(self, start=0, end=3): + self.start = start + self.end = end + + class FakeProd: + priority = 0 + + def __init__(self, index): + self.index = index + + def choose(families): + """families: list of (prod, children); all scored equally. + Returns the index of the winning family.""" + reducer = FakeReducer() + scope = _ReductionScope(reducer, FakeNode()) # type: ignore + for ix, (prod, children) in enumerate(families): + scope.start_family(ix, prod, children) # type: ignore + for _ in children: + scope.add_child(ix, {"sc": 1}) # type: ignore + _, chosen = scope.process(FakeNode()) # type: ignore + return chosen + + a, b = FakeNode(0, 1), FakeNode(1, 3) + # The production appearing first in the grammar wins, + # regardless of family order + assert choose([(FakeProd(10), [a, b]), (FakeProd(5), [a, b])]) == 1 + assert choose([(FakeProd(5), [a, b]), (FakeProd(10), [a, b])]) == 0 + # Same production: the derivation whose first child spans more wins + p = FakeProd(7) + left_heavy = [FakeNode(0, 2), FakeNode(2, 3)] + right_heavy = [FakeNode(0, 1), FakeNode(1, 3)] + assert choose([(p, right_heavy), (p, left_heavy)]) == 1 + assert choose([(p, left_heavy), (p, right_heavy)]) == 0 + # A better score still beats a better key + reducer = FakeReducer() + scope = _ReductionScope(reducer, FakeNode()) # type: ignore + scope.start_family(0, FakeProd(5), [a, b]) # type: ignore + scope.add_child(0, {"sc": 1}) # type: ignore + scope.start_family(1, FakeProd(10), [a, b]) # type: ignore + scope.add_child(1, {"sc": 2}) # type: ignore + assert scope.process(FakeNode())[1] == 1 # type: ignore + + +def test_locative_i_prefers_dative(r) -> None: + """When the noun form is the same in accusative and dative, 'í' is + read as locative (dative) unless a verb frame says otherwise""" + for sentence in ( + "Áfram óvissa meðan ástandið er svona í Úkraínu.", + "Regína er sviðsstjóri velferðarsviðs í Reykjavík.", + "Samkomulagið er í höfn.", + "Slysið varð í umdæmi lögreglunnar.", + ): + s = r.parse_single(sentence) + assert s and s.tree, sentence + assert "P fs_þgf /P" in s.tree.flat, sentence + assert "P fs_þf /P" not in s.tree.flat, sentence + # Directional readings governed by the verb frame stay accusative + for sentence in ( + "Chelsea kom í heimsókn.", + "Hann fór í bæinn.", + "Hún setti bókina í hillu.", + ): + s = r.parse_single(sentence) + assert s and s.tree, sentence + assert "P fs_þf /P" in s.tree.flat, sentence + assert "P fs_þgf /P" not in s.tree.flat, sentence + + +def test_verb_with_clitic_subject(r) -> None: + """Interrogative verb forms with a cliticized second person subject + ('ertu', 'viltu', 'geturðu') parse as verbs with an included subject""" + for sentence, lemma in ( + ("Ertu búinn?", "vera"), + ("Viltu koma út að leika?", "vilja"), + ("Geturðu hjálpað mér?", "geta"), + ("Veistu hvað klukkan er?", "vita"), + ("Hefurðu heyrt eitthvað frá íbúunum?", "hafa"), + ("Ertu duglegur að hreyfa þig?", "vera"), + ("Hvernig komstu í kynni við vinnuna?", "koma"), + ("Ferðu á ballið?", "fara"), + ("Áttu einhverja sögu af vandræðalegu stefnumóti?", "eiga"), + ("Þá viltu það ekki.", "vilja"), + ): + s = r.parse_single(sentence) + assert s and s.tree, sentence + flat = s.tree.flat + assert "NP-SUBJ" not in flat, sentence + assert "_sp" in flat, sentence + assert lemma in s.tree.verbs, sentence + # The clitic form is not an imperative of 'erta' + s = r.parse_single("Ertu búinn?") + assert s and s.tree + assert s.tree.flat == ( + "S0 S-QUE IP VP VP-AUX so_et_sp /VP-AUX VP so_lhþt_sb_nf_et_kk /VP " + "/VP /IP p /S-QUE /S0" + ) + # A regular second person form does not get an implied subject + s = r.parse_single("Ert búinn?") + assert s and s.tree is None + # An explicit subject still parses normally + s = r.parse_single("Ert þú búinn?") + assert s and s.tree + assert "NP-SUBJ pfn_et_nf /NP-SUBJ" in s.tree.flat + + +def test_article_with_nominalized_adjective(r) -> None: + """The free article 'hinn/hin/hið' followed by an adjective forms + a noun phrase on its own ('hið sama', 'hið opinbera', 'hið versta'), + or an adverbial phrase with certain superlatives ('hið fyrsta')""" + for sentence, phrase in ( + ("Hið sama á við um starfsfólk deildarinnar.", "NP-SUBJ gr_et_nf_hk lo_nf_et_hk /NP-SUBJ"), + ("Hið rétta er að það kviknaði í bátnum.", "NP-SUBJ gr_et_nf_hk lo_nf_et_hk /NP-SUBJ"), + # 'sama gildir' is disambiguated in Phrases.conf; the adjective + # reading must survive so that the article can attach to it + ("Hið sama gildir um fleiri tegundir.", "NP-SUBJ gr_et_nf_hk lo_nf_et_hk /NP-SUBJ"), + ("Hið opinbera greiðir kostnaðinn.", "NP-SUBJ gr_et_nf_hk lo_nf_et_hk /NP-SUBJ"), + ("Björgunarsveitir óttast hið versta.", "NP-OBJ gr_et_þf_hk lo_þf_et_hk /NP-OBJ"), + ("Eftir útskýringar kom hið sanna í ljós.", "NP-SUBJ gr_et_nf_hk lo_nf_et_hk /NP-SUBJ"), + ("Hann sá hið góða og hið illa.", "gr_et_þf_hk lo_þf_et_hk C st /C gr_et_þf_hk lo_þf_et_hk"), + ("Verkefnið þarf að leysa hið fyrsta.", "ADVP gr_et_þf_hk lo_vb_et_þf_hk /ADVP"), + ("Íbúum var gert að yfirgefa heimili sín hið snarasta.", "ADVP gr_et_þf_hk lo_vb_et_þf_hk /ADVP"), + # The article still attaches to a following noun + ("Hið nýja hús er stórt.", "NP-SUBJ gr_et_nf_hk lo_nf_et_hk no_et_nf_hk /NP-SUBJ"), + ("Hann fékk hið fyrsta verkefni.", "NP-OBJ gr_et_þf_hk lo_þf_et_hk no_et_þf_hk /NP-OBJ"), + ): + s = r.parse_single(sentence) + assert s and s.tree, sentence + assert phrase in s.tree.flat, (sentence, s.tree.flat) + + +def test_quoted_question_attribution(r) -> None: + """A quoted question or exclamation followed directly by an + attribution ('spyr hann', 'sagði hún', 'bætir hún við') without + an intervening comma""" + for sentence, attr in ( + ("„Komst hann ekki í hópinn?“ sagði hann.", "segja"), + ("„Við erum að falla á tíma!“ skrifar Limbourg.", "skrifa"), + ("„Áttu mynd?“ spyr sá þriðji.", "spyrja"), + ("„Er þetta verk dagskrárstjórans?“ spyr hann.", "spyrja"), + ("„Er það ekki bara flott fyrirsögn?“ svarar Ottó hlæjandi.", "svara"), + ("„Hvað er það sem Miðflokkurinn er að gera?“ bætir hún við.", "bæta"), + ("„Er þetta gott?“ sagði Sindri og bætti við: „Þetta er mjög gott.“", "segja"), + ("„Er þátturinn ekki að verða búinn?“ spurði Þorgerður og hló.", "hlæja"), + ): + s = r.parse_single(sentence) + assert s and s.tree, sentence + flat = s.tree.flat + assert flat.startswith("S0 S-QUOTE IP CP-QUOTE "), (sentence, flat) + assert attr in s.tree.verbs, sentence + # A proper noun as the attributed source is third person singular + s = r.parse_single("„Við erum að falla á tíma!“ skrifar Limbourg.") + assert s and s.tree + assert "VP so_0_gm_fh_et_p3 /VP sérnafn" in s.tree.flat + s = r.parse_single("„Þetta er gott,“ segir Limbourg.") + assert s and s.tree + assert "VP so_0_gm_fh_et_p3 /VP sérnafn" in s.tree.flat + + +def test_thess_i_stad(r) -> None: + """'þess í stað' is a fixed adverbial phrase""" + for sentence in ( + "Þess í stað fór hann heim.", + "Hann fór þess í stað í bæinn.", + "Þess í stað hafi flugfélagið fengið leiguvélar.", + "Það er ekki rétt, þess í stað er loftinu hleypt úr dekkjunum.", + "Hann gaf upp ferilinn og fór þess í stað að læra.", + ): + s = r.parse_single(sentence) + assert s and s.tree, sentence + assert any( + t.text.lower() == "þess í stað" and t.terminal == "ao" + for t in s.tree.leaves + ), sentence + + +def test_sagdur_hafa(r) -> None: + """Raising construction: X er sagður/talinn (ekki) hafa gert Y / vera Z""" + for sentence, verbs in ( + ("Maðurinn er sagður hafa stolið bílnum.", ["vera", "segja", "hafa", "stela"]), + ("Konan er sögð hafa stolið bílnum.", ["vera", "segja", "hafa", "stela"]), + ("Mennirnir eru sagðir hafa stolið bílnum.", ["vera", "segja", "hafa", "stela"]), + ("Maðurinn er talinn hafa stolið bílnum.", ["vera", "telja", "hafa", "stela"]), + ("Því var hann ekki talinn hafa misnotað aðstöðu sína.", ["vera", "telja", "hafa", "misnota"]), + ("Hann er sagður vera í haldi lögreglu.", ["vera", "segja", "vera"]), + ("Hún er sögð hafa verið í haldi lögreglu.", ["vera", "segja", "hafa", "vera"]), + ("Hann er sagður búa í Reykjavík.", ["vera", "segja", "búa"]), + ): + s = r.parse_single(sentence) + assert s and s.tree, sentence + assert s.tree.verbs == verbs, (sentence, s.tree.verbs) + assert "lhþt" in s.tree.flat, sentence + + if __name__ == "__main__": # When invoked as a main module, do a verbose test from reynir import Greynir @@ -2559,4 +2840,13 @@ def test_rel_clause_pp_attachment(r) -> None: test_designation_numeral(g) test_skommu_eftir_ad(g) test_rel_clause_pp_attachment(g) + test_reflexive_verb_frames(g) + test_verb_particle_before_object(g) + test_deterministic_tie_break() + test_locative_i_prefers_dative(g) + test_verb_with_clitic_subject(g) + test_article_with_nominalized_adjective(g) + test_quoted_question_attribution(g) + test_thess_i_stad(g) + test_sagdur_hafa(g) g.__class__.cleanup()