Bir lexer'ın çözümleyicisine döndürdüğü belirteçlerin veri türü ne olmalıdır?


21

Başlıkta belirtildiği gibi, bir lexer çözümleyiciye hangi veri türünü iade etmeli / vermelidir? Vikipedi'nin sahip olduğu sözcüksel analiz makalesini okurken , şunu belirtmiştir:

Bilgisayar bilimlerinde sözcüksel analiz, bir dizi karakterin (bir bilgisayar programında veya web sayfasında olduğu gibi) bir dizi dizinin ( tanımlanmış "anlamı" olan dizeler ) dönüştürülmesi işlemidir .

Ancak, yukarıdaki ifadeye tamamen aykırı bir biçimde, Farklı bir sitede sorduğum başka bir soru ( merak ediyorsanız Kod İnceleme ) yanıtlandığında, Cevap veren kişi şunları söyledi:

Sözlük genellikle dizgiyi okur ve bunu bir sözlük akışına dönüştürür. Sözlüklerin yalnızca bir sayı akışı olması gerekir .

ve o bu görsel verdi:

nl_output => 256
output    => 257
<string>  => 258

Daha sonra makalede Flexdaha önce var olan bir sözcüden bahsetti ve onunla 'kurallar' yazmanın elle bir söz sahibi yazmaktan daha kolay olacağını söyledi. Bana bu örneği vermeye devam etti:

Space              [ \r\n\t]
QuotedString       "[^"]*"
%%
nl_output          {return 256;}
output             {return 257;}
{QuotedString}     {return 258;}
{Space}            {/* Ignore */}
.                  {error("Unmatched character");}
%%

İçgörümü daha fazla bilgi edinmek ve daha fazla bilgi edinmek için Flex ile ilgili Wikipedia makalesini okudum . Flex makalesi, tokenlerle birlikte bir dizi sözdizimi kuralını şu şekilde tanımlayabileceğinizi gösterdi:

digit         [0-9]
letter        [a-zA-Z]

%%
"+"                  { return PLUS;       }
"-"                  { return MINUS;      }
"*"                  { return TIMES;      }
"/"                  { return SLASH;      }
"("                  { return LPAREN;     }
")"                  { return RPAREN;     }
";"                  { return SEMICOLON;  }
","                  { return COMMA;      }
"."                  { return PERIOD;     }
":="                 { return BECOMES;    }
"="                  { return EQL;        }
"<>"                 { return NEQ;        }
"<"                  { return LSS;        }
">"                  { return GTR;        }
"<="                 { return LEQ;        }
">="                 { return GEQ;        }
"begin"              { return BEGINSYM;   }
"call"               { return CALLSYM;    }
"const"              { return CONSTSYM;   }
"do"                 { return DOSYM;      }
"end"                { return ENDSYM;     }
"if"                 { return IFSYM;      }
"odd"                { return ODDSYM;     }
"procedure"          { return PROCSYM;    }
"then"               { return THENSYM;    }
"var"                { return VARSYM;     }
"while"              { return WHILESYM;   }

Bana öyle geliyor ki, Flex lexer \ string tokens dizelerini döndürüyor. Ancak belirli sayılara eşit olan sabitler döndürüyor olabilir.

Lexer rakamları döndürecekti, string değişmezlerini nasıl okuyacaktı? sayı döndürmek, tek anahtar kelimeler için iyidir, ancak bir dize ile nasıl ilgilenirsiniz? Lexer, dizgiyi ikili sayılara dönüştürmek zorunda kalmazdı ve daha sonra çözümleyici, sayıları tekrar dizgeye dönüştürürdü. Lexer'in dizeleri döndürmesi ve ardından çözümleyicinin herhangi bir sayı dizgisinin değişmezlerini gerçek sayılara dönüştürmesine izin vermek çok daha mantıklı (ve daha kolay) görünüyor.

Yoksa lexer her ikisini de geri alabilir mi? İşlevleriniz için yalnızca bir dönüş türüne sahip olmanızı sağlayan c ++ dilinde basit bir sözlük yazmaya çalışıyorum . Böylece sorumu sormam için beni yönlendir.

Sorumu bir paragrafa sıkıştırmak için: Bir sözlük yazarken ve bunun yalnızca bir veri türü (karakter dizileri veya sayılar) getirebileceğini varsayarsak , hangisi daha mantıklı bir seçim olabilir?


Lexer, geri dönmesini söylediğin şeyi geri verir. Tasarımınız sayıları ararsa, sayıları döndürür. Açıkçası, dize değişmezlerini temsil etmek, bundan biraz daha fazlasını gerektirecek. Ayrıca bakınız Sayıların ve İplerin Ayrıştırılması Lex's Job mu? Dize değişmezleri genellikle "Dil Öğeleri" olarak kabul edilmez.
Robert Harvey,

