Articles

Reading Code Right, With Some Help From The Lexer

Vaidehi Joshi
Vaidehi Joshi

Follow

Nov 27, 2017 · 16 min read

Reading code right, with some help from the lexer.

Software is all about logic. Programmering har fået et ry for at være et felt, der er tungt på matematik og skøre ligninger. Og datalogi synes at være kernen i denne misforståelse.

sikker på, der er noget matematik, og der er nogle formler — men ingen af os behøver faktisk at have pH. D. ‘ er i beregning for at vi kan forstå, hvordan vores maskiner fungerer! Faktisk er mange af de regler og paradigmer, som vi lærer i processen med at skrive kode, de samme regler og paradigmer, der gælder for komplekse computervidenskabelige begreber. Og nogle gange stammer disse ideer faktisk fra datalogi, og vi vidste det aldrig nogensinde.

uanset hvilket programmeringssprog vi bruger, når de fleste af os skriver vores kode, sigter vi mod at indkapsle forskellige ting i klasser, objekter eller metoder og med vilje adskille, hvilke forskellige dele af vores kode der vedrører. Med andre ord ved vi, at det generelt er gode ting at opdele vores kode, så en klasse, objekt eller metode kun er bekymret for og ansvarlig for en enkelt ting. Hvis vi ikke gjorde dette, kunne tingene blive super rodet og sammenflettet i et rod på et net. Nogle gange sker dette stadig, selv med adskillelse af bekymringer.

som det viser sig, følger selv de indre funktioner i vores computere meget lignende designparadigmer. Vores compiler har for eksempel forskellige dele til det, og hver del er ansvarlig for at håndtere en bestemt del af kompileringsprocessen. Vi stødte på en lille smule af denne sidste uge, da vi lærte om parseren, som er ansvarlig for at skabe parse træer. Men parseren kan umuligt få til opgave alt.parseren har brug for hjælp fra sine venner, og det er endelig tid for os at lære, hvem de er!

da vi lærte om parsing for nylig, dyppede vi tæerne i grammatik, syntaks og hvordan kompilatoren reagerer og reagerer på disse ting inden for et programmeringssprog. Men vi har aldrig rigtig fremhævet, hvad en compiler er! Når vi kommer ind i den indre funktion af kompileringsprocessen, lærer vi meget om compiler design, så det er vigtigt for os at forstå, hvad vi præcist taler om her.

compilere kan lyde lidt skræmmende, men deres job er faktisk ikke for komplekse at forstå — især når vi bryder de forskellige dele af en complier ned i bidstore dele.

men først, lad os starte med den enkleste definition muligt. En compiler er et program, der læser vores kode (eller en hvilken som helst kode på ethvert programmeringssprog) og oversætter det til et andet sprog.

compiler: en definition.

generelt set vil en kompilator virkelig kun oversætte kode fra et højt niveau sprog til et lavere niveau sprog. De sprog på lavere niveau, som en kompilator oversætter kode til, kaldes ofte samlingskode, maskinkode eller objektkode. Det er værd at nævne, at de fleste programmører ikke rigtig beskæftiger sig med eller skriver nogen maskinkode; snarere er vi afhængige af, at kompilatoren tager vores programmer og oversætter dem til maskinkode, hvilket er, hvad vores computer kører som et eksekverbart program.

Vi kan tænke på compilere som mellemmand mellem os, programmørerne og vores computere, som kun kan køre eksekverbare programmer på sprog på lavere niveau.

kompilatoren gør arbejdet med at oversætte, hvad vi vil ske på en måde, der er forståelig og eksekverbar af vores maskiner.

uden kompilatoren ville vi blive tvunget til at kommunikere med vores computere ved at skrive maskinkode, hvilket er utroligt ulæseligt og svært at dechiffrere. Maskinkode kan ofte bare ligne en flok 0 ‘er og 1’ er til det menneskelige øje — det hele er binært, husk? – hvilket gør det super svært at læse, skrive og debug. Compileren abstraherede maskinkode for os som programmører, fordi det gjorde det meget let for os at ikke tænke på maskinkode og skrive programmer ved hjælp af langt mere elegante, klare og letlæselige sprog.

vi fortsætter med at pakke mere og mere ud om den mystiske kompilator i løbet af de næste par uger, hvilket forhåbentlig vil gøre det mindre af en gåde i processen. Men for nu, lad os komme tilbage til det aktuelle spørgsmål: Hvad er de enkleste mulige dele af en kompilator?

