Sıfırdan Tercüman Yazmaya Nasıl Yaklaşılır?
Yayınlanan: 2022-03-11Bazıları "her şey bir ve sıfıra iner" der—ama programlarımızın bu parçalara nasıl çevrildiğini gerçekten anlıyor muyuz?
Derleyiciler ve yorumlayıcılar, bir programı temsil eden ham bir dize alır, onu ayrıştırır ve anlamlandırır. Tercümanlar ikisinin daha basit olmasına rağmen, çok basit bir tercüman (sadece toplama ve çarpma yapan) bile yazmak öğretici olacaktır. Derleyicilerin ve yorumlayıcıların ortak noktalarına odaklanacağız: girdiyi lexing ve ayrıştırma.
Kendi Tercümanınızı Yazmanın Yapılması ve Yapılmaması Gerekenler
Okuyucular, normal ifadenin nesi var diye merak edebilirler. Normal ifadeler güçlüdür, ancak kaynak kod dilbilgileri onlar tarafından ayrıştırılacak kadar basit değildir. Etki alanına özgü diller (DSL'ler) de değildir ve örneğin, bir istemcinin yetkilendirme ifadeleri için özel bir DSL'ye ihtiyacı olabilir. Ancak bu beceriyi doğrudan uygulamadan bile, bir yorumlayıcı yazmak, birçok programlama dilinin, dosya biçiminin ve DSL'nin arkasındaki çabayı takdir etmeyi çok daha kolaylaştırır.
Ayrıştırıcıları elle doğru şekilde yazmak, ilgili tüm uç durumlarda zor olabilir. Bu nedenle, birçok popüler programlama dili için ayrıştırıcılar oluşturabilen ANTLR gibi popüler araçlar vardır. Ayrıca geliştiricilerin ayrıştırıcıları doğrudan tercih ettikleri programlama dillerinde yazmasına olanak tanıyan ayrıştırıcı birleştiriciler adı verilen kitaplıklar da vardır. Örnekler, Scala için FastParse ve Python için Parsec'i içerir.
Profesyonel bir bağlamda okuyucuların, tekerleği yeniden icat etmekten kaçınmak için bu tür araçları ve kitaplıkları kullanmalarını öneririz. Yine de, sıfırdan bir tercüman yazmanın zorluklarını ve olanaklarını anlamak, geliştiricilerin bu tür çözümlerden daha etkili bir şekilde yararlanmasına yardımcı olacaktır.
Tercüman Bileşenlerine Genel Bakış
Bir tercüman karmaşık bir programdır, bu nedenle birden fazla aşaması vardır:
- Sözcük , bir karakter dizisini (düz metin) bir dizi simgeye dönüştüren bir yorumlayıcının parçasıdır.
- Bir ayrıştırıcı sırayla bir dizi belirteç alır ve bir dilin soyut sözdizimi ağacını (AST) üretir. Bir ayrıştırıcının çalıştığı kurallar genellikle resmi bir dilbilgisi ile belirtilir.
- Yorumlayıcı , bir programın kaynağının AST'sini anında (ilk önce derlemeden) yorumlayan bir programdır.
Burada belirli, entegre bir tercüman oluşturmayacağız. Bunun yerine, bu bölümlerin her birini ve ortak sorunlarını ayrı örneklerle inceleyeceğiz. Sonunda, kullanıcı kodu şöyle görünecektir:
val input = "2 * 7 + 5" val tokens = Lexer(input).lex() val ast = Parser(tokens).parse() val res = Interpreter(ast).interpret() println(s"Result is: $res") Üç aşamayı takiben, bu kodun nihai bir değer hesaplamasını ve Result is: 19 yazdırmasını bekleyeceğiz. Bu eğitimde Scala kullanılıyor çünkü:
- Çok özlü, çok sayıda kodu tek ekrana sığdırmak.
- Başlatılmamış/boş değişkenlere gerek kalmadan ifade odaklı.
- Güçlü bir koleksiyon kitaplığı, numaralandırmalar ve vaka sınıflarıyla güvenli yazın.
Spesifik olarak, buradaki kod Scala3 isteğe bağlı parantez sözdiziminde (Python benzeri, girinti tabanlı bir sözdizimi) yazılmıştır. Ancak yaklaşımların hiçbiri Scala'ya özgü değildir ve Scala diğer birçok dile benzer: Okuyucular bu kod örneklerini diğer dillere dönüştürmeyi kolay bulacaktır. Bunun dışında, örnekler Scastie kullanılarak çevrimiçi olarak çalıştırılabilir.
Son olarak, Lexer, Parser ve Interpreter bölümleri farklı örnek gramerlere sahiptir. İlgili GitHub deposunun gösterdiği gibi, sonraki örneklerdeki bağımlılıklar bu gramerleri uygulamak için biraz değişir, ancak genel kavramlar aynı kalır.
Tercüman Bileşen 1: Bir Lexer Yazma
Diyelim ki şu dizgeyi yazmak istiyoruz: "123 + 45 true * false1" . Farklı türde belirteçler içerir:
- Tamsayı değişmezleri
- A
+operatörü - A
*operatörü -
truebir edebi - Bir tanımlayıcı,
false1
Bu örnekte belirteçler arasındaki boşluk atlanacaktır.
Bu aşamada, ifadelerin anlamlı olması gerekmez; lexer, giriş dizesini bir belirteç listesine dönüştürür. (“Belirteçleri anlamlandırma” işi ayrıştırıcıya bırakılmıştır.)
Bir jetonu temsil etmek için bu kodu kullanacağız:
case class Token( tpe: Token.Type, text: String, startPos: Int ) object Token: enum Type: case Num case Plus case Times case Identifier case True case False case EOFHer belirtecin orijinal girişte bir türü, metinsel gösterimi ve konumu vardır. Konum, lexer'ın son kullanıcılarına hata ayıklama konusunda yardımcı olabilir.
EOF belirteci, girişin sonunu işaretleyen özel bir belirteçtir. Kaynak metinde yok; sadece ayrıştırıcı aşamasını basitleştirmek için kullanıyoruz.
Bu, lexer'ımızın çıktısı olacaktır:
Lexing input: 123 + 45 true * false1 Tokens: List( Token(tpe = Num, text = "123", tokenStartPos = 0), Token(tpe = Plus, text = "+", tokenStartPos = 4), Token(tpe = Num, text = "45", tokenStartPos = 6), Token(tpe = True, text = "true", tokenStartPos = 9), Token(tpe = Times, text = "*", tokenStartPos = 14), Token(tpe = Identifier, text = "false1", tokenStartPos = 16), Token(tpe = EOF, text = "<EOF>", tokenStartPos = 22) )uygulamasını inceleyelim:
class Lexer(input: String): def lex(): List[Token] = val tokens = mutable.ArrayBuffer.empty[Token] var currentPos = 0 while currentPos < input.length do val tokenStartPos = currentPos val lookahead = input(currentPos) if lookahead.isWhitespace then currentPos += 1 // ignore whitespace else if lookahead == '+' then currentPos += 1 tokens += Token(Type.Plus, lookahead.toString, tokenStartPos) else if lookahead == '*' then currentPos += 1 tokens += Token(Type.Times, lookahead.toString, tokenStartPos) else if lookahead.isDigit then var text = "" while currentPos < input.length && input(currentPos).isDigit do text += input(currentPos) currentPos += 1 tokens += Token(Type.Num, text, tokenStartPos) else if lookahead.isLetter then // first must be letter var text = "" while currentPos < input.length && input(currentPos).isLetterOrDigit do text += input(currentPos) currentPos += 1 val tpe = text match case "true" => Type.True // special casing literals case "false" => Type.False case _ => Type.Identifier tokens += Token(tpe, text, tokenStartPos) else error(s"Unknown character '$lookahead' at position $currentPos") tokens += Token(Type.EOF, "<EOF>", currentPos) // special end marker tokens.toListBoş bir jeton listesiyle başlıyoruz, sonra dizeyi gözden geçirip jetonları geldikçe ekliyoruz.
Bir sonraki belirtecin türüne karar vermek için ileriye dönük karakteri kullanırız. İleriye dönük karakterin her zaman incelenmekte olan en uzak karakter olmadığını unutmayın. Öngörüye dayanarak, belirtecin neye benzediğini biliyoruz ve mevcut belirteçte beklenen tüm karakterleri taramak için currentPos kullanıyoruz, ardından belirteci listeye ekliyoruz:
Öngörü boşluksa, onu atlarız. Tek harfli jetonlar önemsizdir; onları ekliyoruz ve dizini artırıyoruz. Tamsayılar için yalnızca dizine bakmamız gerekir.
Şimdi biraz karmaşık bir şeye geliyoruz: tanımlayıcılara karşı değişmez değerler. Kural şu ki , mümkün olan en uzun eşleşmeyi alıyoruz ve bunun değişmez olup olmadığını kontrol ediyoruz; değilse, bu bir tanımlayıcıdır.
< ve <= gibi operatörleri kullanırken dikkatli olun. Burada bir <= operatörü olduğu sonucuna varmadan önce bir karakter daha ileriye bakmanız ve = olup olmadığına bakmanız gerekir. Aksi takdirde, sadece bir < .
Bununla birlikte, lexer'ımız bir belirteç listesi üretti.
Yorumlayıcı Bileşen 2: Ayrıştırıcı Yazma
Belirteçlerimize biraz yapı vermeliyiz - tek başına bir liste ile pek bir şey yapamayız. Örneğin şunları bilmemiz gerekir:
Hangi ifadeler iç içedir? Hangi operatörler hangi sırayla uygulanır? Varsa hangi kapsam kuralları geçerlidir?
Bir ağaç yapısı yuvalamayı ve sıralamayı destekler. Ama önce, ağaç inşa etmek için bazı kurallar tanımlamamız gerekiyor. Ayrıştırıcımızın açık olmasını - belirli bir girdi için her zaman aynı yapıyı döndürmesini istiyoruz.
Aşağıdaki ayrıştırıcının önceki lexer örneğini kullanmadığına dikkat edin . Bu sayı eklemek içindir, bu nedenle dilbilgisinde yalnızca iki belirteç vardır, '+' ve NUM :
expr -> expr '+' expr expr -> NUM Normal ifadelerde olduğu gibi bir "veya" sembolü olarak bir çizgi karakteri ( | ) kullanan bir eşdeğer:
expr -> expr '+' expr | NUM Her iki durumda da, iki expr var: biri iki ifadeyi toplayabileceğimizi söyleyen ve diğeri expr bir NUM belirteci olabileceğini söyleyen ve burada negatif olmayan bir tamsayı anlamına gelecek.
Kurallar genellikle resmi bir dilbilgisi ile belirtilir. Resmi bir dilbilgisi şunlardan oluşur: Yukarıda gösterildiği gibi kuralların kendisi Bir başlangıç kuralı (gelenek başına belirtilen ilk kural) Kuralları tanımlamak için iki tür sembol: Uçlar: dilimizin “harfleri” (ve diğer karakterler)— belirteçleri oluşturan indirgenemez semboller Terminal Olmayanlar: ayrıştırma için kullanılan ara yapılar (yani, değiştirilebilir semboller)
Bir kuralın sol tarafında yalnızca bir non-terminal olabilir; sağ tarafta hem terminaller hem de terminal olmayanlar olabilir. Yukarıdaki örnekte, terminaller '+' ve NUM ve tek terminal olmayan expr . Daha geniş bir örnek için, Java dilinde 'true' , '+' , Identifier ve '[' gibi terminallerimiz ve BlockStatements , ClassBody ve MethodOrFieldDecl gibi terminal olmayanlarımız var.
Bu ayrıştırıcıyı uygulamanın birçok yolu vardır. Burada bir “özyinelemeli iniş” ayrıştırma tekniği kullanacağız. En yaygın türdür çünkü anlaşılması ve uygulanması en basit olanıdır.
Özyinelemeli bir iniş ayrıştırıcısı, dilbilgisindeki her bir terminal olmayan için bir işlev kullanır. Başlangıç kuralından başlar ve oradan aşağı iner (dolayısıyla “iniş”), her fonksiyonda hangi kuralın uygulanacağını belirler. "Yinelemeli" kısım çok önemlidir çünkü terminal olmayanları özyinelemeli olarak iç içe geçirebiliriz! Normal ifadeler bunu yapamaz: Dengeli parantezleri bile kaldıramazlar. Bu yüzden daha güçlü bir araca ihtiyacımız var.
İlk kural için bir ayrıştırıcı şöyle görünür (tam kod):
def expr() = expr() eat('+') expr() eat() işlevi, ileriye dönük işaretin beklenen belirteçle eşleşip eşleşmediğini kontrol eder ve ardından ileriye dönük dizini hareket ettirir. Ne yazık ki, bu henüz işe yaramayacak çünkü gramerimizle ilgili bazı sorunları düzeltmemiz gerekiyor.
Dilbilgisi Belirsizliği
İlk konu, ilk bakışta anlaşılmayan dilbilgimizin belirsizliğidir:
expr -> expr '+' expr | NUM 1 + 2 + 3 girişi göz önüne alındığında, ayrıştırıcımız, elde edilen expr önce sol expr veya sağ ifadeyi hesaplamayı seçebilir:

Bu yüzden bazı asimetriyi tanıtmamız gerekiyor:
expr -> expr '+' NUM | NUMBu dilbilgisi ile temsil edebileceğimiz ifadeler seti, ilk versiyonundan bu yana değişmedi. Ancak şimdi net : Ayrıştırıcı her zaman sola gider. Tam ihtiyacımız olan şey!
Bu, + işlemimizi ilişkisel bıraktı , ancak Tercüman bölümüne geldiğimizde bu belirginleşecek.
Sol özyinelemeli Kurallar
Ne yazık ki, yukarıdaki düzeltme diğer sorunumuzu çözmez, sol özyineleme:
def expr() = expr() eat('+') eat(NUM)Burada sonsuz özyineleme var. Bu işleve adım atarsak, sonunda bir yığın taşması hatası alırdık. Ancak ayrıştırma teorisi yardımcı olabilir!
Alfa'nın herhangi bir uçbirim ve uçbirim olmayan dizisi olabileceği, buna benzer bir alpha olduğunu varsayalım:
A -> A alpha | BBu grameri şu şekilde yeniden yazabiliriz:
A -> BA' A' -> alpha A' | epsilon Orada, epsilon boş bir dizedir—hiçbir şey, jeton yok.
Şimdi gramerimizin güncel revizyonunu ele alalım:
expr -> expr '+' NUM | NUM Alfa'nın '+' NUM belirteçlerimiz olduğu, yukarıda ayrıntıları verilen ayrıştırma kurallarını yeniden yazma yöntemini izleyerek, alpha şöyle olur:
expr -> NUM exprOpt exprOpt -> '+' NUM exprOpt | epsilonŞimdi dilbilgisi tamam ve özyinelemeli bir iniş ayrıştırıcısı ile ayrıştırabiliriz. Bakalım böyle bir ayrıştırıcı, gramerimizin bu en son yinelemesini nasıl arayacak:
class Parser(allTokens: List[Token]): import Token.Type private var tokens = allTokens private var lookahead = tokens.head def parse(): Unit = expr() if lookahead.tpe != Type.EOF then error(s"Unknown token '${lookahead.text}' at position ${lookahead.tokenStartPos}") private def expr(): Unit = eat(Type.Num) exprOpt() private def exprOpt(): Unit = if lookahead.tpe == Type.Plus then eat(Type.Plus) eat(Type.Num) exprOpt() // else: end recursion, epsilon private def eat(tpe: Type): Unit = if lookahead.tpe != tpe then error(s"Expected: $tpe, got: ${lookahead.tpe} at position ${lookahead.startPos}") tokens = tokens.tail lookahead = tokens.head Burada ayrıştırıcımızı basitleştirmek için EOF belirtecini kullanıyoruz. Listemizde en az bir jeton olduğundan her zaman eminiz, bu nedenle özel bir boş liste durumuyla uğraşmamıza gerek yok.
Ayrıca, bir akış sözlüğüne geçersek, bir bellek içi listemiz değil, bir yineleyicimiz olur, bu nedenle girdinin sonuna geldiğimizi bilmek için bir işaretçiye ihtiyacımız var. Sona geldiğimizde, kalan son jeton EOF jetonu olmalıdır.
Kodu incelerken, bir ifadenin sadece bir sayı olabileceğini görebiliriz. Geriye hiçbir şey kalmazsa, bir sonraki belirteç Plus olmaz, bu nedenle ayrıştırmayı durdururuz. Son belirteç EOF olur ve işimiz biter.
Giriş dizesinde daha fazla belirteç varsa, bunların + 123 gibi görünmesi gerekir. İşte burada exprOpt() üzerinde özyineleme devreye girer!
AST oluşturma
Artık ifademizi başarıyla ayrıştırdığımıza göre, onunla olduğu gibi bir şey yapmak zor. Ayrıştırıcımıza bazı geri aramalar koyabiliriz, ancak bu çok hantal ve okunamaz olurdu. Bunun yerine, giriş ifadesini temsil eden bir ağaç olan bir AST döndüreceğiz:
case class Expr(num: Int, exprOpt: ExprOpt) enum ExprOpt: case Opt(num: Int, exprOpt: ExprOpt) case EpsilonBu, basit veri sınıflarını kullanan kurallarımıza benzer.
Ayrıştırıcımız artık kullanışlı bir veri yapısı döndürüyor:
class Parser(allTokens: List[Token]): import Token.Type private var tokens = allTokens private var lookahead = tokens.head def parse(): Expr = val res = expr() if lookahead.tpe != Type.EOF then error(s"Unknown token '${lookahead.text}' at position ${lookahead.tokenStartPos}") else res private def expr(): Expr = val num = eat(Type.Num) Expr(num.text.toInt, exprOpt()) private def exprOpt(): ExprOpt = if lookahead.tpe == Type.Plus then eat(Type.Plus) val num = eat(Type.Num) ExprOpt.Opt(num.text.toInt, exprOpt()) else ExprOpt.Epsilon eat() , error() ve diğer uygulama detayları için lütfen beraberindeki GitHub deposuna bakın.
Basitleştirme Kuralları
ExprOpt hala geliştirilebilir:
'+' NUM exprOpt | epsilonDilbilgimizde temsil ettiği kalıbı sadece ona bakarak tanımak zor. Bu özyinelemeyi daha basit bir yapıyla değiştirebileceğimiz ortaya çıktı:
('+' NUM)* Bu yapı basitçe '+' NUM sıfır veya daha fazla kez gerçekleştiği anlamına gelir.
Şimdi tam gramerimiz şöyle görünüyor:
expr -> NUM exprOpt* exprOpt -> '+' NUMVe AST'miz daha güzel görünüyor:
case class Expr(num: Int, exprOpts: Seq[ExprOpt]) case class ExprOpt(num: Int) Ortaya çıkan ayrıştırıcı aynı uzunluktadır ancak anlaşılması ve kullanılması daha kolaydır. Şimdi boş bir yapı ile başlayarak ima edilen Epsilon .
Burada ExprOpt sınıfına bile ihtiyacımız yoktu. case class Expr(num: Int, exprOpts: Seq[Int]) veya NUM ('+' NUM)* dilbilgisi biçiminde koyabilirdik. Peki neden yapmadık?
- veya * gibi birden fazla olası operatörümüz olsaydı, bunun gibi bir dilbilgimiz olurdu:
expr -> NUM exprOpt* exprOpt -> [+-*] NUM Bu durumda, AST operatör türünü barındırmak için ExprOpt ihtiyaç duyar:
case class Expr(num: Int, exprOpts: Seq[ExprOpt]) case class ExprOpt(op: String, num: Int) Dilbilgisindeki [+-*] sözdiziminin normal ifadelerdekiyle aynı anlama geldiğini unutmayın: "bu üç karakterden biri." Bunu yakında eylemde göreceğiz.
Tercüman Bileşen 3: Bir Tercüman Yazma
Tercümanımız, girdi ifademizin AST'sini almak için lexer ve parser'ımızı kullanacak ve ardından bu AST'yi istediğimiz şekilde değerlendirecektir. Bu durumda sayılarla uğraşıyoruz ve toplamlarını değerlendirmek istiyoruz.
Tercüman örneğimizin uygulanmasında bu basit dilbilgisini kullanacağız:
expr -> NUM exprOpt* exprOpt -> [+-] NUMVe bu AST:
case class Expr(num: Int, exprOpts: Seq[ExprOpt]) case class ExprOpt(op: Token.Type, num: Int)(Benzer gramerler için bir sözlük ve ayrıştırıcının nasıl uygulanacağını ele aldık, ancak takılan herhangi bir okuyucu, depodaki bu tam dilbilgisi için sözlük ve ayrıştırıcı uygulamalarını inceleyebilir.)
Şimdi yukarıdaki dilbilgisi için nasıl yorumlayıcı yazılacağını göreceğiz:
class Interpreter(ast: Expr): def interpret(): Int = eval(ast) private def eval(expr: Expr): Int = var tmp = expr.num expr.exprOpts.foreach { exprOpt => if exprOpt.op == Token.Type.Plus then tmp += exprOpt.num else tmp -= exprOpt.num } tmp Girişimizi bir hatayla karşılaşmadan bir AST'ye ayrıştırırsak, her zaman en az bir NUM sahip olacağımızdan eminiz. Sonra isteğe bağlı sayıları alıp sonucumuza ekleriz (veya çıkarırız).
+ 'nın sol çağrışımıyla ilgili en baştan not şimdi açık: En soldaki sayıdan başlıyoruz ve soldan sağa doğru başkalarını ekliyoruz. Bu toplama için önemsiz görünebilir, ancak çıkarmayı düşünün: 5 - 2 - 1 ifadesi (5 - 2) - 1 = 3 - 1 = 2 olarak değerlendirilir ve 5 - (2 - 1) = 5 - 1 = 4 olarak değerlendirilmez. !
Ancak artı ve eksi operatörlerini yorumlamanın ötesine geçmek istiyorsak, tanımlamamız gereken başka bir kural daha var.
Öncelik
1 + 2 + 3 gibi basit bir ifadeyi nasıl ayrıştıracağımızı biliyoruz, ancak 2 + 3 * 4 + 5 söz konusu olduğunda biraz sorunumuz var.
Çoğu insan, çarpmanın toplamadan daha yüksek önceliğe sahip olduğu konusunda hemfikirdir. Ancak ayrıştırıcı bunu bilmiyor. Sadece ((2 + 3) * 4) + 5 olarak değerlendiremeyiz. Bunun yerine (2 + (3 * 4)) + 5 istiyoruz.
Bu, önce çarpma işlemini değerlendirmemiz gerektiği anlamına gelir. Toplamadan önce değerlendirilmeye zorlamak için çarpmanın AST'nin kökünden daha uzak olması gerekir. Bunun için başka bir dolaylı katman eklememiz gerekiyor.
Naif Bir Dil Bilgisini Baştan Sona Düzeltmek
Bu, öncelik kuralı olmayan orijinal, sol özyinelemeli dilbilgimizdir:
expr -> expr '+' expr | expr '*' expr | NUMİlk olarak, ona öncelik kuralları veriyoruz ve belirsizliğini ortadan kaldırıyoruz:
expr -> expr '+' term | term term -> term '*' NUM | NUMSonra sol özyinelemeli olmayan kurallar alır:
expr -> term exprOpt* exprOpt -> '+' term term -> NUM termOpt* termOpt -> '*' NUMSonuç, güzel bir şekilde ifade edilen bir AST'dir:
case class Expr(term: Term, exprOpts: Seq[ExprOpt]) case class ExprOpt(term: Term) case class Term(num: Int, termOpts: Seq[TermOpt]) case class TermOpt(num: Int)Bu bize kısa bir tercüman uygulaması bırakır:
class Interpreter(ast: Expr): def interpret(): Int = eval(ast) private def eval(expr: Expr): Int = var tmp = eval(expr.term) expr.exprOpts.foreach { exprOpt => tmp += eval(exprOpt.term) } tmp private def eval(term: Term): Int = var tmp = term.num term.termOpts.foreach { termOpt => tmp *= termOpt.num } tmpDaha önce olduğu gibi, gerekli sözlük ve dilbilgisindeki fikirler daha önce ele alındı, ancak okuyucular gerekirse bunları depoda bulabilirler.
Tercüman Yazmanın Sonraki Adımları
Bunu ele almadık, ancak hata işleme ve raporlama , herhangi bir ayrıştırıcının çok önemli özellikleridir. Geliştiriciler olarak, bir derleyici kafa karıştırıcı veya yanıltıcı hatalar ürettiğinde bunun ne kadar sinir bozucu olabileceğini biliyoruz. Doğru ve kesin hata mesajları vermek, kullanıcıyı gereğinden fazla mesajla caydırmamak ve hatalardan zarafetle kurtulmak gibi çözülmesi gereken birçok ilginç problemi olan bir alandır. Gelecekteki kullanıcılarının daha iyi bir deneyime sahip olmasını sağlamak için bir tercüman veya derleyici yazmak geliştiricilerin görevidir.
Örnek sözcük oluşturucularımız, ayrıştırıcılarımız ve yorumlayıcılarımızı incelerken, derleyicilerin ve yorumlayıcıların arkasındaki, aşağıdaki gibi konuları kapsayan teorilerin yalnızca yüzeyini kazıdık:
- Kapsamlar ve sembol tabloları
- Statik türler
- Derleme zamanı optimizasyonu
- Statik program analizörleri ve linterler
- Kod biçimlendirme ve güzel baskı
- Alana özel diller
Daha fazla okuma için şu kaynakları tavsiye ederim:
- Terence Parr'dan Dil Uygulama Kalıpları
- Bob Nystrom'un ücretsiz çevrimiçi kitabı Crafting Interpreters
- Gramer ve Ayrıştırmaya Giriş , Paul Klint
- İyi Derleyici Hata Mesajları Yazma , Caleb Meredith
- East Carolina Üniversitesi “Program Çevirisi ve Derleme” dersinden notlar
