Compiler

Uit Wikipedia, de vrije encyclopedie
(Doorverwezen vanaf Compileren)

Een compiler (Nederlands voor samensteller of opbouwer) is een computerprogramma dat code in de ene formele taal (de brontaal) vertaalt naar code in een andere formele taal (de doeltaal).[1] Dit vertaalproces wordt compilatie of compileren genoemd. Het doel van compileren is veelal om code die in een hogere programmeertaal is geschreven te vertalen naar een programmeertaal op een lager abstractieniveau, vaak machine- of assembleertaal, maar ook C of JavaScript. Voorbeelden van compilers zijn C++-compilers, die in C++ geschreven broncode naar machinetaal vertalen, en de TypeScript-compiler, die TypeScript-programma's naar JavaScript vertaalt.

De invoer van de compiler wordt broncode en de uitvoer objectcode genoemd. Vaak is de objectcode machinetaal of machine-onafhankelijke bytecode, die door een computer of virtuele machine kan worden uitgevoerd.[2] Deze uitvoerbare instructies bestaan uit binaire gegevens, die moeilijk voor een mens zijn te begrijpen. Compilers fungeren als het ware als een brug tussen de programmeur en de hardware.[3]

Ontstaansgeschiedenis[bewerken | brontekst bewerken]

Vanaf de jaren veertig startten onafhankelijk een paar partijen met het bouwen van computers. Deze computers hadden echter functionele nadelen, zo had de ENIAC als enig doel om snelle berekeningen te maken van trajecten van granaten en raketten. Hardware en software waren eerst onlosmakelijk met elkaar verbonden waardoor telkens een nieuwe computer diende gebouwd te worden voor een specifiek doel.[4]

In 1951 kregen computers meer mogelijkheden waarna Grace Hopper als eerste inzag om subroutines in het geheugensysteem op te slaan. Het programmeren ging sneller maar de uitvoer ervan trager waardoor haar toenmalige werkgever Remington Rand geen interesse vertoonde.

Omdat ieder bedrijf voor hun computer andere vereisten nodig had, begon Hopper in haar vrije tijd te schrijven aan een compiler. Hoppers geschreven software was daarna in staat om geschreven programma's te vertalen naar machinetaal van een specifieke computer. Deze compiler werd toen onder meer gebruikt om complexe vergelijkingen op te lossen door wiskundigen. Andere programmeurs begonnen haar stukjes code toe te sturen om die aan de bibliotheek van de compiler toe te voegen. De hardware en software waren vanaf dan twee afzonderlijke componenten. Later zei Hopper: "Niemand had daar eerder aan gedacht want ze waren niet zo lui als ik." Uiteindelijk resulteerde haar compiler tot een van de eerste programmeertalen COBOL, waarvan Hopper de technische leiding had.[3][4]

In 1963 werd daarna de zelfhostende compiler ontwikkeld door Mike Levin en Tim Hart onder leiding van John McCarthy. De compiler werd opgebouwd met een bootstrapping-systeem en zorgde ervoor dat een compiler zichzelf kon compileren. Dit werd voor het eerst in de programmeertaal LISP geïntroduceerd.[5]

Onderdelen[bewerken | brontekst bewerken]

Een compiler bestaat doorgaans uit twee onderdelen, een frontend en een backend, niet te verwarren met de front-end (cliënt-side) en backend (server-side) bij web-applicaties.

Frontend[bewerken | brontekst bewerken]

De frontend van de compiler is specifiek voor een bepaalde programmeertaal geschreven. De frontend is verantwoordelijk voor het verwerken van de broncode. Dit bestaat vaak uit de volgende fases:

  • Lexicale analyse
  • Parsen
  • Semantische analyse
  • Genereren van de interne representatie
  • Platformonafhankelijke optimalisatie

De broncode wordt omgezet in een tussenrepresentatie en dan naar de backend gestuurd.

Backend[bewerken | brontekst bewerken]

De backend is specifiek voor een bepaald platform, bijvoorbeeld een specifieke processor in combinatie met een specifiek besturingssysteem. Voor iedere processor of besturingssysteem is een andere backend nodig. De backend is verantwoordelijk voor het genereren en optimaliseren van de objectcode. Dit kan uit de volgende fases bestaan:

  • Registerallocatie
  • Codegeneratie
  • Optimalisatie

Modulariteit[bewerken | brontekst bewerken]

