Kirjoitin ohjelmointikielen. Näin voit myös.

Viimeisten kuuden kuukauden aikana olen työskennellyt Pinecone-nimisellä ohjelmointikielellä. En kutsuisi sitä vielä kypsäksi, mutta sillä on jo tarpeeksi ominaisuuksia, jotka toimivat käytettäväksi, kuten:

  • muuttujat
  • toimintoja
  • käyttäjän määrittelemät rakenteet

Jos olet kiinnostunut siitä, tarkista Pineconen aloitussivu tai sen GitHub-repo.

En ole asiantuntija. Kun aloitin tämän projektin, minulla ei ollut aavistustakaan siitä, mitä tein, enkä vieläkään. Olen käynyt nolla kurssia kielen luomisesta, lukenut vain vähän siitä verkossa, enkä ole noudattanut paljoakaan minulle annettuja neuvoja.

Ja silti tein silti täysin uuden kielen. Ja se toimii. Joten minun täytyy tehdä jotain oikein.

Tässä viestissä sukellan konepellin alle ja näytän sinulle putkilinjan Pinecone (ja muut ohjelmointikielet) avulla, jolla lähdekoodi muutetaan taikaksi.

Kosken myös joitain kompromisseja, jotka olen tehnyt, ja miksi tein tekemäni päätökset.

Tämä ei ole suinkaan täydellinen opetusohjelma ohjelmointikielen kirjoittamiseen, mutta se on hyvä lähtökohta, jos olet kiinnostunut kielen kehityksestä.

Päästä alkuun

"Minulla ei ole aavistustakaan, mistä edes aloittaisin", kuulen paljon, kun kerron muille kehittäjille, että kirjoitan kieltä. Jos tämä on reaktiosi, käyn nyt läpi joitain tehtyjä alustavia päätöksiä ja vaiheita, jotka tehdään aloitettaessa uutta kieltä.

Koottu vs tulkittu

Kieliä on kahta päätyyppiä: käännetty ja tulkittu:

  • Kääntäjä selvittää kaiken, mitä ohjelma tekee, muuttaa sen "konekoodiksi" (muodossa, jota tietokone voi ajaa todella nopeasti) ja tallentaa sen myöhemmin suoritettavaksi.
  • Tulkki käy läpi lähdekoodin rivi riviltä selvittäen mitä se tekee menossa.

Teknisesti mitä tahansa kieltä voitaisiin kääntää tai tulkita, mutta jommallakummalla kielellä on yleensä enemmän järkeä. Yleensä tulkkaus on yleensä joustavampaa, kun taas käännöksellä on yleensä parempi suorituskyky. Mutta tämä vain naarmuttaa hyvin monimutkaisen aiheen pintaa.

Arvostan suuresti suorituskykyä ja huomasin, että ohjelmointikielistä puuttuu sekä korkean suorituskyvyn että yksinkertaisuuden suuntautunut ohjelmointikieli, joten jatkoin kääntämistä Pineconelle.

Tämä oli tärkeä päätös tehdä varhaisessa vaiheessa, koska se vaikuttaa moniin kielisuunnittelupäätöksiin (esimerkiksi staattinen kirjoittaminen on suuri etu käännetyille kielille, mutta ei niin paljon tulkituille kielille).

Huolimatta siitä, että Pinecone on suunniteltu kääntämistä ajatellen, sillä on täysin toimiva tulkki, joka oli ainoa tapa käyttää sitä jonkin aikaa. Tähän on useita syitä, jotka selitän myöhemmin.

Kielen valitseminen

Tiedän, että se on vähän meta, mutta ohjelmointikieli on itse ohjelma, joten sinun on kirjoitettava se kielellä. Valitsin C ++: n sen suorituskyvyn ja suuren ominaisuuksien takia. Lisäksi nautin todella työskentelystä C ++: ssa.

Jos kirjoitat tulkittua kieltä, on järkevää kirjoittaa se kootulle kielelle (kuten C, C ++ tai Swift), koska tulkkisi ja tulkkisi tulkin kielellä menetetty suorituskyky yhdistyy.

Jos aiot kääntää, hitaampi kieli (kuten Python tai JavaScript) on hyväksyttävämpi. Kääntöaika voi olla huono, mutta mielestäni se ei ole läheskään yhtä iso juttu kuin huono ajoaika.

Korkean tason suunnittelu

Ohjelmointikieli on yleensä rakennettu putkijonoksi. Eli sillä on useita vaiheita. Jokaisessa vaiheessa tiedot on muotoiltu tietyllä, hyvin määritellyllä tavalla. Sillä on myös toimintoja tietojen muuntamiseksi jokaisesta vaiheesta toiseen.

