diff --git a/contrib/nvim/README.md b/contrib/nvim/README.md new file mode 100644 index 0000000..c61fa52 --- /dev/null +++ b/contrib/nvim/README.md @@ -0,0 +1,80 @@ +# Neovim setup for .sloka / .sutra files + +Requires Neovim 0.11+ and `uv` on your PATH. Both extensions get the +`nirukta` filetype. + +## Install + +### With lazy.nvim (recommended) + +Add a spec that puts the tree-sitter grammar on the runtimepath and loads +this file: + +```lua +local NIRUKTA = vim.fn.expand("~/Software/nirukta") + +return { + dir = NIRUKTA .. "/tree-sitter-nirukta", + name = "tree-sitter-nirukta", + lazy = false, + build = "tree-sitter generate" + .. " && cc -shared -fPIC -O2 -I src src/parser.c -o parser/nirukta.so", + config = function() + dofile(NIRUKTA .. "/contrib/nvim/nirukta.lua") + end, +} +``` + +After changing `grammar.js`, rebuild with `:Lazy build tree-sitter-nirukta`. + +### Without a plugin manager + +`:source` this file (or copy it into your config); it adds the grammar +directory to the runtimepath itself. Build the parser once as described in +`tree-sitter-nirukta/README.md`. + +The server runs out of this repo's virtualenv via `uv run --project`, so +`uv sync` must have been run once. + +## What you get + +- Syntax highlighting via the tree-sitter grammar in `tree-sitter-nirukta/` + (error-tolerant: highlighting survives while a line is mid-edit). +- Parse errors as diagnostics on every edit (about 200ms after you stop + typing). +- Sandhi, vocabulary, declension, and gloss validation as diagnostics on + open and save, streamed in sloka by sloka. Results are cached per line + (in memory and in `~/.cache/nirukta-lsp/`), so only edited lines are + revalidated and reopening unchanged files is fast; the first-ever pass + over a new verse is the only slow one. +- `K` on a word shows its IAST/Devanagari forms, glosses, declension + analysis, and MW/Apte definitions. +- Completion after `stem->` offers every declined form of the stem with its + case, number, and paradigm (works with nvim-cmp or ``; also for + merged compounds, via the parametric paradigms). +- `vocabulary suggestion` warnings carry quickfixes: `gra` (or + `vim.lsp.buf.code_action()`) inserts the suggested `stem->` prefix while + keeping glosses and accents intact. +- In `.sutra` files, broken `file:` references are flagged; referenced + `.sloka` files are validated when you open them. + +## Theming + +Highlighting uses standard captures (`@markup.heading`, `@string`, +`@string.special`, `@attribute`, `@operator`, `@punctuation.bracket`, +`@punctuation.delimiter`, `@comment`, `@function`, `@function.method`, +`@string.special.url`). Override them for this language only via the +`.nirukta` suffix, e.g.: + +```lua +vim.api.nvim_set_hl(0, "@string.special.nirukta", { fg = "#33B1FF" }) +``` + +## Troubleshooting + +- `:checkhealth vim.lsp` shows whether the client attached. +- Set `NIRUKTA_LSP_LOG=/tmp/nirukta-lsp.log` before launching nvim to + capture server logs. Sending `SIGUSR1` to the server process dumps all + thread stacks to `.stacks` if it ever seems stuck. +- No highlighting means `parser/nirukta.so` has not been built yet; see + `tree-sitter-nirukta/README.md`. diff --git a/contrib/nvim/nirukta.lua b/contrib/nvim/nirukta.lua new file mode 100644 index 0000000..cebda02 --- /dev/null +++ b/contrib/nvim/nirukta.lua @@ -0,0 +1,35 @@ +-- Neovim (0.11+) integration for the nirukta language server and +-- tree-sitter grammar. Source this file, or load it from a plugin +-- manager (see README.md). + +-- this file lives at /contrib/nvim/nirukta.lua +local script = debug.getinfo(1, "S").source:sub(2) +local repo = vim.fs.dirname(vim.fs.dirname(vim.fs.dirname(script))) + +vim.filetype.add({ + extension = { + sloka = "nirukta", + sutra = "nirukta", + }, +}) + +local grammar_dir = repo .. "/tree-sitter-nirukta" +if not vim.tbl_contains(vim.opt.runtimepath:get(), grammar_dir) then + vim.opt.runtimepath:append(grammar_dir) +end + +vim.api.nvim_create_autocmd("FileType", { + pattern = "nirukta", + callback = function(ev) + vim.bo[ev.buf].commentstring = "/*%s*/" + pcall(vim.treesitter.start, ev.buf, "nirukta") + end, +}) + +vim.lsp.config("nirukta", { + cmd = { "uv", "run", "--project", repo, "nirukta-lsp" }, + filetypes = { "nirukta" }, + root_markers = { "pyproject.toml", ".git" }, +}) + +vim.lsp.enable("nirukta") diff --git a/library/aBinayadarpaRe_1.sloka b/library/aBinayadarpaRe_1.sloka index d5acefc..478d380 100644 --- a/library/aBinayadarpaRe_1.sloka +++ b/library/aBinayadarpaRe_1.sloka @@ -11,5 +11,5 @@ vAcika[verbal expression]->vAcikam=>vAcikaM sarva[all]+(vANmaya[language]->vANma AhArya[ornamental expression]->AhAryam=>AhAryaM candra[moon]+tAra[stars]+Adi[etc]=candratArAdi "Whose ornamental expression is the moon, stars, etc." -tat[that]=>taM nam[praise]->numaH[We] sAttvika[pure]->sAttvikam=>sAttvikaM Siva[lord shiva]->Sivam .. +tat[that]=>tan nam[praise]->numaH[We] sAttvika[pure]->sAttvikam=>sAttvikaM Siva[lord shiva]->Sivam .. "We praise that pure lord shiva." diff --git a/library/birthday.sloka b/library/birthday.sloka index bf42f1f..4908b6f 100644 --- a/library/birthday.sloka +++ b/library/birthday.sloka @@ -10,8 +10,8 @@ amfta[nectar]->amftam=>amftaM --- line --- sUrya[sun]->sUryaH[the]=>sUryo -jyotis[heavenly bodies]+(gaRa[all]->gaRasya[among])+iva[like]=jyotirgaRasyeva -"like the sun among all the heavenly bodies," +jyotis[heavenly bodies]+(gaRa[multitude]->gaRasya[among])+iva[like]=jyotirgaRasyeva +"like the sun among the multitude of heavenly bodies," taTA[likewise]+eva[indeed]=taTEva tava[your]->te saKitvana[friendship]->saKitvanam .. "likewise indeed is your friendship." diff --git a/library/mahAmftyuYjayamantra.sloka b/library/mahAmftyuYjayamantra.sloka index 0bc558f..986f6ec 100644 --- a/library/mahAmftyuYjayamantra.sloka +++ b/library/mahAmftyuYjayamantra.sloka @@ -5,19 +5,19 @@ oM[om] "om" --- line --- -tri[three]+ambakam[eyes]=tryambakam=>trya\'mbakaM yajAmahe[We][worship] +tri[three]+(ambaka->ambakam[eyes])=tryambakam=>trya\'mbakaM yajAmahe[We][worship] "We worship the one with three eyes," -su[good]+ganDim[fragrance]=suganDim=>su\_ganDiM\' -puzwi[nourisher]+varDanam[growth]=puzwi\_varDa\'nam . +su[good]+(ganDi->ganDim[fragrance])=suganDim=>su\_ganDiM\' +puzwi[nourisher]+(varDana[growth]->varDanam)=puzwi\_varDa\'nam . "the one of good fragrance, and nourisher of growth" --- line --- -urvArukam[cucumber]+iva[Like]=u\_rvA\_ru\_kami\'va\_ banDanAt[from][the][stem]=>banDa\'nAn +(urvAruka->urvArukam[cucumber])+iva[Like]=u\_rvA\_ru\_kami\'va\_ banDanAt[from][the][stem]=>banDa\'nAn "Like a cucumber from the stem," -mftyoH[from][death]=>mf\_tyor +mftyu[death]->mftyoH[from]=>mf\_tyor mu\'kzIya\_[May][I][be][liberated] -mA[not]+amftAt[from][immortality]=mAmftA\'t .. +mA[not]+(amfta[immortality]->amftAt[from])=mAmftA\'t .. "May I be liberated from death, but not from immortality." diff --git a/library/sUtrARi/SrIsarasvatIstotraM.sutra b/library/sUtrARi/SrIsarasvatIstotraM.sutra index 8cd6729..bc17bee 100644 --- a/library/sUtrARi/SrIsarasvatIstotraM.sutra +++ b/library/sUtrARi/SrIsarasvatIstotraM.sutra @@ -3,8 +3,8 @@ === sloka === --- line --- -SrI[radiant]+(gaReSa[to][Ganesha]->gaReSAya)=SrIgaReSAya namas[Reverence]->namaH . -"Reverence to the radiant Ganesha." +SrI[radiant]+(gaReSa[to][Ganesha]->gaReSAya)=SrIgaReSAya namas[Adoration]->namaH . +"Adoration to the radiant Ganesha." === sloka === @@ -31,27 +31,27 @@ Sveta[white]+padma[lotus]+AsanA[seated]=SvetapadmAsanA . --- line --- yA[She who] -(brahma[Brahma]+acyuta[Viṣṇu]+SaNkara[Śiva]+(praBfti[other]->praBftiBiH)+(deva[devas]->devEH)=brahmAcyutaSaNkarapraBftiBirdevEH)+sadA[always]=brahmAcyutaSaNkarapraBftiBirdevEssadA +(brahma[Brahma]+acyuta[Viṣṇu]+SaNkara[Śiva]+(praBfti[offerings]->praBftiBiH[using])+(deva[devas]->devEH)=brahmAcyutaSaNkarapraBftiBirdevEH)+sadA[always]=brahmAcyutaSaNkarapraBftiBirdevEssadA pUjitA[worshipped] -"She who is always worshipped by Brahma, Viṣṇu, Śiva, and other devas." +"She who is always worshipped by Brahma, Viṣṇu, Śiva, using offerings." --- line --- sA[that] mA[me]->mAm=>mAM pAtu[May][protect] sarasvatI[Sarasvatī] BagavatI[divine] -niSSeza[completely]+jAqya[ignorance]+apaha[removes]=niSSezajAqyApahA .. 1 .. +niSSeza[completely]+jAqya[coldness]+apaha[removing]=niSSezajAqyApahA .. 1 .. "May that divine Sarasvatī who removes ignorance completely, protect me." === sloka === --- line --- -(dorBi[arms]->dorBiH)+yuktA[Endowed]=dorBiryuktA +(dorBis[arms]->dorBiH)+yuktA[Endowed]=dorBiryuktA catur[four]->catUrBiH sPawika[crystal]+maRi[gems]+(niBa[resembling]->niBEH)+akzamAlAn[rosary]+daDAnA[holding]=sPawikamaRiniBErakzamAlAndaDAnA "Endowed with four arms, holding a rosary of beads resembling crystal gems," (hasta[in][hand]->hastena)+ekena[one]=hastenEkena -padma[lotus]=>padmaM +padma[lotus]->padmam=>padmaM (sita[white]->sitam)+api[also]+ca[and]=sitamapica Sukam[parrot]=>SukaM pustaka[book]->pustakam=>pustakaM @@ -66,10 +66,10 @@ BAsamAnA[Shining]+asamAnA[incomparable]=BAsamAnA'samAnA --- line --- sA[she] -me[my] -(vAc[speech]=>vAK)+devatA[Goddess]+iyam[this]=vAgdevateyam=>vAgdevateyaM +ma[my]->me +(vAc[speech]->vAk)+devatA[Goddess]+(ida[this]->iyam)=vAgdevateyam=>vAgdevateyaM nivasatu[may][reside] -vadan[in][mouth]=>vadane +vadana[mouth]->vadane[in] sarvadA[always] su[very]+prasannA[pleased]=suprasannA .. 2 .. "may she, this Goddess of speech, be very pleased and always reside in my mouth." @@ -78,7 +78,7 @@ su[very]+prasannA[pleased]=suprasannA .. 2 .. --- line --- sura[gods]+asurA[demons]+sevita[served]+pAda[feet]+paNkajA[lotus]=surAsurAsevitapAdapaNkajA -kare[in her hand] +kara[hand]->kare[in] virAjat[resplendently]+kamanIya[beautiful]+pustakA[book]=virAjatkamanIyapustakA . "She whose lotus feet are served by both gods and demons, she who holds in her hand a resplendently beautiful book," @@ -87,7 +87,7 @@ viriYci[Brahma]+patnI[wife]=viriYcipatnI kamala[lotus]+Asana[seat]+sTitA[situated]=kamalAsanasTitA sarasvatI[Sarasvatī] nftyatu[May][dance] -vAci[in][speech] +vac[speech]->vAci[in] me[for me][my] sadA[always] .. 3 .. "the wife of Brahma, situated in a lotus seat. May Sarasvatī always dance for me in my speech." @@ -105,8 +105,8 @@ sita[white]+kamala[lotus]+Asana[seat]+priyA[fond of]=sitakamalAsanapriyA . --- line --- Gana[full]+stanI[breasted]=GanastanI -kamala[lotus]+vilola[darting]+locanA[eyes]=kamalavilolalocanA -"full breasted, with darting lotus eyes -" +kamala[lotus]+vilola[rolling]+locanA[eyes]=kamalavilolalocanA +"full breasted, with rolling lotus eyes -" manasvinI[high-minded] Bavatu[may][be] @@ -117,10 +117,10 @@ vara[boons]+prasAdinI[gracious]=varaprasAdinI .. 4 .. --- line --- sarasvati[O Sarasvatī] -(namas[reverence]->namaH)+tuByam[to you]=namastuByam=>namastuByaM +(namas[adoration]->namaH)+tuByam[to you]=namastuByam=>namastuByaM varadA[boon-giver]->varade kAmarUpiRi[form of longing] . -"O Sarasvatī, boon-giver, the very form of longing; reverence to you." +"O Sarasvatī, boon-giver, the very form of longing; adoration to you." --- line --- vidyA[learning]+(AramBa[beginning]->AramBam)=vidyAramBam=>vidyAramBaM @@ -136,12 +136,12 @@ sadA[always] .. 5 .. --- line --- sarasvati[O Sarasvatī] -(namas[reverence]->namaH)+tuByam[to you]=namastuByam=>namastuByaM -"O Sarasvatī, reverence to you." +(namas[adoration]->namaH)+tuByam[to you]=namastuByam=>namastuByaM +"O Sarasvatī, adoration to you." sarva[all]+devi[O Goddess]=sarvadevi -namas[reverence]->namaH=>namo namas[reverence]->namaH . -"O Goddess of all, reverence, reverence." +namas[adoration]->namaH=>namo namas[adoration]->namaH . +"O Goddess of all, adoration, adoration." --- line --- SAnta[tranquil]+rUpa[form]=SAntarUpa->SAntarUpe @@ -149,8 +149,8 @@ SaSi[moon]+(DarA[bearer]->Dare)=SaSiDare "O you of tranquil form, O bearer of the moon," sarva[all]+yoga[disciplines]=sarvayoga->sarvayoge -namas[reverence]->namaH=>namo namas[reverence]->namaH .. 6 .. -"O you in whom all disciplines reside, reverence, reverence." +namas[adoration]->namaH=>namo namas[adoration]->namaH .. 6 .. +"O you in whom all disciplines reside, adoration, adoration." === sloka === @@ -159,8 +159,8 @@ nitya[constant]+Ananda[in][bliss]=nityAnanda->nityAnande nir[un]+ADAra[supported]=nirADAra->nirADAre "O you in constant bliss, O unsupported one -" -nizkalA[undivided]->nizkalAyE namas[reverence]->namaH=>namo namas[reverence]->namaH . -"reverence, reverence to her who is undivided." +nizkalA[undivided]->nizkalAyE namas[adoration]->namaH=>namo namas[adoration]->namaH . +"adoration, adoration to her who is undivided." --- line --- vidyA[knowledge]+Dara[bearer]=vidyADara->vidyADare @@ -168,16 +168,16 @@ viSA[large]+lAkzi[eyed]=viSAlAkzi "O bearer of knowledge, O large eyed one," SudDa[pure]+jYAna[knowledge]=SudDajYAna->SudDajYAne -namas[reverence]->namaH=>namo namas[reverence]->namaH .. 7 .. -"O embodier of pure knowledge, reverence, reverence." +namas[adoration]->namaH=>namo namas[adoration]->namaH .. 7 .. +"O embodier of pure knowledge, adoration, adoration." === sloka === --- line --- SudDa[pure]+sPawika[crystal]+rUpA[form]=SudDasPawikarUpA->SudDasPawikarUpAyE sUkzma[subtle]+rUpa[form]=sUkzmarUpa->sUkzmarUpe -namas[reverence]->namaH=>namo namas[reverence]->namaH . -"O you of subtle form - reverence, reverence to her of pure crystal form." +namas[adoration]->namaH=>namo namas[adoration]->namaH . +"O you of subtle form - adoration, adoration to her of pure crystal form." --- line --- Sabda[Sabda]+brahmi[Brahman]=Sabdabrahmi @@ -185,22 +185,22 @@ catur[four]+(hasta[handed]->haste)=caturhaste "O you who are Sabda Brahman, O four-handed one -" sarva[all]+(sidDi[accomplishment]->sidDyE)=sarvasidDyE -namas[reverence]->namaH=>namo namas[reverence]->namaH .. 8 .. -"reverence, reverence to her who is all accomplishment." +namas[adoration]->namaH=>namo namas[adoration]->namaH .. 8 .. +"adoration, adoration to her who is all accomplishment." === sloka === --- line --- mukta[pearls]+AlaNkfta[adorned]+sarva[every]+aNgi[limb]=muktAlaNkftasarvANgi->muktAlaNkftasarvANgyE[to][her] mUlADAra[root chakra]->mUlADAre[O] -namas[reverence]->namaH=>namo namas[reverence]->namaH . -"O you who are the root chakra - reverence, reverence to her whose every limb is adorned by pearls." +namas[adoration]->namaH=>namo namas[adoration]->namaH . +"O you who are the root chakra - adoration, adoration to her whose every limb is adorned by pearls." --- line --- mUla[basis]+mantra[mantra]+(svarUpA[self-form]->svarUpAyE[to])=mUlamantrasvarUpAyE mUla[origin]+(Sakti[ability]->SaktyE[to])=mUlaSaktyE -namas[Reverence]->namaH=>namo namas[reverence]->namaH .. 9 .. -"Reverence, reverence to her whose self-form is the basis of mantra itself, to her who is the origin of ability." +namas[Adoration]->namaH=>namo namas[adoration]->namaH .. 9 .. +"Adoration, adoration to her whose self-form is the basis of mantra itself, to her who is the origin of ability." === sloka === @@ -210,15 +210,15 @@ mahA[great]+yoga[yoga]=mahAyoga->mahAyoge "O you who consist of the mind, O embodier of great yoga," vAk[speech]+ISvarI[Goddess]=vAgISvarI->vAgISvari -namas[reverence]->namaH=>namo namas[reverence]->namaH . -"O Goddess of speech, reverence, reverence." +namas[adoration]->namaH=>namo namas[adoration]->namaH . +"O Goddess of speech, adoration, adoration." --- line --- -vARi[music]->vARyE +vARI[music]->vARyE vara[boon]+da[giving]+hastA[hand]=varadahastA->varadahastAyE vara[boon]+dA[giver]=varadA->varadAyE -namas[Reverence]->namaH=>namo namas[reverence]->namaH .. 10 .. -"Reverence, reverence to the embodiment of music, to her who extends a boon-giving hand, to the giver of boons." +namaH[Adoration]=>namo namas[adoration]->namaH .. 10 .. +"Adoration, adoration to the embodiment of music, to her who extends a boon-giving hand, to the giver of boons." === sloka === @@ -226,14 +226,14 @@ namas[Reverence]->namaH=>namo namas[reverence]->namaH .. 10 .. veda[knowledge]->vedA->vedAyE veda[knowledge]+rUpA[form]=vedarUpA->vedarUpAyE veda[Vedas]+anta[culmination]=vedAnta->vedAntA->vedAntAyE -namas[Reverence]->namaH=>namo namas[reverence]->namaH . -"Reverence, reverence to her who is knowledge itself, to her in the form of knowledge, to her who is the culmination of all the Vedas." +namas[Adoration]->namaH=>namo namas[adoration]->namaH . +"Adoration, adoration to her who is knowledge itself, to her in the form of knowledge, to her who is the culmination of all the Vedas." --- line --- guRa[virtues]+doza[faults]+vivarjini[free from]=guRadozavivarjini->guRadozavivarjinyE guRa[qualities]+dIpti[shining]=guRadIpti->guRadIptyE -namas[Reverence]->namaH=>namo namas[reverence]->namaH .. 11 .. -"Reverence, reverence to her who is free from virtues and faults, to her of shining qualities." +namas[Adoration]->namaH=>namo namas[adoration]->namaH .. 11 .. +"Adoration, adoration to her who is free from virtues and faults, to her of shining qualities." === sloka === @@ -243,15 +243,15 @@ sadA[always]+Ananda[bliss]=sadAnanda->sadAnande "O you who are all knowledge, O you who are always in bliss," sarva[all]+rUpa[form]=sarvarUpa->sarvarUpe -namas[reverence]->namaH=>namo namas[reverence]->namaH . -"O you who are all form - reverence, reverence." +namas[adoration]->namaH=>namo namas[adoration]->namaH . +"O you who are all form - adoration, adoration." --- line --- sampannA[perfect]->sampannAyE kumArI[youth]->kumAryE ca[and] sarvajYA[omniscient]->sarvajYAyE -namas[reverence]->namaH=>namo namas[reverence]->namaH .. 12 .. -"O omniscient one - reverence, reverence to her who is perfect and to her who is youth." +namas[adoration]->namaH=>namo namas[adoration]->namaH .. 12 .. +"O omniscient one - adoration, adoration to her who is perfect and to her who is youth." === sloka === @@ -259,15 +259,15 @@ namas[reverence]->namaH=>namo namas[reverence]->namaH .. 12 .. yoga[yoga]+rUpa[form]=yogarUpa->yogarUpe ramA[Lakṣmī]+devI[Goddess]=ramAdevI->ramAdevyE yoga[yoga]+Ananda[bliss]=yogAnanda->yogAnande -namas[reverence]->namaH=>namo namas[reverence]->namaH . -"O you who are the form of yoga, O embodiment of the bliss of yoga - reverence, reverence to that Goddess Lakṣmī." +namas[adoration]->namaH=>namo namas[adoration]->namaH . +"O you who are the form of yoga, O embodiment of the bliss of yoga - adoration, adoration to that Goddess Lakṣmī." --- line --- divya[divine]+jYA[knower]=divyajYA->divyajYAyE tri[three]+netrA[eyed]=trinetrA->trinetrAyE divya[divine]+(mUrti[embodiment]->mUrtI)=divyamUrtI->divyamUrtyE -namas[Reverence]->namaH=>namo namas[reverence]->namaH .. 13 .. -"Reverence, reverence to the knower of the divine, to the three-eyed one, to she who has divine embodiment." +namas[Adoration]->namaH=>namo namas[adoration]->namaH .. 13 .. +"Adoration, adoration to the knower of the divine, to the three-eyed one, to she who has divine embodiment." === sloka === @@ -276,16 +276,16 @@ arDa[half]+candra[moon]+jawA[twisted hair]+DAra[wears]=arDacandrajawADAra->arDac "O you who wears the half moon in twisted hair," candra[moon]+bimba[reflection]=candrabimba->candrabimbe -namas[reverence]->namaH=>namo namas[reverence]->namaH . -"O reflection of the moon - reverence, reverence." +namas[adoration]->namaH=>namo namas[adoration]->namaH . +"O reflection of the moon - adoration, adoration." --- line --- candra[moon]+aditya[sun]+jawA[twisted hair]+DAra[wear]=candrAdityajawADAra->candrAdityajawADAri "O you who wear the sun and moon in twisted hair," candra[moon]+bimba[reflection]=candrabimba->candrabimbe -namas[reverence]->namaH=>namo namas[reverence]->namaH .. 14 .. -"O reflection of the moon - reverence, reverence." +namas[adoration]->namaH=>namo namas[adoration]->namaH .. 14 .. +"O reflection of the moon - adoration, adoration." === sloka === @@ -295,28 +295,28 @@ mahA[large]+rUpa[form]=mahArUpa->mahArUpe "O you of atomic form, O you of large form," viSva[universal]+rUpa[form]=viSvarUpa->viSvarUpe -namas[reverence]->namaH=>namo namas[reverence]->namaH . -"O you of universal form - reverence, reverence." +namas[adoration]->namaH=>namo namas[adoration]->namaH . +"O you of universal form - adoration, adoration." --- line --- aRima[Aṇimā]+Adi[beginning]+azwa[eight-fold]+sidDA[powers]=aRimAdyazwasidDA->aRimAdyazwasidDAyE Ananda[bliss]->AnandA->AnandAyE -namas[Reverence]->namaH=>namo namas[reverence]->namaH .. 15 .. -"Reverence, reverence to the embodiment of bliss, to the eight-fold powers beginning with Aṇimā." +namas[Adoration]->namaH=>namo namas[adoration]->namaH .. 15 .. +"Adoration, adoration to the embodiment of bliss, to the eight-fold powers beginning with Aṇimā." === sloka === --- line --- jYAna[knowledge]+vijYAna[wisdom]+rUpA[form]=jYAnavijYAnarUpA->jYAnavijYAnarUpAyE jYAna[knowledge]+mUrti[embodiment]=jYAnamUrti->jYAnamUrte -namas[reverence]->namaH=>namo namas[reverence]->namaH . -"O embodiment of knowledge - reverence, reverence to you who are the form of knowledge and wisdom." +namas[adoration]->namaH=>namo namas[adoration]->namaH . +"O embodiment of knowledge - adoration, adoration to you who are the form of knowledge and wisdom." --- line --- nAnA[various]+SAstra[scriptures]+svarUpA[nature]=nAnASAstrasvarUpA->nAnASAstrasvarUpAyE nAnA[many]+rUpa[forms]=nAnArUpa->nAnArUpe -namas[reverence]->namaH=>namo namas[reverence]->namaH .. 16 .. -"O you of many forms - reverence, reverence to her who is the true nature of various scriptures." +namas[adoration]->namaH=>namo namas[adoration]->namaH .. 16 .. +"O you of many forms - adoration, adoration to her who is the true nature of various scriptures." === sloka === @@ -327,15 +327,15 @@ ca[and] "O lotus-giver, O you of the lotus lineage and" padma[lotus]+rUpa[form]=padmarUpa->padmarUpe -namas[reverence]->namaH=>namo namas[reverence]->namaH . -"you in the form of a lotus - reverence, reverence." +namas[adoration]->namaH=>namo namas[adoration]->namaH . +"you in the form of a lotus - adoration, adoration." --- line --- paramezWyE[supreme][Goddess] parA[supreme]+mUrtI[embodiment]=parAmUrtI->parAmUrtyE -(namas[reverence]->namaH)+te[to you]=namaste +(namas[adoration]->namaH)+te[to you]=namaste pApa[sins]+(nASin[destroyer]->nASinI)=pApanASinI .. 17 .. -"O destroyer of sins - reverence to you, supreme Goddess of supreme embodiment." +"O destroyer of sins - adoration to you, supreme Goddess of supreme embodiment." === sloka === @@ -343,14 +343,14 @@ pApa[sins]+(nASin[destroyer]->nASinI)=pApanASinI .. 17 .. maha[great]+devI[goddess]=mahadevI->mahadevyE maha[great]+kalI[kālī]=mahakalI->mahakalyE maha[great]+lakzmI[lakṣmī]=mahalakzmI->mahalakzmyE -namah[reverence]=>namo namah[reverence] . -"reverence, reverence to the great goddess, to the great kālī, to the great lakṣmī" +namas[adoration]->namaH=>namo namah[adoration] . +"adoration, adoration to the great goddess, to the great kālī, to the great lakṣmī" --- line --- brahma[brahma]+vizRu[viṣṇu]+siva[śiva]+akyA[known as]=brahmavizRusivAkyA->brahmavizRusivAkyAyE -brahma[divine]+narI[woman]=brahmanarI=>brahmanarye -namah[reverence]=>namo namah[reverence] .. 18 .. -"reverence, reverence to she who is known as brahman, viṣṇu, śiva, to the divine woman." +brahma[divine]+(narI[woman]->naryE)=brahmanaryE +namas[adoration]->namaH=>namo namah[adoration] .. 18 .. +"adoration, adoration to she who is known as brahman, viṣṇu, śiva, to the divine woman." === sloka === @@ -358,15 +358,15 @@ namah[reverence]=>namo namah[reverence] .. 18 .. kamala[lotus]+Akara[source]+puzpA[flower]=kamalAkarapuzpA ca[and] kAma[desire]+rUpa[the form]=kAmarUpa->kAmarUpe -namas[reverence]->namaH=>namo namas[reverence]->namaH . -"O source of the lotus flower and O you who takes the form that you desire - reverence, reverence." +namas[adoration]->namaH=>namo namas[adoration]->namaH . +"O source of the lotus flower and O you who takes the form that you desire - adoration, adoration." --- line --- kapAli[skull-bearer] (karman[actions]->karma)+dIptA[shine]=karmadIptA->karmadIptAyE (karman[action]->karma)+dA[giver]=karmadA->karmadAyE -namas[reverence]->namaH=>namo namas[reverence]->namaH .. 19 .. -"O skull-bearer - reverence, reverence to the giver of action, to she whose actions shine." +namas[adoration]->namaH=>namo namas[adoration]->namaH .. 19 .. +"O skull-bearer - adoration, adoration to the giver of action, to she whose actions shine." === sloka === @@ -387,9 +387,9 @@ SfRvatAm[those who listen]+api[Also]=SfRvatAmapi .. 20 .. === sloka === --- line --- -itTaM[Thus] -sarasvatI[Sarasvatī]+(stotra[stotra]->stotram)+agastya[Agastya]+muni[sage]+(vAcaka[the speaking of]->vAcakam)=sarasvatIstotramagastyamunivAcakam . -"Thus, the speaking of sage Agastya's Sarasvatī stotram" +itTam[Thus]=>itTaM +sarasvatI[Sarasvatī]+(stotra[hymn]->stotram)+agastya[Agastya]+muni[sage]+(vAcaka[the speaking of]->vAcakam)=sarasvatIstotramagastyamunivAcakam . +"Thus, the speaking of sage Agastya's Sarasvatī hymn" --- line --- sarva[all]+sidDi[skills]+kara[production]=sarvasidDikara->sarvasidDikaram=>sarvasidDikaraM diff --git a/library/sUtrARi/SrIsarasvatIstotraM_gloss.sutra b/library/sUtrARi/SrIsarasvatIstotraM_gloss.sutra deleted file mode 100644 index 6f7191d..0000000 --- a/library/sUtrARi/SrIsarasvatIstotraM_gloss.sutra +++ /dev/null @@ -1,254 +0,0 @@ -=== SrIsarasvatIstotraM agastyamuniproktam === - -=== sloka === - ---- line --- -SrI[radiant]+gaReSAya[to][Ganesha]=SrIgaReSAya namaH[Reverence] . -"Reverence to the radiant Ganesha." - -=== sloka === - ---- line --- -yA[She who] kunda[Jasmine flower]+indu[Moon]+tuzAra[frosty]+hAra[garland of pearls]+Davala[dazzling]=kundendutuzArahAraDavalA -"She who is white like Jasmine flower," -"frosty like the Moon," -"and dazzling like a garland of pearls." - -yA[She who] SuBra[shining]+vastra[clothing]+Avfta[covered]=SuBravastrAvftA -"She who is covered by shining clothing." - ---- line --- -yA[She whose] vIRA[veena]+(vara[boon]+daRqa[staff]=varadaRqa)+maRqita[adorned]+karA[hands]=vIRAvaradaRqamaRqitakarA -"She whose hands are adorned by veena" -"and boon-giving staff;" - -yA[She who] Sveta[white]+padma[lotus]+AsanA[seated]=SvetapadmAsanA . -"She who is seated on a white lotus." - ---- line --- -yA[She who] (brahma[Brahma]+acyuta[Viṣṇu]+SaNkara[Śiva]+praBftiBiH[and other]+deva[devas]=brahmAcyutaSaNkarapraBftiBirdeva)+sadA[always]=brahmAcyutaSaNkarapraBftiBirdevEssadA pUjitA[worshipped] -"She who is always worshipped by Brahma, Viṣṇu, Śiva, and other devas." - ---- line --- -sA[that] mAM[me] pAtu[May][protect] sarasvatI[Sarasvatī] BagavatI[divine] niSSeza[completely]+jAqya[ignorance]+apahA[removes]=niSSezajAqyApahA .. 1 .. -"May that divine Sarasvatī who removes ignorance completely, protect me." - -=== sloka === - ---- line --- -dorBiH[arms]+yuktA[Endowed]=dorBiryuktA catur[four]=caturBiH sPawika[crystal]+maRi[gems]+niBEH[resembling]+akzamAlAn[rosary]+daDAnA[holding]=sPawikamaRiniBErakzamAlAndaDAnA -"Endowed with four arms, holding a rosary of beads resembling crystal gems," - -hastena[in][hand]+ekena[one]=hastenEkena padma[lotus]=padmaM sitam[white]+api[also]+ca[and]=sitamapica Sukam[parrot]=SukaM pustakam[book]=pustakaM ca[and]+apareRa[with another]=cApareRa . -"in one hand is a white lotus, also holding a parrot and book with another." - ---- line --- -BAs[lustre]=BAsA kunda[Jasmine flower]+indu[moon]+SaNKa[conch]+sPawita[crystal]+maRi[gem]+niBA[resembling]=kundenduSaNKasPawikamaRiniBA BAsamAnA[Shining]+asamAnA[incomparable]=BAsamAnA'samAnA -"Shining with an incomparable lustre, resembling Jasmine flower, moon, conch, crystal, and gem," - ---- line --- -sA[she] me[my] (vAc[speech]+devatA[Goddess]+iyam[this]=vAgdevateyam)=vAgdevateyaM nivasatu[may][reside] vadan[in][mouth]=vadane sarvadA[always] su[very]+prasannA[pleased]=suprasannA .. 2 .. -"may she, this Goddess of speech, be very pleased and always reside in my mouth." - -=== sloka === - ---- line --- -sura[gods]+asurA[demons]+sevita[served]+pAda[feet]+paNkajA[lotus]=surAsurAsevitapAdapaNkajA kare[in her hand] virAjat[resplendently]+kamanIya[beautiful]+pustakA[book]=virAjatkamanIyapustakA . -"She whose lotus feet are served by both gods and demons, she who holds in her hand a resplendently beautiful book," - ---- line --- -viriYci[Brahma]+patnI[wife]=viriYcipatnI kamala[lotus]+Asana[seat]+sTitA[situated]=kamalAsanasTitA sarasvatI[Sarasvatī] nftyatu[May][dance] vAci[in][speech] me[for me][my] sadA[always] .. 3 .. -"the wife of Brahma, situated in a lotus seat. May Sarasvatī always dance for me in my speech." - - ---- line --- -sarasvatI[Sarasvatī] sarasija[lotus]+kesara[filaments]+praBA[radiance]=sarasijakesarapraBA tapasvinI[ascetic] sita[white]+kamala[lotus]+Asana[seat]+priyA[fond of]=sitakamalAsanapriyA . -"Sarasvatī, whose radiance is like lotus filaments, the ascetic one, fond of a white lotus seat," - ---- line --- -Gana[full]+stanI[breasted]=GanastanI kamala[lotus]+vilola[darting]+locanA[eyes]=kamalavilolalocanA manasvinI[high-minded] Bavatu[may][be] vara[boons]+prasAdinI[gracious]=varaprasAdinI .. 4 .. -"full breasted, with darting lotus eyes - may she be high-minded and gracious with her boons." - -=== sloka === - ---- line --- -sarasvati[O Sarasvatī] (namas[reverence]+tuByam[to you]=namastuByam)=namastuByaM varade[boon-giver] kAmarUpiRi[form of longing] . -"O Sarasvatī, boon-giver, the very form of longing; reverence to you." - ---- line --- -(vidyA[learning]+AramBam[beginning]=vidyAramBam)=vidyAramBaM karizyAmi[I will undertake] -"I will undertake the beginning of learning;" - -sidDiH[accomplishment]+Bavatu[may][arise]=sidDirBavatu me[for me] sadA[always] .. 5 .. -"may accomplishment arise for me, always." - -=== sloka === - ---- line --- -sarasvati[O Sarasvatī] (namas[reverence]+tuByam[to you]=namastuByam)=namastuByaM -"O Sarasvatī, reverence to you." - -sarva[all]+devi[O Goddess]=sarvadevi namaH[reverence]=namo namaH[reverence] . -"O Goddess of all, reverence, reverence." - ---- line --- -SAnta[tranquil]+rupa[form]=SAntarUpe{VOC} SaSi[moon]+Dare[bearer]=SaSiDare{VOC} sarva[all]+yoga[disciplines]=sarvayoge{VOC} namaH[reverence]=namo namaH[reverence] .. 6 .. -"O you of tranquil form, O bearer of the moon, O you in whom all disciplines reside, reverence, reverence." - -=== sloka === - ---- line --- -nitya[constant]+Ananda[in][bliss]=nityAnande{COMP.BV.F.SG.VOC} -nir[un]+ADara[supported]=nirADAre{COMP.TP.F.SG.VOC} -nizkala[undivided]=nizkalAyE{ADJ.F.SG.DAT} namaH[reverence]=namo namaH[reverence] . -"O you in constant bliss, O unsupported one - reverence, reverence to her who is undivided." - ---- line --- -vidyA[knowledge]+Dara[bearer]=vidyADare{VOC} viSA[large]+lAkzi[eyed]=viSAlAkzi{VOC} SudDa[pure]+jYana[knowledge]=SudDajYAne{VOC} namaH[reverence]=namo namaH[reverence] .. 7 .. -"O bearer of knowledge, O large eyed one, O embodier of pure knowledge, reverence, reverence." - -=== sloka === - ---- line --- -(SudDa[pure]+sPawika[crystal]+rUpa[form]=SudDasPawikarUpa)+yE[to]{DAT}=SudDasPawikarUpAyE{TP} sUkzma[subtle]+rupa[form]+e[O]=sUkzmarUpe{VOC} namaH[reverence]=namo namaH[reverence] . -"O you of subtle form - reverence, reverence to her of pure crystal form." - ---- line --- -Sabda[Sabda]+brahma[Brahman]=Sabdabrahmi{VOC} catur[four]+hasta[handed]=caturhaste{VOC} sarva[all]+sidDa[accomplishment]=sarvasidDyE{DAT} namaH[reverence]=namo namaH[reverence] .. 8 .. -"O you who are Sabda Brahman, O four-handed one - reverence, reverence to her who is all accomplishment." - -=== sloka === - ---- line --- -mukta[pearls]+AlaNkfta[adorned]+sarva[every]+aNga[limb]=muktAlaNkftasarvANgyE{DAT} mUlADAra[root chakra]=mUlADAre{VOC} namaH[reverence]=namo namaH[reverence] . -"O you who are the root chakra - reverence, reverence to her whose every limb is adorned by pearls." - ---- line --- -mUla[basis]+mantra[mantra]+svarUpa[self-form]=mUlamantrasvarUpAyE{DAT} mUla[origin]+Sakta[ability]=mUlaSaktyE{DAT} namaH[Reverence]=namo namaH[reverence] .. 9 .. -"Reverence, reverence to her whose self-form is the basis of mantra itself, to her who is the origin of ability." - -=== sloka === - ---- line --- -manas[mind]+mayi[consist]=manomayi mahA[great]+yoga[yoga]=mahAyoge vAc[speech]+ISvarI[Goddess]=vAgISvari namaH[reverence]=namo namaH[reverence] . -"O you who consist of the mind, O embodier of great yoga, O Goddess of speech, reverence, reverence." - ---- line --- -vARa[music]=vARyE vara[boon]+da[giving]+hasta[hand]=varadahastAyE vara[boon]+dA[giver]=varadAyE namaH[Reverence]=namo namaH[reverence] .. 10 .. -"Reverence, reverence to the embodiment of music, to her who extends a boon-giving hand, to the giver of boons." - -=== sloka === - ---- line --- -veda[knowledge]=vedyAyE veda[knowledge]+rUpa[form]=vedarUpAyE veda[Vedas]+antara[culmination]=vedAntAyE namaH[Reverence]=namo namaH[reverence] . -"Reverence, reverence to her who is knowledge itself, to her in the form of knowledge, to her who is the culmination of all the Vedas." - ---- line --- -guRa[virtues]+doza[faults]+vivarjita[free from]=guRadozavivarjinyE guRa[qualities]+dIpta[shining]=guRadIptyE namaH[Reverence]=namo namaH[reverence] .. 11 .. -"Reverence, reverence to her who is free from virtues and faults, to her of shining qualities." - -=== sloka === - ---- line --- -sarva[all]+jYAna[knowledge]=sarvajYAne sadA[always]+Ananda[bliss]=sadAnande sarva[all]+rUpa[form]=sarvarUpe namaH[reverence]=namo namaH[reverence] . -"O you who are all knowledge, O you who are always in bliss, O you who are all form - reverence, reverence." - ---- line --- -sampanna[perfect]=sampannAyE kumAra[youth]=kumAryE ca[and] sarvajYa[omniscient]=sarvajYAyE namaH[reverence]=namo namaH[reverence] .. 12 .. -"O omniscient one - reverence, reverence to her who is perfect and to her who is youth." - -=== sloka === - ---- line --- -yoga[yoga]+rUpa[form]=yogarUpe ramA[Lakṣmī]+devI[Goddess]=ramAdevyE yoga[yoga]+Ananda[bliss]=yogAnande namaH[reverence]=namo namaH[reverence] . -"O you who are the form of yoga, O embodiment of the bliss of yoga - reverence, reverence to that Goddess Lakṣmī." - ---- line --- -divya[divine]+jYA[knower]=divyajYAyE tri[three]+netra[eyed]=trinetrAyE divya[divine]+mUrtI[embodiment]=divyamUrtyE namaH[Reverence]=namo namaH[reverence] .. 13 .. -"Reverence, reverence to the knower of the divine, to the three-eyed one, to she who has divine embodiment." - -=== sloka === - ---- line --- -arDa[half]+candra[moon]+jawA[twisted hair]+DAra[wears]=arDacandrajawADAri candra[moon]+bimba[reflection]=candrabimbe namaH[reverence]=namo namaH[reverence] . -"O you who wears the half moon in twisted hair, O reflection of the moon - reverence, reverence." - ---- line --- -candra[moon]+aditya[sun]+jawA[twisted hair]+DAra[wear]=candrAdityajawADAri candra[moon]+bimba[reflection]=candrabimbe namaH[reverence]=namo namaH[reverence] .. 14 .. -"O you who wear the sun and moon in twisted hair, O reflection of the moon - reverence, reverence." - -=== sloka === - ---- line --- -aRu[atomic]+rUpa[form]=aRurUpe mahA[large]+rUpa[form]=mahArUpe viSva[universal]+rUpa[form]=viSvarUpe namaH[reverence]=namo namaH[reverence] . -"O you of atomic form, O you of large form, O you of universal form - reverence, reverence." - ---- line --- -aRima[Aṇimā]+Adi[beginning]+azwa[eight-fold]+sidDi[powers]=aRimAdyazwasidDAyE Ananda[bliss]=AnandAyE namaH[Reverence]=namo namaH[reverence] .. 15 .. -"Reverence, reverence to the embodiment of bliss, to the eight-fold powers beginning with Aṇimā." - -=== sloka === - ---- line --- -jYAna[knowledge]+vijYana[wisdom]+rUpa[form]=jYAnavijYAnarUpAyE jYAna[knowledge]+mURtI[embodiment]=jYAnamUrte namaH[reverence]=namo namaH[reverence] . -"O embodiment of knowledge - reverence, reverence to you who are the form of knowledge and wisdom." - ---- line --- -nAna[various]+SAstra[scriptures]+svarUpa[nature]=nAnASAstrasvarUpAyE nAnA[many]+rUpa[forms]=nAnArUpe namaH[reverence]=namo namaH[reverence] .. 16 .. -"O you of many forms - reverence, reverence to her who is the true nature of various scriptures." - -=== sloka === - ---- line --- -padma[lotus]+da[giver]=padmade padma[lotus]+vaMSa[lineage]=padmavaMSe ca[and] padma[lotus]+rUpa[form]=padmarUpe namaH[reverence]=namo namaH[reverence] . -"O lotus-giver, O you of the lotus lineage and you in the form of a lotus - reverence, reverence." - ---- line --- -paramezWyE[supreme Goddess] parA[supreme]+mUrtI[embodiment]=parAmUrtyE namas[reverence]+te[to you]=namaste pApa[sins]+nASinI[destroyer]=pApanASinI .. 17 .. -"O destroyer of sins - reverence to you, supreme Goddess of supreme embodiment." - -=== sloka === - ---- line --- -mahA[great]+devI[Goddess]=mahAdevyE mahA[great]+kAlI[Kālī]=mahAkAlyE mahA[great]+lakzmI[Lakṣmī]=mahAlakzmyE namaH[Reverence]=namo namaH[reverence] . -"Reverence, reverence to the great Goddess, to the great Kālī, to the great Lakṣmī" - ---- line --- -brahma[Brahma]+vizRu[Viṣṇu]+Siva[Śiva]+AKyA[known as]=brahmavizRuSivAKyAyE brahma[divine]+nArI[woman]=brahmanAryE namaH[Reverence]=namo namaH[reverence] .. 18 .. -"Reverence, reverence to she who is known as Brahman, Viṣṇu, Śiva, to the divine woman." - -=== sloka === - ---- line --- -kamala[lotus]+Akara[source]+puzpA[flower]=kamalAkarapuzpA ca[and] kAma[desire]+rUpa[the form]=kAmarUpe namaH[reverence]=namo namaH[reverence] . -"O source of the lotus flower and O you who takes the form that you desire - reverence, reverence." - ---- line --- -kapAli[skull-bearer] karma[actions]+dIpta[shine]=karmadIptAyE karma[action]+dA[giver]=karmadAyE namaH[reverence]=namo namaH[reverence] .. 19 .. -"O skull-bearer - reverence, reverence to the giver of action, to she whose actions shine." - -=== sloka === - ---- line --- -sAyam[in the evening]=sAyaM prAtaH[in the morning] (paWen[should][recite]+nityam[daily]=paWennityam)=paWennityaM (zAR[six]+mAsAt[months]=zARmAsAt)+(sidDih[attainment]+ucyate[It is said]=sidDirucyate)=zARmAsAtsidDirucyate . -"It is said that one should recite this daily in the evening and in the morning for six months to reach attainment." - ---- line --- -((cora[thieves]+vyAGra[tigers]=coravyAGra)+Baya[fear]=coravyAGraBayam)=coravyAGraBayaM na[not]+asti[exist]=nAsti paWatAm[those who recite]=paWatAM SfRvatAm[those who listen]+api[Also]=SfRvatAmapi .. 20 .. -"Also, fear of thieves and tigers does not exist for those who recite this or those who listen to this." - -=== sloka === - ---- line --- -itTaM[Thus] sarasvatI[Sarasvatī]+stotra[stotra]+agastya[Agastya]+muni[sage]+vAcaka[the speaking of]=sarasvatIstotramagastyamunivAcakam . -"Thus, the speaking of sage Agastya's Sarasvatī stotram" - ---- line --- -(sarva[all]+sidDi[skills]+kara[production]=sarvasidDikaram)=sarvasidDikaraM nFRAM[leads to] sarva[all]+pApa[sins]+praRASana[destruction]=sarvapApapraRASanam .. 21 .. -"leads to the production of all skills and the destruction of all sins." - -=== sloka === - ---- line --- -.. (iti[Thus]+(agastya[Agastya]+muni[sage]=agastyamuni)+proktam[spoken]=ityagastyamuniproktam)=ityagastyamuniproktaM (sarasvatI[Sarasvatī]+stotra[stotra]=sarasvatIstotram)=sarasvatIstotraM sampUrRam[complete] .. -"Thus, the Sarasvatī stotra spoken by sage Agastya is complete." diff --git a/library/sUtrARi/SrIsarasvatIstotraM_middle.sutra b/library/sUtrARi/SrIsarasvatIstotraM_middle.sutra deleted file mode 100644 index 948d1b6..0000000 --- a/library/sUtrARi/SrIsarasvatIstotraM_middle.sutra +++ /dev/null @@ -1,178 +0,0 @@ -=== SrIsarasvatIstotraM agastyamuniproktam === - -=== sloka === - ---- line --- -sura[gods]+asurA[demons]+sevita[served]+pAda[feet]+paNkajA[lotus]=surAsurAsevitapAdapaNkajA -kare[in her hand] -virAjat[resplendently]+kamanIya[beautiful]+pustakA[book]=virAjatkamanIyapustakA . -"She whose lotus feet are served by both gods and demons, she who holds in her hand a resplendently beautiful book," - ---- line --- -viriYci[Brahma]+patnI[wife]=viriYcipatnI -kamala[lotus]+Asana[seat]+sTitA[situated]=kamalAsanasTitA -sarasvatI[Sarasvatī] -nftyatu[May][dance] -vAci[in][speech] -me[for me][my] -sadA[always] .. 3 .. -"the wife of Brahma, situated in a lotus seat. May Sarasvatī always dance for me in my speech." - - ---- line --- -sarasvatI[Sarasvatī] -sarasija[lotus]+kesara[filaments]+praBA[radiance]=sarasijakesarapraBA -tapasvinI[ascetic] -sita[white]+kamala[lotus]+Asana[seat]+priyA[fond of]=sitakamalAsanapriyA . -"Sarasvatī, whose radiance is like lotus filaments, the ascetic one, fond of a white lotus seat," - ---- line --- -Gana[full]+stanI[breasted]=GanastanI -kamala[lotus]+vilola[darting]+locanA[eyes]=kamalavilolalocanA manasvinI[high-minded] -Bavatu[may][be] -vara[boons]+prasAdinI[gracious]=varaprasAdinI .. 4 .. -"full breasted, with darting lotus eyes - may she be high-minded and gracious with her boons." - -=== sloka === - ---- line --- -sarasvati[O Sarasvatī] -(namas[reverence]+tuByam[to you]=namastuByam)=namastuByaM -varade[boon-giver] -kAmarUpiRi[form of longing] . -"O Sarasvatī, boon-giver, the very form of longing; reverence to you." - ---- line --- -(vidyA[learning]+AramBam[beginning]=vidyAramBam)=vidyAramBaM -karizyAmi[I will undertake] -"I will undertake the beginning of learning;" - -sidDiH[accomplishment]+Bavatu[may][arise]=sidDirBavatu -me[for me] -sadA[always] .. 5 .. -"may accomplishment arise for me, always." - -=== sloka === - ---- line --- -sarasvati[O Sarasvatī] -(namas[reverence]+tuByam[to you]=namastuByam)=namastuByaM -"O Sarasvatī, reverence to you." - -sarva[all]+devi[O Goddess]=sarvadevi -namaH[reverence]=namo namaH[reverence] . -"O Goddess of all, reverence, reverence." - ---- line --- -SAnta[tranquil]+rupa[form]=SAntarUpe -SaSi[moon]+Dare[bearer]=SaSiDare -sarva[all]+yoga[disciplines]=sarvayoge -namaH[reverence]=namo namaH[reverence] .. 6 .. -"O you of tranquil form, O bearer of the moon, O you in whom all disciplines reside, reverence, reverence." - -=== sloka === - ---- line --- -nitya[constant]+Ananda[in][bliss]=nityAnande -nir[un]+ADara[supported]=nirADAre -nizkala[undivided]=nizkalAyE namaH[reverence]=namo namaH[reverence] . -"O you in constant bliss, O unsupported one - reverence, reverence to her who is undivided." - ---- line --- -vidyA[knowledge]+Dara[bearer]=vidyADare -viSA[large]+lAkzi[eyed]=viSAlAkzi -SudDa[pure]+jYana[knowledge]=SudDajYAne -namaH[reverence]=namo namaH[reverence] .. 7 .. -"O bearer of knowledge, O large eyed one, O embodier of pure knowledge, reverence, reverence." - -=== sloka === - ---- line --- -(SudDa[pure]+sPawika[crystal]+rUpa[form]=SudDasPawikarUpa)=SudDasPawikarUpAyE -sUkzma[subtle]+rupa[form]=sUkzmarUpe -namaH[reverence]=namo namaH[reverence] . -"O you of subtle form - reverence, reverence to her of pure crystal form." - ---- line --- -Sabda[Sabda]+brahma[Brahman]=Sabdabrahmi -catur[four]+hasta[handed]=caturhaste sarva[all]+sidDa[accomplishment]=sarvasidDyE -namaH[reverence]=namo namaH[reverence] .. 8 .. -"O you who are Sabda Brahman, O four-handed one - reverence, reverence to her who is all accomplishment." - -=== sloka === - ---- line --- -mukta[pearls]+AlaNkfta[adorned]+sarva[every]+aNga[limb]=muktAlaNkftasarvANgyE -mUlADAra[root chakra]=mUlADAre -namaH[reverence]=namo namaH[reverence] . -"O you who are the root chakra - reverence, reverence to her whose every limb is adorned by pearls." - ---- line --- -mUla[basis]+mantra[mantra]+svarUpa[self-form]=mUlamantrasvarUpAyE -mUla[origin]+Sakta[ability]=mUlaSaktyE -namaH[Reverence]=namo namaH[reverence] .. 9 .. -"Reverence, reverence to her whose self-form is the basis of mantra itself, to her who is the origin of ability." - -=== sloka === - ---- line --- -manas[mind]+mayi[consist]=manomayi -mahA[great]+yoga[yoga]=mahAyoge -vAc[speech]+ISvarI[Goddess]=vAgISvari -namaH[reverence]=namo namaH[reverence] . -"O you who consist of the mind, O embodier of great yoga, O Goddess of speech, reverence, reverence." - ---- line --- -vARa[music]=vARyE -vara[boon]+da[giving]+hasta[hand]=varadahastAyE -vara[boon]+dA[giver]=varadAyE -namaH[Reverence]=namo namaH[reverence] .. 10 .. -"Reverence, reverence to the embodiment of music, to her who extends a boon-giving hand, to the giver of boons." - -=== sloka === - ---- line --- -veda[knowledge]=vedyAyE -veda[knowledge]+rUpa[form]=vedarUpAyE -veda[Vedas]+antara[culmination]=vedAntAyE -namaH[Reverence]=namo namaH[reverence] . -"Reverence, reverence to her who is knowledge itself, to her in the form of knowledge, to her who is the culmination of all the Vedas." - ---- line --- -guRa[virtues]+doza[faults]+vivarjita[free from]=guRadozavivarjinyE -guRa[qualities]+dIpta[shining]=guRadIptyE -namaH[Reverence]=namo namaH[reverence] .. 11 .. -"Reverence, reverence to her who is free from virtues and faults, to her of shining qualities." - -=== sloka === - ---- line --- -sarva[all]+jYAna[knowledge]=sarvajYAne -sadA[always]+Ananda[bliss]=sadAnande -sarva[all]+rUpa[form]=sarvarUpe -namaH[reverence]=namo namaH[reverence] . -"O you who are all knowledge, O you who are always in bliss, O you who are all form - reverence, reverence." - ---- line --- -sampanna[perfect]=sampannAyE -kumAra[youth]=kumAryE ca[and] -sarvajYa[omniscient]=sarvajYAyE -namaH[reverence]=namo namaH[reverence] .. 12 .. -"O omniscient one - reverence, reverence to her who is perfect and to her who is youth." - -=== sloka === - ---- line --- -yoga[yoga]+rUpa[form]=yogarUpe -ramA[Lakṣmī]+devI[Goddess]=ramAdevyE -yoga[yoga]+Ananda[bliss]=yogAnande -namaH[reverence]=namo namaH[reverence] . -"O you who are the form of yoga, O embodiment of the bliss of yoga - reverence, reverence to that Goddess Lakṣmī." - ---- line --- -divya[divine]+jYA[knower]=divyajYAyE -tri[three]+netra[eyed]=trinetrAyE -divya[divine]+mUrtI[embodiment]=divyamUrtyE -namaH[Reverence]=namo namaH[reverence] .. 13 .. -"Reverence, reverence to the knower of the divine, to the three-eyed one, to she who has divine embodiment." - diff --git a/library/sUtrARi/draft.sutra b/library/sUtrARi/draft.sutra deleted file mode 100644 index d2bb362..0000000 --- a/library/sUtrARi/draft.sutra +++ /dev/null @@ -1,151 +0,0 @@ -=== SrIsarasvatIstotraM agastyamuniproktam === - ---- line --- -SrI[radiant]+gaReSAya[to][Ganesha]=SrIgaReSAya namaH[I bow] . -"I bow to the radiant Ganesha" - -=== sloka === - ---- line --- -yA[She who] kunda[Jasmine flower]+indu[Moon]+tuzAra[frosty]+hAra[garland of pearls]+Davala[dazzling]=kundendutuzArahAraDavalA -"She who is white like Jasmine flower, frosty like the Moon, and dazzling like a garland of pearls." - -yA[She who] SuBra[shining]+vastra[clothes]+Avfta[covered]=SuBravastrAvftA -"She who is covered by shining white clothes." - ---- line --- -yA[She whose] vIRA[veena]+(vara[boon-giving]+daRqa[staff]=varadaRqa)+maRqita[adorned]+karA[hands] vIRAvaradaRqamaRqitakarA -"She whose hands are adorned by veena and boon-giving staff;" - -yA[She who] Sveta[white]+padma[lotus]+AsanA[seated]=SvetapadmAsanA . -"She who is seated on a white lotus." - ---- line --- -yA[She who] (brahma[Brahma]+acyuta[Vishnu]+SaNkara[Shiva]+praBftiBiH[and other]+deva[devas]=brahmAcyutaSaNkarapraBftiBirdeva)+sadA[always]=brahmAcyutaSaNkarapraBftiBirdevEssadA pUjitA[worshipped] -"She who is always worshipped by Brahma, Vishnu, Shiva, and other devas." - -sA[She] mAM[me] pAtu[] sarasvatI BagavatI niSSezajAqyApahA .. 1 .. - -=== - ---- line --- -dorBiryuktA caturBiH sPawikamaRiniBErakzamAlAndaDAnA -hastenEkena padmaM sitamapica SukaM pustakaM cApareRa . - ---- line --- -BAsA kundenduSaNKasPawikamaRiniBA BAsamAnA'samAnA -sA me vAgdevateyaM nivasatu vadane sarvadA suprasannA .. 2.. - -=== sloka === - ---- line --- -surAsurAsevitapAdapaNkajA kare virAjatkamanIyapustakA . - ---- line --- -viriYcipatnI kamalAsanasTitA sarasvatI nftyatu vAci me sadA .. 3.. - -=== - ---- line --- -sarasvatI sarasijakesarapraBA tapasvinI sitakamalAsanapriyA . - ---- line --- -GanastanI kamalavilolalocanA manasvinI Bavatu varaprasAdinI .. 4.. - -=== - ---- line --- -sarasvati namastuByaM varade kAmarUpiRi . - ---- line --- -vidyAramBaM karizyAmi sidDirBavatu me sadA .. 5.. - -=== - ---- line --- -sarasvati namastuByaM sarvadevi namo namaH . - ---- line --- -SAntarUpe SaSiDare sarvayoge namo namaH .. 6.. - -=== - ---- line --- -nitya[constantly]+Anande[in bliss]=nityAnande nirADAre nizkalAyE namo namaH . - ---- line --- -vidyADare viSAlAkzi SudDajYAne namo namaH .. 7.. - -=== - -SudDasPawikarUpAyE sUkzmarUpe namo namaH . -Sabdabrahmi caturhaste sarvasidDyE namo namaH .. 8.. - -=== - -muktAlaNkftasarvANgyE mUlADAre namo namaH . -mUlamantrasvarUpAyE mUlaSaktyE namo namaH .. 9.. - -=== - -manomayi mahAyoge vAgISvari namo namaH . -vARyE varadahastAyE varadAyE namo namaH .. 10.. - -=== - -vedyAyE vedarUpAyE vedAntAyE namo namaH . -guRadozavivarjinyE guRadIptyE namo namaH .. 11.. - -=== - -sarvajYAne sadAnande sarvarUpe namo namaH . -sampannAyE kumAryE ca sarvajYAyE namo namaH .. 12.. - -=== - -yogarUpe ramAdevyE yogAnande namo namaH . -divyajYAyE trinetrAyE divyamUrtyE namo namaH .. 13.. - -=== - -arDacandrajawADAri candrabimbe namo namaH . (arDacandraDare devicandrarUpe) -candrAdityajawADAri candrabimbe namo namaH .. 14.. (candrAdityasame devi candraBUze) - -=== - -aRurUpe mahArUpe viSvarUpe namo namaH . -aRimAdyazwasidDAyE AnandAyE namo namaH .. 15.. - -=== - -jYAnavijYAnarUpAyE jYAnamUrte namo namaH . -nAnASAstrasvarUpAyE nAnArUpe namo namaH .. 16.. - -=== - -padmade padmavaMSe ca padmarUpe namo namaH . -paramezWyE parAmUrtyE namaste pApanASinI .. 17.. - -=== - -mahAdevyE mahAkAlyE mahAlakzmyE namo namaH . -brahmavizRuSivAKyAyE brahmanAryE namo namaH .. 18.. - -=== - -kamalAkarapuzpA ca kAmarUpe namo namaH . (kamalAkaragehAyE) -kapAli karmadIptAyE karmadAyE namo namaH .. 19.. (kapAliprARanATAyE) - -=== - -sAyaM prAtaH paWennityaM zARmAsAtsidDirucyate . -coravyAGraBayaM nAsti paWatAM SfRvatAmapi .. 20.. - -=== - -itTaM sarasvatIstotramagastyamunivAcakam . -sarvasidDikaraM nFRAM sarvapApapraRASanam .. 21.. - -=== - -.. ityagastyamuniproktaM sarasvatIstotraM sampUrRam .. diff --git a/library/sUtrARi/pt2.sutra b/library/sUtrARi/pt2.sutra deleted file mode 100644 index a6db651..0000000 --- a/library/sUtrARi/pt2.sutra +++ /dev/null @@ -1,78 +0,0 @@ -=== sloka === - ---- line --- -mano+mayi=manomayi mahA+yoga=mahAyoge vAc[speech]+ISvari[Goddess]=vAgISvari namo[reverence] namaH[reverence] . -"" - ---- line --- -vARyE varadahastAyE varadAyE namo namaH .. 10.. - -=== sloka === - ---- line --- -vedyAyE vedarUpAyE vedAntAyE namo namaH . ---- line --- -guRadozavivarjinyE guRadIptyE namo namaH .. 11.. - -=== sloka === ---- line --- -sarvajYAne sadAnande sarvarUpe namo namaH . ---- line --- -sampannAyE kumAryE ca sarvajYAyE namo namaH .. 12.. - -=== sloka === ---- line --- -yogarUpe ramAdevyE yogAnande namo namaH . ---- line --- -divyajYAyE trinetrAyE divyamUrtyE namo namaH .. 13.. - -=== sloka === ---- line --- -arDacandrajawADAri candrabimbe namo namaH . (arDacandraDare devicandrarUpe) ---- line --- -candrAdityajawADAri candrabimbe namo namaH .. 14.. (candrAdityasame devi candraBUze) - -=== sloka === ---- line --- -aRurUpe mahArUpe viSvarUpe namo namaH . ---- line --- -aRimAdyazwasidDAyE AnandAyE namo namaH .. 15.. - -=== sloka === ---- line --- -jYAnavijYAnarUpAyE jYAnamUrte namo namaH . ---- line --- -nAnASAstrasvarUpAyE nAnArUpe namo namaH .. 16.. - -=== sloka === ---- line --- -padmade padmavaMSe ca padmarUpe namo namaH . ---- line --- -paramezWyE parAmUrtyE namaste pApanASinI .. 17.. - -=== sloka === ---- line --- -mahAdevyE mahAkAlyE mahAlakzmyE namo namaH . ---- line --- -brahmavizRuSivAKyAyE brahmanAryE namo namaH .. 18.. - -=== sloka === ---- line --- -kamalAkarapuzpA ca kAmarUpe namo namaH . (kamalAkaragehAyE) ---- line --- -kapAli karmadIptAyE karmadAyE namo namaH .. 19.. (kapAliprARanATAyE) - -=== sloka === ---- line --- -sAyaM prAtaH paWennityaM zARmAsAtsidDirucyate . ---- line --- -coravyAGraBayaM nAsti paWatAM SfRvatAmapi .. 20.. - -=== sloka === ---- line --- -itTaM sarasvatIstotramagastyamunivAcakam . ---- line --- -sarvasidDikaraM nFRAM sarvapApapraRASanam .. 21.. - -=== sloka === -.. ityagastyamuniproktaM sarasvatIstotraM sampUrRam .. diff --git a/library/sUtrARi/vakratuRqa_mahAkAya.sutra b/library/sUtrARi/vakratuRqa_mahAkAya.sutra deleted file mode 100644 index d8a8676..0000000 --- a/library/sUtrARi/vakratuRqa_mahAkAya.sutra +++ /dev/null @@ -1,13 +0,0 @@ -=== unknown === - -=== sloka === - ---- line --- -vakra[curved]+tuRqa[trunk]=vakratuRqa mahA[large]+kAya[body]=mahAkAya sUrya[suns]+kowi[10,000,000]=sUryakowi sama[equal to]+praBa[brilliance]=samapraBa . -"O you with the curved trunk, the large body," -"and the brilliance equal to 10,000,000 suns..." - ---- line --- -(nir[free of]+viGnam[obstacles]=nirviGnam)=nirviGnaM kuru[make] me[for me] deva[god] sarva[all] kAryezu[in][endeavors] sarva[all]+dA[times]=sarvadA . 5 . -"O god - make the way free of obstacles for me," -"in all endeavors and all times." diff --git a/library/vakratuRqa_mahAkAya.sloka b/library/vakratuRqa_mahAkAya.sloka index 4d3bb24..670793d 100644 --- a/library/vakratuRqa_mahAkAya.sloka +++ b/library/vakratuRqa_mahAkAya.sloka @@ -10,7 +10,7 @@ sama[equal to]+praBA[brilliance]=samapraBA=>samapraBa . "and the brilliance equal to 10,000,000 suns..." --- line --- -(nis[free of]=>nir)+(viGna[obstacles]->viGnam)=nirviGnam=>nirviGnaM +nis[free of]+(viGna[obstacles]->viGnam)=nirviGnam=>nirviGnaM kuru[make] ma[me]->me[for] deva[god] "O god - make the way free of obstacles for me," diff --git a/nirukta/cache.py b/nirukta/cache.py index 9da646d..55ab4d6 100644 --- a/nirukta/cache.py +++ b/nirukta/cache.py @@ -8,7 +8,7 @@ _RECURSION_LIMIT = 10_000 -CACHE_VERSION = 1 +CACHE_VERSION = 2 def build_cached(cache: dict, key: str, builder: Callable, label: str = "") -> Any: diff --git a/nirukta/lsp/__init__.py b/nirukta/lsp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nirukta/lsp/analysis.py b/nirukta/lsp/analysis.py new file mode 100644 index 0000000..bbd2c2b --- /dev/null +++ b/nirukta/lsp/analysis.py @@ -0,0 +1,224 @@ +import hashlib +import json +import os +from collections import OrderedDict +from dataclasses import dataclass +from typing import List, Optional, Sequence, Tuple, Union + +from parsimonious.exceptions import ParseError + +from nirukta.models import Sloka, SlokaFile, SutraFile +from nirukta.models.span import Span +from nirukta.models.tokens import CompoundToken, SoundChangeToken +from nirukta.models.tokens.token import TokenType +from nirukta.parsing.diagnostics import ( + CollectingSink, + Diagnostic, + Severity, + parse_error_to_diagnostic, +) +from nirukta.parsing.visitors.sloka import SlokaVisitor, _validate_sequence +from nirukta.parsing.visitors.sutra import SutraVisitor + +FileAst = Union[SlokaFile, SutraFile] + +# bump when validator behavior changes so stale disk entries are discarded +# (v6: rewrite payloads know whether the token sits inside a compound) +VALIDATION_CACHE_VERSION = 6 + +# semantic diagnostics per utterance, keyed by the utterance's source text; +# an utterance validates the same way wherever it appears, so results are +# stored with spans relative to the utterance and rebased on replay +_CACHE_MAX_ENTRIES = 8192 +_utterance_cache: "OrderedDict[str, list]" = OrderedDict() +_disk_loaded = False +_disk_dirty = False + + +@dataclass +class DocumentAnalysis: + ast: Optional[FileAst] + syntax_diagnostics: List[Diagnostic] + # every spanned token in the document, nested tokens included + tokens: List[Tuple[Span, TokenType]] + text: str = "" + + +def slokas(analysis: DocumentAnalysis) -> List[Sloka]: + if analysis.ast is None: + return [] + if isinstance(analysis.ast, SutraFile): + return list(analysis.ast.slokas) + return [analysis.ast.sloka] + + +def _collect_tokens( + tokens: Sequence[TokenType], out: List[Tuple[Span, TokenType]] +) -> None: + for token in tokens: + if token.span is not None: + out.append((token.span, token)) + if isinstance(token, CompoundToken): + _collect_tokens(token.parts, out) + elif isinstance(token, SoundChangeToken): + _collect_tokens([token.part], out) + + +def analyze_syntax(path: str, text: str) -> DocumentAnalysis: + """Parse a document without semantic validation. + + Never raises; parse failures become diagnostics. + """ + sink = CollectingSink() + + try: + if path.endswith(".sutra"): + ast = SutraVisitor( + path, validate=False, source=text, sink=sink, resolve_external=False + ).parse() + else: + ast = SlokaVisitor(path, validate=False, source=text, sink=sink).parse() + except ParseError as e: + sink.diagnostics.append(parse_error_to_diagnostic(text, e)) + return DocumentAnalysis(None, sink.diagnostics, [], text) + except Exception as e: + sink.emit(Severity.ERROR, f"failed to parse: {e}", code="parse") + return DocumentAnalysis(None, sink.diagnostics, [], text) + + analysis = DocumentAnalysis(ast, sink.diagnostics, [], text) + tokens: List[Tuple[Span, TokenType]] = [] + for sloka in slokas(analysis): + for line in sloka.lines: + for utterance in line.vAkyAni: + _collect_tokens(utterance.tokens, tokens) + tokens.sort(key=lambda pair: pair[0].start) + analysis.tokens = tokens + + return analysis + + +def _cache_path() -> str: + base = os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache") + return os.path.join(base, "nirukta-lsp", "semantics.json") + + +def _load_disk_cache() -> None: + global _disk_loaded + if _disk_loaded: + return + _disk_loaded = True + try: + with open(_cache_path()) as f: + data = json.load(f) + if data.get("version") == VALIDATION_CACHE_VERSION: + _utterance_cache.update(data.get("entries", {})) + except Exception: + pass + + +def save_disk_cache() -> None: + global _disk_dirty + if not _disk_dirty: + return + _disk_dirty = False + try: + path = _cache_path() + os.makedirs(os.path.dirname(path), exist_ok=True) + tmp = path + ".tmp" + with open(tmp, "w") as f: + json.dump( + { + "version": VALIDATION_CACHE_VERSION, + "entries": dict(_utterance_cache), + }, + f, + ) + os.replace(tmp, path) + except Exception: + pass + + +def _validate_utterance(utterance) -> List[Diagnostic]: + sink = CollectingSink() + _validate_sequence(utterance.tokens, sink=sink) + return [ + d for d in sink.diagnostics if d.severity in (Severity.ERROR, Severity.WARNING) + ] + + +def analyze_sloka_semantics( + analysis: DocumentAnalysis, sloka: Sloka +) -> List[Diagnostic]: + """Run the semantic validators over one sloka, using cached per-utterance + results wherever the source text is unchanged.""" + global _disk_dirty + _load_disk_cache() + + out: List[Diagnostic] = [] + for line in sloka.lines: + for utterance in line.vAkyAni: + if utterance.span is None: + out += _validate_utterance(utterance) + continue + + base = utterance.span.start + utt_text = analysis.text[base : utterance.span.end] + key = hashlib.sha1(utt_text.encode()).hexdigest() + + entry = _utterance_cache.get(key) + if entry is None: + entry = [] + for d in _validate_utterance(utterance): + rel_start = d.span.start - base if d.span else None + rel_end = d.span.end - base if d.span else None + entry.append( + [ + d.severity.value, + d.code, + d.message, + rel_start, + rel_end, + d.data, + ] + ) + _utterance_cache[key] = entry + _disk_dirty = True + while len(_utterance_cache) > _CACHE_MAX_ENTRIES: + _utterance_cache.popitem(last=False) + else: + _utterance_cache.move_to_end(key) + + for severity, code, message, rel_start, rel_end, data in entry: + span = ( + Span(base + rel_start, base + rel_end) + if rel_start is not None + else None + ) + out.append( + Diagnostic(message, Severity(severity), span, code, data) + ) + + return out + + +def analyze_semantics(analysis: DocumentAnalysis) -> List[Diagnostic]: + """Run the sandhi/vocabulary/declension validators over a parsed document. + + Slow on a cache-cold document; run off the request path. + """ + diagnostics: List[Diagnostic] = [] + for sloka in slokas(analysis): + diagnostics += analyze_sloka_semantics(analysis, sloka) + return diagnostics + + +def token_at(analysis: DocumentAnalysis, offset: int) -> Optional[TokenType]: + """The innermost token whose span contains the given offset.""" + best: Optional[Tuple[Span, TokenType]] = None + for span, token in analysis.tokens: + if span.start <= offset < span.end: + if best is None or (span.end - span.start) < ( + best[0].end - best[0].start + ): + best = (span, token) + return best[1] if best else None diff --git a/nirukta/lsp/completion.py b/nirukta/lsp/completion.py new file mode 100644 index 0000000..a845fa6 --- /dev/null +++ b/nirukta/lsp/completion.py @@ -0,0 +1,63 @@ +"""Completion of inflected forms after a ``stem->`` annotation.""" + +import re +from typing import List, Optional, Tuple + +from nirukta_inflect import CELL_ORDER, declension_tables + +from nirukta.models.enums import System +from nirukta.render import transliterate +from nirukta.strings import unswara + +# an SLP1 word directly before "->", plus whatever partial form follows; +# the word class mirrors the grammar's slp1 rule +_INFLECTION_RE = re.compile( + r"""([^\[\]>{}.;=+()"\s-]+) # the stem being inflected + (?:\[[^\]]*\]|\{[^}]*\})* # its glosses, if any + -> + ([^\[\]>{}.;=+()"\s-]*)$ # the partially typed form""", + re.VERBOSE, +) + + +def inflection_context(line_prefix: str) -> Optional[Tuple[str, str]]: + """(stem, partial form) when the cursor sits after ``stem->``, else None.""" + # inside a "translation string" arrows are prose, not annotations + if line_prefix.count('"') % 2 == 1: + return None + match = _INFLECTION_RE.search(line_prefix) + if match is None: + return None + return match.group(1), match.group(2) + + +def inflection_items(stem: str) -> List[dict]: + """Completion candidates: every declined form of ``stem``, labeled. + + Returns plain dicts (label, detail, sort_text) so the caller owns the + lsprotocol types. + """ + stem = unswara(stem) + + # form -> (first cell index, [cell labels]) + seen: dict = {} + for table in declension_tables(stem): + for index, (case, number) in enumerate(CELL_ORDER): + for form in table.get(case, number) or (): + label = f"{case.value} {number.value} ({table.model_name})" + if form not in seen: + seen[form] = (index, [label]) + else: + seen[form][1].append(label) + + items = [] + for form, (index, labels) in seen.items(): + iast = transliterate(System.SLP1, System.IAST, form) + items.append( + { + "label": form, + "detail": f"{iast} · " + " / ".join(labels), + "sort_text": f"{index:02d}{form}", + } + ) + return items diff --git a/nirukta/lsp/hover.py b/nirukta/lsp/hover.py new file mode 100644 index 0000000..221790e --- /dev/null +++ b/nirukta/lsp/hover.py @@ -0,0 +1,120 @@ +from typing import List, Optional + +from nirukta.inflection import Case, SanskritInflection +from nirukta.models import EnglishGloss, System +from nirukta.models.enums import SoundChange +from nirukta.models.tokens import ( + CompoundToken, + PunctuationToken, + SimpleToken, + SoundChangeToken, +) +from nirukta.models.tokens.token import TokenType +from nirukta.parsing.visitors.sloka import _format_cells, _safe_define +from nirukta.render import transliterate +from nirukta.strings import unswara + + +def _iast(slp1: str) -> str: + return transliterate(System.SLP1, System.IAST, unswara(slp1)) + + +def _devanagari(slp1: str) -> str: + return transliterate(System.SLP1, System.DEVANAGARI, unswara(slp1)) + + +def _gloss_lines(token: SimpleToken) -> List[str]: + lines = [] + for gloss in token.glosses: + match gloss: + case EnglishGloss(): + lines.append(f'gloss: "{gloss.text}"') + case SanskritInflection(): + lines.append(f"etymology: {gloss}") + case Case(): + lines.append(f"etymology: {gloss.value}") + case _: + continue + return lines + + +def _dictionary_lines(slp1: str) -> List[str]: + # dictionary data may still be downloading during warm-up; degrade to nothing + try: + from nirukta_inflect import lookup + + lines = [] + entry = lookup(unswara(slp1)) + if entry: + if entry.indeclinable: + lines.append("indeclinable") + genders = getattr(entry, "genders", None) + if genders: + names = [getattr(g, "value", str(g)) for g in genders] + lines.append(f"gender: {', '.join(str(n) for n in names)}") + + shown: dict = {} + for entry in _safe_define(unswara(slp1)): + if not entry.meaning or shown.get(entry.source, 0) >= 2: + continue + shown[entry.source] = shown.get(entry.source, 0) + 1 + lines.append(f"{entry.source.upper()}: {entry.meaning}") + return lines + except Exception: + return [] + + +def _simple_token_markdown(token: SimpleToken) -> str: + lines = [f"## {_iast(token.slp1)}"] + lines.append(f"{_devanagari(token.slp1)} (`{token.slp1}`)") + lines += _gloss_lines(token) + lines += _dictionary_lines(token.slp1) + return "\n\n".join(lines) + + +def _inflection_markdown(token: SoundChangeToken) -> str: + stem = token.part.slp1 + lines = [f"## {_iast(stem)} → {_iast(token.slp1)}"] + + try: + from nirukta_inflect import analyze_declension + + parses = analyze_declension(unswara(stem), unswara(token.slp1)) + groups: dict = {} + for parse in parses: + groups.setdefault(parse.cells, []).append(parse.model_name) + for cells, models in groups.items(): + lines.append(f"{_format_cells(cells)} ({', '.join(models)})") + if not parses: + lines.append("not a known declension") + except Exception: + pass + + if isinstance(token.part, SimpleToken): + lines += _gloss_lines(token.part) + inflected = SimpleToken(slp1=token.slp1, glosses=token.glosses) + lines += _gloss_lines(inflected) + return "\n\n".join(lines) + + +def _compound_markdown(token: CompoundToken) -> str: + parts = " + ".join(_iast(part.slp1) for part in token.parts) + return f"## {_iast(token.slp1)}\n\ncompound: {parts} = {_iast(token.slp1)}" + + +def hover_markdown(token: TokenType) -> Optional[str]: + match token: + case PunctuationToken(): + return None + case SimpleToken(): + return _simple_token_markdown(token) + case CompoundToken(): + return _compound_markdown(token) + case SoundChangeToken(): + if token.kind == SoundChange.INFLECTION: + return _inflection_markdown(token) + return ( + f"## {_iast(token.part.slp1)} ⇒ {_iast(token.slp1)}" + f"\n\nexternal sandhi" + ) + return None diff --git a/nirukta/lsp/server.py b/nirukta/lsp/server.py new file mode 100644 index 0000000..2c486f2 --- /dev/null +++ b/nirukta/lsp/server.py @@ -0,0 +1,288 @@ +import asyncio +import builtins +import logging +import os +import sys +import threading + +# how long to sit out further edits before reparsing a changed buffer +DEBOUNCE_SECONDS = 0.2 + + +def create_server(): + from lsprotocol import types + from pygls.lsp.server import LanguageServer + + from nirukta.lsp import analysis as analyzer + from nirukta.lsp.completion import inflection_context, inflection_items + from nirukta.lsp.hover import hover_markdown + from nirukta.parsing.diagnostics import Severity + + server = LanguageServer("nirukta-lsp", "0.1.0") + + # per-uri generation counters drop debounced and stale background work + syntax_gen: dict[str, int] = {} + semantic_gen: dict[str, int] = {} + analyses: dict[str, analyzer.DocumentAnalysis] = {} + semantic_diags: dict[str, list] = {} + + severity_map = { + Severity.ERROR: types.DiagnosticSeverity.Error, + Severity.WARNING: types.DiagnosticSeverity.Warning, + Severity.INFO: types.DiagnosticSeverity.Information, + Severity.HINT: types.DiagnosticSeverity.Hint, + } + + def to_lsp_range(doc, span): + if span is None: + return types.Range(types.Position(0, 0), types.Position(0, 0)) + return types.Range( + doc.client_position_at_offset(span.start), + doc.client_position_at_offset(span.end), + ) + + def to_lsp_diagnostic(doc, d): + return types.Diagnostic( + range=to_lsp_range(doc, d.span), + message=d.message, + severity=severity_map[d.severity], + source="nirukta", + code=d.code, + # round-trips through the client into code action requests + data=d.data, + ) + + def publish(uri: str): + doc = server.workspace.get_text_document(uri) + diagnostics = [] + analysis = analyses.get(uri) + if analysis is not None: + diagnostics += [ + to_lsp_diagnostic(doc, d) for d in analysis.syntax_diagnostics + ] + diagnostics += [to_lsp_diagnostic(doc, d) for d in semantic_diags.get(uri, [])] + server.text_document_publish_diagnostics( + types.PublishDiagnosticsParams(uri=uri, diagnostics=diagnostics) + ) + + def refresh_syntax(uri: str) -> "analyzer.DocumentAnalysis": + doc = server.workspace.get_text_document(uri) + analysis = analyzer.analyze_syntax(doc.path, doc.source) + analyses[uri] = analysis + publish(uri) + return analysis + + async def run_semantics(uri: str): + gen = semantic_gen.get(uri, 0) + 1 + semantic_gen[uri] = gen + + # always reparse: a save can race the didChange debounce, and the + # stored analysis may describe stale text; parsing is ~free + analysis = refresh_syntax(uri) + if analysis.ast is None: + return + + # validate sloka by sloka so diagnostics stream in as they are + # found instead of arriving all at once at the end + loop = asyncio.get_running_loop() + accumulated = [] + for sloka in analyzer.slokas(analysis): + part = await loop.run_in_executor( + None, analyzer.analyze_sloka_semantics, analysis, sloka + ) + # a newer save superseded this run while the validators were working + if semantic_gen.get(uri) != gen: + return + accumulated += part + semantic_diags[uri] = accumulated + publish(uri) + + await loop.run_in_executor(None, analyzer.save_disk_cache) + + @server.feature(types.TEXT_DOCUMENT_DID_OPEN) + async def did_open(ls, params: types.DidOpenTextDocumentParams): + uri = params.text_document.uri + refresh_syntax(uri) + await run_semantics(uri) + + @server.feature(types.TEXT_DOCUMENT_DID_CHANGE) + async def did_change(ls, params: types.DidChangeTextDocumentParams): + uri = params.text_document.uri + gen = syntax_gen.get(uri, 0) + 1 + syntax_gen[uri] = gen + + await asyncio.sleep(DEBOUNCE_SECONDS) + if syntax_gen.get(uri) != gen: + return + refresh_syntax(uri) + + @server.feature(types.TEXT_DOCUMENT_DID_SAVE) + async def did_save(ls, params: types.DidSaveTextDocumentParams): + await run_semantics(params.text_document.uri) + + @server.feature(types.TEXT_DOCUMENT_DID_CLOSE) + def did_close(ls, params: types.DidCloseTextDocumentParams): + uri = params.text_document.uri + for store in (syntax_gen, semantic_gen, analyses, semantic_diags): + store.pop(uri, None) + + @server.feature(types.TEXT_DOCUMENT_HOVER) + def hover(ls, params: types.HoverParams): + uri = params.text_document.uri + analysis = analyses.get(uri) + if analysis is None: + return None + + doc = server.workspace.get_text_document(uri) + offset = doc.offset_at_position(params.position) + token = analyzer.token_at(analysis, offset) + if token is None: + return None + + markdown = hover_markdown(token) + if markdown is None: + return None + return types.Hover( + contents=types.MarkupContent( + kind=types.MarkupKind.Markdown, value=markdown + ), + range=to_lsp_range(doc, token.span), + ) + + @server.feature( + types.TEXT_DOCUMENT_COMPLETION, + types.CompletionOptions(trigger_characters=[">"]), + ) + def completion(ls, params: types.CompletionParams): + doc = server.workspace.get_text_document(params.text_document.uri) + position = doc.position_codec.position_from_client_units( + doc.lines, params.position + ) + line_prefix = doc.lines[position.line][: position.character] + + context = inflection_context(line_prefix) + if context is None: + return None + stem, partial = context + + # the partial form is pure-ASCII SLP1, so client units == characters + start = types.Position( + line=params.position.line, + character=params.position.character - len(partial), + ) + replace = types.Range(start=start, end=params.position) + + items = [ + types.CompletionItem( + label=item["label"], + detail=item["detail"], + sort_text=item["sort_text"], + kind=types.CompletionItemKind.Value, + text_edit=types.TextEdit(range=replace, new_text=item["label"]), + ) + for item in inflection_items(stem) + ] + return types.CompletionList(is_incomplete=False, items=items) + + @server.feature( + types.TEXT_DOCUMENT_CODE_ACTION, + types.CodeActionOptions( + code_action_kinds=[types.CodeActionKind.QuickFix] + ), + ) + def code_action(ls, params: types.CodeActionParams): + uri = params.text_document.uri + doc = server.workspace.get_text_document(uri) + + actions = [] + for diag in params.context.diagnostics: + data = diag.data if isinstance(diag.data, dict) else {} + stems = data.get("stems") + if not stems: + continue + # the surface word, without its trailing glosses + covered = doc.text_in_client_range(diag.range) + word = covered.split("[")[0].split("{")[0] + start, end = diag.range.start, diag.range.end + in_compound = bool(data.get("in_compound")) + for stem in stems: + # inserting keeps glosses and accents on the inflected form; + # inside an equation the grammar needs the inflection wrapped + # in parentheses to parse + edits = [ + types.TextEdit( + range=types.Range(start=start, end=start), + new_text=f"({stem}->" if in_compound else f"{stem}->", + ) + ] + title = f"rewrite as {stem}->{word}" + if in_compound: + edits.append( + types.TextEdit( + range=types.Range(start=end, end=end), + new_text=")", + ) + ) + title = f"rewrite as ({stem}->{word})" + actions.append( + types.CodeAction( + title=title, + kind=types.CodeActionKind.QuickFix, + diagnostics=[diag], + edit=types.WorkspaceEdit(changes={uri: edits}), + ) + ) + return actions or None + + return server + + +def _warm_up(): + # prime the lexicon and the MW dictionary (a large first-use download) + # so the first save does not stall the validators + try: + from nirukta_inflect import define, resolve + + resolve("rAma") + define("rAma") + except Exception: + pass + + +def main(): + # janim's rich logger and any stray print target sys.stdout, which would + # corrupt the JSON-RPC stream; hand the real fd to pygls and divert the rest + real_stdout = sys.stdout + sys.stdout = sys.stderr + + # nirukta_inflect's vendored pydecl calls exit(1) on unexpected data. + # The exit builtin closes sys.stdin, which deadlocks against the + # protocol reader, and raises SystemExit, which would kill the server. + # Detach sys.stdin from the protocol pipe and turn exit into an + # ordinary exception the validators' error handling already catches. + real_stdin = sys.stdin + sys.stdin = open(os.devnull) + + def _no_exit(code=None): + raise RuntimeError(f"a library called exit({code})") + + builtins.exit = _no_exit + builtins.quit = _no_exit + + # set NIRUKTA_LSP_LOG=/path/to/file to capture server logs for debugging + log_file = os.environ.get("NIRUKTA_LSP_LOG") + if log_file: + logging.basicConfig(filename=log_file, level=logging.DEBUG) + import faulthandler + import signal + + faulthandler.register(signal.SIGUSR1, file=open(log_file + ".stacks", "w")) + + threading.Thread(target=_warm_up, daemon=True).start() + + server = create_server() + server.start_io(real_stdin.buffer, real_stdout.buffer) + + +if __name__ == "__main__": + main() diff --git a/nirukta/models/presentation/line.py b/nirukta/models/presentation/line.py index 8ec8124..5d5dded 100644 --- a/nirukta/models/presentation/line.py +++ b/nirukta/models/presentation/line.py @@ -1,7 +1,8 @@ -from dataclasses import dataclass -from typing import List +from dataclasses import dataclass, field +from typing import List, Optional from nirukta.models.presentation.utterance import Utterance +from nirukta.models.span import Span from nirukta.models.tokens import skip_spaces_str @@ -10,6 +11,7 @@ class Line: """A stanza-level grouping of verse lines (between --- line --- markers).""" vAkyAni: List[Utterance] + span: Optional[Span] = field(default=None, compare=False) def slp1(self) -> str: result = "" diff --git a/nirukta/models/presentation/utterance.py b/nirukta/models/presentation/utterance.py index f4de591..c386e4c 100644 --- a/nirukta/models/presentation/utterance.py +++ b/nirukta/models/presentation/utterance.py @@ -1,6 +1,7 @@ -from dataclasses import dataclass -from typing import Sequence +from dataclasses import dataclass, field +from typing import Optional, Sequence +from nirukta.models.span import Span from nirukta.models.tokens import TokenType, skip_spaces_token @@ -10,6 +11,7 @@ class Utterance: tokens: Sequence[TokenType] english: str + span: Optional[Span] = field(default=None, compare=False) def slp1(self) -> str: result = "" diff --git a/nirukta/models/span.py b/nirukta/models/span.py new file mode 100644 index 0000000..a68c96f --- /dev/null +++ b/nirukta/models/span.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Span: + """A half-open range of code-point offsets into the source text.""" + + start: int + end: int diff --git a/nirukta/models/tokens/compound.py b/nirukta/models/tokens/compound.py index e12bc2d..a51ce1f 100644 --- a/nirukta/models/tokens/compound.py +++ b/nirukta/models/tokens/compound.py @@ -1,10 +1,11 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING, List, Sequence +from typing import TYPE_CHECKING, List, Optional, Sequence from nirukta.models.enums import SoundChange from nirukta.models.gloss import Gloss +from nirukta.models.span import Span if TYPE_CHECKING: from nirukta.models.tokens.token import TokenType @@ -20,6 +21,7 @@ class CompoundToken: parts: Sequence[TokenType] slp1: str + span: Optional[Span] = field(default=None, compare=False) @dataclass @@ -28,6 +30,7 @@ class SoundChangeToken: slp1: str kind: SoundChange glosses: List[Gloss] = field(default_factory=list) + span: Optional[Span] = field(default=None, compare=False) def as_compound(self) -> CompoundToken: - return CompoundToken([self.part], self.slp1) + return CompoundToken([self.part], self.slp1, span=self.span) diff --git a/nirukta/models/tokens/punctuation.py b/nirukta/models/tokens/punctuation.py index d8927d0..ffc1543 100644 --- a/nirukta/models/tokens/punctuation.py +++ b/nirukta/models/tokens/punctuation.py @@ -1,6 +1,10 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field +from typing import Optional + +from nirukta.models.span import Span @dataclass class PunctuationToken: slp1: str + span: Optional[Span] = field(default=None, compare=False) diff --git a/nirukta/models/tokens/simple.py b/nirukta/models/tokens/simple.py index daea572..caf698c 100644 --- a/nirukta/models/tokens/simple.py +++ b/nirukta/models/tokens/simple.py @@ -1,7 +1,8 @@ from dataclasses import dataclass, field -from typing import List, Set +from typing import List, Optional, Set from nirukta.models.gloss import EnglishGloss, Gloss +from nirukta.models.span import Span @dataclass @@ -10,6 +11,7 @@ class SimpleToken: slp1: str glosses: List[Gloss] = field(default_factory=list) + span: Optional[Span] = field(default=None, compare=False) def gloss_refs( self, english: str, visited: Set[tuple[int, int]] diff --git a/nirukta/parsing/diagnostics.py b/nirukta/parsing/diagnostics.py new file mode 100644 index 0000000..d5cc1a1 --- /dev/null +++ b/nirukta/parsing/diagnostics.py @@ -0,0 +1,74 @@ +from dataclasses import dataclass +from enum import Enum +from typing import List, Optional + +from janim.logger import log +from parsimonious.exceptions import ParseError + +from nirukta.models.span import Span + + +class Severity(Enum): + ERROR = 1 + WARNING = 2 + INFO = 3 + HINT = 4 + + +@dataclass +class Diagnostic: + message: str + severity: Severity + # None means the problem could not be located in the source + span: Optional[Span] + # one of: parse, sandhi, declension, vocabulary, gloss, etym-gloss, file + code: str + # optional JSON-safe payload for tooling (e.g. code action rewrites) + data: Optional[dict] = None + + +class DiagnosticSink: + """Receives validation results from the parser and validators.""" + + def emit( + self, + severity: Severity, + message: str, + *, + token=None, + span: Optional[Span] = None, + code: str = "", + data: Optional[dict] = None, + ) -> None: + raise NotImplementedError + + +class LoggingSink(DiagnosticSink): + # preserves the historical console/GUI log output of the validators; + # stacklevel attributes the record to the emitting validator line + def emit(self, severity, message, *, token=None, span=None, code="", data=None): + match severity: + case Severity.ERROR: + log.error(message, stacklevel=2) + case Severity.WARNING: + log.warning(message, stacklevel=2) + case _: + log.info(message, stacklevel=2) + + +class CollectingSink(DiagnosticSink): + def __init__(self) -> None: + self.diagnostics: List[Diagnostic] = [] + + def emit(self, severity, message, *, token=None, span=None, code="", data=None): + if span is None and token is not None: + span = getattr(token, "span", None) + self.diagnostics.append(Diagnostic(message, severity, span, code, data)) + + +DEFAULT_SINK: DiagnosticSink = LoggingSink() + + +def parse_error_to_diagnostic(source: str, e: ParseError) -> Diagnostic: + span = Span(e.pos, min(e.pos + 1, len(source))) + return Diagnostic(str(e), Severity.ERROR, span, "parse") diff --git a/nirukta/parsing/visitors/sloka.py b/nirukta/parsing/visitors/sloka.py index 791a7c4..f440f9e 100644 --- a/nirukta/parsing/visitors/sloka.py +++ b/nirukta/parsing/visitors/sloka.py @@ -1,8 +1,16 @@ import logging import os +import re import traceback -from typing import List, Sequence +from functools import lru_cache +from typing import List, Optional, Sequence from nirukta.models.enums import SoundChange +from nirukta.models.span import Span +from nirukta.parsing.diagnostics import ( + DEFAULT_SINK, + DiagnosticSink, + Severity, +) from nirukta.models.tokens import PunctuationToken, SoundChangeToken from nirukta.render import transliterate from nirukta.strings import unswara @@ -33,6 +41,17 @@ S = sandhi_module.Sandhi() +# resolve() declines large parts of the lexicon per call (~160ms each) and +# analyze_declension is similar; the same words recur constantly, so memoize. +# Results are treated as read-only by all callers. +resolve = lru_cache(maxsize=None)(resolve) +analyze_declension = lru_cache(maxsize=None)(analyze_declension) + + +@lru_cache(maxsize=None) +def _sandhi_wx(a: str, b: str): + return S.sandhi(a, b, input_scheme="wx") + def _format_cells(cells): by_number: dict = {} @@ -44,7 +63,9 @@ def _format_cells(cells): ) -def validate_declension(stem: str, declined: str): +def validate_declension( + stem: str, declined: str, *, sink: DiagnosticSink = DEFAULT_SINK, token=None +): stem = unswara(stem) declined = unswara(declined) @@ -58,40 +79,56 @@ def validate_declension(stem: str, declined: str): for parse in parses: groups.setdefault(parse.cells, []).append(parse.model_name) for cells, models in groups.items(): - log.info( + sink.emit( + Severity.INFO, f"inflection validated [declension]:\t'{print_stem}' -> '{print_declined}' " - f"is a valid {_format_cells(cells)} declension ({', '.join(models)})." + f"is a valid {_format_cells(cells)} declension ({', '.join(models)}).", + token=token, + code="declension", ) return entry = lookup(stem) if entry and entry.indeclinable: - log.info( + sink.emit( + Severity.INFO, f"inflection skipped [declension]:\t'{print_stem}' is indeclinable; " - f"no declension to validate." + f"no declension to validate.", + token=token, + code="declension", ) return - log.warning( - f"inflection invalid [declension]:\t'{print_stem}' -> '{print_declined}' is not a known declension." + sink.emit( + Severity.WARNING, + f"inflection invalid [declension]:\t'{print_stem}' -> '{print_declined}' is not a known declension.", + token=token, + code="declension", ) for table in declension_tables(stem): options = [ transliterate(System.SLP1, System.IAST, form) for form in table.forms() ] - log.info(f"{table.model_name}: {options}") + sink.emit( + Severity.INFO, + f"{table.model_name}: {options}", + token=token, + code="declension", + ) -def _leaf_tokens(sequence: Sequence[TokenType]): - """Yield every SimpleToken leaf, recursing into compounds and sound changes.""" +def _leaf_tokens(sequence: Sequence[TokenType], in_compound: bool = False): + """Yield (SimpleToken, in_compound) for every leaf, recursing into + compounds and sound changes. in_compound marks leaves that sit inside + an equation, where an inflection rewrite needs parentheses to parse.""" for token in sequence: match token: case SimpleToken(): - yield token + yield token, in_compound case CompoundToken(): - yield from _leaf_tokens(token.parts) + yield from _leaf_tokens(token.parts, True) case SoundChangeToken(): - yield from _leaf_tokens([token.part]) + yield from _leaf_tokens([token.part], in_compound) case _: continue @@ -105,20 +142,53 @@ def _safe_define(form: str): return [] +def _gloss_variants(gloss_text: str) -> set[str]: + """Morphological variants of a gloss to try against dictionary prose.""" + needle = " ".join(gloss_text.strip().lower().split()) + if not needle: + return set() + variants = {needle} + # dictionary prose rarely repeats the author's article + for article in ("the ", "a ", "an "): + if needle.startswith(article): + variants.add(needle[len(article):]) + # naive singulars: suns -> sun, bodies -> body, foxes -> fox + for v in tuple(variants): + if v.endswith("ies") and len(v) > 4: + variants.add(v[:-3] + "y") + elif v.endswith("es") and len(v) > 3: + variants.add(v[:-2]) + if v.endswith("s") and len(v) > 3: + variants.add(v[:-1]) + return variants + + def _gloss_in_definitions(gloss_text: str, entries) -> bool: - """True if the author's gloss appears in any MW definition's meaning.""" - needle = gloss_text.strip().lower() - return bool(needle) and any(needle in entry.meaning.lower() for entry in entries) + """True if the author's gloss appears in any dictionary meaning. + + Matches on word boundaries, ignoring case, articles, and simple plurals. + """ + variants = _gloss_variants(gloss_text) + if not variants: + return False + patterns = [re.compile(rf"\b{re.escape(v)}\b") for v in variants] + return any( + pattern.search(entry.meaning.lower()) + for entry in entries + for pattern in patterns + ) -def validate_vocabulary(sequence: Sequence[TokenType]): +def validate_vocabulary( + sequence: Sequence[TokenType], *, sink: DiagnosticSink = DEFAULT_SINK +): """Check that every leaf word resolves to a known stem or indeclinable. Assumes leaves are nominals or indeclinables (verbs are not yet supported and will simply be reported as unrecognized). """ seen: set[str] = set() - for token in _leaf_tokens(sequence): + for token, in_compound in _leaf_tokens(sequence): form = unswara(token.slp1) # strip accent marks before lookup if form in seen: continue @@ -131,7 +201,12 @@ def validate_vocabulary(sequence: Sequence[TokenType]): # log.info(f"\nresolution:{resolutions}\n") if not resolutions: - log.warning(f"vocabulary unrecognized\t[noun]:\t\t'{print_form}'") + sink.emit( + Severity.WARNING, + f"vocabulary unrecognized\t[noun]:\t\t'{print_form}'", + token=token, + code="vocabulary", + ) continue headwords = [r for r in resolutions if r.is_headword] @@ -139,23 +214,34 @@ def validate_vocabulary(sequence: Sequence[TokenType]): kind = ( "indeclinable" if any(r.indeclinable for r in headwords) else "headword" ) - log.info(f"vocabulary resolved\t[{kind}]:\t'{print_form}'") + sink.emit( + Severity.INFO, + f"vocabulary resolved\t[{kind}]:\t'{print_form}'", + token=token, + code="vocabulary", + ) mw = _safe_define(form) # log.info(f"\nmw define:{mw}\n") for gloss in token.glosses: if not isinstance(gloss, EnglishGloss): continue if _gloss_in_definitions(gloss.text, mw): - log.info( - f"gloss verified\t\t[dictionary]:\t'{print_form}' = '{gloss.text}' ✓" + sink.emit( + Severity.INFO, + f"gloss verified\t\t[dictionary]:\t'{print_form}' = '{gloss.text}' ✓", + token=token, + code="gloss", ) else: # senses = ( # " | ".join(entry.meaning for entry in mw) or "(no MW entry)" # ) - log.warning( - f"gloss unverified\t[dictionary]:\t'{print_form}' != '{gloss.text}' ✘" + sink.emit( + Severity.WARNING, + f"gloss unverified\t[dictionary]:\t'{print_form}' != '{gloss.text}' ✘", # f"not found in MW. Definitions: {senses}" + token=token, + code="gloss", ) else: # A bare inflected form resolves only via declension. @@ -164,96 +250,137 @@ def validate_vocabulary(sequence: Sequence[TokenType]): {transliterate(System.SLP1, System.IAST, r.stem) for r in resolutions} ) rewrites = sorted({f"{r.stem}->{form}" for r in resolutions}) - log.warning( + sink.emit( + Severity.WARNING, f"vocabulary suggestion\t[declension]:\t'{print_form}' is an " - f"inflected form of: {stems}. rewrite as {rewrites}." + f"inflected form of: {stems}. rewrite as {rewrites}.", + token=token, + code="vocabulary", + # SLP1 stems, so editors can offer the rewrite as a quickfix; + # inside an equation the rewrite must be parenthesized + data={ + "stems": sorted({r.stem for r in resolutions}), + "in_compound": in_compound, + }, ) -# Result should be provided in slp1 -def validate_equation(parts: Sequence[TokenType], result: str, kind: SoundChange): - if len(parts) > 1: - built = "" - - partinfo = [] - for part in parts: - partinfo.append(f"'{part.slp1}'") - log.debug(f"parts: {partinfo}") - - for i in range(len(parts)): - A = built - B = transliterate( - System.SLP1, - System.WX, - unswara(parts[i].slp1), - ) - log.debug( - f"adding: '{transliterate(System.WX, System.IAST, A)}' + '{transliterate(System.WX, System.IAST, B)}'" - ) - - results = S.sandhi(A, B, input_scheme="wx") - valid_forms = {r[0] for r in results} - # compact_results = list(filter(lambda x: " " not in x, valid_forms)) - compact_results = list( - map(lambda x: x.split(" ")[0] if " " in x else x, valid_forms) - ) - compact_results = list(map(lambda x: x.replace("_", ""), compact_results)) - # log.info(f"compact_results: {compact_results}") - - if len(compact_results) == 0: - unverified = list( - map( - lambda x: transliterate(System.WX, System.IAST, x), - compact_results, - ) - ) - log.warning(f"cannot validate: {unverified}") - else: - built = compact_results[0] +# How many candidate joins to carry between sandhi steps. +_SANDHI_CANDIDATE_CAP = 32 - built = transliterate(System.WX, System.IAST, built) - final_result = transliterate(System.SLP1, System.IAST, unswara(result)) - built = final_result.replace("'", "") - final_result = final_result.replace("'", "") - # log.info(f"parts: {parts}") - - undone_parts = list( - map(lambda x: transliterate(System.SLP1, System.IAST, x.slp1), parts) +# Result should be provided in slp1 +def validate_equation( + parts: Sequence[TokenType], + result: str, + kind: SoundChange, + *, + sink: DiagnosticSink = DEFAULT_SINK, + token=None, +): + if len(parts) <= 1: + sink.emit( + Severity.WARNING, + f"no need to validate parts of n<2 {parts}", + token=token, + code="sandhi", ) + return - if built != final_result: - log.warning( - f"unable for verify sandhi for these parts: {undone_parts}\n" - f"expected '{final_result}' but got '{built}'" + log.debug(f"parts: {[repr(p.slp1) for p in parts]}") + + # Each sandhi step can produce several valid joins; carry them all so + # validation checks membership rather than hinging on an arbitrary pick. + candidates: set[str] = {""} + for part in parts: + B = transliterate(System.SLP1, System.WX, unswara(part.slp1)) + merged: set[str] = set() + for A in candidates: + for r in _sandhi_wx(A, B): + # the engine may return either a fused join or a spaced + # word pair; keep the first word and the fused whole so + # both compound and word-boundary annotations can match + raw = r[0].replace("_", "") + for form in (raw.split(" ")[0], raw.replace(" ", "")): + if form: + merged.add(form) + if not merged: + sink.emit( + Severity.WARNING, + f"cannot validate: no sandhi result when adding " + f"'{transliterate(System.WX, System.IAST, B)}'", + token=token, + code="sandhi", ) - else: - if kind == SoundChange.EXTERNAL_SANDHI: - log.info( - f"sandhi validated\t[external]:\t'{undone_parts[0]}' => '{final_result}' when preceding '{undone_parts[1]}'" - ) - else: - equation_string = " + ".join( - list(map(lambda x: f"'{x}'", undone_parts)) - ) - log.info( - f"sandhi validated\t[internal]:\t{equation_string} = '{final_result}'" - ) + return + # sorted before capping so results are deterministic across runs + candidates = set(sorted(merged)[:_SANDHI_CANDIDATE_CAP]) + + final_result = transliterate(System.SLP1, System.IAST, unswara(result)) + final_result = final_result.replace("'", "") + built = { + transliterate(System.WX, System.IAST, c).replace("'", "") + for c in candidates + } + + undone_parts = list( + map(lambda x: transliterate(System.SLP1, System.IAST, x.slp1), parts) + ) + verified = final_result in built + if not verified and kind == SoundChange.EXTERNAL_SANDHI: + # an external annotation names only the first word's changed form; + # when the engine fused the pair, test expected + second word + second = transliterate( + System.SLP1, System.IAST, unswara(parts[-1].slp1) + ).replace("'", "") + verified = (final_result + second) in built + + if not verified: + options = ", ".join(f"'{b}'" for b in sorted(built)[:5]) + # a definitive disagreement with the engine is an error; the + # indeterminate "cannot validate" case above stays a warning + sink.emit( + Severity.ERROR, + f"invalid sandhi for these parts: {undone_parts}\n" + f"expected '{final_result}' but got {options}", + token=token, + code="sandhi", + ) else: - log.warning(f"no need to validate parts of n<2 {parts}") + if kind == SoundChange.EXTERNAL_SANDHI: + sink.emit( + Severity.INFO, + f"sandhi validated\t[external]:\t'{undone_parts[0]}' => '{final_result}' when preceding '{undone_parts[1]}'", + token=token, + code="sandhi", + ) + else: + equation_string = " + ".join( + list(map(lambda x: f"'{x}'", undone_parts)) + ) + sink.emit( + Severity.INFO, + f"sandhi validated\t[internal]:\t{equation_string} = '{final_result}'", + token=token, + code="sandhi", + ) -def validate_sandhi(sequence: Sequence[TokenType]): +def validate_sandhi( + sequence: Sequence[TokenType], *, sink: DiagnosticSink = DEFAULT_SINK +): def validate_compound(compound: CompoundToken): # External - validate_sandhi(compound.parts) + validate_sandhi(compound.parts, sink=sink) # Internal validate_equation( compound.parts, compound.slp1, SoundChange.INTERNAL_SANDHI, + sink=sink, + token=compound, ) for i in range(len(sequence)): @@ -267,21 +394,25 @@ def validate_compound(compound: CompoundToken): case SoundChangeToken(): match current.kind: case SoundChange.INFLECTION: - validate_declension(current.part.slp1, current.slp1) + validate_declension( + current.part.slp1, current.slp1, sink=sink, token=current + ) # An inflection applied to a whole compound still needs # its internal sandhi checked. A chain of inflections # recurses until it bottoms out at the compound. if isinstance(current.part, CompoundToken): validate_compound(current.part) elif isinstance(current.part, SoundChangeToken): - validate_sandhi([current.part]) + validate_sandhi([current.part], sink=sink) case SoundChange.EXTERNAL_SANDHI: inner = current.part if ( isinstance(inner, SoundChangeToken) and inner.kind == SoundChange.INFLECTION ): - validate_declension(inner.part.slp1, inner.slp1) + validate_declension( + inner.part.slp1, inner.slp1, sink=sink, token=current + ) elif isinstance(inner, CompoundToken): validate_compound(inner) if i < len(sequence) - 2: @@ -290,30 +421,36 @@ def validate_compound(compound: CompoundToken): [current.part, next], current.slp1, SoundChange.EXTERNAL_SANDHI, + sink=sink, + token=current, ) case CompoundToken(): validate_compound(current) -def _validate_sequence(tokens: Sequence[TokenType]): - # run both validators on one token sequence, logging instead of raising +def _validate_sequence( + tokens: Sequence[TokenType], sink: DiagnosticSink = DEFAULT_SINK +): + # run both validators on one token sequence, reporting instead of raising try: - validate_sandhi(tokens) + validate_sandhi(tokens, sink=sink) except Exception as e: - log.error(f"Failed to validate sandhi: {e}") + sink.emit(Severity.ERROR, f"Failed to validate sandhi: {e}", code="sandhi") try: - validate_vocabulary(tokens) + validate_vocabulary(tokens, sink=sink) except Exception as e: - log.error(f"Failed to validate vocabulary: {e}") + sink.emit( + Severity.ERROR, f"Failed to validate vocabulary: {e}", code="vocabulary" + ) -def validate_sloka(sloka: Sloka): +def validate_sloka(sloka: Sloka, sink: DiagnosticSink = DEFAULT_SINK): # validate a single already-parsed sloka, reproducing the per-sequence # validation a full parse would have done for just this sloka for line in sloka.lines: for utterance in line.vAkyAni: - _validate_sequence(utterance.tokens) + _validate_sequence(utterance.tokens, sink=sink) class SlokaVisitor(NodeVisitor): @@ -321,20 +458,32 @@ class SlokaVisitor(NodeVisitor): dir: str source: str validate: bool - - def __init__(self, file: str, validate: bool = True): + sink: DiagnosticSink + + def __init__( + self, + file: str, + validate: bool = True, + source: Optional[str] = None, + sink: Optional[DiagnosticSink] = None, + ): NodeVisitor.__init__(self) - print(f"Loading {file}...") + log.debug(f"Loading {file}...") - with open(file) as f: - self.source = f.read() + # source overrides the file contents so unsaved editor + # buffers can be parsed without touching disk + if source is None: + with open(file) as f: + source = f.read() + self.source = source self.file = file self.directory = os.path.dirname(self.file) # when False, parse only builds the tree and skips the expensive # sandhi/vocabulary validators (used to enumerate slokas cheaply) self.validate = validate + self.sink = sink if sink is not None else DEFAULT_SINK def _print_parse_error(self, e: ParseError) -> None: lines = self.source.splitlines() @@ -375,16 +524,16 @@ def visit_citation_text(self, node, _): # -- line / verse line -------------------------------------------------- - def visit_line(self, _, visited_children): + def visit_line(self, node, visited_children): _, _, verse_lines = visited_children - return Line(vAkyAni=list(verse_lines)) + return Line(vAkyAni=list(verse_lines), span=Span(node.start, node.end)) - def visit_verse_line(self, _, visited_children): + def visit_verse_line(self, node, visited_children): # visited_children: [lookahead, token_seq, ws, first_quoted_str, rest_quoted_strs, ws] _, tokens, _, first, rest, _ = visited_children extra = [pair[1] for pair in rest] english = "#linebreak()".join([first] + extra) - return Utterance(tokens=tokens, english=english) + return Utterance(tokens=tokens, english=english, span=Span(node.start, node.end)) # -- token sequence ----------------------------------------------------- @@ -395,7 +544,7 @@ def visit_token_seq(self, _, visited_children): tokens.append(pair[1]) if self.validate: - _validate_sequence(tokens) + _validate_sequence(tokens, sink=self.sink) return tokens @@ -404,7 +553,7 @@ def visit_token(self, _, visited_children): # -- compound (sandhi) tokens ------------------------------------------- - def visit_text_token(self, _, visited_children): + def visit_text_token(self, node, visited_children): initial, inflect_parts, external_parts = visited_children # Start construction @@ -435,12 +584,13 @@ def visit_text_token(self, _, visited_children): part=result, slp1=slp1, kind=SoundChange.EXTERNAL_SANDHI ) + result.span = Span(node.start, node.end) return result - def visit_equation_part(self, _, visited_children): + def visit_equation_part(self, node, visited_children): first_part, plus_parts, _, slp1 = visited_children parts = list([first_part]) + list(plus_parts) - return CompoundToken(parts, slp1) + return CompoundToken(parts, slp1, span=Span(node.start, node.end)) def visit_inflect_part(self, _, visited_children): _, simple_token = visited_children @@ -463,13 +613,15 @@ def visit_paren_compound(self, _, visited_children): # -- simple tokens & glosses -------------------------------------------- - def visit_simple_token(self, _, visited_children): + def visit_simple_token(self, node, visited_children): slp1, glosses = visited_children # `gloss*` matching zero glosses falls through generic_visit, which # returns the raw parse Node instead of a list. Normalize to a list. if not isinstance(glosses, list): glosses = [] - return SimpleToken(slp1=slp1, glosses=glosses) + return SimpleToken( + slp1=slp1, glosses=glosses, span=Span(node.start, node.end) + ) def visit_gloss(self, _, visited_children): return visited_children[0] @@ -478,7 +630,7 @@ def visit_trans_gloss(self, _, visited_children): _, content, _ = visited_children return EnglishGloss(text=content) - def visit_etym_gloss(self, _, visited_children): + def visit_etym_gloss(self, node, visited_children): _, content, _ = visited_children try: inflection = SanskritInflection.parse(content) @@ -486,11 +638,16 @@ def visit_etym_gloss(self, _, visited_children): except Exception: try: case = Case.parse(content) - print(f"case: {case}") + log.debug(f"case: {case}") return case except Exception: - print(f'Error! invalid etymological glossing: "{content}"\n') - logging.error(traceback.format_exc()) + self.sink.emit( + Severity.ERROR, + f'invalid etymological glossing: "{content}"', + span=Span(node.start, node.end), + code="etym-gloss", + ) + logging.debug(traceback.format_exc()) return None def visit_trans_content(self, node, _): @@ -502,7 +659,7 @@ def visit_etym_content(self, node, _): # -- terminals ---------------------------------------------------------- def visit_punct(self, node, _): - return PunctuationToken(slp1=node.text) + return PunctuationToken(slp1=node.text, span=Span(node.start, node.end)) def visit_slp1(self, node, _): return node.text diff --git a/nirukta/parsing/visitors/sutra.py b/nirukta/parsing/visitors/sutra.py index 173e506..03bbd63 100644 --- a/nirukta/parsing/visitors/sutra.py +++ b/nirukta/parsing/visitors/sutra.py @@ -1,12 +1,36 @@ import os -from typing import List +from dataclasses import dataclass +from typing import List, Optional from nirukta.models import Sloka, SutraFile +from nirukta.models.span import Span +from nirukta.parsing.diagnostics import DiagnosticSink, Severity from nirukta.parsing.grammars import SUTRA_GRAMMAR from nirukta.parsing.visitors.sloka import SlokaVisitor from parsimonious.exceptions import ParseError +@dataclass +class FileRef: + """An unresolved file: reference to an external .sloka.""" + + path: str + span: Span + + class SutraVisitor(SlokaVisitor): + def __init__( + self, + file: str, + validate: bool = True, + source: Optional[str] = None, + sink: Optional[DiagnosticSink] = None, + resolve_external: bool = True, + ): + SlokaVisitor.__init__(self, file, validate=validate, source=source, sink=sink) + # when False, external file: references are existence-checked and + # reported instead of parsed, so editors do not recurse on each edit + self.resolve_external = resolve_external + def parse(self) -> SutraFile: try: tree = SUTRA_GRAMMAR.parse(self.source) @@ -24,11 +48,25 @@ def visit_sutra(self, _, visited_children): sloka = sloka[0] if isinstance(sloka, Sloka): processed_slokas.append(sloka) - else: - sloka_file = os.path.normpath(os.path.join(self.directory, sloka)) - processed_slokas.append( - SlokaVisitor(sloka_file, validate=self.validate).parse().sloka - ) + continue + + ref: FileRef = sloka + sloka_file = os.path.normpath(os.path.join(self.directory, ref.path)) + if not self.resolve_external: + if not os.path.exists(sloka_file): + self.sink.emit( + Severity.ERROR, + f"referenced sloka file not found: {ref.path}", + span=ref.span, + code="file", + ) + continue + + processed_slokas.append( + SlokaVisitor(sloka_file, validate=self.validate, sink=self.sink) + .parse() + .sloka + ) return SutraFile(citation, processed_slokas) @@ -44,9 +82,9 @@ def visit_external_sloka(self, _, visited_children): _, _, _, file, _ = visited_children return file - def visit_file(self, _, visited_children): + def visit_file(self, node, visited_children): _, file_content = visited_children - return file_content + return FileRef(file_content, Span(node.start, node.end)) def visit_file_content(self, _, visited_children): first, rest = visited_children diff --git a/pyproject.toml b/pyproject.toml index 727d2aa..ac0db24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,17 +6,26 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "aksharamukha>=2.3", - "nirukta-inflect", "dill>=0.4.1", "janim[gui]>=4.2.0", + "nirukta-inflect", "parsimonious>=0.11.0", + "pygls>=2.1", "sandhi>=1.0.0", "skrutable>=2.6.3", ] +[project.scripts] +nirukta-lsp = "nirukta.lsp.server:main" + +[dependency-groups] +dev = [ + "pytest>=9.1.1", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.uv.sources] -nirukta-inflect = { git = "https://github.com/recursivepaws/nirukta-inflect", branch = "main" } +nirukta-inflect = { git = "ssh://git@github.com/recursivepaws/nirukta-inflect.git", branch = "main" } diff --git a/tests/test_completion.py b/tests/test_completion.py new file mode 100644 index 0000000..56a5b8d --- /dev/null +++ b/tests/test_completion.py @@ -0,0 +1,46 @@ +from nirukta.lsp.completion import inflection_context, inflection_items + + +def test_context_after_arrow(): + assert inflection_context("deva->") == ("deva", "") + assert inflection_context("kuru[make] deva->dev") == ("deva", "dev") + + +def test_context_skips_glosses_on_the_stem(): + assert inflection_context("aSocya[not worthy of grief]->") == ("aSocya", "") + assert inflection_context("deva{N.M.SG.NOM}->de") == ("deva", "de") + + +def test_context_after_merged_compound(): + prefix = "(gata+asu=gatAsu)+a=gatAsUn->" + assert inflection_context(prefix) == ("gatAsUn", "") + + +def test_no_context_for_external_sandhi_or_prose(): + assert inflection_context("samapraBA=>") is None + assert inflection_context('"an arrow -> like this') is None + assert inflection_context("deva namaH") is None + + +def test_items_cover_the_paradigms(): + items = inflection_items("deva") + by_label = {item["label"]: item for item in items} + + assert "devaH" in by_label + assert "devAya" in by_label + assert "nominative singular" in by_label["devaH"]["detail"] + assert "dative singular" in by_label["devAya"]["detail"] + # deva has masculine and neuter tables; shared forms merge their labels + assert "(m_a)" in by_label["devAya"]["detail"] + assert "(n_a)" in by_label["devAya"]["detail"] + + # paradigm ordering: nominative singular sorts before dative + assert by_label["devaH"]["sort_text"] < by_label["devAya"]["sort_text"] + + +def test_items_fall_back_to_parametric_paradigms(): + # stems outside the lexicon (e.g. freshly merged compounds) still + # complete via the parametric consonant/vowel paradigms, mirroring + # what validate_declension would accept + items = inflection_items("xyzzyq") + assert any(item["label"] == "xyzzyqaH" for item in items) diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py new file mode 100644 index 0000000..4e2c59b --- /dev/null +++ b/tests/test_diagnostics.py @@ -0,0 +1,78 @@ +import pathlib + +import pytest +from parsimonious.exceptions import ParseError + +from nirukta.lsp.analysis import analyze_syntax, token_at +from nirukta.models.tokens import SimpleToken +from nirukta.parsing.diagnostics import ( + CollectingSink, + Severity, + parse_error_to_diagnostic, +) +from nirukta.parsing.visitors.sloka import SlokaVisitor + +LIBRARY = pathlib.Path(__file__).parent.parent / "library" + +GOOD = """=== test 1.1 === + +--- line --- +deva[god] namaH . +"salutations to the god" +""" + + +def test_bad_etym_gloss_is_collected_with_span(): + source = GOOD.replace("deva[god]", "deva{NOT.A.REAL.TAG}") + sink = CollectingSink() + SlokaVisitor("test.sloka", validate=False, source=source, sink=sink).parse() + + (diag,) = sink.diagnostics + assert diag.severity == Severity.ERROR + assert diag.code == "etym-gloss" + assert source[diag.span.start : diag.span.end] == "{NOT.A.REAL.TAG}" + + +def test_parse_error_becomes_diagnostic_at_error_position(): + source = GOOD.replace("deva[god]", "deva[unclosed") + with pytest.raises(ParseError) as excinfo: + SlokaVisitor("test.sloka", validate=False, source=source).parse() + + diag = parse_error_to_diagnostic(source, excinfo.value) + assert diag.severity == Severity.ERROR + assert diag.code == "parse" + assert source[diag.span.start] == "[" + + +def test_analyze_syntax_never_raises(): + analysis = analyze_syntax("broken.sloka", "complete nonsense ===") + assert analysis.ast is None + assert any(d.code == "parse" for d in analysis.syntax_diagnostics) + + +def test_sutra_missing_file_reference(): + source = """=== test sutra === + +=== sloka === +file:does/not/exist.sloka +""" + analysis = analyze_syntax("test.sutra", source) + (diag,) = analysis.syntax_diagnostics + assert diag.severity == Severity.ERROR + assert diag.code == "file" + assert source[diag.span.start : diag.span.end] == "file:does/not/exist.sloka" + + +def test_token_at_returns_innermost_token(): + path = str(LIBRARY / "vakratuRqa_mahAkAya.sloka") + source = open(path).read() + analysis = analyze_syntax(path, source) + + # inside 'vakra' of the compound vakra[curved]+tuRqa[trunk]=vakratuRqa + offset = source.index("vakra[curved]") + 1 + token = token_at(analysis, offset) + assert isinstance(token, SimpleToken) + assert token.slp1 == "vakra" + + # offsets outside any token yield nothing + assert token_at(analysis, 0) is None diff --git a/tests/test_grammar_parity.py b/tests/test_grammar_parity.py new file mode 100644 index 0000000..8d16077 --- /dev/null +++ b/tests/test_grammar_parity.py @@ -0,0 +1,49 @@ +"""The tree-sitter grammar and the parsimonious PEG must accept the same files.""" + +import pathlib +import shutil +import subprocess + +import pytest + +from nirukta.lsp.analysis import analyze_syntax + +ROOT = pathlib.Path(__file__).parent.parent +TS_DIR = ROOT / "tree-sitter-nirukta" + +pytestmark = pytest.mark.skipif( + shutil.which("tree-sitter") is None, + reason="tree-sitter CLI not available", +) + + +def _library_files(): + return sorted( + list(ROOT.glob("library/**/*.sloka")) + list(ROOT.glob("library/**/*.sutra")) + ) + + +def test_tree_sitter_accepts_everything_the_peg_accepts(): + """One-directional by design: the tree-sitter grammar is deliberately + flat/forgiving so highlighting survives structural errors, so it may + accept files the PEG rejects. It must never reject a valid file.""" + mismatches = [] + for path in _library_files(): + analysis = analyze_syntax(str(path), path.read_text()) + # only grammar-level acceptance matters; missing file: refs are + # semantic and invisible to tree-sitter + peg_ok = analysis.ast is not None and not any( + d.code == "parse" for d in analysis.syntax_diagnostics + ) + if not peg_ok: + continue + ts = subprocess.run( + ["tree-sitter", "parse", "--quiet", str(path)], + cwd=TS_DIR, + capture_output=True, + ) + if ts.returncode != 0: + mismatches.append(str(path.relative_to(ROOT))) + assert not mismatches, ( + "tree-sitter rejects files the PEG accepts:\n" + "\n".join(mismatches) + ) diff --git a/tests/test_lsp.py b/tests/test_lsp.py new file mode 100644 index 0000000..8ae6fff --- /dev/null +++ b/tests/test_lsp.py @@ -0,0 +1,301 @@ +"""End-to-end test of nirukta-lsp over stdio.""" + +import json +import os +import pathlib +import queue +import subprocess +import threading + +import pytest + +ROOT = pathlib.Path(__file__).parent.parent +SLOKA = ROOT / "library" / "vakratuRqa_mahAkAya.sloka" +URI = f"file://{SLOKA}" + + +class Client: + def __init__(self): + self.proc = subprocess.Popen( + ["uv", "run", "nirukta-lsp"], + cwd=ROOT, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + self.incoming: "queue.Queue[dict]" = queue.Queue() + self._id = 0 + threading.Thread(target=self._read, daemon=True).start() + + def _read(self): + while True: + headers = {} + while True: + line = self.proc.stdout.readline() + if not line: + return + line = line.decode().strip() + if not line: + break + key, _, value = line.partition(":") + headers[key.lower()] = value.strip() + body = self.proc.stdout.read(int(headers["content-length"])) + self.incoming.put(json.loads(body)) + + def send(self, method, params, request=True): + msg = {"jsonrpc": "2.0", "method": method, "params": params} + if request: + self._id += 1 + msg["id"] = self._id + raw = json.dumps(msg).encode() + self.proc.stdin.write(f"Content-Length: {len(raw)}\r\n\r\n".encode() + raw) + self.proc.stdin.flush() + return self._id if request else None + + def wait_for(self, pred, timeout=60): + import time + + deadline = time.time() + timeout + while time.time() < deadline: + try: + msg = self.incoming.get(timeout=max(0.1, deadline - time.time())) + except queue.Empty: + break + if pred(msg): + return msg + raise TimeoutError("no matching message from server") + + def close(self): + try: + self.send("shutdown", {}) + self.send("exit", {}, request=False) + self.proc.wait(timeout=10) + except Exception: + self.proc.kill() + + +@pytest.fixture(scope="module") +def client(): + c = Client() + rid = c.send( + "initialize", + {"processId": None, "rootUri": f"file://{ROOT}", "capabilities": {}}, + ) + c.wait_for(lambda m: m.get("id") == rid) + c.send("initialized", {}, request=False) + yield c + c.close() + + +def _publish(msg): + return msg.get("method") == "textDocument/publishDiagnostics" + + +def test_open_change_hover_roundtrip(client): + text = SLOKA.read_text() + client.send( + "textDocument/didOpen", + { + "textDocument": { + "uri": URI, + "languageId": "sloka", + "version": 1, + "text": text, + } + }, + request=False, + ) + diag = client.wait_for(_publish) + # the library file is syntactically clean + parse_errors = [d for d in diag["params"]["diagnostics"] if d["code"] == "parse"] + assert parse_errors == [] + + # break a gloss and expect a parse error at the edit point + broken = text.replace("vakra[curved]", "vakra[curved", 1) + client.send( + "textDocument/didChange", + { + "textDocument": {"uri": URI, "version": 2}, + "contentChanges": [{"text": broken}], + }, + request=False, + ) + diag = client.wait_for( + lambda m: _publish(m) + and any(d["code"] == "parse" for d in m["params"]["diagnostics"]), + timeout=30, + ) + (err,) = [d for d in diag["params"]["diagnostics"] if d["code"] == "parse"] + assert err["severity"] == 1 + + # restore and hover the first word of the compound + client.send( + "textDocument/didChange", + { + "textDocument": {"uri": URI, "version": 3}, + "contentChanges": [{"text": text}], + }, + request=False, + ) + client.wait_for( + lambda m: _publish(m) + and not any(d["code"] == "parse" for d in m["params"]["diagnostics"]), + timeout=30, + ) + + line, character = 3, 1 + rid = client.send( + "textDocument/hover", + { + "textDocument": {"uri": URI}, + "position": {"line": line, "character": character}, + }, + ) + resp = client.wait_for(lambda m: m.get("id") == rid, timeout=60) + value = resp["result"]["contents"]["value"] + assert "vakra" in value + + +def test_completion_after_arrow(client): + text = SLOKA.read_text() + # append "deva->" to the token line so the file still parses around it + lines = text.splitlines(keepends=True) + lines[3] = lines[3].rstrip("\n") + " deva->\n" + edited = "".join(lines) + client.send( + "textDocument/didChange", + { + "textDocument": {"uri": URI, "version": 10}, + "contentChanges": [{"text": edited}], + }, + request=False, + ) + character = len(lines[3].rstrip("\n")) + rid = client.send( + "textDocument/completion", + { + "textDocument": {"uri": URI}, + "position": {"line": 3, "character": character}, + }, + ) + resp = client.wait_for(lambda m: m.get("id") == rid, timeout=60) + labels = {item["label"] for item in resp["result"]["items"]} + assert "devAya" in labels and "devaH" in labels + + # => must not offer inflection completion + rid = client.send( + "textDocument/completion", + { + "textDocument": {"uri": URI}, + # line 8 (0-based 7) ends "...=samapraBA=>samapraBa ." + "position": {"line": 7, "character": 43}, + }, + ) + resp = client.wait_for(lambda m: m.get("id") == rid, timeout=60) + assert resp["result"] is None + + +def test_code_action_inserts_rewrite(client): + # a bare inflected form (not a headword) provokes the rewrite suggestion + text = SLOKA.read_text() + lines = text.splitlines(keepends=True) + lines[3] = lines[3].rstrip("\n") + " devAya[to the god]\n" + client.send( + "textDocument/didChange", + { + "textDocument": {"uri": URI, "version": 20}, + "contentChanges": [{"text": "".join(lines)}], + }, + request=False, + ) + client.send( + "textDocument/didSave", + {"textDocument": {"uri": URI}}, + request=False, + ) + # wait for a semantic publish containing a rewrite suggestion + diag_msg = client.wait_for( + lambda m: _publish(m) + and any( + (d.get("data") or {}).get("stems") + for d in m["params"]["diagnostics"] + ), + timeout=120, + ) + suggestion = next( + d + for d in diag_msg["params"]["diagnostics"] + if (d.get("data") or {}).get("stems") + ) + + rid = client.send( + "textDocument/codeAction", + { + "textDocument": {"uri": URI}, + "range": suggestion["range"], + "context": {"diagnostics": [suggestion]}, + }, + ) + resp = client.wait_for(lambda m: m.get("id") == rid, timeout=60) + actions = resp["result"] + assert actions + action = actions[0] + assert action["kind"] == "quickfix" + assert action["title"].startswith("rewrite as ") + + (edit,) = action["edit"]["changes"][URI] + stem = suggestion["data"]["stems"][0] + assert edit["newText"] == f"{stem}->" + # zero-width insertion at the token start + assert edit["range"]["start"] == edit["range"]["end"] + assert edit["range"]["start"] == suggestion["range"]["start"] + + +def test_code_action_parenthesizes_inside_compounds(client): + # the same inflected form inside an equation needs a wrapped rewrite + text = SLOKA.read_text() + lines = text.splitlines(keepends=True) + lines[3] = lines[3].rstrip("\n") + " mahA[large]+devAya=mahAdevAya\n" + client.send( + "textDocument/didChange", + { + "textDocument": {"uri": URI, "version": 30}, + "contentChanges": [{"text": "".join(lines)}], + }, + request=False, + ) + client.send( + "textDocument/didSave", {"textDocument": {"uri": URI}}, request=False + ) + diag_msg = client.wait_for( + lambda m: _publish(m) + and any( + (d.get("data") or {}).get("in_compound") + for d in m["params"]["diagnostics"] + ), + timeout=120, + ) + suggestion = next( + d + for d in diag_msg["params"]["diagnostics"] + if (d.get("data") or {}).get("in_compound") + ) + + rid = client.send( + "textDocument/codeAction", + { + "textDocument": {"uri": URI}, + "range": suggestion["range"], + "context": {"diagnostics": [suggestion]}, + }, + ) + resp = client.wait_for(lambda m: m.get("id") == rid, timeout=60) + action = resp["result"][0] + stem = suggestion["data"]["stems"][0] + assert action["title"].startswith(f"rewrite as ({stem}->") + + open_edit, close_edit = action["edit"]["changes"][URI] + assert open_edit["newText"] == f"({stem}->" + assert open_edit["range"]["start"] == suggestion["range"]["start"] + assert close_edit["newText"] == ")" + assert close_edit["range"]["start"] == suggestion["range"]["end"] diff --git a/tests/test_semantics_cache.py b/tests/test_semantics_cache.py new file mode 100644 index 0000000..890bd60 --- /dev/null +++ b/tests/test_semantics_cache.py @@ -0,0 +1,86 @@ +import pathlib + +import pytest + +from nirukta.lsp import analysis + +LIBRARY = pathlib.Path(__file__).parent.parent / "library" + +SOURCE = """=== test 1.1 === + +--- line --- +deva[god] namaH . +"salutations to the god" + +--- line --- +rAma[Rama] gacCati[goes] . +"Rama goes" +""" + + +@pytest.fixture(autouse=True) +def isolated_cache(tmp_path, monkeypatch): + monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) + monkeypatch.setattr(analysis, "_utterance_cache", type(analysis._utterance_cache)()) + monkeypatch.setattr(analysis, "_disk_loaded", False) + monkeypatch.setattr(analysis, "_disk_dirty", False) + + +def _counting_validator(monkeypatch): + calls = {"n": 0} + real = analysis._validate_utterance + + def counted(utterance): + calls["n"] += 1 + return real(utterance) + + monkeypatch.setattr(analysis, "_validate_utterance", counted) + return calls + + +def test_unchanged_utterances_are_not_revalidated(monkeypatch): + calls = _counting_validator(monkeypatch) + + a = analysis.analyze_syntax("test.sloka", SOURCE) + analysis.analyze_semantics(a) + first_run = calls["n"] + assert first_run == 2 + + analysis.analyze_semantics(a) + assert calls["n"] == first_run + + +def test_only_edited_utterance_revalidates(monkeypatch): + calls = _counting_validator(monkeypatch) + + a = analysis.analyze_syntax("test.sloka", SOURCE) + d1 = analysis.analyze_semantics(a) + + edited = SOURCE.replace("rAma[Rama]", "rAmaz[Rama]") + a2 = analysis.analyze_syntax("test.sloka", edited) + calls["n"] = 0 + analysis.analyze_semantics(a2) + assert calls["n"] == 1 + + # unchanged utterance's cached diagnostics keep their document positions + d3 = analysis.analyze_semantics(a2) + for d in d3: + if d.span is not None: + assert 0 <= d.span.start < len(edited) + + +def test_cache_survives_disk_round_trip(monkeypatch): + calls = _counting_validator(monkeypatch) + + a = analysis.analyze_syntax("test.sloka", SOURCE) + d1 = analysis.analyze_semantics(a) + analysis.save_disk_cache() + + # simulate a fresh server process + monkeypatch.setattr(analysis, "_utterance_cache", type(analysis._utterance_cache)()) + monkeypatch.setattr(analysis, "_disk_loaded", False) + calls["n"] = 0 + + d2 = analysis.analyze_semantics(a) + assert calls["n"] == 0 + assert [(d.message, d.span) for d in d1] == [(d.message, d.span) for d in d2] diff --git a/tests/test_spans.py b/tests/test_spans.py new file mode 100644 index 0000000..e594eea --- /dev/null +++ b/tests/test_spans.py @@ -0,0 +1,59 @@ +import pathlib + +from nirukta.models.tokens import CompoundToken, SimpleToken, SoundChangeToken +from nirukta.parsing.visitors.sloka import SlokaVisitor + +LIBRARY = pathlib.Path(__file__).parent.parent / "library" + + +def _leaves(tokens): + for token in tokens: + yield token + if isinstance(token, CompoundToken): + yield from _leaves(token.parts) + elif isinstance(token, SoundChangeToken): + yield from _leaves([token.part]) + + +def test_all_tokens_have_spans(): + visitor = SlokaVisitor(str(LIBRARY / "vakratuRqa_mahAkAya.sloka"), validate=False) + sloka_file = visitor.parse() + + count = 0 + for line in sloka_file.sloka.lines: + assert line.span is not None + for utterance in line.vAkyAni: + assert utterance.span is not None + for token in _leaves(utterance.tokens): + assert token.span is not None + count += 1 + assert count > 0 + + +def test_simple_token_span_covers_source_text(): + visitor = SlokaVisitor(str(LIBRARY / "vakratuRqa_mahAkAya.sloka"), validate=False) + sloka_file = visitor.parse() + source = visitor.source + + for line in sloka_file.sloka.lines: + for utterance in line.vAkyAni: + for token in _leaves(utterance.tokens): + if not isinstance(token, SimpleToken): + continue + covered = source[token.span.start : token.span.end] + # a simple token's span starts at its slp1 text and + # extends over any trailing glosses + assert covered.startswith(token.slp1) + + +def test_parse_from_source_string(): + source = """=== test 1.1 === + +--- line --- +deva[god] namaH . +"salutations to the god" +""" + visitor = SlokaVisitor("unsaved.sloka", validate=False, source=source) + sloka_file = visitor.parse() + token = sloka_file.sloka.lines[0].vAkyAni[0].tokens[0] + assert source[token.span.start : token.span.end] == "deva[god]" diff --git a/tests/test_validators.py b/tests/test_validators.py new file mode 100644 index 0000000..6b3bd75 --- /dev/null +++ b/tests/test_validators.py @@ -0,0 +1,111 @@ +from nirukta.lsp.analysis import analyze_syntax +from nirukta.models.enums import SoundChange +from nirukta.models.tokens import SimpleToken +from nirukta.parsing.diagnostics import CollectingSink, Severity +from nirukta.parsing.visitors.sloka import ( + _gloss_in_definitions, + validate_equation, + validate_sloka, +) + + +class FakeEntry: + def __init__(self, meaning): + self.meaning = meaning + + +def _equation(parts, merged): + sink = CollectingSink() + validate_equation( + [SimpleToken(p) for p in parts], + merged, + SoundChange.INTERNAL_SANDHI, + sink=sink, + ) + return sink.diagnostics + + +def test_valid_sandhi_passes(): + (diag,) = _equation(["rAma", "eva"], "rAmEva") + assert diag.severity == Severity.INFO + assert "validated" in diag.message + + +def test_invalid_sandhi_is_an_error(): + # regression: the comparison used to be vacuous and always passed + (diag,) = _equation(["rAma", "eva"], "rAmaeva") + assert diag.severity == Severity.ERROR + assert "invalid sandhi" in diag.message + + +def test_sandhi_is_deterministic(): + runs = {tuple(d.message for d in _equation(["rAma", "eva"], "rAmaeva"))} + for _ in range(3): + runs.add(tuple(d.message for d in _equation(["rAma", "eva"], "rAmaeva"))) + assert len(runs) == 1 + + +def test_gloss_matching_exact(): + entries = [FakeEntry("great, big, mighty")] + assert _gloss_in_definitions("great", entries) + assert not _gloss_in_definitions("small", entries) + + +def test_gloss_matching_normalizes_plurals_and_articles(): + entries = [FakeEntry("the sun or its deity; a body of water")] + assert _gloss_in_definitions("suns", entries) + assert _gloss_in_definitions("The Sun", entries) + assert _gloss_in_definitions("bodies", entries) + + +def test_gloss_matching_respects_word_boundaries(): + entries = [FakeEntry("to sunder, split apart")] + assert not _gloss_in_definitions("sun", entries) + + +REWRITE_SOURCE = """=== test 1.1 === + +--- line --- +devAya[to the god] . +"bare inflected form" + +--- line --- +vakra[curved]+devAya=vakradevAya . +"the same form inside an equation" +""" + + +def _rewrite_suggestions(source): + analysis = analyze_syntax("test.sloka", source) + sink = CollectingSink() + validate_sloka(analysis.ast.sloka, sink=sink) + return analysis, [ + d for d in sink.diagnostics if d.data and d.data.get("stems") + ] + + +def test_rewrite_data_knows_compound_context(): + _, suggestions = _rewrite_suggestions(REWRITE_SOURCE) + contexts = { + (d.data["in_compound"], tuple(d.data["stems"])) for d in suggestions + } + assert (False, ("deva",)) in contexts + assert (True, ("deva",)) in contexts + + +def test_applied_rewrites_still_parse(): + source = REWRITE_SOURCE + _, suggestions = _rewrite_suggestions(source) + # apply right-to-left so earlier spans stay valid + for d in sorted(suggestions, key=lambda d: -d.span.start): + stem = d.data["stems"][0] + covered = source[d.span.start : d.span.end] + if d.data["in_compound"]: + replacement = f"({stem}->{covered})" + else: + replacement = f"{stem}->{covered}" + source = source[: d.span.start] + replacement + source[d.span.end :] + + reparsed = analyze_syntax("test.sloka", source) + assert reparsed.ast is not None + assert not [d for d in reparsed.syntax_diagnostics if d.code == "parse"] diff --git a/tree-sitter-nirukta/.gitignore b/tree-sitter-nirukta/.gitignore new file mode 100644 index 0000000..5843ae6 --- /dev/null +++ b/tree-sitter-nirukta/.gitignore @@ -0,0 +1,3 @@ +parser/ +node_modules/ +build/ diff --git a/tree-sitter-nirukta/README.md b/tree-sitter-nirukta/README.md new file mode 100644 index 0000000..6ea655a --- /dev/null +++ b/tree-sitter-nirukta/README.md @@ -0,0 +1,32 @@ +# tree-sitter-nirukta + +Tree-sitter grammar for `.sloka` / `.sutra` files, used for editor syntax +highlighting. It mirrors the parsimonious PEG in +`nirukta/parsing/grammars.py`; if the PEG changes, update `grammar.js` to +match. + +Unlike the PEG, tree-sitter recovers from errors, so highlighting keeps +working while a line is mid-edit. + +## Build + +Requires the `tree-sitter` CLI and a C compiler (both in the nix dev shell): + +```sh +cd tree-sitter-nirukta +tree-sitter generate +cc -shared -fPIC -O2 -I src src/parser.c -o parser/nirukta.so +``` + +`contrib/nvim/nirukta.lua` adds this directory to Neovim's runtimepath, which +picks up `parser/nirukta.so` and `queries/nirukta/highlights.scm`. + +## Check against the library + +```sh +tree-sitter parse ../library/**/*.sloka +``` + +Files that fail here should also fail the real parser; anything tree-sitter +rejects but `nirukta.lsp.analysis.analyze_syntax` accepts is a grammar drift +bug in this directory. diff --git a/tree-sitter-nirukta/grammar.js b/tree-sitter-nirukta/grammar.js new file mode 100644 index 0000000..a569312 --- /dev/null +++ b/tree-sitter-nirukta/grammar.js @@ -0,0 +1,70 @@ +// Tree-sitter grammar for nirukta .sloka and .sutra files. +// Mirrors the parsimonious PEG in nirukta/parsing/grammars.py, but stays +// deliberately flat at the top level so highlighting survives partial edits. + +module.exports = grammar({ + name: 'nirukta', + + extras: $ => [/\s/, $.comment], + + rules: { + document: $ => repeat(choice( + $.section_header, + $.line_marker, + $.file_reference, + $.translation, + $.text_token, + $.punctuation, + )), + + // === citation === and === sloka === section markers + section_header: $ => seq('===', field('title', $.header_text), '==='), + header_text: _ => /[^=]+/, + + line_marker: _ => '--- line ---', + + file_reference: _ => token(prec(1, /file:[a-zA-Z0-9._]+(\/[a-zA-Z0-9._]+)+/)), + + // the quoted English rendering of a verse line + translation: _ => token(seq('"', /(?:[^"\\]|\\.)*/, '"')), + + // a word with optional inflection and external sandhi transforms + text_token: $ => prec.right(seq( + choice($.equation, $.simple_token), + repeat($.inflection), + repeat($.external_sandhi), + )), + + // a + b = merged + equation: $ => seq( + $.compound_part, + repeat1(seq('+', $.compound_part)), + '=', + field('merged', $.word), + ), + + compound_part: $ => choice($.paren_compound, $.simple_token), + + paren_compound: $ => seq('(', $.text_token, ')'), + + // stem -> inflected form + inflection: $ => seq('->', $.simple_token), + + // surface form => sandhi form + external_sandhi: $ => seq('=>', field('result', $.word)), + + simple_token: $ => prec.right(seq($.word, repeat($.gloss))), + + gloss: $ => choice($.translation_gloss, $.etymology_gloss), + translation_gloss: _ => token(seq('[', /[^\]]+/, ']')), + etymology_gloss: _ => token(seq('{', /[^}]+/, '}')), + + // SLP1 text: anything that is not a format metacharacter or whitespace + word: _ => /[^\[\]>{}.;=+()"\s-]+/, + + // danda, double danda with optional verse number, and separators + punctuation: _ => /\.+(\s*[0-9]+\s*[.,;]*)?|[;,]/, + + comment: _ => /\/\*[^*]*\*+([^/*][^*]*\*+)*\//, + }, +}); diff --git a/tree-sitter-nirukta/queries/nirukta/highlights.scm b/tree-sitter-nirukta/queries/nirukta/highlights.scm new file mode 100644 index 0000000..12127d0 --- /dev/null +++ b/tree-sitter-nirukta/queries/nirukta/highlights.scm @@ -0,0 +1,27 @@ +(section_header) @markup.heading +(line_marker) @keyword.directive +(comment) @comment + +(translation) @string +(translation_gloss) @string.special +(etymology_gloss) @attribute + +(file_reference) @string.special.url + +(equation merged: (word) @function) +(external_sandhi result: (word) @function) +(inflection (simple_token (word) @function.method)) + +(punctuation) @punctuation.delimiter + +[ + "+" + "=" + "->" + "=>" +] @operator + +[ + "(" + ")" +] @punctuation.bracket diff --git a/tree-sitter-nirukta/src/grammar.json b/tree-sitter-nirukta/src/grammar.json new file mode 100644 index 0000000..86f8b23 --- /dev/null +++ b/tree-sitter-nirukta/src/grammar.json @@ -0,0 +1,332 @@ +{ + "$schema": "https://tree-sitter.github.io/tree-sitter/assets/schemas/grammar.schema.json", + "name": "nirukta", + "rules": { + "document": { + "type": "REPEAT", + "content": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "section_header" + }, + { + "type": "SYMBOL", + "name": "line_marker" + }, + { + "type": "SYMBOL", + "name": "file_reference" + }, + { + "type": "SYMBOL", + "name": "translation" + }, + { + "type": "SYMBOL", + "name": "text_token" + }, + { + "type": "SYMBOL", + "name": "punctuation" + } + ] + } + }, + "section_header": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "===" + }, + { + "type": "FIELD", + "name": "title", + "content": { + "type": "SYMBOL", + "name": "header_text" + } + }, + { + "type": "STRING", + "value": "===" + } + ] + }, + "header_text": { + "type": "PATTERN", + "value": "[^=]+" + }, + "line_marker": { + "type": "STRING", + "value": "--- line ---" + }, + "file_reference": { + "type": "TOKEN", + "content": { + "type": "PREC", + "value": 1, + "content": { + "type": "PATTERN", + "value": "file:[a-zA-Z0-9._]+(\\/[a-zA-Z0-9._]+)+" + } + } + }, + "translation": { + "type": "TOKEN", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "\"" + }, + { + "type": "PATTERN", + "value": "(?:[^\"\\\\]|\\\\.)*" + }, + { + "type": "STRING", + "value": "\"" + } + ] + } + }, + "text_token": { + "type": "PREC_RIGHT", + "value": 0, + "content": { + "type": "SEQ", + "members": [ + { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "equation" + }, + { + "type": "SYMBOL", + "name": "simple_token" + } + ] + }, + { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "inflection" + } + }, + { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "external_sandhi" + } + } + ] + } + }, + "equation": { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "compound_part" + }, + { + "type": "REPEAT1", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "+" + }, + { + "type": "SYMBOL", + "name": "compound_part" + } + ] + } + }, + { + "type": "STRING", + "value": "=" + }, + { + "type": "FIELD", + "name": "merged", + "content": { + "type": "SYMBOL", + "name": "word" + } + } + ] + }, + "compound_part": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "paren_compound" + }, + { + "type": "SYMBOL", + "name": "simple_token" + } + ] + }, + "paren_compound": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "(" + }, + { + "type": "SYMBOL", + "name": "text_token" + }, + { + "type": "STRING", + "value": ")" + } + ] + }, + "inflection": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "->" + }, + { + "type": "SYMBOL", + "name": "simple_token" + } + ] + }, + "external_sandhi": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "=>" + }, + { + "type": "FIELD", + "name": "result", + "content": { + "type": "SYMBOL", + "name": "word" + } + } + ] + }, + "simple_token": { + "type": "PREC_RIGHT", + "value": 0, + "content": { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "word" + }, + { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "gloss" + } + } + ] + } + }, + "gloss": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "translation_gloss" + }, + { + "type": "SYMBOL", + "name": "etymology_gloss" + } + ] + }, + "translation_gloss": { + "type": "TOKEN", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "[" + }, + { + "type": "PATTERN", + "value": "[^\\]]+" + }, + { + "type": "STRING", + "value": "]" + } + ] + } + }, + "etymology_gloss": { + "type": "TOKEN", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "{" + }, + { + "type": "PATTERN", + "value": "[^}]+" + }, + { + "type": "STRING", + "value": "}" + } + ] + } + }, + "word": { + "type": "PATTERN", + "value": "[^\\[\\]>{}.;=+()\"\\s-]+" + }, + "punctuation": { + "type": "PATTERN", + "value": "\\.+(\\s*[0-9]+\\s*[.,;]*)?|[;,]" + }, + "comment": { + "type": "PATTERN", + "value": "\\/\\*[^*]*\\*+([^/*][^*]*\\*+)*\\/" + } + }, + "extras": [ + { + "type": "PATTERN", + "value": "\\s" + }, + { + "type": "SYMBOL", + "name": "comment" + } + ], + "conflicts": [], + "precedences": [], + "externals": [], + "inline": [], + "supertypes": [], + "reserved": {} +} \ No newline at end of file diff --git a/tree-sitter-nirukta/src/node-types.json b/tree-sitter-nirukta/src/node-types.json new file mode 100644 index 0000000..9df1881 --- /dev/null +++ b/tree-sitter-nirukta/src/node-types.json @@ -0,0 +1,275 @@ +[ + { + "type": "compound_part", + "named": true, + "fields": {}, + "children": { + "multiple": false, + "required": true, + "types": [ + { + "type": "paren_compound", + "named": true + }, + { + "type": "simple_token", + "named": true + } + ] + } + }, + { + "type": "document", + "named": true, + "root": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "file_reference", + "named": true + }, + { + "type": "line_marker", + "named": true + }, + { + "type": "punctuation", + "named": true + }, + { + "type": "section_header", + "named": true + }, + { + "type": "text_token", + "named": true + }, + { + "type": "translation", + "named": true + } + ] + } + }, + { + "type": "equation", + "named": true, + "fields": { + "merged": { + "multiple": false, + "required": true, + "types": [ + { + "type": "word", + "named": true + } + ] + } + }, + "children": { + "multiple": true, + "required": true, + "types": [ + { + "type": "compound_part", + "named": true + } + ] + } + }, + { + "type": "external_sandhi", + "named": true, + "fields": { + "result": { + "multiple": false, + "required": true, + "types": [ + { + "type": "word", + "named": true + } + ] + } + } + }, + { + "type": "gloss", + "named": true, + "fields": {}, + "children": { + "multiple": false, + "required": true, + "types": [ + { + "type": "etymology_gloss", + "named": true + }, + { + "type": "translation_gloss", + "named": true + } + ] + } + }, + { + "type": "inflection", + "named": true, + "fields": {}, + "children": { + "multiple": false, + "required": true, + "types": [ + { + "type": "simple_token", + "named": true + } + ] + } + }, + { + "type": "paren_compound", + "named": true, + "fields": {}, + "children": { + "multiple": false, + "required": true, + "types": [ + { + "type": "text_token", + "named": true + } + ] + } + }, + { + "type": "section_header", + "named": true, + "fields": { + "title": { + "multiple": false, + "required": true, + "types": [ + { + "type": "header_text", + "named": true + } + ] + } + } + }, + { + "type": "simple_token", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": true, + "types": [ + { + "type": "gloss", + "named": true + }, + { + "type": "word", + "named": true + } + ] + } + }, + { + "type": "text_token", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": true, + "types": [ + { + "type": "equation", + "named": true + }, + { + "type": "external_sandhi", + "named": true + }, + { + "type": "inflection", + "named": true + }, + { + "type": "simple_token", + "named": true + } + ] + } + }, + { + "type": "(", + "named": false + }, + { + "type": ")", + "named": false + }, + { + "type": "+", + "named": false + }, + { + "type": "->", + "named": false + }, + { + "type": "=", + "named": false + }, + { + "type": "===", + "named": false + }, + { + "type": "=>", + "named": false + }, + { + "type": "comment", + "named": true, + "extra": true + }, + { + "type": "etymology_gloss", + "named": true + }, + { + "type": "file_reference", + "named": true + }, + { + "type": "header_text", + "named": true + }, + { + "type": "line_marker", + "named": true + }, + { + "type": "punctuation", + "named": true + }, + { + "type": "translation", + "named": true + }, + { + "type": "translation_gloss", + "named": true + }, + { + "type": "word", + "named": true + } +] \ No newline at end of file diff --git a/tree-sitter-nirukta/src/parser.c b/tree-sitter-nirukta/src/parser.c new file mode 100644 index 0000000..a6bf155 --- /dev/null +++ b/tree-sitter-nirukta/src/parser.c @@ -0,0 +1,1713 @@ +/* Automatically @generated by tree-sitter */ + +#include "tree_sitter/parser.h" + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#endif + +#define LANGUAGE_VERSION 15 +#define STATE_COUNT 53 +#define LARGE_STATE_COUNT 2 +#define SYMBOL_COUNT 32 +#define ALIAS_COUNT 0 +#define TOKEN_COUNT 17 +#define EXTERNAL_TOKEN_COUNT 0 +#define FIELD_COUNT 3 +#define MAX_ALIAS_SEQUENCE_LENGTH 4 +#define MAX_RESERVED_WORD_SET_SIZE 0 +#define PRODUCTION_ID_COUNT 4 +#define SUPERTYPE_COUNT 0 + +enum ts_symbol_identifiers { + anon_sym_EQ_EQ_EQ = 1, + sym_header_text = 2, + sym_line_marker = 3, + sym_file_reference = 4, + sym_translation = 5, + anon_sym_PLUS = 6, + anon_sym_EQ = 7, + anon_sym_LPAREN = 8, + anon_sym_RPAREN = 9, + anon_sym_DASH_GT = 10, + anon_sym_EQ_GT = 11, + sym_translation_gloss = 12, + sym_etymology_gloss = 13, + sym_word = 14, + sym_punctuation = 15, + sym_comment = 16, + sym_document = 17, + sym_section_header = 18, + sym_text_token = 19, + sym_equation = 20, + sym_compound_part = 21, + sym_paren_compound = 22, + sym_inflection = 23, + sym_external_sandhi = 24, + sym_simple_token = 25, + sym_gloss = 26, + aux_sym_document_repeat1 = 27, + aux_sym_text_token_repeat1 = 28, + aux_sym_text_token_repeat2 = 29, + aux_sym_equation_repeat1 = 30, + aux_sym_simple_token_repeat1 = 31, +}; + +static const char * const ts_symbol_names[] = { + [ts_builtin_sym_end] = "end", + [anon_sym_EQ_EQ_EQ] = "===", + [sym_header_text] = "header_text", + [sym_line_marker] = "line_marker", + [sym_file_reference] = "file_reference", + [sym_translation] = "translation", + [anon_sym_PLUS] = "+", + [anon_sym_EQ] = "=", + [anon_sym_LPAREN] = "(", + [anon_sym_RPAREN] = ")", + [anon_sym_DASH_GT] = "->", + [anon_sym_EQ_GT] = "=>", + [sym_translation_gloss] = "translation_gloss", + [sym_etymology_gloss] = "etymology_gloss", + [sym_word] = "word", + [sym_punctuation] = "punctuation", + [sym_comment] = "comment", + [sym_document] = "document", + [sym_section_header] = "section_header", + [sym_text_token] = "text_token", + [sym_equation] = "equation", + [sym_compound_part] = "compound_part", + [sym_paren_compound] = "paren_compound", + [sym_inflection] = "inflection", + [sym_external_sandhi] = "external_sandhi", + [sym_simple_token] = "simple_token", + [sym_gloss] = "gloss", + [aux_sym_document_repeat1] = "document_repeat1", + [aux_sym_text_token_repeat1] = "text_token_repeat1", + [aux_sym_text_token_repeat2] = "text_token_repeat2", + [aux_sym_equation_repeat1] = "equation_repeat1", + [aux_sym_simple_token_repeat1] = "simple_token_repeat1", +}; + +static const TSSymbol ts_symbol_map[] = { + [ts_builtin_sym_end] = ts_builtin_sym_end, + [anon_sym_EQ_EQ_EQ] = anon_sym_EQ_EQ_EQ, + [sym_header_text] = sym_header_text, + [sym_line_marker] = sym_line_marker, + [sym_file_reference] = sym_file_reference, + [sym_translation] = sym_translation, + [anon_sym_PLUS] = anon_sym_PLUS, + [anon_sym_EQ] = anon_sym_EQ, + [anon_sym_LPAREN] = anon_sym_LPAREN, + [anon_sym_RPAREN] = anon_sym_RPAREN, + [anon_sym_DASH_GT] = anon_sym_DASH_GT, + [anon_sym_EQ_GT] = anon_sym_EQ_GT, + [sym_translation_gloss] = sym_translation_gloss, + [sym_etymology_gloss] = sym_etymology_gloss, + [sym_word] = sym_word, + [sym_punctuation] = sym_punctuation, + [sym_comment] = sym_comment, + [sym_document] = sym_document, + [sym_section_header] = sym_section_header, + [sym_text_token] = sym_text_token, + [sym_equation] = sym_equation, + [sym_compound_part] = sym_compound_part, + [sym_paren_compound] = sym_paren_compound, + [sym_inflection] = sym_inflection, + [sym_external_sandhi] = sym_external_sandhi, + [sym_simple_token] = sym_simple_token, + [sym_gloss] = sym_gloss, + [aux_sym_document_repeat1] = aux_sym_document_repeat1, + [aux_sym_text_token_repeat1] = aux_sym_text_token_repeat1, + [aux_sym_text_token_repeat2] = aux_sym_text_token_repeat2, + [aux_sym_equation_repeat1] = aux_sym_equation_repeat1, + [aux_sym_simple_token_repeat1] = aux_sym_simple_token_repeat1, +}; + +static const TSSymbolMetadata ts_symbol_metadata[] = { + [ts_builtin_sym_end] = { + .visible = false, + .named = true, + }, + [anon_sym_EQ_EQ_EQ] = { + .visible = true, + .named = false, + }, + [sym_header_text] = { + .visible = true, + .named = true, + }, + [sym_line_marker] = { + .visible = true, + .named = true, + }, + [sym_file_reference] = { + .visible = true, + .named = true, + }, + [sym_translation] = { + .visible = true, + .named = true, + }, + [anon_sym_PLUS] = { + .visible = true, + .named = false, + }, + [anon_sym_EQ] = { + .visible = true, + .named = false, + }, + [anon_sym_LPAREN] = { + .visible = true, + .named = false, + }, + [anon_sym_RPAREN] = { + .visible = true, + .named = false, + }, + [anon_sym_DASH_GT] = { + .visible = true, + .named = false, + }, + [anon_sym_EQ_GT] = { + .visible = true, + .named = false, + }, + [sym_translation_gloss] = { + .visible = true, + .named = true, + }, + [sym_etymology_gloss] = { + .visible = true, + .named = true, + }, + [sym_word] = { + .visible = true, + .named = true, + }, + [sym_punctuation] = { + .visible = true, + .named = true, + }, + [sym_comment] = { + .visible = true, + .named = true, + }, + [sym_document] = { + .visible = true, + .named = true, + }, + [sym_section_header] = { + .visible = true, + .named = true, + }, + [sym_text_token] = { + .visible = true, + .named = true, + }, + [sym_equation] = { + .visible = true, + .named = true, + }, + [sym_compound_part] = { + .visible = true, + .named = true, + }, + [sym_paren_compound] = { + .visible = true, + .named = true, + }, + [sym_inflection] = { + .visible = true, + .named = true, + }, + [sym_external_sandhi] = { + .visible = true, + .named = true, + }, + [sym_simple_token] = { + .visible = true, + .named = true, + }, + [sym_gloss] = { + .visible = true, + .named = true, + }, + [aux_sym_document_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_text_token_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_text_token_repeat2] = { + .visible = false, + .named = false, + }, + [aux_sym_equation_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_simple_token_repeat1] = { + .visible = false, + .named = false, + }, +}; + +enum ts_field_identifiers { + field_merged = 1, + field_result = 2, + field_title = 3, +}; + +static const char * const ts_field_names[] = { + [0] = NULL, + [field_merged] = "merged", + [field_result] = "result", + [field_title] = "title", +}; + +static const TSMapSlice ts_field_map_slices[PRODUCTION_ID_COUNT] = { + [1] = {.index = 0, .length = 1}, + [2] = {.index = 1, .length = 1}, + [3] = {.index = 2, .length = 1}, +}; + +static const TSFieldMapEntry ts_field_map_entries[] = { + [0] = + {field_title, 1}, + [1] = + {field_result, 1}, + [2] = + {field_merged, 3}, +}; + +static const TSSymbol ts_alias_sequences[PRODUCTION_ID_COUNT][MAX_ALIAS_SEQUENCE_LENGTH] = { + [0] = {0}, +}; + +static const uint16_t ts_non_terminal_alias_map[] = { + 0, +}; + +static const TSStateId ts_primary_state_ids[STATE_COUNT] = { + [0] = 0, + [1] = 1, + [2] = 2, + [3] = 3, + [4] = 4, + [5] = 5, + [6] = 6, + [7] = 7, + [8] = 8, + [9] = 9, + [10] = 10, + [11] = 11, + [12] = 12, + [13] = 13, + [14] = 14, + [15] = 15, + [16] = 16, + [17] = 17, + [18] = 3, + [19] = 6, + [20] = 2, + [21] = 21, + [22] = 5, + [23] = 23, + [24] = 8, + [25] = 10, + [26] = 9, + [27] = 27, + [28] = 11, + [29] = 14, + [30] = 13, + [31] = 12, + [32] = 32, + [33] = 33, + [34] = 16, + [35] = 15, + [36] = 32, + [37] = 37, + [38] = 38, + [39] = 39, + [40] = 40, + [41] = 17, + [42] = 42, + [43] = 37, + [44] = 38, + [45] = 45, + [46] = 46, + [47] = 47, + [48] = 48, + [49] = 49, + [50] = 50, + [51] = 45, + [52] = 47, +}; + +static const TSCharacterRange sym_word_character_set_1[] = { + {0, 0x08}, {0x0e, 0x1f}, {'!', '!'}, {'#', '\''}, {'*', '*'}, {',', ','}, {'/', ':'}, {'<', '<'}, + {'?', 'Z'}, {'\\', '\\'}, {'^', 'z'}, {'|', '|'}, {'~', 0x10ffff}, +}; + +static bool ts_lex(TSLexer *lexer, TSStateId state) { + START_LEXER(); + eof = lexer->eof(lexer); + switch (state) { + case 0: + if (eof) ADVANCE(33); + ADVANCE_MAP( + '"', 3, + '(', 48, + ')', 49, + '+', 44, + ',', 64, + '-', 10, + '.', 66, + '/', 54, + ';', 65, + '=', 46, + '[', 29, + 'f', 62, + '{', 30, + ); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') SKIP(0); + if (lookahead != 0 && + lookahead != '=' && + lookahead != '>' && + lookahead != ']' && + lookahead != '}') ADVANCE(64); + END_STATE(); + case 1: + if (lookahead == ' ') ADVANCE(23); + END_STATE(); + case 2: + if (lookahead == ' ') ADVANCE(14); + END_STATE(); + case 3: + if (lookahead == '"') ADVANCE(43); + if (lookahead == '\\') ADVANCE(28); + if (lookahead != 0) ADVANCE(3); + END_STATE(); + case 4: + if (lookahead == '(') ADVANCE(48); + if (lookahead == '/') ADVANCE(54); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') SKIP(4); + if ((!eof && set_contains(sym_word_character_set_1, 13, lookahead))) ADVANCE(64); + END_STATE(); + case 5: + if (lookahead == ')') ADVANCE(49); + if (lookahead == '+') ADVANCE(44); + if (lookahead == '-') ADVANCE(19); + if (lookahead == '/') ADVANCE(6); + if (lookahead == '=') ADVANCE(17); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') SKIP(5); + END_STATE(); + case 6: + if (lookahead == '*') ADVANCE(8); + END_STATE(); + case 7: + if (lookahead == '*') ADVANCE(7); + if (lookahead == '/') ADVANCE(70); + if (lookahead != 0) ADVANCE(8); + END_STATE(); + case 8: + if (lookahead == '*') ADVANCE(7); + if (lookahead != 0) ADVANCE(8); + END_STATE(); + case 9: + if (lookahead == '+') ADVANCE(44); + if (lookahead == '/') ADVANCE(6); + if (lookahead == '=') ADVANCE(45); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') SKIP(9); + END_STATE(); + case 10: + if (lookahead == '-') ADVANCE(11); + if (lookahead == '>') ADVANCE(50); + END_STATE(); + case 11: + if (lookahead == '-') ADVANCE(1); + END_STATE(); + case 12: + if (lookahead == '-') ADVANCE(40); + END_STATE(); + case 13: + if (lookahead == '-') ADVANCE(12); + END_STATE(); + case 14: + if (lookahead == '-') ADVANCE(13); + END_STATE(); + case 15: + if (lookahead == '/') ADVANCE(27); + if (('.' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(15); + END_STATE(); + case 16: + if (lookahead == '/') ADVANCE(35); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') ADVANCE(38); + if (lookahead != 0 && + lookahead != '=') ADVANCE(39); + END_STATE(); + case 17: + if (lookahead == '=') ADVANCE(18); + if (lookahead == '>') ADVANCE(51); + END_STATE(); + case 18: + if (lookahead == '=') ADVANCE(34); + END_STATE(); + case 19: + if (lookahead == '>') ADVANCE(50); + END_STATE(); + case 20: + if (lookahead == ']') ADVANCE(52); + if (lookahead != 0) ADVANCE(20); + END_STATE(); + case 21: + if (lookahead == 'e') ADVANCE(2); + END_STATE(); + case 22: + if (lookahead == 'i') ADVANCE(24); + END_STATE(); + case 23: + if (lookahead == 'l') ADVANCE(22); + END_STATE(); + case 24: + if (lookahead == 'n') ADVANCE(21); + END_STATE(); + case 25: + if (lookahead == '}') ADVANCE(53); + if (lookahead != 0) ADVANCE(25); + END_STATE(); + case 26: + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') ADVANCE(26); + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(69); + END_STATE(); + case 27: + if (lookahead == '.' || + ('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(42); + END_STATE(); + case 28: + if (lookahead != 0 && + lookahead != '\n') ADVANCE(3); + END_STATE(); + case 29: + if (lookahead != 0 && + lookahead != ']') ADVANCE(20); + END_STATE(); + case 30: + if (lookahead != 0 && + lookahead != '}') ADVANCE(25); + END_STATE(); + case 31: + if (eof) ADVANCE(33); + ADVANCE_MAP( + '"', 3, + '(', 48, + '+', 44, + ',', 64, + '-', 10, + '.', 66, + '/', 54, + ';', 65, + '=', 17, + '[', 29, + 'f', 62, + '{', 30, + ); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') SKIP(31); + if (lookahead != 0 && + lookahead != '(' && + lookahead != ')' && + lookahead != '=' && + lookahead != '>' && + lookahead != ']' && + lookahead != '}') ADVANCE(64); + END_STATE(); + case 32: + if (eof) ADVANCE(33); + if (lookahead == ')') ADVANCE(49); + if (lookahead == '+') ADVANCE(44); + if (lookahead == '-') ADVANCE(19); + if (lookahead == '/') ADVANCE(6); + if (lookahead == '=') ADVANCE(47); + if (lookahead == '[') ADVANCE(29); + if (lookahead == '{') ADVANCE(30); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') SKIP(32); + END_STATE(); + case 33: + ACCEPT_TOKEN(ts_builtin_sym_end); + END_STATE(); + case 34: + ACCEPT_TOKEN(anon_sym_EQ_EQ_EQ); + END_STATE(); + case 35: + ACCEPT_TOKEN(sym_header_text); + if (lookahead == '*') ADVANCE(37); + if (lookahead != 0 && + lookahead != '=') ADVANCE(39); + END_STATE(); + case 36: + ACCEPT_TOKEN(sym_header_text); + if (lookahead == '*') ADVANCE(36); + if (lookahead == '/') ADVANCE(39); + if (lookahead == '=') ADVANCE(8); + if (lookahead != 0) ADVANCE(37); + END_STATE(); + case 37: + ACCEPT_TOKEN(sym_header_text); + if (lookahead == '*') ADVANCE(36); + if (lookahead == '=') ADVANCE(8); + if (lookahead != 0) ADVANCE(37); + END_STATE(); + case 38: + ACCEPT_TOKEN(sym_header_text); + if (lookahead == '/') ADVANCE(35); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') ADVANCE(38); + if (lookahead != 0 && + lookahead != '=') ADVANCE(39); + END_STATE(); + case 39: + ACCEPT_TOKEN(sym_header_text); + if (lookahead != 0 && + lookahead != '=') ADVANCE(39); + END_STATE(); + case 40: + ACCEPT_TOKEN(sym_line_marker); + END_STATE(); + case 41: + ACCEPT_TOKEN(sym_file_reference); + if (lookahead == '.') ADVANCE(42); + if (lookahead == '/') ADVANCE(59); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(41); + END_STATE(); + case 42: + ACCEPT_TOKEN(sym_file_reference); + if (lookahead == '/') ADVANCE(27); + if (('.' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(42); + END_STATE(); + case 43: + ACCEPT_TOKEN(sym_translation); + END_STATE(); + case 44: + ACCEPT_TOKEN(anon_sym_PLUS); + END_STATE(); + case 45: + ACCEPT_TOKEN(anon_sym_EQ); + END_STATE(); + case 46: + ACCEPT_TOKEN(anon_sym_EQ); + if (lookahead == '=') ADVANCE(18); + if (lookahead == '>') ADVANCE(51); + END_STATE(); + case 47: + ACCEPT_TOKEN(anon_sym_EQ); + if (lookahead == '>') ADVANCE(51); + END_STATE(); + case 48: + ACCEPT_TOKEN(anon_sym_LPAREN); + END_STATE(); + case 49: + ACCEPT_TOKEN(anon_sym_RPAREN); + END_STATE(); + case 50: + ACCEPT_TOKEN(anon_sym_DASH_GT); + END_STATE(); + case 51: + ACCEPT_TOKEN(anon_sym_EQ_GT); + END_STATE(); + case 52: + ACCEPT_TOKEN(sym_translation_gloss); + END_STATE(); + case 53: + ACCEPT_TOKEN(sym_etymology_gloss); + END_STATE(); + case 54: + ACCEPT_TOKEN(sym_word); + if (lookahead == '*') ADVANCE(56); + if ((!eof && set_contains(sym_word_character_set_1, 13, lookahead))) ADVANCE(64); + END_STATE(); + case 55: + ACCEPT_TOKEN(sym_word); + if (lookahead == '*') ADVANCE(55); + if (lookahead == '/') ADVANCE(64); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ' || + lookahead == '"' || + ('(' <= lookahead && lookahead <= '+') || + lookahead == '-' || + lookahead == '.' || + lookahead == ';' || + lookahead == '=' || + lookahead == '>' || + lookahead == '[' || + lookahead == ']' || + lookahead == '{' || + lookahead == '}') ADVANCE(8); + if (lookahead != 0) ADVANCE(56); + END_STATE(); + case 56: + ACCEPT_TOKEN(sym_word); + if (lookahead == '*') ADVANCE(55); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ' || + lookahead == '"' || + ('(' <= lookahead && lookahead <= '+') || + lookahead == '-' || + lookahead == '.' || + lookahead == ';' || + lookahead == '=' || + lookahead == '>' || + lookahead == '[' || + lookahead == ']' || + lookahead == '{' || + lookahead == '}') ADVANCE(8); + if (lookahead != 0) ADVANCE(56); + END_STATE(); + case 57: + ACCEPT_TOKEN(sym_word); + if (lookahead == '.') ADVANCE(15); + if (lookahead == '/') ADVANCE(59); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(57); + if ((!eof && set_contains(sym_word_character_set_1, 13, lookahead))) ADVANCE(64); + END_STATE(); + case 58: + ACCEPT_TOKEN(sym_word); + if (lookahead == '.') ADVANCE(15); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(57); + if ((!eof && set_contains(sym_word_character_set_1, 13, lookahead))) ADVANCE(64); + END_STATE(); + case 59: + ACCEPT_TOKEN(sym_word); + if (lookahead == '.') ADVANCE(42); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(41); + if ((!eof && set_contains(sym_word_character_set_1, 13, lookahead))) ADVANCE(64); + END_STATE(); + case 60: + ACCEPT_TOKEN(sym_word); + if (lookahead == ':') ADVANCE(58); + if ((!eof && set_contains(sym_word_character_set_1, 13, lookahead))) ADVANCE(64); + END_STATE(); + case 61: + ACCEPT_TOKEN(sym_word); + if (lookahead == 'e') ADVANCE(60); + if ((!eof && set_contains(sym_word_character_set_1, 13, lookahead))) ADVANCE(64); + END_STATE(); + case 62: + ACCEPT_TOKEN(sym_word); + if (lookahead == 'i') ADVANCE(63); + if ((!eof && set_contains(sym_word_character_set_1, 13, lookahead))) ADVANCE(64); + END_STATE(); + case 63: + ACCEPT_TOKEN(sym_word); + if (lookahead == 'l') ADVANCE(61); + if ((!eof && set_contains(sym_word_character_set_1, 13, lookahead))) ADVANCE(64); + END_STATE(); + case 64: + ACCEPT_TOKEN(sym_word); + if ((!eof && set_contains(sym_word_character_set_1, 13, lookahead))) ADVANCE(64); + END_STATE(); + case 65: + ACCEPT_TOKEN(sym_punctuation); + END_STATE(); + case 66: + ACCEPT_TOKEN(sym_punctuation); + if (lookahead == '.') ADVANCE(66); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') ADVANCE(26); + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(69); + END_STATE(); + case 67: + ACCEPT_TOKEN(sym_punctuation); + if (lookahead == ',' || + lookahead == '.' || + lookahead == ';') ADVANCE(67); + END_STATE(); + case 68: + ACCEPT_TOKEN(sym_punctuation); + if (lookahead == ',' || + lookahead == '.' || + lookahead == ';') ADVANCE(67); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') ADVANCE(68); + END_STATE(); + case 69: + ACCEPT_TOKEN(sym_punctuation); + if (lookahead == ',' || + lookahead == '.' || + lookahead == ';') ADVANCE(67); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ') ADVANCE(68); + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(69); + END_STATE(); + case 70: + ACCEPT_TOKEN(sym_comment); + END_STATE(); + default: + return false; + } +} + +static const TSLexerMode ts_lex_modes[STATE_COUNT] = { + [0] = {.lex_state = 0}, + [1] = {.lex_state = 31}, + [2] = {.lex_state = 31}, + [3] = {.lex_state = 31}, + [4] = {.lex_state = 31}, + [5] = {.lex_state = 31}, + [6] = {.lex_state = 31}, + [7] = {.lex_state = 31}, + [8] = {.lex_state = 31}, + [9] = {.lex_state = 31}, + [10] = {.lex_state = 31}, + [11] = {.lex_state = 31}, + [12] = {.lex_state = 31}, + [13] = {.lex_state = 31}, + [14] = {.lex_state = 31}, + [15] = {.lex_state = 31}, + [16] = {.lex_state = 31}, + [17] = {.lex_state = 31}, + [18] = {.lex_state = 32}, + [19] = {.lex_state = 32}, + [20] = {.lex_state = 32}, + [21] = {.lex_state = 31}, + [22] = {.lex_state = 5}, + [23] = {.lex_state = 4}, + [24] = {.lex_state = 5}, + [25] = {.lex_state = 32}, + [26] = {.lex_state = 5}, + [27] = {.lex_state = 4}, + [28] = {.lex_state = 5}, + [29] = {.lex_state = 5}, + [30] = {.lex_state = 5}, + [31] = {.lex_state = 5}, + [32] = {.lex_state = 9}, + [33] = {.lex_state = 9}, + [34] = {.lex_state = 5}, + [35] = {.lex_state = 5}, + [36] = {.lex_state = 9}, + [37] = {.lex_state = 4}, + [38] = {.lex_state = 32}, + [39] = {.lex_state = 9}, + [40] = {.lex_state = 9}, + [41] = {.lex_state = 5}, + [42] = {.lex_state = 9}, + [43] = {.lex_state = 4}, + [44] = {.lex_state = 32}, + [45] = {.lex_state = 4}, + [46] = {.lex_state = 16}, + [47] = {.lex_state = 4}, + [48] = {.lex_state = 32}, + [49] = {.lex_state = 5}, + [50] = {.lex_state = 32}, + [51] = {.lex_state = 4}, + [52] = {.lex_state = 4}, +}; + +static const uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = { + [STATE(0)] = { + [ts_builtin_sym_end] = ACTIONS(1), + [anon_sym_EQ_EQ_EQ] = ACTIONS(1), + [sym_line_marker] = ACTIONS(1), + [sym_file_reference] = ACTIONS(1), + [sym_translation] = ACTIONS(1), + [anon_sym_PLUS] = ACTIONS(1), + [anon_sym_EQ] = ACTIONS(1), + [anon_sym_LPAREN] = ACTIONS(1), + [anon_sym_RPAREN] = ACTIONS(1), + [anon_sym_DASH_GT] = ACTIONS(1), + [anon_sym_EQ_GT] = ACTIONS(1), + [sym_translation_gloss] = ACTIONS(1), + [sym_etymology_gloss] = ACTIONS(1), + [sym_word] = ACTIONS(1), + [sym_punctuation] = ACTIONS(1), + [sym_comment] = ACTIONS(3), + }, + [STATE(1)] = { + [sym_document] = STATE(48), + [sym_section_header] = STATE(4), + [sym_text_token] = STATE(4), + [sym_equation] = STATE(8), + [sym_compound_part] = STATE(38), + [sym_paren_compound] = STATE(40), + [sym_simple_token] = STATE(5), + [aux_sym_document_repeat1] = STATE(4), + [ts_builtin_sym_end] = ACTIONS(5), + [anon_sym_EQ_EQ_EQ] = ACTIONS(7), + [sym_line_marker] = ACTIONS(9), + [sym_file_reference] = ACTIONS(9), + [sym_translation] = ACTIONS(9), + [anon_sym_LPAREN] = ACTIONS(11), + [sym_word] = ACTIONS(13), + [sym_punctuation] = ACTIONS(15), + [sym_comment] = ACTIONS(3), + }, +}; + +static const uint16_t ts_small_parse_table[] = { + [0] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(19), 2, + sym_translation_gloss, + sym_etymology_gloss, + ACTIONS(22), 2, + sym_word, + sym_punctuation, + STATE(2), 2, + sym_gloss, + aux_sym_simple_token_repeat1, + ACTIONS(17), 9, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_PLUS, + anon_sym_LPAREN, + anon_sym_DASH_GT, + anon_sym_EQ_GT, + [27] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(26), 2, + sym_translation_gloss, + sym_etymology_gloss, + ACTIONS(28), 2, + sym_word, + sym_punctuation, + STATE(6), 2, + sym_gloss, + aux_sym_simple_token_repeat1, + ACTIONS(24), 9, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_PLUS, + anon_sym_LPAREN, + anon_sym_DASH_GT, + anon_sym_EQ_GT, + [54] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(7), 1, + anon_sym_EQ_EQ_EQ, + ACTIONS(11), 1, + anon_sym_LPAREN, + ACTIONS(13), 1, + sym_word, + ACTIONS(30), 1, + ts_builtin_sym_end, + ACTIONS(34), 1, + sym_punctuation, + STATE(5), 1, + sym_simple_token, + STATE(8), 1, + sym_equation, + STATE(38), 1, + sym_compound_part, + STATE(40), 1, + sym_paren_compound, + ACTIONS(32), 3, + sym_line_marker, + sym_file_reference, + sym_translation, + STATE(7), 3, + sym_section_header, + sym_text_token, + aux_sym_document_repeat1, + [95] = 8, + ACTIONS(3), 1, + sym_comment, + ACTIONS(38), 1, + anon_sym_PLUS, + ACTIONS(40), 1, + anon_sym_DASH_GT, + ACTIONS(42), 1, + anon_sym_EQ_GT, + ACTIONS(44), 2, + sym_word, + sym_punctuation, + STATE(9), 2, + sym_inflection, + aux_sym_text_token_repeat1, + STATE(12), 2, + sym_external_sandhi, + aux_sym_text_token_repeat2, + ACTIONS(36), 6, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_LPAREN, + [128] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(26), 2, + sym_translation_gloss, + sym_etymology_gloss, + ACTIONS(48), 2, + sym_word, + sym_punctuation, + STATE(2), 2, + sym_gloss, + aux_sym_simple_token_repeat1, + ACTIONS(46), 9, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_PLUS, + anon_sym_LPAREN, + anon_sym_DASH_GT, + anon_sym_EQ_GT, + [155] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(50), 1, + ts_builtin_sym_end, + ACTIONS(52), 1, + anon_sym_EQ_EQ_EQ, + ACTIONS(58), 1, + anon_sym_LPAREN, + ACTIONS(61), 1, + sym_word, + ACTIONS(64), 1, + sym_punctuation, + STATE(5), 1, + sym_simple_token, + STATE(8), 1, + sym_equation, + STATE(38), 1, + sym_compound_part, + STATE(40), 1, + sym_paren_compound, + ACTIONS(55), 3, + sym_line_marker, + sym_file_reference, + sym_translation, + STATE(7), 3, + sym_section_header, + sym_text_token, + aux_sym_document_repeat1, + [196] = 7, + ACTIONS(3), 1, + sym_comment, + ACTIONS(40), 1, + anon_sym_DASH_GT, + ACTIONS(42), 1, + anon_sym_EQ_GT, + ACTIONS(44), 2, + sym_word, + sym_punctuation, + STATE(9), 2, + sym_inflection, + aux_sym_text_token_repeat1, + STATE(12), 2, + sym_external_sandhi, + aux_sym_text_token_repeat2, + ACTIONS(36), 6, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_LPAREN, + [226] = 7, + ACTIONS(3), 1, + sym_comment, + ACTIONS(40), 1, + anon_sym_DASH_GT, + ACTIONS(42), 1, + anon_sym_EQ_GT, + ACTIONS(69), 2, + sym_word, + sym_punctuation, + STATE(11), 2, + sym_inflection, + aux_sym_text_token_repeat1, + STATE(14), 2, + sym_external_sandhi, + aux_sym_text_token_repeat2, + ACTIONS(67), 6, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_LPAREN, + [256] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(73), 2, + sym_word, + sym_punctuation, + ACTIONS(71), 11, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_PLUS, + anon_sym_LPAREN, + anon_sym_DASH_GT, + anon_sym_EQ_GT, + sym_translation_gloss, + sym_etymology_gloss, + [277] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(77), 1, + anon_sym_DASH_GT, + ACTIONS(80), 2, + sym_word, + sym_punctuation, + STATE(11), 2, + sym_inflection, + aux_sym_text_token_repeat1, + ACTIONS(75), 7, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_LPAREN, + anon_sym_EQ_GT, + [301] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(42), 1, + anon_sym_EQ_GT, + ACTIONS(69), 2, + sym_word, + sym_punctuation, + STATE(13), 2, + sym_external_sandhi, + aux_sym_text_token_repeat2, + ACTIONS(67), 6, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_LPAREN, + [324] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(84), 1, + anon_sym_EQ_GT, + ACTIONS(87), 2, + sym_word, + sym_punctuation, + STATE(13), 2, + sym_external_sandhi, + aux_sym_text_token_repeat2, + ACTIONS(82), 6, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_LPAREN, + [347] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(42), 1, + anon_sym_EQ_GT, + ACTIONS(91), 2, + sym_word, + sym_punctuation, + STATE(13), 2, + sym_external_sandhi, + aux_sym_text_token_repeat2, + ACTIONS(89), 6, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_LPAREN, + [370] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(95), 2, + sym_word, + sym_punctuation, + ACTIONS(93), 8, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_LPAREN, + anon_sym_DASH_GT, + anon_sym_EQ_GT, + [388] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(99), 2, + sym_word, + sym_punctuation, + ACTIONS(97), 8, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_LPAREN, + anon_sym_DASH_GT, + anon_sym_EQ_GT, + [406] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(103), 2, + sym_word, + sym_punctuation, + ACTIONS(101), 7, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_LPAREN, + anon_sym_EQ_GT, + [423] = 5, + ACTIONS(28), 1, + anon_sym_EQ, + ACTIONS(107), 1, + sym_comment, + ACTIONS(105), 2, + sym_translation_gloss, + sym_etymology_gloss, + STATE(19), 2, + sym_gloss, + aux_sym_simple_token_repeat1, + ACTIONS(24), 4, + anon_sym_PLUS, + anon_sym_RPAREN, + anon_sym_DASH_GT, + anon_sym_EQ_GT, + [444] = 5, + ACTIONS(48), 1, + anon_sym_EQ, + ACTIONS(107), 1, + sym_comment, + ACTIONS(105), 2, + sym_translation_gloss, + sym_etymology_gloss, + STATE(20), 2, + sym_gloss, + aux_sym_simple_token_repeat1, + ACTIONS(46), 4, + anon_sym_PLUS, + anon_sym_RPAREN, + anon_sym_DASH_GT, + anon_sym_EQ_GT, + [465] = 5, + ACTIONS(22), 1, + anon_sym_EQ, + ACTIONS(107), 1, + sym_comment, + ACTIONS(109), 2, + sym_translation_gloss, + sym_etymology_gloss, + STATE(20), 2, + sym_gloss, + aux_sym_simple_token_repeat1, + ACTIONS(17), 4, + anon_sym_PLUS, + anon_sym_RPAREN, + anon_sym_DASH_GT, + anon_sym_EQ_GT, + [486] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(114), 2, + sym_word, + sym_punctuation, + ACTIONS(112), 6, + ts_builtin_sym_end, + anon_sym_EQ_EQ_EQ, + sym_line_marker, + sym_file_reference, + sym_translation, + anon_sym_LPAREN, + [502] = 7, + ACTIONS(36), 1, + anon_sym_RPAREN, + ACTIONS(38), 1, + anon_sym_PLUS, + ACTIONS(107), 1, + sym_comment, + ACTIONS(116), 1, + anon_sym_DASH_GT, + ACTIONS(118), 1, + anon_sym_EQ_GT, + STATE(26), 2, + sym_inflection, + aux_sym_text_token_repeat1, + STATE(31), 2, + sym_external_sandhi, + aux_sym_text_token_repeat2, + [526] = 8, + ACTIONS(3), 1, + sym_comment, + ACTIONS(11), 1, + anon_sym_LPAREN, + ACTIONS(120), 1, + sym_word, + STATE(22), 1, + sym_simple_token, + STATE(24), 1, + sym_equation, + STATE(40), 1, + sym_paren_compound, + STATE(44), 1, + sym_compound_part, + STATE(50), 1, + sym_text_token, + [551] = 6, + ACTIONS(36), 1, + anon_sym_RPAREN, + ACTIONS(107), 1, + sym_comment, + ACTIONS(116), 1, + anon_sym_DASH_GT, + ACTIONS(118), 1, + anon_sym_EQ_GT, + STATE(26), 2, + sym_inflection, + aux_sym_text_token_repeat1, + STATE(31), 2, + sym_external_sandhi, + aux_sym_text_token_repeat2, + [572] = 3, + ACTIONS(73), 1, + anon_sym_EQ, + ACTIONS(107), 1, + sym_comment, + ACTIONS(71), 6, + anon_sym_PLUS, + anon_sym_RPAREN, + anon_sym_DASH_GT, + anon_sym_EQ_GT, + sym_translation_gloss, + sym_etymology_gloss, + [587] = 6, + ACTIONS(67), 1, + anon_sym_RPAREN, + ACTIONS(107), 1, + sym_comment, + ACTIONS(116), 1, + anon_sym_DASH_GT, + ACTIONS(118), 1, + anon_sym_EQ_GT, + STATE(28), 2, + sym_inflection, + aux_sym_text_token_repeat1, + STATE(29), 2, + sym_external_sandhi, + aux_sym_text_token_repeat2, + [608] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(11), 1, + anon_sym_LPAREN, + ACTIONS(120), 1, + sym_word, + STATE(39), 1, + sym_compound_part, + STATE(40), 2, + sym_paren_compound, + sym_simple_token, + [625] = 4, + ACTIONS(107), 1, + sym_comment, + ACTIONS(122), 1, + anon_sym_DASH_GT, + ACTIONS(75), 2, + anon_sym_RPAREN, + anon_sym_EQ_GT, + STATE(28), 2, + sym_inflection, + aux_sym_text_token_repeat1, + [640] = 4, + ACTIONS(89), 1, + anon_sym_RPAREN, + ACTIONS(107), 1, + sym_comment, + ACTIONS(118), 1, + anon_sym_EQ_GT, + STATE(30), 2, + sym_external_sandhi, + aux_sym_text_token_repeat2, + [654] = 4, + ACTIONS(82), 1, + anon_sym_RPAREN, + ACTIONS(107), 1, + sym_comment, + ACTIONS(125), 1, + anon_sym_EQ_GT, + STATE(30), 2, + sym_external_sandhi, + aux_sym_text_token_repeat2, + [668] = 4, + ACTIONS(67), 1, + anon_sym_RPAREN, + ACTIONS(107), 1, + sym_comment, + ACTIONS(118), 1, + anon_sym_EQ_GT, + STATE(30), 2, + sym_external_sandhi, + aux_sym_text_token_repeat2, + [682] = 4, + ACTIONS(107), 1, + sym_comment, + ACTIONS(128), 1, + anon_sym_PLUS, + ACTIONS(130), 1, + anon_sym_EQ, + STATE(33), 1, + aux_sym_equation_repeat1, + [695] = 4, + ACTIONS(107), 1, + sym_comment, + ACTIONS(132), 1, + anon_sym_PLUS, + ACTIONS(135), 1, + anon_sym_EQ, + STATE(33), 1, + aux_sym_equation_repeat1, + [708] = 2, + ACTIONS(107), 1, + sym_comment, + ACTIONS(97), 3, + anon_sym_RPAREN, + anon_sym_DASH_GT, + anon_sym_EQ_GT, + [717] = 2, + ACTIONS(107), 1, + sym_comment, + ACTIONS(93), 3, + anon_sym_RPAREN, + anon_sym_DASH_GT, + anon_sym_EQ_GT, + [726] = 4, + ACTIONS(107), 1, + sym_comment, + ACTIONS(128), 1, + anon_sym_PLUS, + ACTIONS(137), 1, + anon_sym_EQ, + STATE(33), 1, + aux_sym_equation_repeat1, + [739] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(13), 1, + sym_word, + STATE(16), 1, + sym_simple_token, + [749] = 3, + ACTIONS(107), 1, + sym_comment, + ACTIONS(128), 1, + anon_sym_PLUS, + STATE(32), 1, + aux_sym_equation_repeat1, + [759] = 2, + ACTIONS(107), 1, + sym_comment, + ACTIONS(135), 2, + anon_sym_PLUS, + anon_sym_EQ, + [767] = 2, + ACTIONS(107), 1, + sym_comment, + ACTIONS(38), 2, + anon_sym_PLUS, + anon_sym_EQ, + [775] = 2, + ACTIONS(107), 1, + sym_comment, + ACTIONS(101), 2, + anon_sym_RPAREN, + anon_sym_EQ_GT, + [783] = 2, + ACTIONS(107), 1, + sym_comment, + ACTIONS(139), 2, + anon_sym_PLUS, + anon_sym_EQ, + [791] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(120), 1, + sym_word, + STATE(34), 1, + sym_simple_token, + [801] = 3, + ACTIONS(107), 1, + sym_comment, + ACTIONS(128), 1, + anon_sym_PLUS, + STATE(36), 1, + aux_sym_equation_repeat1, + [811] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(141), 1, + sym_word, + [818] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(143), 1, + sym_header_text, + [825] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(145), 1, + sym_word, + [832] = 2, + ACTIONS(107), 1, + sym_comment, + ACTIONS(147), 1, + ts_builtin_sym_end, + [839] = 2, + ACTIONS(107), 1, + sym_comment, + ACTIONS(149), 1, + anon_sym_EQ_EQ_EQ, + [846] = 2, + ACTIONS(107), 1, + sym_comment, + ACTIONS(151), 1, + anon_sym_RPAREN, + [853] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(153), 1, + sym_word, + [860] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(155), 1, + sym_word, +}; + +static const uint32_t ts_small_parse_table_map[] = { + [SMALL_STATE(2)] = 0, + [SMALL_STATE(3)] = 27, + [SMALL_STATE(4)] = 54, + [SMALL_STATE(5)] = 95, + [SMALL_STATE(6)] = 128, + [SMALL_STATE(7)] = 155, + [SMALL_STATE(8)] = 196, + [SMALL_STATE(9)] = 226, + [SMALL_STATE(10)] = 256, + [SMALL_STATE(11)] = 277, + [SMALL_STATE(12)] = 301, + [SMALL_STATE(13)] = 324, + [SMALL_STATE(14)] = 347, + [SMALL_STATE(15)] = 370, + [SMALL_STATE(16)] = 388, + [SMALL_STATE(17)] = 406, + [SMALL_STATE(18)] = 423, + [SMALL_STATE(19)] = 444, + [SMALL_STATE(20)] = 465, + [SMALL_STATE(21)] = 486, + [SMALL_STATE(22)] = 502, + [SMALL_STATE(23)] = 526, + [SMALL_STATE(24)] = 551, + [SMALL_STATE(25)] = 572, + [SMALL_STATE(26)] = 587, + [SMALL_STATE(27)] = 608, + [SMALL_STATE(28)] = 625, + [SMALL_STATE(29)] = 640, + [SMALL_STATE(30)] = 654, + [SMALL_STATE(31)] = 668, + [SMALL_STATE(32)] = 682, + [SMALL_STATE(33)] = 695, + [SMALL_STATE(34)] = 708, + [SMALL_STATE(35)] = 717, + [SMALL_STATE(36)] = 726, + [SMALL_STATE(37)] = 739, + [SMALL_STATE(38)] = 749, + [SMALL_STATE(39)] = 759, + [SMALL_STATE(40)] = 767, + [SMALL_STATE(41)] = 775, + [SMALL_STATE(42)] = 783, + [SMALL_STATE(43)] = 791, + [SMALL_STATE(44)] = 801, + [SMALL_STATE(45)] = 811, + [SMALL_STATE(46)] = 818, + [SMALL_STATE(47)] = 825, + [SMALL_STATE(48)] = 832, + [SMALL_STATE(49)] = 839, + [SMALL_STATE(50)] = 846, + [SMALL_STATE(51)] = 853, + [SMALL_STATE(52)] = 860, +}; + +static const TSParseActionEntry ts_parse_actions[] = { + [0] = {.entry = {.count = 0, .reusable = false}}, + [1] = {.entry = {.count = 1, .reusable = false}}, RECOVER(), + [3] = {.entry = {.count = 1, .reusable = false}}, SHIFT_EXTRA(), + [5] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_document, 0, 0, 0), + [7] = {.entry = {.count = 1, .reusable = true}}, SHIFT(46), + [9] = {.entry = {.count = 1, .reusable = true}}, SHIFT(4), + [11] = {.entry = {.count = 1, .reusable = true}}, SHIFT(23), + [13] = {.entry = {.count = 1, .reusable = false}}, SHIFT(3), + [15] = {.entry = {.count = 1, .reusable = false}}, SHIFT(4), + [17] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_simple_token_repeat1, 2, 0, 0), + [19] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_simple_token_repeat1, 2, 0, 0), SHIFT_REPEAT(10), + [22] = {.entry = {.count = 1, .reusable = false}}, REDUCE(aux_sym_simple_token_repeat1, 2, 0, 0), + [24] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_simple_token, 1, 0, 0), + [26] = {.entry = {.count = 1, .reusable = true}}, SHIFT(10), + [28] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_simple_token, 1, 0, 0), + [30] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_document, 1, 0, 0), + [32] = {.entry = {.count = 1, .reusable = true}}, SHIFT(7), + [34] = {.entry = {.count = 1, .reusable = false}}, SHIFT(7), + [36] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_text_token, 1, 0, 0), + [38] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_compound_part, 1, 0, 0), + [40] = {.entry = {.count = 1, .reusable = true}}, SHIFT(37), + [42] = {.entry = {.count = 1, .reusable = true}}, SHIFT(45), + [44] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_text_token, 1, 0, 0), + [46] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_simple_token, 2, 0, 0), + [48] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_simple_token, 2, 0, 0), + [50] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_document_repeat1, 2, 0, 0), + [52] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_document_repeat1, 2, 0, 0), SHIFT_REPEAT(46), + [55] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_document_repeat1, 2, 0, 0), SHIFT_REPEAT(7), + [58] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_document_repeat1, 2, 0, 0), SHIFT_REPEAT(23), + [61] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_document_repeat1, 2, 0, 0), SHIFT_REPEAT(3), + [64] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_document_repeat1, 2, 0, 0), SHIFT_REPEAT(7), + [67] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_text_token, 2, 0, 0), + [69] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_text_token, 2, 0, 0), + [71] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_gloss, 1, 0, 0), + [73] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_gloss, 1, 0, 0), + [75] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_text_token_repeat1, 2, 0, 0), + [77] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_text_token_repeat1, 2, 0, 0), SHIFT_REPEAT(37), + [80] = {.entry = {.count = 1, .reusable = false}}, REDUCE(aux_sym_text_token_repeat1, 2, 0, 0), + [82] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_text_token_repeat2, 2, 0, 0), + [84] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_text_token_repeat2, 2, 0, 0), SHIFT_REPEAT(45), + [87] = {.entry = {.count = 1, .reusable = false}}, REDUCE(aux_sym_text_token_repeat2, 2, 0, 0), + [89] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_text_token, 3, 0, 0), + [91] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_text_token, 3, 0, 0), + [93] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_equation, 4, 0, 3), + [95] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_equation, 4, 0, 3), + [97] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_inflection, 2, 0, 0), + [99] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_inflection, 2, 0, 0), + [101] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_external_sandhi, 2, 0, 2), + [103] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_external_sandhi, 2, 0, 2), + [105] = {.entry = {.count = 1, .reusable = true}}, SHIFT(25), + [107] = {.entry = {.count = 1, .reusable = true}}, SHIFT_EXTRA(), + [109] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_simple_token_repeat1, 2, 0, 0), SHIFT_REPEAT(25), + [112] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_section_header, 3, 0, 1), + [114] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_section_header, 3, 0, 1), + [116] = {.entry = {.count = 1, .reusable = true}}, SHIFT(43), + [118] = {.entry = {.count = 1, .reusable = true}}, SHIFT(51), + [120] = {.entry = {.count = 1, .reusable = false}}, SHIFT(18), + [122] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_text_token_repeat1, 2, 0, 0), SHIFT_REPEAT(43), + [125] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_text_token_repeat2, 2, 0, 0), SHIFT_REPEAT(51), + [128] = {.entry = {.count = 1, .reusable = true}}, SHIFT(27), + [130] = {.entry = {.count = 1, .reusable = true}}, SHIFT(47), + [132] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_equation_repeat1, 2, 0, 0), SHIFT_REPEAT(27), + [135] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_equation_repeat1, 2, 0, 0), + [137] = {.entry = {.count = 1, .reusable = true}}, SHIFT(52), + [139] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_paren_compound, 3, 0, 0), + [141] = {.entry = {.count = 1, .reusable = false}}, SHIFT(17), + [143] = {.entry = {.count = 1, .reusable = false}}, SHIFT(49), + [145] = {.entry = {.count = 1, .reusable = false}}, SHIFT(15), + [147] = {.entry = {.count = 1, .reusable = true}}, ACCEPT_INPUT(), + [149] = {.entry = {.count = 1, .reusable = true}}, SHIFT(21), + [151] = {.entry = {.count = 1, .reusable = true}}, SHIFT(42), + [153] = {.entry = {.count = 1, .reusable = false}}, SHIFT(41), + [155] = {.entry = {.count = 1, .reusable = false}}, SHIFT(35), +}; + +#ifdef __cplusplus +extern "C" { +#endif +#ifdef TREE_SITTER_HIDE_SYMBOLS +#define TS_PUBLIC +#elif defined(_WIN32) +#define TS_PUBLIC __declspec(dllexport) +#else +#define TS_PUBLIC __attribute__((visibility("default"))) +#endif + +TS_PUBLIC const TSLanguage *tree_sitter_nirukta(void) { + static const TSLanguage language = { + .abi_version = LANGUAGE_VERSION, + .symbol_count = SYMBOL_COUNT, + .alias_count = ALIAS_COUNT, + .token_count = TOKEN_COUNT, + .external_token_count = EXTERNAL_TOKEN_COUNT, + .state_count = STATE_COUNT, + .large_state_count = LARGE_STATE_COUNT, + .production_id_count = PRODUCTION_ID_COUNT, + .supertype_count = SUPERTYPE_COUNT, + .field_count = FIELD_COUNT, + .max_alias_sequence_length = MAX_ALIAS_SEQUENCE_LENGTH, + .parse_table = &ts_parse_table[0][0], + .small_parse_table = ts_small_parse_table, + .small_parse_table_map = ts_small_parse_table_map, + .parse_actions = ts_parse_actions, + .symbol_names = ts_symbol_names, + .field_names = ts_field_names, + .field_map_slices = ts_field_map_slices, + .field_map_entries = ts_field_map_entries, + .symbol_metadata = ts_symbol_metadata, + .public_symbol_map = ts_symbol_map, + .alias_map = ts_non_terminal_alias_map, + .alias_sequences = &ts_alias_sequences[0][0], + .lex_modes = (const void*)ts_lex_modes, + .lex_fn = ts_lex, + .primary_state_ids = ts_primary_state_ids, + .name = "nirukta", + .max_reserved_word_set_size = 0, + .metadata = { + .major_version = 0, + .minor_version = 1, + .patch_version = 0, + }, + }; + return &language; +} +#ifdef __cplusplus +} +#endif diff --git a/tree-sitter-nirukta/src/tree_sitter/alloc.h b/tree-sitter-nirukta/src/tree_sitter/alloc.h new file mode 100644 index 0000000..1abdd12 --- /dev/null +++ b/tree-sitter-nirukta/src/tree_sitter/alloc.h @@ -0,0 +1,54 @@ +#ifndef TREE_SITTER_ALLOC_H_ +#define TREE_SITTER_ALLOC_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +// Allow clients to override allocation functions +#ifdef TREE_SITTER_REUSE_ALLOCATOR + +extern void *(*ts_current_malloc)(size_t size); +extern void *(*ts_current_calloc)(size_t count, size_t size); +extern void *(*ts_current_realloc)(void *ptr, size_t size); +extern void (*ts_current_free)(void *ptr); + +#ifndef ts_malloc +#define ts_malloc ts_current_malloc +#endif +#ifndef ts_calloc +#define ts_calloc ts_current_calloc +#endif +#ifndef ts_realloc +#define ts_realloc ts_current_realloc +#endif +#ifndef ts_free +#define ts_free ts_current_free +#endif + +#else + +#ifndef ts_malloc +#define ts_malloc malloc +#endif +#ifndef ts_calloc +#define ts_calloc calloc +#endif +#ifndef ts_realloc +#define ts_realloc realloc +#endif +#ifndef ts_free +#define ts_free free +#endif + +#endif + +#ifdef __cplusplus +} +#endif + +#endif // TREE_SITTER_ALLOC_H_ diff --git a/tree-sitter-nirukta/src/tree_sitter/array.h b/tree-sitter-nirukta/src/tree_sitter/array.h new file mode 100644 index 0000000..56fc8cd --- /dev/null +++ b/tree-sitter-nirukta/src/tree_sitter/array.h @@ -0,0 +1,330 @@ +#ifndef TREE_SITTER_ARRAY_H_ +#define TREE_SITTER_ARRAY_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "./alloc.h" + +#include +#include +#include +#include +#include + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4101) +#elif defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" +#endif + +#define Array(T) \ + struct { \ + T *contents; \ + uint32_t size; \ + uint32_t capacity; \ + } + +/// Initialize an array. +#define array_init(self) \ + ((self)->size = 0, (self)->capacity = 0, (self)->contents = NULL) + +/// Create an empty array. +#define array_new() \ + { NULL, 0, 0 } + +/// Get a pointer to the element at a given `index` in the array. +#define array_get(self, _index) \ + (assert((uint32_t)(_index) < (self)->size), &(self)->contents[_index]) + +/// Get a pointer to the first element in the array. +#define array_front(self) array_get(self, 0) + +/// Get a pointer to the last element in the array. +#define array_back(self) array_get(self, (self)->size - 1) + +/// Clear the array, setting its size to zero. Note that this does not free any +/// memory allocated for the array's contents. +#define array_clear(self) ((self)->size = 0) + +/// Reserve `new_capacity` elements of space in the array. If `new_capacity` is +/// less than the array's current capacity, this function has no effect. +#define array_reserve(self, new_capacity) \ + ((self)->contents = _array__reserve( \ + (void *)(self)->contents, &(self)->capacity, \ + array_elem_size(self), new_capacity) \ + ) + +/// Free any memory allocated for this array. Note that this does not free any +/// memory allocated for the array's contents. +#define array_delete(self) \ + do { \ + if ((self)->contents) ts_free((self)->contents); \ + (self)->contents = NULL; \ + (self)->size = 0; \ + (self)->capacity = 0; \ + } while (0) + +/// Push a new `element` onto the end of the array. +#define array_push(self, element) \ + do { \ + (self)->contents = _array__grow( \ + (void *)(self)->contents, (self)->size, &(self)->capacity, \ + 1, array_elem_size(self) \ + ); \ + (self)->contents[(self)->size++] = (element); \ + } while(0) + +/// Increase the array's size by `count` elements. +/// New elements are zero-initialized. +#define array_grow_by(self, count) \ + do { \ + if ((count) == 0) break; \ + (self)->contents = _array__grow( \ + (self)->contents, (self)->size, &(self)->capacity, \ + count, array_elem_size(self) \ + ); \ + memset((self)->contents + (self)->size, 0, (count) * array_elem_size(self)); \ + (self)->size += (count); \ + } while (0) + +/// Append all elements from one array to the end of another. +#define array_push_all(self, other) \ + array_extend((self), (other)->size, (other)->contents) + +/// Append `count` elements to the end of the array, reading their values from the +/// `contents` pointer. +#define array_extend(self, count, other_contents) \ + (self)->contents = _array__splice( \ + (void*)(self)->contents, &(self)->size, &(self)->capacity, \ + array_elem_size(self), (self)->size, 0, count, other_contents \ + ) + +/// Remove `old_count` elements from the array starting at the given `index`. At +/// the same index, insert `new_count` new elements, reading their values from the +/// `new_contents` pointer. +#define array_splice(self, _index, old_count, new_count, new_contents) \ + (self)->contents = _array__splice( \ + (void *)(self)->contents, &(self)->size, &(self)->capacity, \ + array_elem_size(self), _index, old_count, new_count, new_contents \ + ) + +/// Insert one `element` into the array at the given `index`. +#define array_insert(self, _index, element) \ + (self)->contents = _array__splice( \ + (void *)(self)->contents, &(self)->size, &(self)->capacity, \ + array_elem_size(self), _index, 0, 1, &(element) \ + ) + +/// Remove one element from the array at the given `index`. +#define array_erase(self, _index) \ + _array__erase((void *)(self)->contents, &(self)->size, array_elem_size(self), _index) + +/// Pop the last element off the array, returning the element by value. +#define array_pop(self) ((self)->contents[--(self)->size]) + +/// Assign the contents of one array to another, reallocating if necessary. +#define array_assign(self, other) \ + (self)->contents = _array__assign( \ + (void *)(self)->contents, &(self)->size, &(self)->capacity, \ + (const void *)(other)->contents, (other)->size, array_elem_size(self) \ + ) + +/// Swap one array with another +#define array_swap(self, other) \ + do { \ + void *_array_swap_tmp = (void *)(self)->contents; \ + (self)->contents = (other)->contents; \ + (other)->contents = _array_swap_tmp; \ + _array__swap(&(self)->size, &(self)->capacity, \ + &(other)->size, &(other)->capacity); \ + } while (0) + +/// Get the size of the array contents +#define array_elem_size(self) (sizeof *(self)->contents) + +/// Search a sorted array for a given `needle` value, using the given `compare` +/// callback to determine the order. +/// +/// If an existing element is found to be equal to `needle`, then the `index` +/// out-parameter is set to the existing value's index, and the `exists` +/// out-parameter is set to true. Otherwise, `index` is set to an index where +/// `needle` should be inserted in order to preserve the sorting, and `exists` +/// is set to false. +#define array_search_sorted_with(self, compare, needle, _index, _exists) \ + _array__search_sorted(self, 0, compare, , needle, _index, _exists) + +/// Search a sorted array for a given `needle` value, using integer comparisons +/// of a given struct field (specified with a leading dot) to determine the order. +/// +/// See also `array_search_sorted_with`. +#define array_search_sorted_by(self, field, needle, _index, _exists) \ + _array__search_sorted(self, 0, _compare_int, field, needle, _index, _exists) + +/// Insert a given `value` into a sorted array, using the given `compare` +/// callback to determine the order. +#define array_insert_sorted_with(self, compare, value) \ + do { \ + unsigned _index, _exists; \ + array_search_sorted_with(self, compare, &(value), &_index, &_exists); \ + if (!_exists) array_insert(self, _index, value); \ + } while (0) + +/// Insert a given `value` into a sorted array, using integer comparisons of +/// a given struct field (specified with a leading dot) to determine the order. +/// +/// See also `array_search_sorted_by`. +#define array_insert_sorted_by(self, field, value) \ + do { \ + unsigned _index, _exists; \ + array_search_sorted_by(self, field, (value) field, &_index, &_exists); \ + if (!_exists) array_insert(self, _index, value); \ + } while (0) + +// Private + +// Pointers to individual `Array` fields (rather than the entire `Array` itself) +// are passed to the various `_array__*` functions below to address strict aliasing +// violations that arises when the _entire_ `Array` struct is passed as `Array(void)*`. +// +// The `Array` type itself was not altered as a solution in order to avoid breakage +// with existing consumers (in particular, parsers with external scanners). + +/// This is not what you're looking for, see `array_erase`. +static inline void _array__erase(void* self_contents, uint32_t *size, + size_t element_size, uint32_t index) { + assert(index < *size); + char *contents = (char *)self_contents; + memmove(contents + index * element_size, contents + (index + 1) * element_size, + (*size - index - 1) * element_size); + (*size)--; +} + +/// This is not what you're looking for, see `array_reserve`. +static inline void *_array__reserve(void *contents, uint32_t *capacity, + size_t element_size, uint32_t new_capacity) { + void *new_contents = contents; + if (new_capacity > *capacity) { + if (contents) { + new_contents = ts_realloc(contents, new_capacity * element_size); + } else { + new_contents = ts_malloc(new_capacity * element_size); + } + *capacity = new_capacity; + } + return new_contents; +} + +/// This is not what you're looking for, see `array_assign`. +static inline void *_array__assign(void* self_contents, uint32_t *self_size, uint32_t *self_capacity, + const void *other_contents, uint32_t other_size, size_t element_size) { + void *new_contents = _array__reserve(self_contents, self_capacity, element_size, other_size); + *self_size = other_size; + memcpy(new_contents, other_contents, *self_size * element_size); + return new_contents; +} + +/// This is not what you're looking for, see `array_swap`. +static inline void _array__swap(uint32_t *self_size, uint32_t *self_capacity, + uint32_t *other_size, uint32_t *other_capacity) { + uint32_t tmp_size = *self_size; + uint32_t tmp_capacity = *self_capacity; + *self_size = *other_size; + *self_capacity = *other_capacity; + *other_size = tmp_size; + *other_capacity = tmp_capacity; +} + +/// This is not what you're looking for, see `array_push` or `array_grow_by`. +static inline void *_array__grow(void *contents, uint32_t size, uint32_t *capacity, + uint32_t count, size_t element_size) { + void *new_contents = contents; + uint32_t new_size = size + count; + if (new_size > *capacity) { + uint32_t new_capacity = *capacity * 2; + if (new_capacity < 8) new_capacity = 8; + if (new_capacity < new_size) new_capacity = new_size; + new_contents = _array__reserve(contents, capacity, element_size, new_capacity); + } + return new_contents; +} + +/// This is not what you're looking for, see `array_splice`. +static inline void *_array__splice(void *self_contents, uint32_t *size, uint32_t *capacity, + size_t element_size, + uint32_t index, uint32_t old_count, + uint32_t new_count, const void *elements) { + uint32_t new_size = *size + new_count - old_count; + uint32_t old_end = index + old_count; + uint32_t new_end = index + new_count; + assert(old_end <= *size); + + void *new_contents = _array__reserve(self_contents, capacity, element_size, new_size); + + char *contents = (char *)new_contents; + if (*size > old_end) { + memmove( + contents + new_end * element_size, + contents + old_end * element_size, + (*size - old_end) * element_size + ); + } + if (new_count > 0) { + if (elements) { + memcpy( + (contents + index * element_size), + elements, + new_count * element_size + ); + } else { + memset( + (contents + index * element_size), + 0, + new_count * element_size + ); + } + } + *size += new_count - old_count; + + return new_contents; +} + +/// A binary search routine, based on Rust's `std::slice::binary_search_by`. +/// This is not what you're looking for, see `array_search_sorted_with` or `array_search_sorted_by`. +#define _array__search_sorted(self, start, compare, suffix, needle, _index, _exists) \ + do { \ + *(_index) = start; \ + *(_exists) = false; \ + uint32_t size = (self)->size - *(_index); \ + if (size == 0) break; \ + int comparison; \ + while (size > 1) { \ + uint32_t half_size = size / 2; \ + uint32_t mid_index = *(_index) + half_size; \ + comparison = compare(&((self)->contents[mid_index] suffix), (needle)); \ + if (comparison <= 0) *(_index) = mid_index; \ + size -= half_size; \ + } \ + comparison = compare(&((self)->contents[*(_index)] suffix), (needle)); \ + if (comparison == 0) *(_exists) = true; \ + else if (comparison < 0) *(_index) += 1; \ + } while (0) + +/// Helper macro for the `_sorted_by` routines below. This takes the left (existing) +/// parameter by reference in order to work with the generic sorting function above. +#define _compare_int(a, b) ((int)*(a) - (int)(b)) + +#ifdef _MSC_VER +#pragma warning(pop) +#elif defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + +#ifdef __cplusplus +} +#endif + +#endif // TREE_SITTER_ARRAY_H_ diff --git a/tree-sitter-nirukta/src/tree_sitter/parser.h b/tree-sitter-nirukta/src/tree_sitter/parser.h new file mode 100644 index 0000000..858107d --- /dev/null +++ b/tree-sitter-nirukta/src/tree_sitter/parser.h @@ -0,0 +1,286 @@ +#ifndef TREE_SITTER_PARSER_H_ +#define TREE_SITTER_PARSER_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#define ts_builtin_sym_error ((TSSymbol)-1) +#define ts_builtin_sym_end 0 +#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024 + +#ifndef TREE_SITTER_API_H_ +typedef uint16_t TSStateId; +typedef uint16_t TSSymbol; +typedef uint16_t TSFieldId; +typedef struct TSLanguage TSLanguage; +typedef struct TSLanguageMetadata { + uint8_t major_version; + uint8_t minor_version; + uint8_t patch_version; +} TSLanguageMetadata; +#endif + +typedef struct { + TSFieldId field_id; + uint8_t child_index; + bool inherited; +} TSFieldMapEntry; + +// Used to index the field and supertype maps. +typedef struct { + uint16_t index; + uint16_t length; +} TSMapSlice; + +typedef struct { + bool visible; + bool named; + bool supertype; +} TSSymbolMetadata; + +typedef struct TSLexer TSLexer; + +struct TSLexer { + int32_t lookahead; + TSSymbol result_symbol; + void (*advance)(TSLexer *, bool); + void (*mark_end)(TSLexer *); + uint32_t (*get_column)(TSLexer *); + bool (*is_at_included_range_start)(const TSLexer *); + bool (*eof)(const TSLexer *); + void (*log)(const TSLexer *, const char *, ...); +}; + +typedef enum { + TSParseActionTypeShift, + TSParseActionTypeReduce, + TSParseActionTypeAccept, + TSParseActionTypeRecover, +} TSParseActionType; + +typedef union { + struct { + uint8_t type; + TSStateId state; + bool extra; + bool repetition; + } shift; + struct { + uint8_t type; + uint8_t child_count; + TSSymbol symbol; + int16_t dynamic_precedence; + uint16_t production_id; + } reduce; + uint8_t type; +} TSParseAction; + +typedef struct { + uint16_t lex_state; + uint16_t external_lex_state; +} TSLexMode; + +typedef struct { + uint16_t lex_state; + uint16_t external_lex_state; + uint16_t reserved_word_set_id; +} TSLexerMode; + +typedef union { + TSParseAction action; + struct { + uint8_t count; + bool reusable; + } entry; +} TSParseActionEntry; + +typedef struct { + int32_t start; + int32_t end; +} TSCharacterRange; + +struct TSLanguage { + uint32_t abi_version; + uint32_t symbol_count; + uint32_t alias_count; + uint32_t token_count; + uint32_t external_token_count; + uint32_t state_count; + uint32_t large_state_count; + uint32_t production_id_count; + uint32_t field_count; + uint16_t max_alias_sequence_length; + const uint16_t *parse_table; + const uint16_t *small_parse_table; + const uint32_t *small_parse_table_map; + const TSParseActionEntry *parse_actions; + const char * const *symbol_names; + const char * const *field_names; + const TSMapSlice *field_map_slices; + const TSFieldMapEntry *field_map_entries; + const TSSymbolMetadata *symbol_metadata; + const TSSymbol *public_symbol_map; + const uint16_t *alias_map; + const TSSymbol *alias_sequences; + const TSLexerMode *lex_modes; + bool (*lex_fn)(TSLexer *, TSStateId); + bool (*keyword_lex_fn)(TSLexer *, TSStateId); + TSSymbol keyword_capture_token; + struct { + const bool *states; + const TSSymbol *symbol_map; + void *(*create)(void); + void (*destroy)(void *); + bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist); + unsigned (*serialize)(void *, char *); + void (*deserialize)(void *, const char *, unsigned); + } external_scanner; + const TSStateId *primary_state_ids; + const char *name; + const TSSymbol *reserved_words; + uint16_t max_reserved_word_set_size; + uint32_t supertype_count; + const TSSymbol *supertype_symbols; + const TSMapSlice *supertype_map_slices; + const TSSymbol *supertype_map_entries; + TSLanguageMetadata metadata; +}; + +static inline bool set_contains(const TSCharacterRange *ranges, uint32_t len, int32_t lookahead) { + uint32_t index = 0; + uint32_t size = len - index; + while (size > 1) { + uint32_t half_size = size / 2; + uint32_t mid_index = index + half_size; + const TSCharacterRange *range = &ranges[mid_index]; + if (lookahead >= range->start && lookahead <= range->end) { + return true; + } else if (lookahead > range->end) { + index = mid_index; + } + size -= half_size; + } + const TSCharacterRange *range = &ranges[index]; + return (lookahead >= range->start && lookahead <= range->end); +} + +/* + * Lexer Macros + */ + +#ifdef _MSC_VER +#define UNUSED __pragma(warning(suppress : 4101)) +#else +#define UNUSED __attribute__((unused)) +#endif + +#define START_LEXER() \ + bool result = false; \ + bool skip = false; \ + UNUSED \ + bool eof = false; \ + int32_t lookahead; \ + goto start; \ + next_state: \ + lexer->advance(lexer, skip); \ + start: \ + skip = false; \ + lookahead = lexer->lookahead; + +#define ADVANCE(state_value) \ + { \ + state = state_value; \ + goto next_state; \ + } + +#define ADVANCE_MAP(...) \ + { \ + static const uint16_t map[] = { __VA_ARGS__ }; \ + for (uint32_t i = 0; i < sizeof(map) / sizeof(map[0]); i += 2) { \ + if (map[i] == lookahead) { \ + state = map[i + 1]; \ + goto next_state; \ + } \ + } \ + } + +#define SKIP(state_value) \ + { \ + skip = true; \ + state = state_value; \ + goto next_state; \ + } + +#define ACCEPT_TOKEN(symbol_value) \ + result = true; \ + lexer->result_symbol = symbol_value; \ + lexer->mark_end(lexer); + +#define END_STATE() return result; + +/* + * Parse Table Macros + */ + +#define SMALL_STATE(id) ((id) - LARGE_STATE_COUNT) + +#define STATE(id) id + +#define ACTIONS(id) id + +#define SHIFT(state_value) \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .state = (state_value) \ + } \ + }} + +#define SHIFT_REPEAT(state_value) \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .state = (state_value), \ + .repetition = true \ + } \ + }} + +#define SHIFT_EXTRA() \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .extra = true \ + } \ + }} + +#define REDUCE(symbol_name, children, precedence, prod_id) \ + {{ \ + .reduce = { \ + .type = TSParseActionTypeReduce, \ + .symbol = symbol_name, \ + .child_count = children, \ + .dynamic_precedence = precedence, \ + .production_id = prod_id \ + }, \ + }} + +#define RECOVER() \ + {{ \ + .type = TSParseActionTypeRecover \ + }} + +#define ACCEPT_INPUT() \ + {{ \ + .type = TSParseActionTypeAccept \ + }} + +#ifdef __cplusplus +} +#endif + +#endif // TREE_SITTER_PARSER_H_ diff --git a/tree-sitter-nirukta/tree-sitter.json b/tree-sitter-nirukta/tree-sitter.json new file mode 100644 index 0000000..d800add --- /dev/null +++ b/tree-sitter-nirukta/tree-sitter.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://tree-sitter.github.io/tree-sitter/assets/schemas/config.schema.json", + "grammars": [ + { + "name": "nirukta", + "camelcase": "Nirukta", + "scope": "source.nirukta", + "path": ".", + "file-types": ["sloka", "sutra"], + "injection-regex": "^nirukta$" + } + ], + "metadata": { + "version": "0.1.0", + "license": "MIT", + "description": "Tree-sitter grammar for nirukta .sloka/.sutra files", + "authors": [ + { + "name": "nirukta" + } + ] + } +} diff --git a/uv.lock b/uv.lock index 3b74db5..cd4e5a5 100644 --- a/uv.lock +++ b/uv.lock @@ -61,6 +61,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] +[[package]] +name = "cattrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/ba18945e7d6e55a58364d9fb2e46049c1c2998b3d805f19b703f14e81057/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40", size = 495672, upload-time = "2026-02-18T22:15:19.406Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -269,6 +282,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/c1/65ae96680758615e042415fb1d3a1e573c2419387205135501d96be97bb8/indic_transliteration-2.3.82-py3-none-any.whl", hash = "sha256:243b8a444f14c1a811c03ba07e8aa300da61a7c4172a45556fdce1d963038019", size = 162886, upload-time = "2026-04-06T10:48:04.426Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jaconv" version = "0.5.0" @@ -333,6 +355,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/d1/68e2bcca94c9bbdc122a71e504e0b6a6c3e31541b1bad33fee0205996006/language_data-1.4.0-py3-none-any.whl", hash = "sha256:f741927c24ab14cbed2a57bc2bfe82b00cff266c427179597e8b14123364f084", size = 5572678, upload-time = "2025-11-28T13:45:23.381Z" }, ] +[[package]] +name = "lsprotocol" +version = "2025.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/26/67b84e6ec1402f0e6764ef3d2a0aaf9a79522cc1d37738f4e5bb0b21521a/lsprotocol-2025.0.0.tar.gz", hash = "sha256:e879da2b9301e82cfc3e60d805630487ac2f7ab17492f4f5ba5aaba94fe56c29", size = 74896, upload-time = "2025-06-17T21:30:18.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/f0/92f2d609d6642b5f30cb50a885d2bf1483301c69d5786286500d15651ef2/lsprotocol-2025.0.0-py3-none-any.whl", hash = "sha256:f9d78f25221f2a60eaa4a96d3b4ffae011b107537facee61d3da3313880995c7", size = 76250, upload-time = "2025-06-17T21:30:19.455Z" }, +] + [[package]] name = "lxml" version = "6.0.2" @@ -485,25 +520,35 @@ dependencies = [ { name = "janim", extra = ["gui"] }, { name = "nirukta-inflect" }, { name = "parsimonious" }, + { name = "pygls" }, { name = "sandhi" }, { name = "skrutable" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + [package.metadata] requires-dist = [ { name = "aksharamukha", specifier = ">=2.3" }, { name = "dill", specifier = ">=0.4.1" }, { name = "janim", extras = ["gui"], specifier = ">=4.2.0" }, - { name = "nirukta-inflect", git = "https://github.com/recursivepaws/nirukta-inflect?branch=main" }, + { name = "nirukta-inflect", git = "ssh://git@github.com/recursivepaws/nirukta-inflect.git?branch=main" }, { name = "parsimonious", specifier = ">=0.11.0" }, + { name = "pygls", specifier = ">=2.1" }, { name = "sandhi", specifier = ">=1.0.0" }, { name = "skrutable", specifier = ">=2.6.3" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.1.1" }] + [[package]] name = "nirukta-inflect" version = "0.1.0" -source = { git = "https://github.com/recursivepaws/nirukta-inflect?branch=main#d9ca196407e044a1b86cee8bec41c6688046733c" } +source = { git = "ssh://git@github.com/recursivepaws/nirukta-inflect.git?branch=main#a1f2511dc8268584d69ae9f2b3a4ea966ec2feee" } [[package]] name = "numpy" @@ -634,6 +679,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "psutil" version = "7.2.2" @@ -671,6 +725,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[package]] +name = "pygls" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, + { name = "lsprotocol" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/2e/7bbe061d175c0baddde8fc9edb908a4c31ba5d9165b8c68e3439c3a9f138/pygls-2.1.1.tar.gz", hash = "sha256:1da03ba9053201bb337dcdd8d121df70feb2a91e1a0dcc74de5da79755b1a201", size = 55091, upload-time = "2026-03-25T11:19:10.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl", hash = "sha256:510a6dea2476177230c7d851125e5948efdf3fdb9ebfd8543fc434972f8faed4", size = 68975, upload-time = "2026-03-25T11:19:11.374Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -729,6 +797,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/53/27/d17f25e45820e633a70e6109b35991eda09a5e8000c2a306f0ab7538d48c/pyside6_essentials-6.11.0-cp310-abi3-win_arm64.whl", hash = "sha256:81ca603dbf21bc39f89bb42db215c25ebe0c879a1a4c387625c321d2730ec187", size = 56337457, upload-time = "2026-03-23T12:43:43.573Z" }, ] +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3"