Front- en backend kunnen meer of minder met elkaar verweven zijn. Bij sommige compilers zijn front- en backend precies op elkaar afgestemd. Andere zijn modulair opgebouwd zodat verschillende frontends met verschillende backend gecombineerd kunnen worden, waardoor verschillende programmeertalen voor verschillende platforms gecompileerd kunnen worden. Een voorbeeld hiervan is de GNU Compiler Collection (GCC).

Technische architectuur[bewerken | brontekst bewerken]

Strikt gezien is compilatie het vertalen van expressies uit een formele taal naar expressies van een ander formele taal of doeltaal. Het compileren gebeurt in verschillende fases. De frontend levert zijn resultaten af aan de backend in de vorm van een interne representatie en een symbol table.[6]

Een schema van de verschillende fases waaruit compilatie bestaat

Lexicale analyse[bewerken | brontekst bewerken]

Zie Lexicale analyse voor het hoofdartikel over dit onderwerp.

De taak van een lexicale scanner in de compiler is om de invoer, die in de regel uit een rij karakters bestaat, onder te verdelen in kleinere onderdelen, die symbolen of tokens genoemd worden. Deze tokens worden aan de parser doorgegeven. Een token is een object dat door de parser als eenheid herkend kan worden. Bij programmeertalen zijn de tokens de sleutelwoorden, de namen, de getallen, enzovoorts.

Parsen[bewerken | brontekst bewerken]

Zie Parser voor het hoofdartikel over dit onderwerp.

Tijdens het parsen (ontleden) worden de tokens die het resultaat zijn van de lexicale analyse omgezet in een boomstructuur, een syntaxisboom genoemd. Dit gebeurt volgens regels die gedefinieerd zijn in de contextvrije grammatica van de taal.

Als de parser een serie tokens tegenkomt die aan geen van de regels van de grammatica voldoet is er sprake van een syntax error, een syntaxisfout. In dat geval geeft de compiler een foutmelding.

Het resultaat van het parsen is een concrete syntaxisboom die de structuur van de geparsete broncode weergeeft.

Semantische analyse[bewerken | brontekst bewerken]

De semantische analyse wordt uitgevoerd op de afleidingsboom die het resultaat is van de hierboven beschreven parsen.

De parser definieert een mogelijke afleiding van een woord uit een formele taal in de zin van de contextvrije grammatica van die taal. Een dergelijke afleiding zegt echter niets over de correctheid van de afleiding met betrekking tot de contextgevoelige eigenschappen van de programmeertaal. Een voorbeeld is een regel dat een variabele gedeclareerd moet zijn voordat deze gebruikt wordt, of dat een bepaalde waarde alleen toegekend kan worden aan een variabele van het juiste type.

Een veelgebruikte methode tijdens de semantische analyse is attribuutevaluatie, waarbij semantische regels met een attributengrammatica worden vastgelegd. De attribuutevaluator van een compiler doorloopt nogmaals de gehele afleiding (soms meer dan één keer) om te bepalen of een afgeleid woord alle contextuele regels van de formele taal eerbiedigt. De attribuutevaluator beschouwt vaak deelbomen van de boom beschouwt van wortel naar bladeren en weer terug en daarbij de boom decoreert met invoerinformatie (contextuele beperkingen op deelbomen die van boven uit de boom worden opgelegd – bijvoorbeeld "variabele X heeft hier geen waarde en mag dus niet uitgelezen worden") en uitvoerinformatie (contextuele informatie die bepaalt of een deelboom wel in de bovenliggende boom past – bijvoorbeeld "het type van de uitspraak die gevormd wordt door deze deelboom is A").

Er wordt nog altijd veel werk verricht in de informatica aan attribuutevaluatorgeneratoren. Er zijn wel verschillende systemen die uit een beschrijving een attribuutevaluator kunnen genereren, maar er is nog geen algemene erkende oplosmethode.

Optimalisatie[bewerken | brontekst bewerken]

Zie Optimalisatie (compiler) voor het hoofdartikel over dit onderwerp.

Het optimaliseren heeft een belangrijke taak om de snelheid zo efficiënt mogelijk te houden. Technieken die hierbij kunnen worden toegepast zijn onder andere het vereenvoudigen van wiskundige formules en het ontvouwen van lussen.[7]

Codegeneratie[bewerken | brontekst bewerken]

Zie Codegenerator voor het hoofdartikel over dit onderwerp.

De codegenerator is de laatste stap van de compilatie. Dit onderdeel beschouwt nog een laatste maal de gedecoreerde afleidingsboom en genereert vertalingen in de doeltaal voor iedere deelboom van de afleidingsboom. Omdat alle deelbomen samen de gehele boom vormen, genereert dit proces de gehele vertaling.