Ensimmäinen vaihe on merkkijono, joka sisältää koko syötelähdetiedoston. Viimeinen vaihe on jotain, joka voidaan ajaa. Tämä käy selväksi, kun käymme läpi Pinecone-putkilinjan askel askeleelta.

Lexing

Ensimmäinen vaihe useimmilla ohjelmointikielillä on lexing tai tokenization. 'Lex' on lyhenne sanoista leksikaalinen analyysi, erittäin hieno sana, joka jakaa joukon tekstiä rahakkeiksi. Sanalla "tokenizer" on paljon järkevämpää, mutta "lexer" on niin hauskaa sanoa, että käytän sitä joka tapauksessa.

Tunnukset

Tunnus on pieni kielen yksikkö. Tunnus voi olla muuttujan tai funktion nimi (AKA-tunniste), operaattori tai numero.

Lexerin tehtävä

Lekserin on tarkoitus ottaa merkkijono, joka sisältää kokonaisen tiedostomäärän lähdekoodin, ja sylkeä luettelon, joka sisältää kaikki tunnukset.

Putkilinjan tulevat vaiheet eivät viittaa takaisin alkuperäiseen lähdekoodiin, joten lexerin on tuotettava kaikki tarvitsemansa tiedot. Syy tähän suhteellisen tiukaan putkimuodoon on, että lexer voi tehdä tehtäviä, kuten poistaa kommentteja tai havaita, onko jokin numero tai tunniste. Haluat pitää tämän logiikan lukittuna lexerin sisälle, joten sinun ei tarvitse ajatella näitä sääntöjä kirjoittaessasi muuta kieltä, joten voit muuttaa tämän tyyppistä syntaksia yhdessä paikassa.

Joustava

Päivänä kun aloitin kielen, kirjoitin ensimmäisenä yksinkertaisen lexerin. Pian sen jälkeen aloin oppia työkaluista, jotka tekisivät lexingin yksinkertaisemmaksi ja vähemmän bugiseksi.

Hallitseva tällainen työkalu on Flex, ohjelma, joka tuottaa lexereitä. Annat sille tiedoston, jolla on erityinen syntakse kuvaamaan kielioppi. Siitä se tuottaa C-ohjelman, joka leksoi merkkijonon ja tuottaa halutun tuloksen.

Minun päätökseni

Päätin pitää toistaiseksi kirjoittamani lexerin. Loppujen lopuksi en nähnyt merkittäviä etuja Flexin käytöstä, ainakaan riitä perustelemaan riippuvuuden lisäämistä ja monimutkaistamaan rakennusprosessia.

Lekserini on vain muutama sata riviä pitkä ja aiheuttaa minulle vain vähän ongelmia. Oman lexerin vierittäminen antaa minulle myös enemmän joustavuutta, kuten kyvyn lisätä operaattori kieleen muokkaamatta useita tiedostoja.

Jäsennys

Putkilinjan toinen vaihe on jäsennin. Jäsennys muuttaa merkkiluettelon solmuiksi. Puu, jota käytetään tämän tyyppisten tietojen tallentamiseen, tunnetaan nimellä Abstract Syntax Tree tai AST. Ainakin Pineconessa, AST: llä ei ole tietoa tyyppeistä tai mitkä tunnisteet ovat. Se on yksinkertaisesti jäsennelty rahakkeita.

Parser-velvollisuudet

Jäsennys lisää rakenteen järjestettyyn luetteloon merkkejä, jotka lekseri tuottaa. Epäselvyyksien lopettamiseksi jäsentäjän on otettava huomioon sulkeet ja toimintojen järjestys. Operaattoreiden yksinkertainen jäsentäminen ei ole kovin vaikeaa, mutta kun lisää kielirakenteita lisätään, jäsentämisestä voi tulla hyvin monimutkaista.

Bison

Jälleen kerran tehtiin päätös osallistua kolmannen osapuolen kirjastoon. Hallitseva jäsentävä kirjasto on Bison. Bison toimii paljon kuten Flex. Kirjoitat tiedoston mukautetussa muodossa, joka tallentaa kieliopitiedot, ja sitten Bison käyttää sitä C-ohjelman luomiseen, joka suorittaa jäsentämisen. En valinnut Bisonin käyttöä.

Miksi mukautettu on parempi

Lekserin kanssa päätös käyttää omaa koodiani oli melko ilmeinen. Lekseri on niin triviaali ohjelma, että omien kirjoittamatta jättäminen tuntui melkein yhtä typerältä kuin kirjoittamatta omaa "vasemmanpuoleista".