@RobertHarvey Yani string değişmezini ikili sayılara dönüştürür müsün?
Christian Dean

Anladığım kadarıyla, sözlüğün amacı, dil öğelerini (anahtar kelimeler, operatörler vb.) Almak ve bunları belirteçlere dönüştürmektir. Bu nedenle, alıntılanan dizeler, dilbilgisi için ilgi çekici değildir, çünkü dil öğeleri değildir. Hiçbir zaman kendimi bir lexer yazmamış olsam da, alıntılanan dizenin basitçe değişmeden (alıntılar dahil) geçtiğini hayal ediyorum.
Robert Harvey,

Demek istediğin şudur ki, sözcü dize değişmezlerini okumaz veya önemsemez. Ve bu yüzden ayrıştırıcı bu string değişmezlerini aramalı mı? Bu çok kafa karıştırıcı.
Christian Dean,

Bunu okumak için birkaç dakika harcamak isteyebilirsiniz: en.wikipedia.org/wiki/Lexical_analysis
Robert Harvey

Yanıtlar:


10

Genel olarak, konuşma ve ayrıştırma sırasında bir dili işliyorsanız, sözcük belirteçlerinizin bir tanımını yaparsınız, örneğin:

NUMBER ::= [0-9]+
ID     ::= [a-Z]+, except for keywords
IF     ::= 'if'
LPAREN ::= '('
RPAREN ::= ')'
COMMA  ::= ','
LBRACE ::= '{'
RBRACE ::= '}'
SEMICOLON ::= ';'
...

ve çözümleyici için bir grameriniz var:

STATEMENT ::= IF LPAREN EXPR RPAREN STATEMENT
            | LBRACE STATEMENT BRACE
            | EXPR SEMICOLON
EXPR      ::= ID
            | NUMBER
            | ID LPAREN EXPRS RPAREN
...

Lexer, girdi akışını alır ve bir simge akışı üretir. Belirteç akışı, ayrıştırıcı bir ağaç üretmek için ayrıştırıcı tarafından tüketilir. Bazı durumlarda, belirtecin türünü bilmek yeterlidir (örneğin, LPAREN, RBRACE, FOR), ancak bazı durumlarda belirteçle ilişkilendirilmiş olan gerçek değere ihtiyacınız olacaktır . Örneğin, bir kimlik belirteci ile karşılaştığınızda, hangi kimliğe başvurmaya çalıştığınızı bulmaya çalışırken kimliği oluşturan gerçek karakterleri isteyeceksiniz.

Yani, tipik olarak aşağı yukarı böyle bir şeye sahipsiniz:

enum TokenType {
  NUMBER, ID, IF, LPAREN, RPAREN, ...;
}

class Token {
  TokenType type;
  String value;
}

Böylece, bir belirteç döndüğünde, ne tür olduğunu (ayrıştırma için gerekli olan) ve oluşturduğu karakter dizisini (daha sonra dize ve sayısal hazırlayıcıları, tanımlayıcıları yorumlamak için gerekeceklerini) bilirsiniz. vb.). Çok basit bir toplama türü döndürdüğünüz için iki değer döndürdüğünüzü hissedebilirsiniz, ancak gerçekten iki bölüme de ihtiyacınız var. Sonuçta, aşağıdaki programlara farklı davranmak istersiniz:

if (2 > 0) {
  print("2 > 0");
}
if (0 > 2) {
  print("0 > 2");
}

Bunlar aynı tür token türlerini üretmektedir : IF, LPAREN, NUMBER, GREATER_THAN, NUMBER, RPAREN, LBRACE, ID, LPAREN, STRING, RPAREN, SEMICOLON, RBRACE. Bu da aynı şekilde ayrıştırmaları anlamına geliyor . Ancak, ayrıştırma ağacında bir şey yaptığınızda, ilk sayının değerinin '2' (veya '0') olmasını ve ikinci sayının değerinin '0' (veya '2) olmasını önemsersiniz. ') ve dizginin değerinin' 2> 0 '(veya' 0> 2 ') olduğunu.


Ben en çok senin diyerek ne olsun, ama nasıl yani String valuedoldurulmuş almak için gidiyor? Bir dize veya bir numara ile dolu olacak? Ayrıca, Stringtürü nasıl tanımlarım ?
Christian Dean