De meest gebruikte methode bij codegeneratoren is dat de codegenerator naast drivercode bestaat uit een bibliotheek aan "standaardvertalingen" van stukken afleidingsboom, waarin gaten voorkomen. Deze gaten worden bij de echte codegeneratie gevuld met contextgevoelige informatie. Te denken valt dan aan de precieze naam van een variabele uit de invoer. Omdat codegeneratie afhankelijk is van contextgevoelige informatie en er voor attribuutevaluatie nog niet één echte oplossing is, is er ook nog niet één algemeen mechanisme voor generatie van codegeneratoren.

Daarnaast kampen onderzoekers naar codegeneratie ook met een ander probleem, namelijk dat er vaak veel meer dan één enkele manier is om een deelboom in een doeltaal te vertalen en niet iedere mogelijke vertaling altijd de beste is. Naar dit soort codeoptimalisatie wordt veel onderzoek gedaan.

Foutmelding[bewerken | brontekst bewerken]

Ontwikkelingsomgeving[bewerken | brontekst bewerken]

Met een foutopsporingstool zoals een linter en debugger kunnen specifieke fouten worden opgespoord die een belangrijke rol spelen voor een developer. Beide hebben andere doeleinden waarvoor ze ingezet worden. Deze tools zijn vaak standaard meegeleverd in een integrated development environment (IDE) voor één type platform. Via Visual Studio Code (IDE) kan ook een linter of compiler via extensie geinstalleerd worden ter ondersteuning van een nieuwe programmeertaal.[2]

Linter[bewerken | brontekst bewerken]

Een developer kan tijdens het schrijven van een programma beroep doen op een code-analysator dat instant de broncode screent tijdens het typen. Deze tool checkt dat de taalregels van de syntaxis goed worden opgebouwd. Dit is een preventieve maatregel om foutloos te kunnen compileren als de implementatie is voltooid.[8] Meestal is dit een vast onderdeel van een native debugger.

Debugger[bewerken | brontekst bewerken]

Nadat de broncode succesvol is gecompileerd kan een tester het programma uitvoeren en debuggen door het programma te gebruiken waarvoor het dient om alle aanwezige bugs te verwijderen uit de broncode. Vervolgens kan een tester ook kiezen om de debugconfiguratie op debug-modus te zetten en stapsgewijs alle veranderingen te zien plaatsvinden. Op die manier kan een foute toestand gedetecteerd en opgelost worden die aanvankelijk niet zichtbaar was.[9]

Compilatieproces[bewerken | brontekst bewerken]

Bij het opbouwen controleert de compiler of de invoer welgevormd is en of er een correcte vertaling gemaakt kan worden. Zo niet, zal er een foutmelding optreden. Tegenwoordig werkt dit meestal met een exception handling en stopt de compiler bij de allereerste foutmelding. Bij een klassieke batchproces onderzoekt de compiler het hele programma op fouten in één enkele sequentiële actie om dan alle fouten compact in een lijst weer te geven. Na het fixen of uitbreiden zal het compilatieproces opnieuw moeten plaatsvinden.

Andere translators[bewerken | brontekst bewerken]

Naast compilers zijn er naargelang het beoogde doel nog andere translators of vertalers beschikbaar.[10]

Interpreter[bewerken | brontekst bewerken]

Bij een interpreter wordt wordt de code direct, regel voor regel, uitgevoerd zonder dat hij vooraf gecompileerd wordt. Hierbij zal de gevraagde broncode telkens opnieuw worden geïnterpreteerd als hij wordt aangeroepen. Een voordeel is dat elk apparaat waarvoor een interpreter beschikbaar is dezelfde code kan uitvoeren, maar het belangrijkste nadeel is dat het programma langzamer wordt uitgevoerd omdat het interpreteren tijd kost.

Assembler[bewerken | brontekst bewerken]

De assembler dient voor gegenereerde assembleertaal om te zetten naar machinetaal. Een assembleertaal is een lagere programmeertaal bestaande uit instructiecode met mnemonice-representatie voor een specifieke computer dat snel om te zetten is naar machinetaal. Het wordt vaak gegenereerd door compilers en debuggers. Dit is nog net analyseerbaar voor een programmeur. De allereerste compiler van Grace Hopper was vergelijkbaar met een assembler.