Parserin kanssa on eri asia. Pinecone-jäsennin on tällä hetkellä 750 riviä pitkä, ja olen kirjoittanut niistä kolme, koska kaksi ensimmäistä olivat roskakoria.

Alun perin tein päätökseni useista syistä, ja vaikka se ei ole mennyt täysin sujuvasti, useimmat niistä pitävät paikkansa. Suurimmat ovat seuraavat:

  • Minimoi kontekstivaihto työnkulussa: kontekstinvaihto C ++: n ja Pinecone: n välillä on tarpeeksi huono heittämättä Bisonin kielioppi-kielioppia
  • Pidä rakennus yksinkertaisena: aina kun kielioppi muuttuu, Bison on ajettava ennen koontia. Tämä voidaan automatisoida, mutta siitä tulee tuska vaihdettaessa rakennusjärjestelmistä toiseen.
  • Tykkään rakentaa hienoja paskaa: en tehnyt Pineconea, koska ajattelin, että se olisi helppoa, joten miksi delegoin keskeisen roolin, kun voisin tehdä sen itse? Mukautettu jäsennin ei välttämättä ole triviaali, mutta se on täysin toteutettavissa.

Alussa en ollut täysin varma menenkö elinkelpoisella polulla, mutta minulle annettiin luottamus siihen, mitä Walter Bright (C ++: n varhaisen version kehittäjä ja D-kielen luoja) sanoi aihe:

"Hieman kiistanalaisempi, en vaivaudu tuhlaamaan aikaa lexer- tai jäsenningeneraattoreiden ja muiden niin kutsuttujen" kääntäjien kääntäjien "kanssa. Ne ovat ajanhukkaa. Lekserin ja jäsentimen kirjoittaminen on pieni prosenttiosuus kääntäjän kirjoittamisen työstä. Generaattorin käyttäminen vie noin yhtä paljon aikaa kuin kirjoittaminen yhdellä kädellä, ja se menee naimisiin generaattorin kanssa (mikä on merkitystä, kun käännetään kääntäjä uudelle alustalle). Ja generaattoreilla on myös valitettava maine lähettämällä surkeita virhesanomia. "

Toimintapuu

Olemme nyt poistuneet yleisten, yleismaailmallisten termien alueelta, tai ainakaan en tiedä enää mitä termit ovat. Ymmärrykseni mukaan se, mitä kutsun 'toimintapuuksi', muistuttaa eniten LLVM: n IR: ää (väliesitys).

Toimintapuun ja abstraktin syntaksipuun välillä on hienovarainen, mutta erittäin merkittävä ero. Minulta kesti jonkin aikaa selvittää, että niiden välillä pitäisi olla jopa eroja (mikä vaikutti jäsentimen uudelleenkirjoittamiseen).

Toimintapuu vs. AST

Yksinkertaisesti sanottuna toimintapuu on AST kontekstin kanssa. Tämä asiayhteys on tietoja, kuten minkä tyyppinen funktio palauttaa, tai että kaksi paikkaa, joissa muuttujaa käytetään, käyttävät itse asiassa samaa muuttujaa. Koska sen on selvitettävä ja muistettava koko asiayhteys, toimintapuun luova koodi tarvitsee paljon nimiavaruuden hakutaulukoita ja muita juttuja.

Toimintapuun suorittaminen

Kun meillä on toimintapuu, koodin suorittaminen on helppoa. Jokaisella toimintosolmulla on toiminto 'suorittaa', joka vie jonkin verran syötettä, tekee mitä tahansa toiminnon pitäisi (mukaan lukien mahdollisesti kutsuvan alitoiminnon) ja palauttaa toiminnan tuloksen. Tämä on tulkki toiminnassa.

Käännösvaihtoehdot

"Mutta odota!" Kuulen sinun sanovan: "Eikö Pinecone ole koottu?" Kyllä se on. Mutta kääntäminen on vaikeampaa kuin tulkinta. On olemassa muutama mahdollinen lähestymistapa.

Rakenna oma kääntäjä

Tämä kuulosti aluksi hyvältä ajatukselta. Rakastan tehdä asioita itse, ja minulla on ollut kutina tekosyystä saada hyvä kokoonpano.

Valitettavasti kannettavan kääntäjän kirjoittaminen ei ole yhtä helppoa kuin koneenkoodin kirjoittaminen kullekin kielielementille. Arkkitehtuurien ja käyttöjärjestelmien lukumäärän takia on epäkäytännöllistä, että kuka tahansa kirjoittaa ristitasoisen kääntäjän taustat.