1
@ Mr.Python En basit durumda, sadece sözcüksel üretimle eşleşen karakter dizisidir. Yani, foo (23, "bar") görürseniz, belirteçleri [ID, "foo"], [LPAREN "("], [NUMBER, "23"], [COMMA "," ], [STRING, "" 23 ""], [RPAREN, ")"] . Bu bilgiyi korumak önemli olabilir. Veya başka bir yaklaşım izleyebilir ve değerin bir dize veya sayı vb. Olabilen bir birleşim türüne sahip olabilir ve ne tür bir belirteç türünüze (örneğin belirteç türü NUMBER olduğunda) doğru değer türünü seçebilirsiniz. , value.num kullanın ve STRING ise, value.str) kullanın.
Joshua Taylor,

@ MrPython "Ve ayrıca, String tipini nasıl tanımlarım?" Java ish zihniyetinden yazı yazıyordum. C ++ 'da çalışıyorsanız, C ++' ın string tipini kullanabilirsiniz veya C 'de çalışıyorsanız, bir char * kullanabilirsiniz. Mesele şu ki bir jetonla ilişkili, ilgili değere veya değeri üretmek için yorumlayabileceğiniz metne sahipsiniz.
Joshua Taylor,

1
@ ollydbg23 bu bir seçenek ve mantıksız bir seçenek değil, ancak sistemi daha az dahili olarak tutarlı hale getiriyor. Örneğin, ayrıştırdığınız son kasabanın dize değerini istiyorsanız, şimdi açıkça boş bir değer denetlemeniz ve sonra dizenin ne olacağını bulmak için tersten bir dize araması kullanmanız gerekir. Artı, lexer ve ayrıştırıcı arasında daha sıkı bir bağlantı kurar; LPAREN'in farklı veya çoklu dizelerle eşleşip eşleşmediğini güncellemek için daha fazla kod olabilir
Joshua Taylor

2
@ ollydbg23 Bir vaka basit bir sahte minifier olacaktır. Bunu yapmak yeterince kolaydır parse(inputStream).forEach(token -> print(token.string); print(' '))(yani, belirteçlerin dize değerlerini boşlukla ayırarak yazdırın). Bu oldukça hızlı. Ve LPAREN sadece "(" den gelse bile, bellekte sabit bir dize olabilir, bu yüzden belirtecine bir referans eklemek, boş referansı eklemekten daha pahalı olmayabilir. Beni özel bir durum yapmayan bir kod (Kod)
Joshua Taylor

6

Başlıkta belirtildiği gibi, bir lexer çözümleyiciyi hangi veri türüne döndürmeli / vermelidir?

Belli ki "Token". Bir lexer bir belirteç akışı üretir, bu yüzden bir belirteç akışı döndürmelidir .

Zaten var olan bir Lex olan Flex'ten bahsetti ve onunla 'kurallar' yazmanın elle bir lexer yazmaktan daha kolay olacağını söyledi.

Makine tarafından üretilen lexers, onları hızlı bir şekilde üretebilme avantajına sahiptir; bu, özellikle sözcüksel dilbilginizin çok değişeceğini düşünüyorsanız yararlıdır. Uygulama tercihlerinizde sıklıkla fazla esneklik kazanmamanın dezavantajı vardır.

Bu "basit" olup olmadığını kim umursar dedi? Lexer'ı yazmak genellikle zor kısım değildir!

Bir sözlük yazarken ve bunun yalnızca bir veri türü (karakter dizisi veya sayı) döndürebileceğini varsayarsak, hangisi daha mantıklı bir seçim olurdu?

Ne. Bir lexer genellikle bir belirteç döndüren "sonraki" bir işleme sahiptir, bu nedenle bir belirteç döndürmelidir . Belirteç, bir dize veya sayı değildir. Bu bir belirteç.

Yazdığım en son sözcü "tam bir sadakat" sözlüğü idi, bu, programda ve belirtecinin yanı sıra, programda "boşluk" ve "trivia" dediğimiz tüm boşlukların ve yorumların konumunu izleyen bir belirteç döndürdüğü anlamına geliyordu. Lexer'imde bir belirteç şöyle tanımlandı:

  • Önde gelen trivia dizisi
  • Bir belirteç türü
  • Karakterlerde belirteç genişliği
  • Sondaki önemsiz bir dizi

Diğer bilgiler şöyle tanımlandı:

  • Bir trivia tür - boşluk, yeni satır, yorum vb.
  • Karakterlerde trivia genişliği

Öyleyse gibi bir şeyimiz olsaydı

    foo + /* comment */
/* another comment */ bar;

