From d143a14ac951162fbfee891d36bfb6d9f2277bdb Mon Sep 17 00:00:00 2001 From: Vilhjalmur Thorsteinsson Date: Fri, 31 Jul 2026 17:47:48 +0000 Subject: [PATCH 1/3] Fix six parse coverage gaps found in real-world sentences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Phrases.conf: the "vera* bent á" ambiguous phrase forced 'á' to be an adverb, making the impersonal passive "er bent á" + Nl unparseable; now fs/ao ("Í skýrslunni er bent á kostina") - bintokenizer: non-inflecting (foreign) person names, with identical forms in nf/þf/þgf, now also get an ef interpretation, so they work as genitive qualifiers ("Samtök Paul Watson"); inflecting Icelandic names are unaffected - Grammar: allow a SamanburðurNl ("sem" + Nl_nf role complement) inside subject-gap relative clauses ("...sem starfar sem sérfræðingur hjá Eflingu") - Grammar: allow an oblique subject with a perfect infinitive in the complement of segja/telja/álíta ("segir Spáni hafa mistekist að...", "segir mig hafa dreymt vel") - Grammar: support the postposed order genitive-Nl + "vegna" ("sýningarinnar vegna", "mín vegna") as a prepositional phrase; previously only misparsed as an adjective - Grammar: allow a personal pronoun followed by an agreeing adjective phrase as a noun phrase ("með hann fremstan í fararbroddi") Each fix has a regression test in test_parse.py, mostly with exact flat-tree assertions. Co-Authored-By: Claude Fable 5 --- src/reynir/Greynir.grammar | 18 +++- src/reynir/bintokenizer.py | 20 ++++- src/reynir/config/Phrases.conf | 2 +- test/test_parse.py | 154 +++++++++++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 3 deletions(-) diff --git a/src/reynir/Greynir.grammar b/src/reynir/Greynir.grammar index f83ec5d3..631aaae5 100644 --- a/src/reynir/Greynir.grammar +++ b/src/reynir/Greynir.grammar @@ -1662,7 +1662,7 @@ SetningÁnF_et_p3_hk → EinSetningÁnF/tala/pers/kyn → Atviksliður? ÖfugurSagnliður/tala/pers/kyn - LoTengtSögn_nf/tala/kyn? ÖfugtFramhald/tala/pers/kyn* + LoTengtSögn_nf/tala/kyn? SamanburðurNl? ÖfugtFramhald/tala/pers/kyn* # Lýsingarorð á undan sögn: # (Ráðherrar taka þær ákvarðanir sem) nauðsynlegar eru/þykja/teljast til rækslu starfa þeirra | LoTengtSögn_nf/tala/kyn Sögn_0/tala/pers/kyn SagnInnskotAtv? @@ -2796,6 +2796,12 @@ SagnarBotn → | Nl_ef AtvFs? so_subj_nh_ef # (Anna telur) fólki fækka í landinu | Nl_þgf AtvFs? so_subj_nh_þgf + # (Hún segir) mig hafa dreymt (vel) + | Nl/aukafall AtvFs? 'hafa:so'_0_nh so_subj_sagnb/aukafall ÓpSagnarBotn? + # (Hún segir) Spáni hafa mistekist (að vernda landamærin) + # (miðmyndarsögn með frumlagi í þágufalli, sbr. finítu regluna + # 'NlFrumlag_þgf ... HjSögnMM so_mm_sagnb MiðmyndarBotn?') + | Nl_þgf AtvFs? 'hafa:so'_0_nh so_mm_sagnb MiðmyndarBotn? # (Már segir) [í grundvallaratriðum] gæta misskilnings (hjá Hannesi) | FsAtv? so_subj_nh_ef Nl_ef # !!! Ekki virðist þörf fyrir öfuga orðaröð í þgf eða þf @@ -3893,6 +3899,7 @@ NlStak_p3/tala/fall/kyn → NlKjarni/tala/fall/kyn NlStakTagl/tala/fall/kyn? | NlFornafn/tala/fall/kyn | Pfn/kyn/tala/fall to/tala/fall/kyn? 'persónulega:ao'? # 'þið tvær', 'hann einn', 'hán eitt' + | Pfn/kyn/tala/fall LoEftirNl/tala/fall/kyn # '(með) hann fremstan (í fararbroddi)', '(ég hitti) hana glaða' | LoTöluorð_ef 'konar:kk'_ef_et NlKjarni/tala/fall/kyn # 'tvenns/þrenns/ferns konar innstungur' | Raðnr/fall/kyn NlKjarni/tala/fall/kyn Fornafn/tala/fall/kyn* # 1. verðlaunin öll, 1.-3. sætið, 10. og 12. árið | NlStakNema/tala/fall/kyn @@ -5954,6 +5961,7 @@ FsMeðFallstjórn → # Fleiryrtar forsetningar sem eru ekki dregnar saman í Phrases.conf | 'nær:fs'_þgf 'draga:so'_op_gm_et_p3 Nl_þgf # 'þegar nær dró/dregur jólum' | 'beggja:fn'_hk_ef_ft 'vegna:fs'_ef Nl_ef # 'beggja vegna vegarins' + | FsVegnaEftirNl # 'sýningarinnar vegna', 'mín vegna' | 'bera:so'_lhþt_sb_hk_nf_et "saman:ao" 'við:fs'_þf Nl_þf # borið saman við (fyrra ár) | "fyrir:ao" "nú:ao"? 'utan:fs'_þf Nl_þf | "innan:ao" "sem:st" 'utan:fs'_ef Nl_ef @@ -5993,6 +6001,14 @@ FsMeðFallstjórn → $score(+1) FsMeðFallstjórn +# Eignarfallsliður á undan 'vegna': 'sýningarinnar vegna', +# 'veðursins vegna', 'mín vegna' +FsVegnaEftirNl → + Nl_ef 'vegna:fs'_ef + +# Þarf að vinna gegn rangri greiningu á 'vegna' sem lýsingarorði +$score(+4) FsVegnaEftirNl + MiðaðVið → "miðað:so" 'við:fs'_þf | 'miðað_við:fs'_þf # Samtals eigið fé m.v. síðustu tölur ... diff --git a/src/reynir/bintokenizer.py b/src/reynir/bintokenizer.py index 88cd2260..3a9fbcb0 100644 --- a/src/reynir/bintokenizer.py +++ b/src/reynir/bintokenizer.py @@ -1259,6 +1259,24 @@ def _compatible(pn: PersonNameTuple, npn: PersonNameTuple) -> bool: return True +def _add_uninflected_genitive(gn: List[PersonNameTuple]) -> List[PersonNameTuple]: + """If a name appears in the same form in the nominative, accusative + and dative cases, it is a non-inflecting (foreign) name. Such names + are also used unmodified in genitive position ('Samtök Paul Watson', + 'bók Mary Robinson'), so we add the genitive case to the list of + possible interpretations.""" + seen: Dict[Tuple[str, Optional[str]], Set[str]] = defaultdict(set) + for pn in gn: + if pn.case: + seen[(pn.name, pn.gender)].add(pn.case) + additions = [ + PersonNameTuple(name=name, gender=gender, case="ef") + for (name, gender), cases in seen.items() + if "ef" not in cases and cases >= {"nf", "þf", "þgf"} + ] + return gn + additions if additions else gn + + def parse_phrases_2( token_stream: TokenIterator, token_ctor: TokenConstructor, auto_uppercase: bool ) -> TokenIterator: @@ -1604,7 +1622,7 @@ def eat_surnames( if not weak: # Return a person token with the accumulated name # and the intersected set of possible cases - token = token_ctor.Person(w, gn) + token = token_ctor.Person(w, _add_uninflected_genitive(gn)) token.original = namespan # Yield the current token and advance to the lookahead diff --git a/src/reynir/config/Phrases.conf b/src/reynir/config/Phrases.conf index cefaa649..deb91f6e 100644 --- a/src/reynir/config/Phrases.conf +++ b/src/reynir/config/Phrases.conf @@ -1573,7 +1573,7 @@ meaning = fs frasi - "láta* slag standa" so kk so "láta* þess getið" so pfn so "vísa* málinu" so hk -"vera* bent á" so so ao +"vera* bent á" so so fs/ao "um síðustu áramót" fs lo hk "um síðustu mánaðamót" fs lo hk "um sig" fs * diff --git a/test/test_parse.py b/test/test_parse.py index 707d33c9..7c1c076d 100644 --- a/test/test_parse.py +++ b/test/test_parse.py @@ -2245,6 +2245,154 @@ def test_aukafall(r): assert s and s.tree +def test_impersonal_passive_prep(r): + """The 'vera* bent á' ambiguous phrase must retain the preposition + meaning of 'á', so that 'er bent á' + Nl parses""" + s = r.parse_single("Í skýrslunni er bent á kostina.") + assert s and s.tree + assert ( + s.tree.flat_with_all_variants + == "S0 S-MAIN IP PP P fs_þgf /P NP no_et_gr_kvk_þgf /NP /PP " + "VP VP-AUX so_et_fh_gm_nt_p3 /VP-AUX VP so_gm_sagnb /VP " + "PP P fs_þf /P NP no_ft_gr_kk_þf /NP /PP /VP /IP /S-MAIN p /S0" + ) + s = r.parse_single("Í skýrslunni er bent á kosti og galla upptöku evru.") + assert s and s.tree + # 'bent á' followed by a subordinate clause must still work + s = r.parse_single("Í skýrslunni er bent á að vandinn sé mikill.") + assert s and s.tree + s = r.parse_single( + "Í nýrri ársskýrslu sjóðsins um Ísland er bent á kosti og galla " + "upptöku evru og varað við því að verðtrygging í íslensku " + "efnahagslífi geti viðhaldið verðbólgu." + ) + assert s and s.tree + + +def test_uninflected_genitive_names(r): + """Non-inflecting (foreign) person names, i.e. names having identical + forms in nf/þf/þgf, should also be accepted in genitive contexts""" + s = r.parse_single("Samtök Paul Watson mótmæltu hvalveiðum.") + assert s and s.tree + assert ( + s.tree.flat_with_all_variants + == "S0 S-MAIN IP NP-SUBJ no_ft_hk_nf NP-POSS person_ef_kk person_ef_kk " + "/NP-POSS /NP-SUBJ VP VP so_1_þgf_fh_ft_gm_p3_þt /VP " + "NP-OBJ no_ft_kvk_þgf /NP-OBJ /VP /IP /S-MAIN p /S0" + ) + s = r.parse_single("Ég las bók John Lennon.") + assert s and s.tree + s = r.parse_single( + "Samtök Paul Watson leiða líkur að því að þau hafi með aðgerðum " + "sínum frá því að hvalveiðitímabilið hófst bjargað lífi " + "allt að þrjátíu langreyða." + ) + assert s and s.tree + # Inflecting Icelandic names must not gain a genitive interpretation + s = r.parse_single("Samtök Guðmundur Jónsson eru góð.") + assert s.tree is None + # Token-level check of person name cases + from reynir.bintokenizer import tokenize + + toks = [t for t in tokenize("Samtök Paul Watson mótmæltu hvalveiðum.")] + pw = next(t for t in toks if t.txt == "Paul Watson") + assert set(pn.case for pn in pw.person_names) == {"nf", "þf", "þgf", "ef"} + toks = [t for t in tokenize("Þarna gengur Jón.")] + jon = next(t for t in toks if t.txt == "Jón") + assert "ef" not in set(pn.case for pn in jon.person_names) + + +def test_sem_role_relative_clause(r): + """A 'sem' + Nl_nf role complement ('starfar sem sérfræðingur') + must be allowed inside a subject-gap relative clause""" + s = r.parse_single("Konan, sem starfar sem sérfræðingur hjá bankanum, er glöð.") + assert s and s.tree + assert ( + s.tree.flat_with_all_variants + == "S0 S-MAIN IP NP-SUBJ no_et_gr_kvk_nf p CP-REL C stt /C " + "IP VP so_0_et_fh_gm_nt_p3 /VP CP-ADV-CMP C st /C NP no_et_kk_nf " + "PP P fs_þgf /P NP no_et_gr_kk_þgf /NP /PP /NP /CP-ADV-CMP /IP " + "/CP-REL p /NP-SUBJ VP VP-AUX so_et_fh_gm_nt_p3 /VP-AUX " + "NP-PRD lo_et_kvk_nf_sb /NP-PRD /VP /IP /S-MAIN p /S0" + ) + s = r.parse_single( + "Doktor Stefán Ólafsson, sem starfar sem sérfræðingur hjá Eflingu, " + "gefur ekki mikið fyrir myljandi hagnað bankanna." + ) + assert s and s.tree + + +def test_oblique_subject_infinitive(r): + """Oblique subject + perfect infinitive ('hafa' + supine) in the + complement of segja/telja: 'segir Spáni hafa mistekist'""" + s = r.parse_single("Hún segir Spáni hafa mistekist að vernda landamærin.") + assert s and s.tree + assert ( + s.tree.flat_with_all_variants + == "S0 S-MAIN IP NP-SUBJ pfn_et_kvk_nf_p3 /NP-SUBJ VP VP " + "so_1_þf_et_fh_gm_nt_p3 /VP IP NP no_et_kk_þgf /NP VP so_0_gm_nh /VP " + "VP so_mm_sagnb /VP IP-INF TO nhm /TO VP VP so_1_þf_gm_nh /VP " + "NP-OBJ no_ft_gr_hk_þf /NP-OBJ /VP /IP-INF /IP /VP /IP /S-MAIN p /S0" + ) + s = r.parse_single("Hún segir mig hafa dreymt vel.") + assert s and s.tree + s = r.parse_single("Hún telur Spáni hafa mistekist.") + assert s and s.tree + s = r.parse_single( + "Hún segir Spáni hafa mistekist að vernda ytri landamæri " + "Schengen-svæðisins gegn innrás." + ) + assert s and s.tree + + +def test_postposed_vegna(r): + """A genitive Nl preceding 'vegna' ('sýningarinnar vegna') + should parse as a prepositional phrase""" + s = r.parse_single("Sýningarinnar vegna ákvað hann þetta.") + assert s and s.tree + assert ( + s.tree.flat_with_all_variants + == "S0 S-MAIN IP PP NP no_ef_et_gr_kvk /NP P fs_ef /P /PP " + "VP VP so_1_þf_et_fh_gm_p3_þt /VP NP-SUBJ pfn_et_kk_nf_p3 /NP-SUBJ " + "NP-OBJ fn_et_hk_þf /NP-OBJ /VP /IP /S-MAIN p /S0" + ) + s = r.parse_single("Hann gerði þetta mín vegna.") + assert s and s.tree + s = r.parse_single("Hann gerði þetta veðursins vegna.") + assert s and s.tree + # Normal (preposed) order must be unaffected + s = r.parse_single("Hann gerði þetta vegna veðursins.") + assert s and s.tree + s = r.parse_single("Trén standa beggja vegna vegarins.") + assert s and s.tree + s = r.parse_single( + "Umboðsmaður Boy George sagði að sýningarinnar vegna hefði hann " + "ákveðið að söngvarinn yrði ekki með í henni." + ) + assert s and s.tree + + +def test_pronoun_postposed_adjective(r): + """A personal pronoun followed by an agreeing adjective phrase + should form a noun phrase: '(með) hann fremstan (í fararbroddi)'""" + s = r.parse_single("Með hann fremstan í fararbroddi vann liðið leikinn.") + assert s and s.tree + assert ( + s.tree.flat_with_all_variants + == "S0 S-MAIN IP PP P fs_þf /P NP pfn_et_kk_p3_þf lo_esb_et_kk_þf " + "PP P fs_þgf /P NP no_et_kk_þgf /NP /PP /NP /PP " + "VP VP so_1_þf_et_fh_gm_p3_þt /VP NP-SUBJ no_et_gr_hk_nf /NP-SUBJ " + "NP-OBJ no_et_gr_kk_þf /NP-OBJ /VP /IP /S-MAIN p /S0" + ) + s = r.parse_single("Ég hitti hana glaða.") + assert s and s.tree + s = r.parse_single( + "Með hann fremstan í fararbroddi tókst liði Levante " + "að bjarga sér frá falli." + ) + assert s and s.tree + + if __name__ == "__main__": # When invoked as a main module, do a verbose test from reynir import Greynir @@ -2296,4 +2444,10 @@ def test_aukafall(r): print(e) test_foreign(g) test_aukafall(g) + test_impersonal_passive_prep(g) + test_uninflected_genitive_names(g) + test_sem_role_relative_clause(g) + test_oblique_subject_infinitive(g) + test_postposed_vegna(g) + test_pronoun_postposed_adjective(g) g.__class__.cleanup() From 13e31925709f14e4e0c442597e8b8fe7c76fc15a Mon Sep 17 00:00:00 2001 From: Vilhjalmur Thorsteinsson Date: Fri, 31 Jul 2026 17:48:16 +0000 Subject: [PATCH 2/3] Add error grammar rule for non-agreeing postposed adjectives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New VillaLoEftirNlMeðAndlagi rule in the $if(include_errors) section, used by GreynirCorrect: matches a postposed adjective (typically a past participle) with a case-governed complement that does not agree with its head noun phrase in case, e.g. 'móðurfélag fleiri félaga tengdum rekstri Morgunblaðsins' where 'tengdum' should be 'tengdra'. The corresponding annotation handler is in GreynirCorrect PR #69 (already merged; the handler is inert until this rule ships). Co-Authored-By: Claude Fable 5 --- src/reynir/Greynir.grammar | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/reynir/Greynir.grammar b/src/reynir/Greynir.grammar index 631aaae5..698fe59d 100644 --- a/src/reynir/Greynir.grammar +++ b/src/reynir/Greynir.grammar @@ -7687,6 +7687,30 @@ VillaEndingIR → $tag(error) VillaEndingIR $score(-999) VillaEndingIR +# Regla fyrir villu þar sem lýsingarorð (oft lýsingarháttur þátíðar) +# með fallstýrðu andlagi stendur á eftir nafnlið en sambeygist honum +# ekki í falli, t.d. 'móðurfélag fleiri félaga tengdum rekstri +# Morgunblaðsins' (rétt: 'tengdra'); villan stafar oft af aðlöðun +# að falli andlagsins ('félögum tengdum rekstri') + +LoEftirNl/tala/fall/kyn → + > VillaLoEftirNlMeðAndlagi/fall/tala/kyn + +VillaLoEftirNlMeðAndlagi_nf/tala/kyn → + LoEftirNlMeðAndlagi/fallxnf/tala/kyn + +VillaLoEftirNlMeðAndlagi_þf/tala/kyn → + LoEftirNlMeðAndlagi/fallxþf/tala/kyn + +VillaLoEftirNlMeðAndlagi_þgf/tala/kyn → + LoEftirNlMeðAndlagi/fallxþgf/tala/kyn + +VillaLoEftirNlMeðAndlagi_ef/tala/kyn → + LoEftirNlMeðAndlagi/fallxef/tala/kyn + +$tag(error) VillaLoEftirNlMeðAndlagi/fall/tala/kyn +$score(-999) VillaLoEftirNlMeðAndlagi/fall/tala/kyn + # Orð sem endar á -ana í þolfalli notað í stað eignarfalls: # 'niðurstaða þingflokkana', 'hattar karlana' From 32d3be2110f32d9fb081aef5747631763d339efb Mon Sep 17 00:00:00 2001 From: Vilhjalmur Thorsteinsson Date: Fri, 31 Jul 2026 18:25:29 +0000 Subject: [PATCH 3/3] Support designation numerals after nouns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An uninflected neuter nominative numeral used as a designation ('lögreglustöð tvö', 'stofa fimm', 'hlið átta') had no grammar production; digit forms only parsed via a spurious genitive reading. Added a NúmerTala alternative to the Númer slot in NlKjarni, with a score penalty so that agreeing numerals ('mennina þrjá') are still preferred wherever agreement is possible. Co-Authored-By: Claude Fable 5 --- src/reynir/Greynir.grammar | 9 +++++++++ test/test_parse.py | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/reynir/Greynir.grammar b/src/reynir/Greynir.grammar index 698fe59d..3286b9d8 100644 --- a/src/reynir/Greynir.grammar +++ b/src/reynir/Greynir.grammar @@ -3749,8 +3749,17 @@ Fyrirbæri_ft/fall/kyn → Númer → 'númer:hk' Tala_hk_nf + | NúmerTala + +# Óbeygð tala í hvorugkyni nefnifalls sem auðkenni á eftir nafnorði: +# '(lögreglu)stöð tvö', 'stofa fimm', 'hlið átta', 'rás eitt' +NúmerTala → + Töluorð_et_nf_hk + | Töluorð_ft_nf_hk $score(+8) Númer +# Velja frekar sambeygða tölu (Fjöldi) en auðkennistölu, ef báðar koma til greina +$score(-4) NúmerTala Fjöldi_ft/fall/kyn → to_ft/fall/kyn | töl_ft/fall/kyn diff --git a/test/test_parse.py b/test/test_parse.py index 7c1c076d..d726553a 100644 --- a/test/test_parse.py +++ b/test/test_parse.py @@ -2393,6 +2393,30 @@ def test_pronoun_postposed_adjective(r): assert s and s.tree +def test_designation_numeral(r) -> None: + """An uninflected neuter nominative numeral used as a designation + after a noun: '(lögreglu)stöð tvö', 'stofa fimm'""" + s = r.parse_single("Hann býr í umdæmi lögreglustöðvar tvö.") + assert s and s.tree + assert ( + s.tree.flat_with_all_variants + == "S0 S-MAIN IP NP-SUBJ pfn_et_kk_nf_p3 /NP-SUBJ VP VP so_0_et_fh_gm_nt_p3 /VP " + "PP P fs_þgf /P NP no_et_hk_þgf NP-POSS no_ef_et_kvk to_ft_hk_nf " + "/NP-POSS /NP /PP /VP /IP /S-MAIN p /S0" + ) + s = r.parse_single("Hann býr í stofu fimm.") + assert s and s.tree + s = r.parse_single( + "Það átti sér stað í umdæmi lögreglustöðvar tvö, " + "sem sér um Hafnarfjörð og Garðabæ." + ) + assert s and s.tree + # An agreeing numeral must still be preferred where possible + s = r.parse_single("Ég sá mennina þrjá.") + assert s and s.tree + assert "to_ft_kk_þf" in s.tree.flat_with_all_variants + + if __name__ == "__main__": # When invoked as a main module, do a verbose test from reynir import Greynir @@ -2450,4 +2474,5 @@ def test_pronoun_postposed_adjective(r): test_oblique_subject_infinitive(g) test_postposed_vegna(g) test_pronoun_postposed_adjective(g) + test_designation_numeral(g) g.__class__.cleanup()