Compilervarianten[bewerken | brontekst bewerken]

  • De meeste compilers doen hun werk voordat het programma wordt uitgevoerd. Dit wordt een ahead-of-time (AOT)-compiler genoemd. Een just-in-time (JIT)-compiler compileert het programma terwijl het programma wordt uitgevoerd. Functies worden dan gecompileerd op het moment dat ze voor de eerste keer worden aangeroepen. Een JIT-compiler combineert voordelen van compilers en interpreters[11], maar is ingewikkelder te schrijven en tijdens het compileren loopt het programma langzamer.
  • AI-compiler: met kunstmatige intelligentie via machinaal leren om de optimalisatietijd te verkorten. Een voorbeeld is Milepost GCC van IBM.[12]
  • Een self-hosting compiler is een compiler die in zijn eigen brontaal is geschreven, bijvoorbeeld een C++-compiler die in C++ is geschreven of een Rust-compiler die in Rust is geschreven. Aangezien een self-hosting compiler zichzelf nodig heeft om gecompileerd te worden, is het ingewikkeld zo'n compiler naar een uitvoerbaar programma te vertalen. Dit proces wordt bootstrappen genoemd.
  • Een cross-compiler is een compiler wiens objectcode op een ander platform wordt uitgevoerd dan het platform waarop de compiler zelf wordt uitgevoerd. Veel mobiele applicaties worden bijvoorbeeld op een pc of laptop geschreven en gecompileerd en vervolgens op een mobiele telefoon uitgevoerd.
  • Decompiler: vertaalt een programma van een lagere programmeertaal naar een hogere programmeertaal.
  • Native compiler: genereert een codeobject voor hetzelfde platform en besturingssysteem waarop het draait.
  • Parallelle compiler: produceert een tussen compilatie en optimaliseert de abstracte syntaxisboom (AST) om te verspreiden naar meerdere processors.
  • Source-to-source-compiler: compileert van en naar dezelfde programmeertaal.

Softwareontwikkeling[bewerken | brontekst bewerken]

Compilers worden gebruikt binnen het programmeren. Een tekst in de vorm van broncode in een bepaalde programmeertaal wordt omgezet, meestal naar een vorm waarin het direct door een computer kan worden uitgevoerd. Soms zal het naar een vorm worden omgezet zodanig dat het door een ander programma, een zogenaamde interpreter of runtime module, uitgevoerd kan worden.

Een programmeur schrijft de broncode van het programma in een teksteditor (een tekstbewerkingprogramma, meestal speciaal geschikt voor de te gebruiken programmeertaal) en slaat deze op in een bestand. Wanneer de programmeur de compiler gebruikt zal deze de broncode omzetten naar een uitvoerbaar bestand.

Een broncode kan ook gecompileerd worden via het aanroepen vanaf de opdrachtregel of met behulp van een standalone compilertool zoals "Expo dev" via QR-code om met een fysieke tablet en smartphone te debuggen.[13]

De resulterende objectbestanden (verwar object hier niet met objectoriëntatie) moeten doorgaans dan nog gelinkt worden (samengevoegd met bijvoorbeeld opstartcode), waarna het programma klaar is voor uitvoering, de zogenaamde executable. In het geval van een variant die nog een interpreter vereist wordt de linkfase gewoonlijk overgeslagen. Het compilatieproces is hierdoor meestal wat sneller, maar de uitvoering trager. Een voordeel van interpretergebaseerde compilatie kan wel zijn dat de gegenereerde object code portable is naar andere systemen. De interpreter zelf is dan niet portable en handelt platformspecifieke zaken af.

Voor -en nadelen[bewerken | brontekst bewerken]

De voor -en nadelen van een compiler worden vaak vergeleken met een interpreter. Beide hebben met elkaar gemeen, dat ze broncode transformeren naar uitvoerbare instructies.

Voordelen compiler Nadelen compiler
Snel en efficiënt bij het online draaien. Het kan ook goed debuggen met omvangrijke projecten. Tijdens onderhoud en hercompileren is vaak het gehele systeem bezet.
Kan zeer goed optimaliseren en inspelen op de hardwarearchitectuur. Het geheugencapaciteit is omvangrijker door zijn complex structuur.
Kan zeer goed overweg met grafische doeleinde zoals geavanceerde spellen en consoles. Kan enkel compileren via slimme apparaten. Bijvoorbeeld een eenvoudige rekenmachine kan niet compileren.
Het is goed te beveiligen en kan moeilijk uitgelezen worden omdat de broncode werd gecompileerd naar machinetaal. De broncode wordt echter in zijn geheel vertaald wat meer tijd en geld kost tijdens het updaten en stilleggen.

Voorbeelden[bewerken | brontekst bewerken]

Voorbeelden van compilers (in alfabetische, geen chronologische volgorde):

Voorbeelden van integrated development environments met ingebouwde compilers (software-ontwikkelomgeving):

Externe links[bewerken | brontekst bewerken]