belirteç türlü dört belirteçleri olarak Lex ki Identifier, Plus, Identifier, Semicolon, ve genişlikte 3, 1, 3, 1, birinci tanımlayıcı trivia oluşan öncü sahiptir Whitespace4 bir genişliğe sahip ve önemsiz şeyler arka Whitespace1. genişliği ile Plusbir öncü trivia vardır ve Bir boşluk, yorum ve yeni satırdan oluşan son bilgiler. Son tanımlayıcı, yorumda ve boşluğun önde gelen bir önemsizine sahiptir, vb.

Bu şema ile dosyadaki her karakter, sözdizimi renklendirmesi gibi şeyler için kullanışlı bir özellik olan lexer'ın çıktısında dikkate alınır.

Tabii ki, eğer triviaya ihtiyacınız yoksa, basit bir şekilde iki şeyi yapabilirsiniz: tür ve genişlik.

Belirteç ve önemsizliğin kaynak kodundaki mutlak konumlarını değil, yalnızca genişliklerini içerdiğini fark edebilirsiniz. Bu kasıtlı. Böyle bir planın avantajları vardır:

  • Hafıza ve kablo formatında kompakt
  • Düzenlemeler üzerinde re-lexing sağlar; lexer bir IDE içinde çalışıyorsa bu kullanışlıdır. Diğer bir deyişle, bir belirtecin içindeki bir düzenleme tespit ederseniz, düzenleme işleminden önce sözlüğünüzü birkaç belirteçe yedekler ve önceki belirteç akışıyla eşitlenene kadar yeniden yazmaya başlarsınız. Bir karakter yazdığınızda, bu karakter değiştikten sonra her belirtecin konumu değişir, ancak genellikle yalnızca bir veya iki belirteç genişlik değiştirir, böylece tüm bu durumu yeniden kullanabilirsiniz.
  • Her belirtecin tam karakter ofsetleri, belirteç akışı üzerinde yinelenerek ve mevcut ofseti takip ederek kolayca elde edilebilir. Tam karakter ofsetlerine sahip olduğunuzda, gerektiğinde metni çıkarmak kolaydır.

Bu senaryoların hiçbirini umursamıyorsanız, bir belirteç, bir çeşit ve genişlik yerine bir tür ve ofset olarak gösterilebilir.

Ancak buradaki kilit paket servisi şudur: programlama, faydalı soyutlamalar yapma sanatıdır . Belirteçleri değiştiriyorsunuz, bu yüzden belirteçler üzerinde faydalı bir soyutlama yapın ve bunun altında hangi uygulama ayrıntılarının altında olduğunuzu kendiniz seçin.


3

Genellikle, simgeyi (veya kullanım kolaylığı için enum değerini) belirten bir sayıya ve isteğe bağlı bir değere (dizge veya muhtemelen genel / şablonlanmış değer) sahip küçük bir yapı döndürürsünüz. Başka bir yaklaşım, ekstra veri taşıması gereken elementler için türetilmiş bir tip döndürmektir. Her ikisi de hafif derecede dengesizdir ancak pratik bir soruna yeterince iyi çözümler sunar.


Hafif derecede tatsız derken ne demek istiyorsun ? String değerlerini almanın yetersiz yolları var mı?
Christian Dean

@ Mr.Python - kod kullanılmadan önce verimsiz olan birçok denetime yol açacaklar, ancak moreso kodu biraz daha karmaşık / kırılgan hale getiriyor.
Telastyn

Ben C ++ lexer tasarımı benzer bir soru var, bir geri dönebilirler Token *sadece ya bir Tokenya da bir TokenPtrbiri bir ortak gösterici Tokensınıfın. Ancak bazı lexer'lerin sadece bir TokenType döndürdüğünü ve dize veya sayı değerini diğer global veya statik değişkenlerde sakladığını da görüyorum. Başka bir soru da Konum bilgilerini nasıl depolayabileceğimizdir, TokenType, String ve Location alanlarına sahip bir Token yapısına ihtiyacım var mı? Teşekkürler.
ollydbg23

@ ollydbg23 - bunlardan herhangi biri işe yarayabilir. Bir yapı kullanırdım. Ve öğrenmeyen diller için, yine de bir ayrıştırıcı oluşturucu kullanacaksınız.
Telastyn

@Telastyn Cevabınız için teşekkürler. Bir Token yapısı gibi bir şey olabilir demek struct Token {TokenType id; std::string lexeme; int line; int column;}? Örneğin, Lexer'ın genel bir işlevi PeekToken()için işlev a Token *veya TokenPtr. Bir süre için, eğer işlev sadece TokenType'ı döndürürse, Ayrıştırıcı Token hakkında diğer bilgileri nasıl almaya çalışır? Dolayısıyla, bu tür bir işlevden geri dönüş için veri türü gibi bir işaretçi tercih edilir. Fikrim hakkında yorumunuz var mı? Teşekkürler
ollydbg23
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.