hver kompilator, uanset hvordan den kan designes, har forskellige faser. Disse faser er, hvordan vi kan skelne de unikke dele af kompilatoren.

Syntaksanalyse: fase en af en compiler

Vi har allerede stødt på en af faserne i vores kompileringseventyr, da vi for nylig lærte om parser og parse træer. Vi ved, at parsing er processen med at tage nogle input og opbygge et parse træ ud af det, som undertiden omtales som parsing. Som det viser sig, er arbejdet med parsing specifikt for en fase i kompileringsprocessen kaldet syntaksanalyse.

parseren bygger dog ikke bare et parse-træ ud af tynd luft. Det har noget hjælp! Vi vil huske, at parseren får nogle tokens (også kaldet terminaler), og det bygger et parse træ fra disse tokens. Men hvor får det disse tokens fra? Heldig for parseren, det behøver ikke at fungere i et vakuum; i stedet har det noget hjælp.

dette bringer os til en anden fase af kompileringsprocessen, en der kommer før syntaksanalysefasen: den leksikalske analysefase.

de indledende faser af en compiler

udtrykket “leksikalsk” henviser til betydningen af et ord isoleret fra den sætning, der indeholder det, og uanset dets grammatiske kontekst. Hvis vi forsøger at gætte vores egen mening udelukkende baseret på denne definition, kunne vi hævde, at den leksikale analysefase har at gøre med de enkelte ord/udtryk i selve programmet og intet at gøre med grammatikken eller betydningen af sætningen, der indeholder ordene.

den leksikale analysefase er det første trin i kompileringsprocessen. Det kender ikke eller bekymrer sig om grammatikken i en sætning eller betydningen af en tekst eller et program; alt det ved om er betydningen af ordene selv.

leksikalsk analyse skal forekomme, før nogen kode fra et kildeprogram kan analyseres. Før det kan læses af parseren, skal ET program først scannes, opdeles og grupperes på bestemte måder.

da vi begyndte at se på syntaksanalysefasen i sidste uge, lærte vi, at parse-træet er bygget ved at se på individuelle dele af sætningen og nedbryde udtryk i enklere dele. Men i den leksikalske analysefase kender kompilatoren ikke eller har adgang til disse”individuelle dele”. Hellere, det skal først identificere og finde dem, og derefter gøre arbejdet med at opdele teksten i individuelle stykker.

Når vi for eksempel læser en sætning fra Shakespeare som To sleep, perchance to dream., ved vi, at mellemrummet og tegnsætningen deler “ordene” i en sætning. Dette er, selvfølgelig, fordi vi er blevet uddannet til at læse en sætning, “LEKS” det, og analysere det for grammatik.

men til en kompilator kan den samme sætning se sådan ud første gang den læser den:Tosleepperhachancetodream. Når vi læser denne sætning, er det lidt sværere for os at bestemme, hvad de faktiske “ord” er! Jeg er sikker på, at vores compiler har det på samme måde.

så hvordan håndterer vores maskine dette problem? Under den leksikale analysefase i kompileringsprocessen gør den altid to vigtige ting: den scanner koden, og derefter evaluerer den den.

de to trin af den leksikalske analyseproces!

arbejdet med scanning og evaluering kan undertiden klumpes sammen i et enkelt program, eller det kan være to separate programmer, der afhænger af hinanden; det er egentlig bare et spørgsmål om, hvordan en complier tilfældigvis blev designet. Programmet inden for kompilatoren, der er ansvarlig for at udføre arbejdet med scanning og evaluering, kaldes ofte lekseren eller tokenisatoren, og hele leksikalsk analysefase kaldes undertiden processen med leksering eller tokenisering.

for at scanne, perchance for at læse

den første af de to kernetrin i leksikalsk analyse er scanning. Vi kan tænke på scanning som arbejdet med faktisk at “læse” noget indtastningstekst. Husk, at denne indtastningstekst kan være en streng, sætning, udtryk eller endda et helt program! Det betyder ikke rigtig noget, for i denne fase af processen er det bare en kæmpe blob af tegn, der ikke betyder noget endnu, og er en sammenhængende del.

lad os se på et eksempel for at se, hvordan dette sker. Vi bruger vores oprindelige sætning, To sleep, perchance to dream., som er vores kildetekst eller kildekode. Til vores kompilator læses denne kildetekst som indtastningstekst, der ligner Tosleep,perchancetodream., som kun er en streng af tegn, der endnu ikke er dechiffreret.

scanningsprocessen, trin 1.

det første, vores kompilator skal gøre, er faktisk at opdele den klat af tekst i dens mindste mulige stykker, hvilket vil gøre det meget lettere at identificere, hvor ordene i klat af tekst faktisk er.

den enkleste måde at dykke op på et kæmpe stykke tekst er ved at læse det langsomt og systematisk, et tegn ad gangen. Og det er præcis, hvad kompilatoren gør.

ofte håndteres scanningsprocessen af et separat program kaldet scanneren, hvis eneste job det er at gøre arbejdet med at læse en kildefil/tekst, et tegn ad gangen. Til vores scanner betyder det ikke rigtig noget, hvor stor vores tekst er; alt det vil se, når det “læser” vores fil er et tegn ad gangen.

Her er hvad vores Shakespeare-sætning ville blive læst som af vores scanner:

scanningen proces, trin 2.

Vi vil bemærke, at To sleep, perchance to dream. er blevet opdelt i individuelle tegn af vores scanner. Desuden behandles selv mellemrummene mellem ordene som tegn, ligesom tegnsætningen i vores sætning. Der er også et tegn i slutningen af denne sekvens, der er særligt interessant: eof. Dette er tegnet “end of file”, og det ligner tabspace og newline. Da vores kildetekst kun er en enkelt sætning, når vores scanner kommer til slutningen af filen (i dette tilfælde slutningen af sætningen), læser den slutningen af filen og behandler den som et tegn.

så i virkeligheden, da vores scanner læste vores indtastningstekst, fortolkede den det som individuelle tegn, hvilket resulterede i dette: .

scanningsprocessen, trin 3.

nu hvor vores scanner har læst og opdelt vores kildetekst i sine mindste mulige dele, vil det have en meget lettere tid til at finde ud af” ordene ” i vores sætning.

Dernæst skal scanneren se på dens opdelte tegn i rækkefølge og bestemme hvilke tegn der er dele af et ord, og hvilke der ikke er. For hvert tegn, som scanneren læser, markerer den linjen og placeringen af, hvor tegnet blev fundet i kildeteksten.

billedet vist her illustrerer denne proces for vores Shakespeare-sætning. Vi kan se, at vores scanner markerer linjen og kolonnen for hvert tegn i vores sætning. Vi kan tænke på linjen og kolonnerepræsentationen som en matrice eller en række tegn.

Husk, at da vores fil kun har en enkelt linje i den, lever alt på linje0. Men når vi arbejder os gennem sætningen, øges kolonnen for hvert tegn. Det er også værd at nævne, at da vores scanner læser spacesnewlineseof og alle tegnsætning som tegn, vises de også i vores tegntabel!

scanningsprocessen, trin 4.

Når kildeteksten er blevet scannet og markeret, er vores kompilator klar til at gøre disse tegn til ord. Da scanneren ikke kun ved, hvor spacesnewlinesog eof i filen er, men også hvor de bor i forhold til de andre tegn, der omgiver dem, kan den scanne gennem tegnene og opdele dem i individuelle strenge efter behov.

i vores eksempel vil scanneren se på tegneneT, såo, og derefter enspace. Når den finder et mellemrum, deler den To i sit eget ord — den enkleste kombination af tegn, der er mulig, før scanneren møder et mellemrum.

det er en lignende historie for det næste ord, den finder, hvilket er sleep. I dette scenario lyder det dog s-l-e-e-p og læser derefter et ,, et tegnsætningstegn. Da dette komma er flankeret af et tegn (p) og et space på begge sider betragtes kommaet i sig selv som et “ord”.

både ordet sleepog tegnsætningssymbolet , kaldes leksemer, som er understrenger kildeteksten. Et lekseme er en gruppering af de mindst mulige sekvenser af tegn i vores kildekode. Leksemerne i en kildefil betragtes som de enkelte “ord” i selve filen. Når vores scanner er færdig med at læse de enkelte tegn i Vores fil, returnerer den et sæt leksemer, der ser sådan ud: .

iv proces, trin 5.

bemærk, hvordan vores scanner tog en klat tekst som input, som den ikke oprindeligt kunne læse, og fortsatte med at scanne den en gang tegn ad gangen, samtidig med at læse og markere det indholdet. Det fortsatte derefter med at opdele strengen i deres mindste mulige leksemer ved at bruge mellemrum og tegnsætning mellem tegn som afgrænsere.

på trods af alt dette arbejde ved vores scanner på dette tidspunkt i den leksikale analysefase ikke noget om disse ord. Jo da, det opdeler teksten i ord i forskellige former og størrelser, men hvad disse ord er, har scanneren ingen anelse om! Ordene kunne være en bogstavelig streng, eller de kunne være et tegnsætningstegn, eller de kunne være noget helt andet!

scanneren ved ikke noget om selve ordene, eller hvilken “type” ord de er. Det ved bare, hvor ordene slutter og begynder i selve teksten.

dette sætter os op til anden fase af leksikalsk analyse: evaluering. Når vi har scannet vores tekst og opdelt kildekoden i individuelle leksemenheder, skal vi evaluere de ord, som scanneren vendte tilbage til os, og finde ud af, hvilke typer ord vi har at gøre med — især skal vi kigge efter vigtige ord, der betyder noget specielt på det sprog, vi prøver at kompilere.

evaluering af de vigtige dele

Når vi er færdige med at scanne vores kildetekst og identificeret vores leksemer, bliver vi nødt til at gøre noget med vores lekseme “ord”. Dette er evalueringstrinnet for leksikalsk analyse, som ofte omtales i complier-design som processen med leksering eller tokenisering af vores input.

hvad betyder det at evaluere den scannede kode?

Når vi evaluerer vores scannede kode, er alt, hvad vi virkelig gør, at se nærmere på hvert af de leksemer, som vores scanner genererede. Vores kompilator bliver nødt til at se på hvert leksemord og bestemme, hvilken slags ord det er. Processen med at bestemme, hvilken slags lekseme hvert” ord ” i vores tekst er, hvordan vores kompilator forvandler hvert enkelt lekseme til et token og derved tilkendegiver vores inputstreng.

Vi stødte først på tokens tilbage, da vi lærte om parse træer. Tokens er specielle symboler, der er kernen i hvert programmeringssprog. Tokens, såsom (, ), +, -, if, else, then, hjælper alle en kompilator med at forstå, hvordan forskellige dele af et udtryk og forskellige elementer forholder sig til hinanden. Parseren, som er central i syntaksanalysefasen, afhænger af at modtage tokens fra et sted og derefter forvandler disse tokens til et parse-træ.

Tokens: en definition.

Nå, gæt hvad? Vi har endelig fundet ud af “et eller andet sted”! Som det viser sig, genereres de tokens, der sendes til parseren, i den leksikalske analysefase af tokenisatoren, også kaldet lekseren.

tokenisering vores Shakespeare-sætning!

Så hvad ser et token ud? Et token er ret simpelt og er normalt repræsenteret som et par, der består af et tokennavn og en vis værdi (som er valgfri).

for eksempel, hvis vi tokeniserer vores Shakespeare-streng, ville vi ende med tokens, der for det meste ville være strenglitteraler og separatorer. Vi kunne repræsentere leksemet "dream” som et token som sådan: <string literal, "dream">. På samme måde kunne vi repræsentere leksemet . som token, <separator, .>.

Vi vil bemærke, at hver af disse tokens ikke ændrer leksemet overhovedet — de tilføjer blot yderligere oplysninger til dem. Et token er lekseme eller leksikalsk enhed med flere detaljer; specifikt fortæller den tilføjede detalje os, hvilken kategori af token (hvilken type “ord”) vi har at gøre med.

nu hvor vi har tokeniseret vores Shakespeare-sætning, kan vi se, at der ikke er så meget variation i typerne af tokens i vores kildefil. Vores sætning havde kun strenge og tegnsætning i det — men det er bare toppen af token isbjerget! Der er masser af andre typer “ord”, som et lekseme kunne kategoriseres i.

almindelige former for tokens fundet i vores kildekode.

tabellen vist her illustrerer nogle af de mest almindelige tokens, som vores kompilator ville se, når vi læser en kildefil på stort set ethvert programmeringssprog. Vi så eksempler på literals, som kan være en hvilken som helst streng, nummer eller logik/boolsk værdi samt separators, som er enhver form for tegnsætning, inklusive seler ({}) og parenteser (()).

However, there are also keywords, which are terms that are reserved in the language (such as ifvarwhilereturn), as well as operators, which operate on arguments and return some value ( +-x/). Vi kunne også støde på leksemer, der kunne tilkendegives som identifiers, som normalt er variable navne eller ting skrevet af brugeren/programmøren for at henvise til noget andet, samt , som kunne være linje-eller blokkommentarer skrevet af brugeren.

vores oprindelige sætning viste os kun to eksempler på tokens. Lad os omskrive vores sætning for i stedet at læse: var toSleep = "to dream";. Hvordan kan vores compiler lekse denne version af Shakespeare?

Hvordan vil vores tokenisere denne sætning?

Her ser vi, at vi har et større udvalg af tokens. Vi har en keyword i var, hvor vi erklærer en variabel, og en identifiertoSleep, hvilket er den måde, vi navngiver vores variabel eller henviser til den værdi, der kommer. Næste er vores=, som er etoperator token, efterfulgt af strengen bogstavelig"to dream". Vores Erklæring slutter med en; separator, der angiver slutningen af en linje og afgrænser mellemrum.

en vigtig ting at bemærke om tokeniseringsprocessen er, at vi hverken tokeniserer nogen hvide rum (mellemrum, nye linjer, faner, ende af linjen osv.), heller ikke videregive det til parseren. Husk, at kun tokens er givet til parseren og vil ende i parse træet.

det er også værd at nævne, at forskellige sprog vil have forskellige tegn, der udgør som hvide rum. For eksempel bruger Python — programmeringssproget i nogle situationer indrykning — inklusive faner og MELLEMRUM-for at indikere, hvordan omfanget af en funktion ændres. Så Python-kompilatorens tokenisator skal være opmærksom på, at en tab eller space faktisk skal tokeniseres som et ord, fordi det faktisk skal overføres til parseren!

begrænsninger af skanneren mod scanneren.

dette aspekt af tokenisatoren er en god måde at kontrastere, hvordan en lekser/tokenisator er forskellig fra en scanner. Mens en scanner er uvidende og kun ved, hvordan man opdeler en tekst i dens mindre mulige dele (dens “ord”), er en lekser/tokenisator meget mere opmærksom og mere præcis i sammenligning.

tokenisatoren skal kende indviklingen og specifikationerne for det sprog, der udarbejdes. Hvis tabser vigtige, skal det vide det; hvis newlines kan have visse betydninger på det sprog, der udarbejdes, skal tokenisatoren være opmærksom på disse detaljer. På den anden side ved scanneren ikke engang, hvad de ord, den deler selv, er, meget mindre hvad de betyder.

scanneren af en complier er langt mere sprog-agnostiker, mens en tokenisator skal være sprogspecifik per definition.

disse to dele af den leksikale analyseproces går hånd i hånd, og de er centrale i den første fase af kompileringsprocessen. Selvfølgelig er forskellige compliers designet på deres egne unikke måder. Nogle kompilatorer gør trinnet med scanning og tokenisering i en enkelt proces og som et enkelt program, mens andre vil opdele dem i forskellige klasser, i hvilket tilfælde tokenisatoren ringer til scannerklassen, når den køres.

leksikalsk analyse: en hurtig visuel oversigt!

i begge tilfælde er trinnet i leksikalsk analyse super vigtigt for kompilering, fordi syntaksanalysefasen direkte afhænger af den. Og selvom hver del af kompilatoren har sine egne specifikke roller, læner de sig på hinanden og er afhængige af hinanden — ligesom gode venner altid gør.

ressourcer

da der er mange forskellige måder at skrive og designe en kompilator på, er der også mange forskellige måder at lære dem på. Hvis du gør nok forskning på det grundlæggende i kompilering, bliver det ret klart, at nogle forklaringer går langt mere detaljeret end andre, hvilket måske eller måske ikke er nyttigt. Hvis du finder dig selv ønsker at lære mere, nedenfor er en række ressourcer på kompilatorer — med fokus på den leksikale analysefase.

  1. Kapitel 4-Crafting tolke, Robert Nystrom
  2. Compiler konstruktion, Professor Allan Gottlieb
  3. Compiler Basics, Professor James Alan Farrell
  4. skrivning af et programmeringssprog — Lekseren, Andy Bileam
  5. noter om, hvordan parsere og kompilatorer fungerer, Stephen Raymond Ferg
  6. hvad er forskellen mellem et token og et lekseme?, Stackoverløb