Jopa Swiftin, Rustin ja Clangin takana olevat joukkueet eivät halua vaivautua kaikessa yksin, joten sen sijaan kaikki käyttävät…

LLVM

LLVM on kokoelma kääntäjän työkaluja. Se on pohjimmiltaan kirjasto, joka muuttaa kielesi kootuksi suoritettavaksi binaariksi. Se näytti täydelliseltä valinnalta, joten hyppäsin sisään. Valitettavasti en tarkistanut veden syvyyttä ja hukkuin heti.

Vaikka LLVM ei ole kokoonpanokieli kovaa, se on jättimäinen monimutkainen kirjasto. Sitä ei ole mahdotonta käyttää, ja heillä on hyvät opetusohjelmat, mutta tajusin, että minun pitäisi saada käytäntöä, ennen kuin olin valmis ottamaan Pinecone-kääntäjän täysin käyttöön.

Transpiling

Halusin jonkinlainen koottu Pinecone ja halusin sen nopeasti, joten käännyin yhden menetelmän puoleen, jonka tiesin pystyvän tekemään työtä: transpiling.

Kirjoitin Pinecone C ++ -lähetinlaitteeseen ja lisäsin mahdollisuuden kääntää lähtölähde automaattisesti GCC: llä. Tämä toimii tällä hetkellä melkein kaikissa Pinecone-ohjelmissa (vaikka on olemassa muutamia reunatapauksia, jotka rikkovat sen). Se ei ole erityisen kannettava tai skaalautuva ratkaisu, mutta se toimii toistaiseksi.

Tulevaisuus

Olettaen, että jatkan Pineconen kehittämistä, se saa LLVM-kääntämisen tuen ennemmin tai myöhemmin. Epäilen, ettei mater ole, kuinka paljon työskentelen sen kanssa, transpileri ei koskaan ole täysin vakaa ja LLVM: n edut ovat lukuisia. Se on vain kysymys siitä, milloin minulla on aikaa tehdä joitain näyteprojekteja LLVM: ssä ja saada ne kiinni.

Siihen asti tulkki on hieno triviaalien ohjelmien kohdalla, ja C ++ -tulkinta toimii useimmissa asioissa, jotka tarvitsevat enemmän suorituskykyä.

Johtopäätös

Toivon, että olen tehnyt ohjelmointikielistä hieman vähemmän salaperäisiä sinulle. Jos haluat tehdä sellaisen itse, suosittelen sitä. Täytäntöönpanon yksityiskohtia on paljon selvitettävissä, mutta pääpiirteiden pitäisi olla riittävät aloittamaan sinut.

Tässä on korkean tason neuvoni pääsyn aloittamiseen (muista, en todellakaan tiedä, mitä teen, joten ota se jyvän suolalla):

  • Jos olet epävarma, mene tulkinnaksi. Tulkitut kielet ovat yleensä helpompia suunnitella, rakentaa ja oppia. En estä sinua kirjoittamasta koottua, jos tiedät, että haluat tehdä sen, mutta jos olet aidalla, tulisin tulkitsemaan.
  • Kun kyseessä on lexers ja parsers, tee mitä haluat. Oman kirjoittamisen puolesta ja sitä vastaan ​​on perusteltuja argumentteja. Loppujen lopuksi, jos ajattelet suunnittelua ja toteutat kaiken järkevällä tavalla, sillä ei ole väliä.
  • Opi putkilinjasta, johon päädyin. Nyt suunnitellun putkilinjan suunnittelussa käytettiin paljon kokeiluja ja virheitä. Olen yrittänyt poistaa AST: t, AST: t, jotka muuttuvat toimintapuiksi, ja muut kauheat ideat. Tämä putki toimii, joten älä muuta sitä, ellei sinulla ole todella hyvää ideaa.
  • Jos sinulla ei ole aikaa tai motivaatiota monimutkaisen yleiskielen käyttöönottoon, kokeile ottaa esoteerinen kieli, kuten Brainfuck. Nämä tulkit voivat olla niin lyhyitä kuin muutama sata riviä.

Minulla on hyvin vähän pahoillani Pinecone-kehityksestä. Tein useita huonoja valintoja matkan varrella, mutta olen kirjoittanut suurimman osan koodista, johon tällaiset virheet vaikuttavat.

Tällä hetkellä Pinecone on riittävän hyvässä tilassa, jotta se toimii hyvin ja sitä voidaan helposti parantaa. Pinecone-kirjoitus on ollut minulle erittäin opettavainen ja nautinnollinen kokemus, ja se on vasta alkamassa.