Titeln på den här artikeln återspeglar en fråga som jag får höra om och om igen i forum eller i e-postmeddelanden som jag får.
Jag tror att alla nyfikna utvecklare har ställt den minst en gång. Det är normalt att vara fascinerad av hur programmeringsspråk fungerar. Tyvärr är de flesta svar vi läser väldigt akademiska eller teoretiska. Vissa andra innehåller för mycket implementeringsdetaljer. Efter att ha läst dem undrar vi fortfarande hur saker och ting fungerar i praktiken.
Så vi ska besvara den. Ja, vi kommer att se vad som är processen för att skapa ditt eget fullständiga språk med en kompilator för det och vad inte.
Översikten
De flesta personer som vill lära sig hur man ”skapar ett programmeringsspråk” letar i själva verket efter information om hur man bygger en kompilator. De vill förstå den mekanik som gör det möjligt att utföra ett nytt programmeringsspråk.
En kompilator är en grundläggande pusselbit, men för att skapa ett nytt programmeringsspråk krävs mer än så:
1) Ett språk måste utformas: Språkskaparen måste fatta några grundläggande beslut om vilka paradigm som ska användas och om språkets syntax
2) En kompilator måste skapas
3) Ett standardbibliotek måste implementeras
4) Stödverktyg som redaktörer och byggsystem måste tillhandahållas
Låt oss se mer i detalj vad var och en av dessa punkter innebär.
Design av ett programmeringsspråk
Om du bara vill skriva din egen kompilator för att lära dig hur dessa saker fungerar kan du hoppa över denna fas. Du kan bara ta en delmängd av ett befintligt språk eller komma på en enkel variant av det och börja. Om du däremot har planer på att skapa ditt alldeles egna programmeringsspråk måste du fundera på saken.
Jag anser att utformningen av ett programmeringsspråk är uppdelad i två faser:
- Den stora fasen
- Förfiningsfasen
I den första fasen besvarar vi de grundläggande frågorna om vårt språk.
- Hurdant exekveringsparadigm vill vi använda? Ska det vara imperativt eller funktionellt? Eller kanske baserad på tillståndsmaskiner eller affärsregler?
- Vill vi ha statisk typning eller dynamisk typning?
- Vilken typ av program kommer språket att vara bäst för? Kommer det att användas för små skript eller stora system?
- Vad är viktigast för oss: prestanda? Läsbarhet?
- Vill vi att det ska likna ett befintligt programmeringsspråk? Kommer det att rikta sig till C-utvecklare eller vara lätt att lära sig för dem som kommer från Python?
- Vill vi att det ska fungera på en viss plattform (JVM, CLR)?
- Vad för slags metaprogrammeringsmöjligheter vill vi stödja, om det finns några? Makroer? Mallar? Reflection?
I den andra fasen kommer vi att fortsätta att utveckla språket allteftersom vi använder det. Vi kommer att stöta på problem, på saker som är mycket svåra eller omöjliga att uttrycka i vårt språk och vi kommer att sluta med att utveckla det. Den andra fasen är kanske inte lika glamorös som den första, men det är den fas där vi fortsätter att trimma vårt språk för att göra det användbart i praktiken, så vi ska inte underskatta den.
Bygga en kompilator
Bygga en kompilator är det mest spännande steget i skapandet av ett programmeringsspråk. När vi väl har en kompilator kan vi faktiskt väcka vårt språk till liv. En kompilator gör det möjligt för oss att börja leka med språket, använda det och identifiera vad vi saknar i den ursprungliga utformningen. Den gör det möjligt att se de första resultaten. Det är svårt att överträffa glädjen i att köra det första programmet som är skrivet i vårt helt nya programmeringsspråk, oavsett hur enkelt programmet är.
Men hur bygger vi en kompilator?
Som allting som är komplext gör vi det i flera steg:
- Vi bygger en parser: Parsern är den del av kompilatorn som tar emot texten i våra program och förstår vilka kommandon de uttrycker. Den känner igen uttrycken, påståendena, klasserna och skapar interna datastrukturer för att representera dem. Resten av parsern kommer att arbeta med dessa datastrukturer, inte med den ursprungliga texten
- (valfritt) Vi översätter parseringsträdet till ett abstrakt syntaxträd. Typiskt sett är de datastrukturer som skapas av parsern lite låg nivå eftersom de innehåller många detaljer som inte är avgörande för vår kompilator. På grund av detta vill vi ofta omorganisera datastrukturerna till något lite högre nivå
- Vi löser upp symboler. I koden skriver vi saker som
a + 1
. Vår kompilator måste ta reda på vada
hänvisar till. Är det ett fält? Är det en variabel? Är det en metodparameter? Vi undersöker koden för att svara på det - Vi validerar trädet. Vi måste kontrollera att programmeraren inte har begått några fel. Försöker han summera en boolean och en int? Eller får han tillgång till ett icke-existerande fält? Vi måste producera lämpliga felmeddelanden
- Vi genererar maskinkoden. I det här skedet översätter vi koden till något som maskinen kan exekvera. Det kan vara riktig maskinkod eller bytecode för en virtuell maskin
- (valfritt) Vi utför länkningen. I vissa fall måste vi kombinera den maskinkod som produceras för våra program med koden för statiska bibliotek som vi vill inkludera, för att generera en enda körbar dator
Behövs det alltid en kompilator? Nej. Vi kan ersätta den med andra medel för att exekvera koden:
- Vi kan skriva en tolk: en tolk är i huvudsak ett program som gör steg 1-4 av en kompilator och sedan direkt exekverar det som specificeras av det abstrakta syntaxträdet
- Vi kan skriva en transpiler: En transpiler gör det som anges i steg 1-4 och ger sedan ut en kod på ett språk för vilket vi redan har en kompilator (t.ex. C++ eller Java)
De här två alternativen är helt giltiga och ofta är det vettigt att välja ett av dessa två eftersom den ansträngning som krävs vanligtvis är mindre.
Vi skrev en artikel som förklarar hur man skriver en transpiler. Ta en titt på den om du vill se ett praktiskt exempel, med kod.
I den här artikeln förklarar vi mer detaljerat skillnaden mellan en kompilator och en tolk.
Ett standardbibliotek för ditt programmeringsspråk
Varje programmeringsspråk behöver göra några saker:
- Utskrift på skärmen
- Access till filsystemet
- Användning av nätverksanslutningar
- Skapa grafiska användargränssnitt
Dessa är de grundläggande funktionaliteterna för att interagera med resten av systemet. Utan dem är ett språk i princip värdelöst. Hur tillhandahåller vi dessa funktioner? Genom att skapa ett standardbibliotek. Detta kommer att vara en uppsättning funktioner eller klasser som kan anropas i de program som skrivs i vårt programmeringsspråk men som kommer att skrivas i något annat språk. Många språk har till exempel standardbibliotek som åtminstone delvis är skrivna i C.
Ett standardbibliotek kan sedan innehålla mycket mer. Till exempel klasser för att representera de viktigaste samlingarna som listor och kartor, eller för att bearbeta vanliga format som JSON eller XML. Ofta innehåller det avancerade funktioner för att behandla strängar och reguljära uttryck.
Med andra ord är det mycket arbete att skriva ett standardbibliotek. Det är inte glamoröst, det är inte konceptuellt lika intressant som att skriva en kompilator, men det är ändå en grundläggande komponent för att göra ett programmeringsspråk livskraftigt.
Det finns sätt att undvika detta krav. Ett är att låta språket köras på någon plattform och göra det möjligt att återanvända ett annat språks standardbibliotek. Till exempel kan alla språk som körs på JVM helt enkelt återanvända Javas standardbibliotek.
Stödverktyg för ett nytt programmeringsspråk
För att göra ett språk användbart i praktiken behöver vi ofta skriva några stödverktyg.
Det mest uppenbara är en editor. En specialiserad editor med syntaxmarkering, inline-felkontroll och automatisk komplettering är numera ett måste för att göra varje utvecklare produktiv.
Men idag är utvecklare bortskämda och de förväntar sig alla möjliga andra stödverktyg. En felsökare kan till exempel vara mycket användbar för att ta itu med ett otäckt fel. Eller ett byggsystem som liknar maven eller gradle kan vara något som användarna kommer att efterfråga senare.
I början kan det räcka med en editor, men i takt med att användarbasen växer kommer även projektens komplexitet att öka och fler stödverktyg kommer att behövas. Förhoppningsvis kommer det då att finnas en gemenskap som är villig att hjälpa till att bygga dem.
Sammanfattning
Skapa ett programmeringsspråk är en process som verkar mystisk för många utvecklare. I den här artikeln har vi försökt visa att det bara är en process. Det är fascinerande och inte lätt, men det går att göra.
Du kanske vill bygga ett programmeringsspråk av olika anledningar. Ett bra skäl är för skojs skull, ett annat för att lära sig hur kompilatorer fungerar. Ditt språk kan i slutändan bli mycket användbart eller inte, beroende på många faktorer. Men om du har roligt och/eller lär dig när du bygger det så är det värt att investera lite tid i detta.
Och naturligtvis kommer du att kunna skryta med dina medutvecklare.
Om du vill lära dig mer om hur man skapar ett språk så ta en titt på de andra resurserna som vi har skapat: Lär dig hur man bygger språk.
Du kanske också är intresserad av några av våra artiklar: