Overskriften på denne artikel afspejler et spørgsmål, som jeg hører igen og igen i fora eller i e-mails, jeg modtager.
Jeg tror, at alle nysgerrige udviklere har stillet det mindst én gang. Det er normalt at være fascineret af, hvordan programmeringssprog fungerer. Desværre er de fleste svar, vi læser, meget akademiske eller teoretiske. Nogle andre indeholder for mange implementeringsdetaljer. Efter at have læst dem undrer vi os stadig over, hvordan tingene fungerer i praksis.
Så vi har tænkt os at besvare det. Ja, vi vil se, hvad der er processen for at skabe dit eget fulde sprog med en compiler til det og hvad ikke.
Oversigten
De fleste personer, der ønsker at lære, hvordan man “skaber et programmeringssprog”, leder i virkeligheden efter oplysninger om, hvordan man bygger en compiler. De ønsker at forstå de mekanismer, der gør det muligt at udføre et nyt programmeringssprog.
En compiler er en grundlæggende brik i puslespillet, men at lave et nyt programmeringssprog kræver mere end det:
1) Et sprog skal designes: sprogskaberen skal træffe nogle grundlæggende beslutninger om de paradigmer, der skal anvendes, og sprogets syntaks
2) Der skal oprettes en compiler
3) Der skal implementeres et standardbibliotek
4) Der skal leveres understøttende værktøjer som editors og build-systemer
Lad os se nærmere på, hvad hvert af disse punkter indebærer.
Design af et programmeringssprog
Hvis du blot ønsker at skrive din egen compiler for at lære, hvordan disse ting fungerer, kan du springe denne fase over. Du kan bare tage en delmængde af et eksisterende sprog eller finde på en simpel variation af det og komme i gang. Men hvis du har planer om at skabe dit helt eget programmeringssprog, er du nødt til at tænke dig om.
Jeg tænker på at designe et programmeringssprog som opdelt i to faser:
- Den store fase
- Forfiningsfasen
I den første fase besvarer vi de grundlæggende spørgsmål om vores sprog.
- Hvilket eksekveringsparadigme ønsker vi at bruge? Vil det være imperativt eller funktionelt? Eller måske baseret på tilstandsmaskiner eller forretningsregler?
- Vil vi have statisk typning eller dynamisk typning?
- Hvilken slags programmer vil dette sprog være bedst til? Skal det bruges til små scripts eller store systemer?
- Hvad betyder mest for os: ydeevne? Læsbarhed?
- Vil vi have det til at ligne et eksisterende programmeringssprog? Vil det være rettet mod C-udviklere eller let at lære for dem, der kommer fra Python?
- Vil vi have det til at fungere på en bestemt platform (JVM, CLR)?
- Hvilken slags metaprogrammeringsmuligheder ønsker vi at understøtte, hvis der er nogen? Makroer? Skabeloner? Refleksion?
I anden fase vil vi fortsætte med at udvikle sproget i takt med, at vi bruger det. Vi vil støde på problemer, på ting, der er meget vanskelige eller umulige at udtrykke i vores sprog, og vi vil ende med at udvikle det. Den anden fase er måske ikke så glamourøs som den første, men det er den fase, hvor vi bliver ved med at finjustere vores sprog, så det bliver brugbart i praksis, så vi bør ikke undervurdere den.
Bygning af en compiler
Bygning af en compiler er det mest spændende skridt i skabelsen af et programmeringssprog. Når vi først har en compiler, kan vi faktisk bringe vores sprog til live. En compiler gør det muligt for os at begynde at lege med sproget, bruge det og identificere, hvad vi savner i det oprindelige design. Den giver os mulighed for at se de første resultater. Det er svært at slå glæden ved at udføre det første program, der er skrevet i vores helt nye programmeringssprog, uanset hvor simpelt programmet er.
Men hvordan bygger vi en compiler?
Som alt andet komplekst gør vi det i flere trin:
- Vi bygger en parser: Parseren er den del af vores compiler, der tager teksten fra vores programmer og forstår, hvilke kommandoer de udtrykker. Den genkender udtrykkene, udsagnene og klasserne, og den skaber interne datastrukturer til at repræsentere dem. Resten af parseren vil arbejde med disse datastrukturer, ikke med den oprindelige tekst
- (valgfrit) Vi oversætter parsetræet til et abstrakt syntaksetræ. Typisk er de datastrukturer, der produceres af parseren, lidt lavt niveau, da de indeholder en masse detaljer, som ikke er afgørende for vores compiler. På grund af dette ønsker vi ofte at omarrangere datastrukturerne i noget lidt højere niveau
- Vi opløser symboler. I koden skriver vi ting som
a + 1
. Vores compiler skal finde ud af, hvada
henviser til. Er det et felt? Er det en variabel? Er det en metodeparameter? Vi undersøger koden for at få svar på det - Vi validerer træet. Vi skal kontrollere, at programmøren ikke har begået fejl. Forsøger han at summere en boolean og en int? Eller har han adgang til et ikke-eksisterende felt? Vi er nødt til at producere passende fejlmeddelelser
- Vi genererer maskinkoden. På dette tidspunkt oversætter vi koden til noget, som maskinen kan afvikle. Det kan være rigtig maskinkode eller bytekode til en virtuel maskine
- (valgfrit) Vi udfører linking. I nogle tilfælde skal vi kombinere den maskinkode, der er produceret for vores programmer, med koden for statiske biblioteker, som vi ønsker at inkludere, for at generere en enkelt eksekverbar fil
Har vi altid brug for en compiler? Nej. Vi kan erstatte den med andre midler til at udføre koden:
- Vi kan skrive en fortolker: En fortolker er i det væsentlige et program, der udfører trin 1-4 af en compiler og derefter direkte udfører det, der er specificeret af det abstrakte syntaksetræ
- Vi kan skrive en transpiler: en transpiler udfører det, der er specificeret i trin 1-4, og udsender derefter noget kode i et sprog, som vi allerede har en compiler til (f.eks. C++ eller Java)
Disse to alternativer er fuldt ud gyldige, og ofte giver det mening at vælge en af disse to, fordi den krævede indsats typisk er mindre.
Vi har skrevet en artikel, der forklarer, hvordan man skriver en transpiler. Tag et kig på den, hvis du vil se et praktisk eksempel, med kode.
I denne artikel forklarer vi mere detaljeret forskellen mellem en compiler og en fortolker.
Et standardbibliotek til dit programmeringssprog
Alle programmeringssprog skal kunne nogle få ting:
- Udskrivning på skærmen
- Access til filsystemet
- Anvendelse af netværksforbindelser
- Skabelse af GUI’er
Dette er de grundlæggende funktionaliteter til at interagere med resten af systemet. Uden dem er et sprog stort set ubrugeligt. Hvordan tilvejebringer vi disse funktionaliteter? Ved at skabe et standardbibliotek. Dette vil være et sæt af funktioner eller klasser, som kan kaldes i de programmer, der er skrevet i vores programmeringssprog, men som vil være skrevet i et andet sprog. F.eks. har mange sprog standardbiblioteker, der i det mindste delvist er skrevet i C.
Et standardbibliotek kan så indeholde meget mere. F.eks. klasser til at repræsentere de vigtigste samlinger som lister og kort, eller til at behandle almindelige formater som JSON eller XML. Ofte vil det indeholde avancerede funktionaliteter til behandling af strenge og regulære udtryk.
Med andre ord er det et stort arbejde at skrive et standardbibliotek. Det er ikke glamourøst, det er ikke konceptuelt set lige så interessant som at skrive en compiler, men det er stadig en grundlæggende komponent for at gøre et programmeringssprog levedygtigt.
Der er måder at undgå dette krav på. Den ene er at få sproget til at køre på en eller anden platform og gøre det muligt at genbruge standardbiblioteket i et andet sprog. F.eks. kan alle sprog, der kører på JVM’en, simpelthen genbruge Java-standardbiblioteket.
Støtteværktøjer til et nyt programmeringssprog
For at gøre et sprog anvendeligt i praksis er vi ofte nødt til at skrive nogle få støtteværktøjer.
Det mest oplagte er en editor. En specialiseret editor med syntaksmarkering, inlinefejlkontrol og autokomplettering er i dag et musthave for at gøre enhver udvikler produktiv.
Men i dag er udviklere forkælede, og de forventer alle mulige andre understøttende værktøjer. F.eks. kan en debugger være meget nyttig, når der er tale om en ubehagelig fejl. Eller et build-system i lighed med maven eller gradle kunne være noget, som brugerne senere vil bede om.
I begyndelsen kan en editor være nok, men efterhånden som din brugerbase vokser, vil også kompleksiteten af projekterne vokse, og der vil være behov for flere understøttende værktøjer. Forhåbentlig vil der til den tid være et fællesskab, der er villigt til at hjælpe med at bygge dem.
Summary
Skabelse af et programmeringssprog er en proces, der virker mystisk for mange udviklere. I denne artikel har vi forsøgt at vise, at det blot er en proces. Det er fascinerende og ikke let, men det kan lade sig gøre.
Du ønsker måske at opbygge et programmeringssprog af forskellige årsager. En god grund er for sjov, en anden er for at lære, hvordan compilere fungerer. Dit sprog kan ende med at blive meget nyttigt eller ej, afhængigt af mange faktorer. Men hvis du har det sjovt og/eller lærer, mens du bygger det, så er det værd at investere noget tid i det.
Og selvfølgelig vil du kunne prale med dine medudviklere.
Hvis du vil lære mere om at skabe et sprog, så tag et kig på de andre ressourcer, vi har oprettet: Lær hvordan man bygger sprog.
Du er måske også interesseret i nogle af vores artikler: