Programmering

Hvordan bygge en tolk i Java, del 1: GRUNNLAGENE

Da jeg fortalte en venn at jeg hadde skrevet en BASIC tolk på Java, lo han så godt at han nesten sølte brusen han hadde over hele klærne. "Hvorfor i all verden vil du bygge en BASIC-tolk i Java?" var det forutsigbare første spørsmålet ut av munnen hans. Svaret er både enkelt og komplekst. Det enkle svaret er at det var morsomt å skrive en tolk på Java, og hvis jeg skulle skrive en tolk, kunne jeg like godt skrive en som jeg har gode minner om fra de tidlige dagene med personlig databehandling. På den komplekse siden har jeg lagt merke til at mange mennesker som bruker Java i dag, har kommet forbi poenget med å lage tumlende Duke-appletter og går videre til seriøse applikasjoner. Ofte, når du bygger en applikasjon, vil du at den skal kunne konfigureres. Valgmekanismen for omkonfigurering er en slags dynamisk utførelsesmotor.

Kjent som makrospråk eller konfigurasjonsspråk, er dynamisk kjøring funksjonen som gjør at et program kan "programmeres" av brukeren. Fordelen med å ha en dynamisk utførelsesmotor er at verktøy og applikasjoner kan tilpasses for å utføre komplekse oppgaver uten å erstatte verktøyet. Java-plattformen tilbyr et bredt utvalg av dynamiske utførelsesmotoralternativer.

HotJava og andre varme alternativer

La oss kort utforske noen av de tilgjengelige alternativene for dynamiske kjøremotorer, og se på implementeringen av tolkene mine i dybden. En dynamisk utførelsesmotor er en innebygd tolk. En tolk trenger tre fasiliteter for å kunne operere:

  1. Et middel til å bli lastet med instruksjoner
  2. Et modulformat for lagring av instruksjoner som skal utføres
  3. En modell eller et miljø for samhandling med vertsprogrammet

HotJava

Den mest berømte innebygde tolken må være HotJava "applet" -miljøet som har omformet måten folk ser på nettlesere.

HotJava "applet" -modellen var basert på forestillingen om at en Java-applikasjon kunne opprette en generisk baseklasse med et kjent grensesnitt, og deretter laste underklasser dynamisk av den klassen og utføre dem på kjøretid. Disse appletene ga nye muligheter, og innenfor rammen av baseklassen ga dynamisk kjøring. Denne dynamiske utførelsesfunksjonen er en grunnleggende del av Java-miljøet og en av tingene som gjør det så spesielt. Vi vil se nærmere på dette spesielle miljøet i en senere kolonne.

GNU EMACS

Før HotJava ankom, var kanskje den mest vellykkede applikasjonen med dynamisk kjøring GNU EMACS. Denne redaktørens LISP-lignende makrospråk har blitt en stift for mange programmerere. Kort fortalt består EMACS LISP-miljøet av en LISP-tolk og mange redigeringsfunksjoner som kan brukes til å komponere de mest komplekse makroene. Det bør ikke betraktes som overraskende at EMACS-redigereren opprinnelig ble skrevet i makroer designet for en redaktør kalt TECO. Dermed tillot tilgjengeligheten av et rikt (hvis uleselig) makrospråk i TECO at det ble konstruert en helt ny redaktør. I dag er GNU EMACS basisredaktør, og hele spill har blitt skrevet i ingenting mer enn EMACS LISP-koden, kjent som el-code. Denne konfigurasjonsevnen har gjort GNU EMACS til en redaktør for bærebjelken, mens VT-100-terminalene den ble designet for å kjøre på har blitt bare fotnoter i en forfatterkolonne.

REXX

Et av favorittspråkene mine, som aldri helt gjorde det fortjent plasket, var REXX, designet av Mike Cowlishaw fra IBM. Selskapet trengte et språk for å kontrollere applikasjoner på store hovedrammer som kjører VM-operativsystemet. Jeg oppdaget REXX på Amiga hvor det var tett kombinert med et bredt utvalg av applikasjoner gjennom "REXX-porter." Disse portene tillot applikasjoner å kjøres eksternt via REXX-tolk. Denne koblingen av tolk og applikasjon skapte et mye kraftigere system enn det som var mulig med komponentdelene. Heldigvis lever språket videre i NETREXX, en versjon Mike skrev som ble samlet inn i Java-kode.

Da jeg så på NETREXX og et mye tidligere språk (LISP i Java), slo det meg at disse språkene utgjorde viktige deler av Java-applikasjonshistorien. Hvilken bedre måte å fortelle denne delen av historien på enn å gjøre noe gøy her - som å gjenopplive BASIC-80? Enda viktigere, det ville være nyttig å vise en måte som skriptspråk kan skrives på Java, og gjennom deres integrasjon med Java, vise hvordan de kan forbedre funksjonene til Java-applikasjonene dine.

GRUNNLEGGENDE krav til forbedring av Java-appene dine

BASIC er ganske enkelt et grunnleggende språk. Det er to tanker om hvordan man kan gå frem for å skrive en tolk for det. En tilnærming er å skrive en programmeringssløyfe der tolkeprogrammet leser en tekstlinje fra det tolket programmet, analyserer den og deretter kaller en underrutine for å utføre den. Sekvensen av lesing, parsing og utføring gjentas til en av uttalelsene til det tolket programmet forteller tolk om å stoppe.

Den andre og mye mer interessante måten å takle prosjektet på er å analysere språket i et parse-tre og deretter utføre parse-treet "på plass." Slik fungerer tokeniserende tolker og måten jeg valgte å gå frem på. Tokeniserende tolker er også raskere, da de ikke trenger å skanne inngangen på nytt hver gang de utfører en uttalelse.

Som jeg nevnte ovenfor, er de tre komponentene som er nødvendige for å oppnå dynamisk kjøring, et middel for å bli lastet, et modulformat og kjøringsmiljøet.

Den første komponenten, et middel for å bli lastet, vil bli behandlet av en Java InputStream. Siden inngangsstrømmer er grunnleggende i I / O-arkitekturen til Java, er systemet designet for å lese i et program fra en InputStream og konverter den til kjørbar form. Dette representerer en veldig fleksibel måte å mate kode inn i systemet. Selvfølgelig vil protokollen for dataene som går over inngangsstrømmen være BASIC kildekode. Det er viktig å merke seg at hvilket som helst språk kan brukes; ikke gjør feilen ved å tro at denne teknikken ikke kan brukes på applikasjonen din.

Etter at kildekoden til det tolkede programmet er lagt inn i systemet, konverterer systemet kildekoden til en intern representasjon. Jeg valgte å bruke parse-treet som det interne representasjonsformatet for dette prosjektet. Når parse-treet er opprettet, kan det manipuleres eller utføres.

Den tredje komponenten er utførelsesmiljøet. Som vi får se er kravene til denne komponenten ganske enkle, men implementeringen har et par interessante vendinger.

En veldig rask BASIC tour

For de av dere som kanskje aldri har hørt om BASIC, vil jeg gi deg et kort glimt av språket, slik at du kan forstå parsing og utførelsesutfordringene fremover. For mer informasjon om BASIC, anbefaler jeg ressursene på slutten av denne kolonnen.

BASIC står for Beginners All-purpose Symbolic Instructional Code, og den ble utviklet ved Dartmouth University for å undervise beregningskonsepter til studenter. Siden utviklingen har BASIC utviklet seg til en rekke dialekter. Den enkleste av disse dialektene brukes som styrespråk for industrielle prosesskontrollere; de mest komplekse dialektene er strukturerte språk som inneholder noen aspekter av objektorientert programmering. For prosjektet mitt valgte jeg en dialekt kjent som BASIC-80 som var populær på CP / M-operativsystemet på slutten av syttitallet. Denne dialekten er bare moderat mer kompleks enn de enkleste dialektene.

Uttalelsessyntaks

Alle uttalelseslinjene er av skjemaet

[ : [ : ... ] ]

der "Linje" er et utsagnslinjenummer, "Nøkkelord" er et GRUNNLEGGENDE søkeord, og "Parametere" er et sett med parametere som er knyttet til det søkeordet.

Linjenummeret har to formål: Det fungerer som en etikett for utsagn som styrer utførelsesflyten, for eksempel en gå til uttalelse, og den fungerer som en sorteringsmerke for utsagn satt inn i programmet. Som en sorteringsmerke forenkler linjenummeret et redigeringsmiljø der redigering og kommandobehandling blandes i en enkelt interaktiv økt. Forresten, dette var nødvendig når alt du hadde var en teletype. :-)

Selv om det ikke er veldig elegant, gir linjenumre tolkemiljøet muligheten til å oppdatere programmet en uttalelse om gangen. Denne evnen stammer fra det faktum at en setning er en enkelt analysert enhet og kan kobles i en datastruktur med linjenumre. Uten linjenumre er det ofte nødvendig å analysere hele programmet når en linje endres.

Nøkkelordet identifiserer BASIC-setningen. I eksemplet vil vår tolk støtte et litt utvidet sett med BASIC nøkkelord, inkludert gå til, gosub, komme tilbake, skrive ut, hvis, slutt, data, restaurere, lese, , rem, til, neste, la, inngang, Stoppe, dim, randomisere, tron, og troff. Åpenbart vil vi ikke gå gjennom alle disse i denne artikkelen, men det vil være litt dokumentasjon på nettet i min neste måneds "Java In Depth" som du kan utforske.

Hvert søkeord har et sett med lovlige søkeordparametere som kan følge det. For eksempel gå til nøkkelordet må følges av et linjenummer, hvis uttalelse må følges av et betinget uttrykk samt nøkkelordet deretter -- og så videre. Parametrene er spesifikke for hvert nøkkelord. Jeg vil dekke et par av disse parameterlistene i detalj litt senere.

Uttrykk og operatører

Ofte er en parameter spesifisert i en uttalelse et uttrykk. Versjonen av BASIC jeg bruker her støtter alle standard matematiske operasjoner, logiske operasjoner, eksponentiering og et enkelt funksjonsbibliotek. Den viktigste komponenten i uttrykket grammatikk er evnen til å ringe funksjoner. Uttrykkene i seg selv er ganske standard og ligner de som ble analysert av eksemplet i min forrige StreamTokenizer-kolonne.

Variabler og datatyper

En del av grunnen til at BASIC er et så enkelt språk, er at det bare har to datatyper: tall og strenger. Noen skriptspråk, som REXX og PERL, skiller ikke engang dette skillet mellom datatyper før de blir brukt. Men med BASIC brukes en enkel syntaks til å identifisere datatyper.

Variable navn i denne versjonen av BASIC er strenger av bokstaver og tall som alltid begynner med en bokstav. Variabler er ikke store og små bokstaver. Dermed er A, B, FOO og FOO2 alle gyldige variabelnavn. Videre, i BASIC, tilsvarer variabelen FOOBAR FooBar. For å identifisere strenger legges et dollartegn ($) til variabelnavnet; således er variabelen FOO $ en variabel som inneholder en streng.

Til slutt støtter denne versjonen av språket matriser ved hjelp av dim nøkkelord og en variabel syntaks av skjemaet NAME (index1, index2, ...) for opptil fire indekser.

Programstruktur

Programmer i BASIC starter som standard på den laveste nummererte linjen og fortsetter til det ikke er flere linjer å behandle eller Stoppe eller slutt nøkkelord blir utført. Et veldig enkelt BASIC-program er vist nedenfor:

100 REM Dette er sannsynligvis det kanoniske BASIC eksempelet 110 REM-programmet. Merk at REM-setninger ignoreres. 120 SKRIV UT "Dette er et testprogram." 130 UTSKRIFT "Oppsummering av verdiene mellom 1 og 100" 140 LET totalt = 0 150 FOR I = 1 TIL 100 160 LET totalt = totalt + i 170 NESTE I 180 UTSKRIFT "Totalt av alle sifrene mellom 1 og 100 er" totalt 190 END 

Linjenumrene over angir den leksikale rekkefølgen på utsagnene. Når de kjøres, skriver linje 120 og 130 ut meldinger til utgangen, linje 140 initialiserer en variabel, og sløyfen i linjene 150 til 170 oppdaterer verdien til den variabelen. Til slutt blir resultatene skrevet ut. Som du kan se, er BASIC et veldig enkelt programmeringsspråk og derfor en ideell kandidat for undervisning i beregningskonsepter.

Organisere tilnærmingen

BASIC er typisk for skriptspråk og involverer et program som består av mange utsagn som kjører i et bestemt miljø. Designutfordringen er altså å konstruere objektene for å implementere et slikt system på en nyttig måte.

Da jeg så på problemet, sprang en rett datastruktur ganske ut på meg. Denne strukturen er som følger:

Det offentlige grensesnittet til skriptspråket skal bestå av

  • En fabrikkmetode som tar kildekoden som inndata og returnerer et objekt som representerer programmet.
  • Et miljø som gir rammeverket programmet kjøres med, inkludert "I / O" -enheter for tekstinput og tekstoutput.
  • En standard måte å endre objektet på, kanskje i form av et grensesnitt, som gjør det mulig å kombinere programmet og miljøet for å oppnå nyttige resultater.

Internt var strukturen til tolk litt mer komplisert. Spørsmålet var hvordan man skal ta i betraktning de to fasettene ved skriptspråket, analysering og utførelse? Tre grupper av klasser resulterte - en for parsing, en for det strukturelle rammeverket for å representere analyserte og kjørbare programmer, og en som dannet basismiljøklassen for utførelse.

I analysegruppen kreves følgende objekter:

  • Leksisk analyse for behandling av koden som tekst
  • Uttrykksparsing, for å konstruere parse-trær av uttrykkene
  • Uttalelse av analyse, for å konstruere parse-trær av selve uttalelsene
  • Feilklasser for rapportering av feil i parsing

Rammegruppen består av gjenstander som holder parsentrærne og variablene. Disse inkluderer:

  • Et utsagnobjekt med mange spesialiserte underklasser som representerer analyserte utsagn
  • Et uttrykksobjekt som representerer uttrykk for evaluering
  • Et variabelt objekt med mange spesialiserte underklasser som representerer atomforekomster av data
$config[zx-auto] not found$config[zx-overlay] not found