Programozási nyelvet írtam. Így tudod te is.

Az elmúlt 6 hónapban a Pinecone nevű programozási nyelven dolgoztam. Még nem nevezném kiforrottnak, de már elég funkciója van ahhoz, hogy használható legyen, például:

  • változók
  • funkciókat
  • felhasználó által definiált struktúrák

Ha érdekli, nézze meg a Pinecone céloldalát vagy a GitHub repót.

Nem vagyok szakértő. Amikor elkezdtem ezt a projektet, fogalmam sem volt, mit csinálok, és még mindig nem. Nulla órát vettem részt a nyelvalkotásban, csak keveset olvastam róla online, és nem nagyon követtem a kapott tanácsokat.

És mégis, még mindig készítettem egy teljesen új nyelvet. És működik. Tehát biztosan csinálok valamit jól.

Ebben a bejegyzésben a motorháztető alá merülök, és megmutatom a Pinecone (és más programozási nyelvek) csővezetékét, amellyel a forráskód varázslattá válik.

Kitérek néhány kompromisszumra is, amelyeket már meghoztam, és azt, hogy miért hoztam meg a meghozott döntéseket.

Ez korántsem teljes oktatóanyag a programozási nyelv megírásához, de jó kiindulópont, ha kíváncsi a nyelv fejlesztésére.

Elkezdeni

"Fogalmam sincs, hogy hol is kezdeném", ezt nagyon sokat hallom, amikor azt mondom más fejlesztőknek, hogy nyelvet írok. Abban az esetben, ha ez a reakciója, most végigviszek néhány kezdeti döntést és lépéseket, amelyeket minden új nyelv indításakor megteszünk.

Összeállítva vs Értelmezve

Kétféle nyelv létezik: összeállítva és értelmezve:

  • A fordító kitalálja mindazt, amit egy program elvégez, „gépi kódra” alakítja (egy olyan formátumot, amelyet a számítógép nagyon gyorsan képes futtatni), majd elmenti, hogy később végrehajtsa.
  • Egy tolmács soronként lépeget a forráskódban, és kitalálja, hogy mit csinál.

Gyakorlatilag bármilyen nyelvet össze lehet állítani vagy értelmezni, de az egyik vagy a másik általában értelmesebb egy adott nyelv számára. A tolmácsolás általában rugalmasabb, míg a fordítás nagyobb teljesítményt mutat. De ez csak egy nagyon összetett téma felületét vakarja.

Nagyra értékelem a teljesítményt, és azt tapasztaltam, hogy hiányoznak a nagy teljesítményre és az egyszerűségre orientált programozási nyelvek, ezért a Pinecone-hoz fordítottam.

Ezt fontos döntés volt korán meghozni, mert sok nyelvtervezési döntést ez befolyásol (például a statikus gépelés nagy előnyt jelent az összeállított nyelvek számára, de az értelmezettek számára nem annyira).

Annak ellenére, hogy a Pinecone-t a fordítás szem előtt tartásával tervezték, van egy teljesen funkcionális tolmácsa, amely egy ideig csak így működtethető. Ennek számos oka van, amelyeket később elmagyarázok.

Nyelv kiválasztása

Tudom, hogy ez egy kicsit meta, de a programozási nyelv maga is egy program, és így nyelven kell írnod. A C ++ -ot a teljesítménye és a nagy szolgáltatáskészlet miatt választottam. Valójában szívesen dolgozom a C ++ nyelven.

Ha értelmezett nyelvet ír, sok értelme van egy lefordított nyelvűnek írni (például C, C ++ vagy Swift), mert a tolmácsának és a tolmácsának a nyelvén elveszett teljesítmény összetett lesz.

Ha fordítást tervez, akkor egy lassabb nyelv (például Python vagy JavaScript) elfogadhatóbb. Lehet, hogy a fordítási idő rossz, de véleményem szerint ez közel sem akkora probléma, mint a rossz futási idő.

Magas szintű tervezés

A programozási nyelv általában csővezetékként van felépítve. Vagyis több szakasza van. Minden szakaszban vannak egy meghatározott, jól meghatározott formátumú adatok. Ezenkívül funkciói vannak az adatok átalakításáról az egyes szakaszokról a következőre.

