Normalne parsery, jak się zwykle uczy, mają etap leksykalny, zanim parser dotknie wejścia. Lexer (także „skaner” lub „tokenizer”) dzieli dane wejściowe na małe tokeny opatrzone adnotacjami typu. Pozwala to głównemu parserowi używać tokenów jako elementów terminalu, zamiast traktować każdy znak jako terminal, co prowadzi do zauważalnego wzrostu wydajności. W szczególności leksykon może również usunąć wszystkie komentarze i białe znaki. Jednak oddzielna faza tokenizera oznacza, że słowa kluczowe nie mogą być również używane jako identyfikatory (chyba że język obsługuje stropowanie, które nieco popadło w niełaskę, lub poprzedza wszystkie identyfikatory znakiem podobnym do sigil $foo).
Dlaczego? Załóżmy, że mamy prosty tokenizer, który rozumie następujące tokeny:
FOR = 'for'
LPAREN = '('
RPAREN = ')'
IN = 'in'
IDENT = /\w+/
COLON = ':'
SEMICOLON = ';'
Tokenizer zawsze będzie pasował do najdłuższego tokena i woli słowa kluczowe niż identyfikatory. interestingBędzie więc leksykalny jako IDENT:interesting, ale inbędzie leksykalny jako IN, nigdy jako IDENT:interesting. Fragment kodu podobny do
for(var in expression)
zostaną przetłumaczone na strumień tokenu
FOR LPAREN IDENT:var IN IDENT:expression RPAREN
Jak dotąd to działa. Ale każda zmienna inbyłaby leksykowana jako słowo kluczowe, INa nie zmienna, która złamałaby kod. Lexer nie utrzymuje żadnego stanu między tokenami i nie może wiedzieć, że inzwykle powinna to być zmienna, z wyjątkiem sytuacji, gdy jesteśmy w pętli for. Ponadto następujący kod powinien być legalny:
for(in in expression)
Pierwszy inbyłby identyfikatorem, drugi byłby słowem kluczowym.
Istnieją dwie reakcje na ten problem:
Kontekstowe słowa kluczowe są mylące, zamiast tego użyjmy słów kluczowych.
Java ma wiele zastrzeżonych słów, z których niektóre nie mają żadnego zastosowania poza dostarczaniem bardziej pomocnych komunikatów o błędach programistom przechodzącym na Javę z C ++. Dodanie nowych słów kluczowych powoduje uszkodzenie kodu. Dodanie kontekstowych słów kluczowych jest mylące dla czytelnika kodu, chyba że mają dobre wyróżnianie składni i utrudniają implementację narzędzi, ponieważ będą musieli użyć bardziej zaawansowanych technik analizy (patrz poniżej).
Gdy chcemy rozszerzyć język, jedynym rozsądnym podejściem jest użycie symboli, które wcześniej nie były legalne w tym języku. W szczególności nie mogą to być identyfikatory. Dzięki składni pętli foreach Java ponownie wykorzystała istniejące :słowo kluczowe z nowym znaczeniem. Do lambdas Java dodało ->słowo kluczowe, które wcześniej nie mogło występować w żadnym legalnym programie ( -->nadal byłoby leksykowane jako '--' '>'zgodne z prawem, i ->mogło być wcześniej leksykowane jako '-', '>', ale ta sekwencja zostałaby odrzucona przez parser).
Kontekstowe słowa kluczowe upraszczają języki, zaimplementujmy je
Lexery są bezdyskusyjnie przydatne. Ale zamiast uruchamiania leksera przed analizatorem składni, możemy uruchamiać je razem z analizatorem składni. Parsery oddolne zawsze znają zestaw typów tokenów, który byłby akceptowalny w danym miejscu. Analizator składni może następnie poprosić leksera o dopasowanie dowolnego z tych typów w bieżącej pozycji. W pętli dla każdego analizator składni byłby w pozycji oznaczonej przez ·(uproszczoną) gramatykę po znalezieniu zmiennej:
for_loop = for_loop_cstyle | for_each_loop
for_loop_cstyle = 'for' '(' declaration · ';' expression ';' expression ')'
for_each_loop = 'for' '(' declaration · 'in' expression ')'
W tej pozycji legalne żetony są SEMICOLONlub INnie są IDENT. Słowo kluczowe inbyłoby całkowicie jednoznaczne.
W tym konkretnym przykładzie parsery odgórne również nie miałyby problemu, ponieważ możemy przepisać powyższą gramatykę na
for_loop = 'for' '(' declaration · for_loop_rest ')'
for_loop_rest = · ';' expression ';' expression
for_loop_rest = · 'in' expression
i wszystkie żetony niezbędne do podjęcia decyzji można zobaczyć bez cofania się.
Rozważ użyteczność
Java zawsze dążyła do semantycznej i syntaktycznej prostoty. Na przykład język nie obsługuje przeciążania operatora, ponieważ znacznie skomplikowałby kod. Więc przy podejmowaniu decyzji pomiędzy ini :dla każdej pętli FOR-składni, musimy zastanowić się, które jest mniej skomplikowany i bardziej widoczne dla użytkowników. Prawdopodobnie byłby to skrajny przypadek
for (in in in in())
for (in in : in())
(Uwaga: Java ma osobne przestrzenie nazw dla nazw typów, zmiennych i metod. Myślę, że to głównie pomyłka. To nie znaczy, że późniejszy projekt języka musi dodać więcej błędów.)
Która alternatywa zapewnia wyraźniejsze wizualne rozdzielenie między zmienną iteracyjną a iterowaną kolekcją? Którą alternatywę można rozpoznać szybciej, gdy spojrzysz na kod? Przekonałem się, że symbole rozdzielające są lepsze niż ciąg słów, jeśli chodzi o te kryteria. Inne języki mają różne wartości. Np. Python określa wiele operatorów w języku angielskim, aby można je było czytać w sposób naturalny i są łatwe do zrozumienia, ale te same właściwości mogą utrudnić zrozumienie fragmentu Pythona na pierwszy rzut oka.