Az első szakasz egy karaktersorozat, amely a teljes bemeneti forrásfájlt tartalmazza. Az utolsó szakasz futtatható. Ez mind egyértelművé válik, amikor lépésről lépésre haladunk a Pinecone csővezetéken.

Lexing

Az első lépés a legtöbb programozási nyelvben a lexing vagy tokenizálás. A „Lex” rövidítése a lexikai elemzésnek, nagyon divatos szó arra, hogy egy csomó szöveget tokenekre bontson. A „tokenizer” szónak sokkal több értelme van, de a „lexer” annyira szórakoztató azt mondani, hogy én mégis használom.

Tokenek

A token a nyelv kis egysége. A token lehet változó vagy függvény neve (AKA azonosító), operátor vagy szám.

A Lexer feladata

A lexer állítólag egy teljes fájl forráskódot tartalmazó karakterláncot vesz be, és kiköpi az összes tokent tartalmazó listát.

A folyamat további szakaszai nem utalnak vissza az eredeti forráskódra, ezért a lexernek minden szükséges információt elő kell állítania. Ennek a viszonylag szigorú csővezeték-formátumnak az az oka, hogy a lexer olyan feladatokat végezhet, mint például a megjegyzések eltávolítása vagy annak észlelése, hogy valami szám vagy azonosító. Ezt a logikát zárva szeretné tartani a lexer belsejében, így nem kell ezekre a szabályokra gondolnia a nyelv többi részének írásakor, és így az ilyen típusú szintaxist egy helyen módosíthatja.

Flex

Azon a napon, amikor elkezdtem a nyelvet, először egy egyszerű lexert írtam. Röviddel ezután elkezdtem olyan eszközöket megismerni, amelyek állítólag egyszerűbbé és kevésbé hibássá teszik a lexinget.

Az uralkodó ilyen eszköz a Flex, egy program, amely lexereket generál. Olyan fájlt adsz neki, amelynek speciális szintaxisa van a nyelv nyelvtanának leírására. Ebből generál egy C programot, amely lexxeli egy karakterláncot és előállítja a kívánt kimenetet.

A dontesem

Úgy döntöttem, hogy egyelőre megtartom az általam írt lexert. Végül nem láttam jelentős előnyöket a Flex használatának, legalábbis nem elégségesnek ahhoz, hogy igazoljam a függőséget és bonyolítsam a felépítési folyamatot.

A lexerem csak néhány száz soros, és ritkán okoz gondot. A saját lexerem gördítése szintén nagyobb rugalmasságot biztosít, például képes vagyok operátort hozzáadni a nyelvhez több fájl szerkesztése nélkül.

Elemzés

A csővezeték második szakasza az elemző. Az elemző a tokenek listáját csomópontok fájává változtatja. Az ilyen típusú adatok tárolására használt fa absztrakt szintaxisfa vagy AST néven ismert. Legalábbis a Pinecone-ban az AST-nek nincs információja a típusokról vagy arról, hogy melyik azonosítók vannak. Ez egyszerűen strukturált tokenek.

Elemző feladatai

Az elemző struktúrát ad hozzá a lexer által előállított tokenek rendezett listájához. A kétértelműségek megállításához az elemzőnek figyelembe kell vennie a zárójeleket és a műveletek sorrendjét. Az operátorok egyszerű elemzése nem túl nehéz, de ahogy több nyelvi konstrukciót adunk hozzá, az elemzés nagyon összetetté válhat.

Bölény

Ismét döntöttek egy harmadik fél könyvtárának bevonásáról. Az uralkodó elemző könyvtár a Bison. Bölény sokat dolgozik, mint a Flex. Írsz egy fájlt egyéni formátumban, amely tárolja a nyelvtani információkat, majd Bison ezt felhasználva létrehoz egy C programot, amely elvégzi az elemzést. Nem a Bölényt választottam.

Miért jobb az egyedi?

A lexerrel meglehetősen nyilvánvaló volt a saját kódom használata. A lexer olyan triviális program, hogy ha nem írok sajátot, akkora butaságnak érzem magam, mintha nem írnám meg a saját baloldalt.

Az elemzővel ez más kérdés. A Pinecone elemzőm jelenleg 750 soros, és ebből hármat írtam, mert az első kettő kuka volt.

Eredetileg számos okból hoztam meg a döntést, és bár ez nem ment teljesen zökkenőmentesen, a legtöbbjük igaz. A legfontosabbak a következők:

  • Minimalizálja a kontextusváltást a munkafolyamatban: a kontextusváltás a C ++ és a Pinecone között elég rossz anélkül, hogy bedobná Bison nyelvtanát.
  • Legyen egyszerű az összeállítás: minden alkalommal, amikor a nyelvtan megváltozik, a bölényt az összeállítás előtt futtatni kell. Ez automatizálható, de fájdalomossá válik, amikor a build rendszerek között váltunk.
  • Szeretek klassz szart építeni: Nem készítettem Pinecone-t, mert azt hittem, hogy könnyű lesz, akkor miért delegálnék egy központi szerepet, amikor magam is meg tudom csinálni? Lehet, hogy az egyedi elemző nem triviális, de teljesen kivitelezhető.

Kezdetben nem voltam teljesen biztos abban, hogy járok-e járható úton, de bizalmat kaptam arról, amit Walter Bright (a C ++ korai változatának fejlesztője és a D nyelv megalkotója) mondott a téma:

"Valamivel ellentmondásosabb, nem zavarnám az időt a lexer vagy elemző generátorokkal és más úgynevezett" fordító fordítókkal ". Időpocsékolás. A lexer és az elemző írása a fordító írásának apró százaléka. A generátor használata körülbelül annyi időt vesz igénybe, mint egy kézzel írni, és feleségül veszi a generátorhoz (ami fontos, ha a fordítót egy új platformra portáljuk). És a generátoroknak az a sajnálatos hírnevük is van, hogy silány hibaüzeneteket bocsátanak ki. ”

Akciófa

Most elhagytuk a közönséges, univerzális kifejezések területét, vagy legalábbis már nem tudom, mik a kifejezések. Megértésem szerint az, amit „akciófának” nevezek, leginkább hasonlít az LLVM IR-jére (köztes reprezentáció).

Finom, de nagyon jelentős különbség van az akciófa és az absztrakt szintaxisfa között. Elég sok időbe telt, mire rájöttem, hogy még különbségnek is kell lennie közöttük (ami hozzájárult az elemző újraírásának szükségességéhez).

Akciófa vs AST

Leegyszerűsítve: az akciófa az AST kontextussal. Ez a kontextus olyan információ, mint például, hogy egy függvény milyen típussal tér vissza, vagy hogy egy változó két helye valójában ugyanazt a változót használja. Mivel ki kell találnia és emlékeznie kell erre az egész kontextusra, az akciófát létrehozó kódnak sok névtér-kereső táblára és egyéb dologababokra van szüksége.

Az Action Tree futtatása

Ha megvan az akciófa, a kód futtatása egyszerű. Minden cselekvési csomópontnak van egy „végrehajtási” funkciója, amely bevesz valamilyen bemenetet, mindent megtesz, amit a műveletnek meg kell tennie (beleértve az esetleges alművelet meghívását is) és visszaadja a művelet kimenetét. Ez a tolmács cselekvésben.

Opciók összeállítása

"De várj!" Hallom, hogy azt mondod: "Nem kell Pinecone-t összeállítani?" Igen, ez az. De az összeállítás nehezebb, mint az értelmezés. Van néhány lehetséges megközelítés.

Készítsd el a Saját fordítót

Ez elsőre jó ötletnek tűnt. Nagyon szeretek magam is készíteni a dolgokat, és mentségemre fakadt, hogy jó legyen az összeszerelés.

Sajnos a hordozható fordító megírása nem olyan egyszerű, mint egyes gépi kódok írása minden nyelvi elemhez. Az architektúrák és az operációs rendszerek száma miatt célszerűtlen, hogy egyén keresztplatform fordító háttérprogramot írjon.

Még a Swift, Rust és Clang mögött álló csapatok sem akarnak egyedül bajlódni mindennel, ezért ehelyett mindannyian használják…

LLVM

Az LLVM a fordítóeszközök gyűjteménye. Ez alapvetően egy könyvtár, amely a nyelvet lefordított bináris futtathatóvá teszi. Tökéletes választásnak tűnt, ezért rögtön beugrottam. Sajnos nem ellenőriztem, milyen mély a víz, és azonnal megfulladtam.

Az LLVM ugyan nem nehéz az összeszerelési nyelvhez, de gigantikus komplex könyvtár. Nem lehetetlen használni, és jó oktatóikkal rendelkeznek, de rájöttem, hogy gyakorolnom kell, mielőtt készen állok a Pinecone fordító teljes körű megvalósítására.

Transpiling

Valamiféle összeállított Pinecone-t akartam, és gyorsan akartam, ezért az egyik módszer felé fordultam, amelyről tudtam, hogy működni tudok: az átültetés.

Írtam egy Pinecone-t a C ++ transzpilerbe, és hozzáadtam a képességet, hogy a kimeneti forrást automatikusan GCC-vel fordítsam. Ez jelenleg szinte az összes Pinecone programnál működik (bár van néhány éles eset, amely megtöri). Ez nem különösebben hordozható vagy méretezhető megoldás, de egyelőre működik.

Jövő

Feltéve, hogy folytatom a Pinecone fejlesztését, előbb-utóbb megkapja az LLVM fordítási támogatását. Gyanítom, hogy nincs anyám, mennyit dolgozom rajta, a transzpiler soha nem lesz teljesen stabil, és az LLVM előnyei számosak. Csak arról van szó, hogy mikor lesz időm néhány mintaprojektet készíteni az LLVM-ben, és lerázom magam.

Addig a tolmács kiválóan használható triviális programokhoz, a C ++ nyelvű transzponálás pedig a legtöbb nagyobb teljesítményt igénylő dologhoz működik.

Következtetés

Remélem, a programozási nyelveket kicsit kevésbé titokzatosabbá tettem számodra. Ha mégis szeretne magának készíteni egyet, nagyon ajánlom. Rengeteg megvalósítási részletet kell kitalálni, de az itt található vázlatnak elegendőnek kell lennie az induláshoz.

Itt van a magas szintű tanácsom az induláshoz (ne feledje, nem igazán tudom, mit csinálok, ezért vegye be egy szem sóval):

  • Ha kétségei vannak, menjen értelmezni. Az értelmezett nyelveket általában könnyebb megtervezni, felépíteni és megtanulni. Nem tántorítom el, hogy írj egy összeállítottat, ha tudod, hogy ezt akarod csinálni, de ha a kerítésen állsz, elmagyaráznám.
  • Ha lexerekről és parserekről van szó, tegyen bármit. Megalapozott érvek szólnak a saját írása mellett. Végül, ha végiggondolja a tervét, és mindent ésszerűen megvalósít, akkor nem számít.
  • Tanuljon abból a csővezetékből, amivel végül kötöttem ki. Sok próba és hiba történt a mostani csővezeték tervezésében. Megpróbáltam megszüntetni az AST-ket, az AST-ket, amelyek cselekvési fákká változnak a helyükön, és más szörnyű ötleteket. Ez a csővezeték működik, ezért ne változtassa meg, hacsak nincs igazán jó ötlete.
  • Ha nincs ideje vagy motivációja egy komplex, általános célú nyelv bevezetésére, próbáljon meg egy olyan ezoterikus nyelvet bevezetni, mint a Brainfuck. Ezek a tolmácsok akár néhány száz sorosak is lehetnek.

Nagyon keveset sajnálom a Pinecone fejlesztését. Számos rossz döntést hoztam útközben, de az ilyen hibák által érintett kód nagy részét átírtam.

Jelenleg a Pinecone elég jó állapotban van ahhoz, hogy jól működjön és könnyen javítható legyen. A toboz írása számomra rendkívül oktató és élvezetes élmény volt, és ez csak most kezdődik.