Składnia (języki programowania) - Syntax (programming languages)

Podświetlanie składni i styl wcięcia są często używane do pomocy programistom w rozpoznawaniu elementów kodu źródłowego. Ten kod Pythona wykorzystuje podświetlanie kodowane kolorami.

W informatyce , składnia z języka komputerowego jest zbiorem zasad, które określa kombinacje symboli, które są uważane być poprawnie skonstruowane sprawozdania lub wyrażenia w tym języku. Dotyczy to zarówno języków programowania , w których dokument reprezentuje kod źródłowy , jak i języków znaczników , w których dokument reprezentuje dane.

Składnia języka określa jego formę powierzchniową. Tekstowe języki komputerowe opierają się na ciągach znaków , podczas gdy wizualne języki programowania opierają się na układzie przestrzennym i połączeniach między symbolami (które mogą być tekstowe lub graficzne). Dokumenty, które są niepoprawne składniowo, zawierają błąd składniowy . Projektując składnię języka, projektant może zacząć od spisania przykładów zarówno legalnych, jak i nielegalnych ciągów , zanim spróbuje zrozumieć ogólne zasady z tych przykładów.

Składnia odnosi się zatem do formy kodu i jest skontrastowana z semantykąznaczeniem . W przetwarzaniu języków komputerowych przetwarzanie semantyczne zwykle następuje po przetwarzaniu składniowym; jednak w niektórych przypadkach przetwarzanie semantyczne jest konieczne do pełnej analizy składniowej, a są one wykonywane razem lub równolegle . W kompilatorze analiza składniowa obejmuje frontend , podczas gdy analiza semantyczna obejmuje backend (i środkowy koniec, jeśli ta faza jest rozróżniona).

Poziomy składni

Składnia języka komputerowego dzieli się na trzy poziomy:

  • Wyrazy – poziom leksykalny, określający sposób, w jaki znaki tworzą tokeny ;
  • Zwroty – poziom gramatyki, mówiąc wąsko, określający, w jaki sposób tokeny tworzą frazy;
  • Kontekst – określanie do jakich obiektów lub zmiennych odwołują się nazwy, czy typy są poprawne itp.

Wyróżnienie w ten sposób daje modułowość, pozwalając na opisanie i przetwarzanie każdego poziomu oddzielnie i często niezależnie. Po pierwsze, leksyk zamienia liniowy ciąg znaków w liniowy ciąg znaków; jest to znane jako „ analiza leksykalna ” lub „leksowanie”. Po drugie, parser przekształca liniową sekwencję tokenów w hierarchiczne drzewo składni; nazywa się to wąsko „ parsowaniem ”. Po trzecie, analiza kontekstowa rozwiązuje nazwy i typy sprawdzeń. Ta modularność jest czasami możliwa, ale w wielu rzeczywistych językach wcześniejszy krok zależy od późniejszego kroku – na przykład hack lexer w C jest spowodowany tym, że tokenizacja zależy od kontekstu. Nawet w takich przypadkach analiza syntaktyczna jest często postrzegana jako przybliżająca ten idealny model.

Sam etap analizowania można podzielić na dwie części: drzewo analizowania lub „konkretne drzewo składni”, które jest określone przez gramatykę, ale generalnie jest zbyt szczegółowe, aby można je było zastosować w praktyce, oraz abstrakcyjne drzewo składni (AST), które upraszcza to w użyteczną formę. Kroki AST i analizy kontekstowej można uznać za formę analizy semantycznej, ponieważ dodają znaczenie i interpretację do składni, lub alternatywnie jako nieformalne, ręczne implementacje reguł składniowych, które byłyby trudne lub niewygodne do formalnego opisania lub zaimplementowania.

Poziomy generalnie odpowiadają poziomom w hierarchii Chomsky'ego . Słowa są w języku regularnym , określonym w gramatyce leksykalnej , która jest gramatykią typu 3, zazwyczaj podawaną jako wyrażenia regularne . Zwroty są w języku bezkontekstowym (CFL), ogólnie deterministycznym języku bezkontekstowym (DCFL), określonym w gramatyce struktury fraz , która jest gramatykiem typu 2, ogólnie podaną jako reguły produkcji w formie Backus-Naur (BNF ). Gramatyki fraz są często określane w znacznie bardziej ograniczonych gramatykach niż pełne gramatyki bezkontekstowe , aby ułatwić ich analizę; podczas gdy parser LR może analizować dowolny DCFL w czasie liniowym, prosty parser LALR i jeszcze prostszy parser LL są bardziej wydajne, ale mogą analizować tylko gramatyki, których reguły produkcji są ograniczone. Zasadniczo strukturę kontekstową można opisać za pomocą gramatyki kontekstowej i automatycznie analizować za pomocą takich środków, jak gramatyki atrybutów , chociaż ogólnie rzecz biorąc, ten krok jest wykonywany ręcznie, za pomocą reguł rozpoznawania nazw i sprawdzania typów , oraz implementowany za pomocą tablicy symboli który przechowuje nazwy i typy dla każdego zakresu.

Zostały napisane narzędzia, które automatycznie generują lekser ze specyfikacji leksykalnej zapisanej w wyrażeniach regularnych oraz parser z frazy gramatyka napisanej w BNF: pozwala to na użycie programowania deklaratywnego , zamiast konieczności programowania proceduralnego lub funkcjonalnego. Godnym uwagi przykładem jest para lex - yacc . Automatycznie tworzą one konkretne drzewo składni; autor parsera musi następnie ręcznie napisać kod opisujący sposób konwersji do abstrakcyjnego drzewa składni. Analiza kontekstowa jest również na ogół wdrażana ręcznie. Pomimo istnienia tych automatycznych narzędzi, parsowanie jest często implementowane ręcznie, z różnych powodów – być może struktura fraz nie jest bezkontekstowa, lub alternatywna implementacja poprawia wydajność lub raportowanie błędów lub umożliwia łatwiejszą zmianę gramatyki. Parsery są często pisane w językach funkcjonalnych, takich jak Haskell lub w językach skryptowych, takich jak Python lub Perl lub w C lub C++ .

Przykłady błędów

Na przykład (add 1 1)jest to poprawny składniowo program Lisp (zakładając, że istnieje funkcja „dodaj”, w przeciwnym razie rozpoznawanie nazw nie powiedzie się), dodając 1 i 1. Jednak poniższe są nieprawidłowe:

(_ 1 1)    lexical error: '_' is not valid
(add 1 1   parsing error: missing closing ')'

Zauważ, że lekser nie jest w stanie zidentyfikować pierwszego błędu – wie tylko, że po wygenerowaniu tokena LEFT_PAREN, '(' reszta programu jest nieprawidłowa, ponieważ żadna reguła słowna nie zaczyna się od '_'. Drugi błąd jest wykryty na etapie parsowania: Parser zidentyfikował regułę produkcyjną „lista” na podstawie tokenu „(” (jako jedynego dopasowania), a zatem może wyświetlić komunikat o błędzie; ogólnie może być niejednoznaczny .

Błędy typu i niezadeklarowane błędy zmiennych są czasami uważane za błędy składniowe, gdy zostaną wykryte w czasie kompilacji (co zwykle ma miejsce podczas kompilowania języków o silnych typach), chociaż często klasyfikuje się tego rodzaju błędy jako błędy semantyczne .

Jako przykład, kod Pythona

'a' + 1

zawiera błąd typu, ponieważ dodaje literał ciągu do literału liczby całkowitej. Tego rodzaju błędy typu można wykryć w czasie kompilacji: mogą zostać wykryte podczas parsowania (analizy fraz), jeśli kompilator używa oddzielnych reguł, które zezwalają na „integerLiteral + integerLiteral”, ale nie na „stringLiteral + integerLiteral”, chociaż jest bardziej prawdopodobne, że kompilator użyje reguły parsowania, która zezwala na wszystkie wyrażenia w postaci "LiteralOrIdentifier + LiteralOrIdentifier" i wtedy błąd zostanie wykryty podczas analizy kontekstowej (gdy nastąpi sprawdzanie typu). W niektórych przypadkach ta walidacja nie jest wykonywana przez kompilator, a błędy te są wykrywane tylko w czasie wykonywania.

W języku o typie dynamicznym, w którym typ można określić tylko w czasie wykonywania, wiele błędów typu można wykryć tylko w czasie wykonywania. Na przykład kod Pythona

a + b

jest syntaktycznie poprawny na poziomie frazy, ale poprawność typów aib można określić tylko w czasie wykonywania, ponieważ zmienne nie mają typów w Pythonie, tylko wartości. Podczas gdy istnieje spór dotyczący tego, czy błąd typu wykryty przez kompilator powinien być nazywany błędem składni (a nie statycznym błędem semantycznym ), błędy typu, które można wykryć tylko w czasie wykonywania programu, są zawsze uważane za błędy semantyczne, a nie składniowe.

Definicja składni

Parsuj drzewo kodu Pythona z tokenizacją wstawek

Składnia tekstowych języków programowania jest zwykle definiowana za pomocą kombinacji wyrażeń regularnych (dla struktury leksykalnej ) i formy Backusa-Naura (dla struktury gramatycznej ) w celu indukcyjnego określenia kategorii składniowych (nieterminali) i symboli końcowych . Kategorie syntaktyczne są definiowane przez reguły zwane productions , które określają wartości należące do określonej kategorii składniowej. Symbole końcowe to konkretne znaki lub ciągi znaków (na przykład słowa kluczowe, takie jak define , if , let lub void ), z których konstruowane są programy poprawne składniowo.

Język może mieć różne gramatyki równoważne, takie jak równoważne wyrażenia regularne (na poziomie leksykalnym) lub różne reguły fraz, które generują ten sam język. Korzystanie z szerszej kategorii gramatyk, takich jak gramatyki LR, może pozwolić na krótsze lub prostsze gramatyki w porównaniu z bardziej ograniczonymi kategoriami, takimi jak gramatyka LL, która może wymagać dłuższych gramatyk z większą liczbą reguł. Różne, ale równoważne gramatyki fraz dają różne drzewa analizy, chociaż język bazowy (zestaw poprawnych dokumentów) jest taki sam.

Przykład: Lisp S-wyrażenia

Poniżej znajduje się prosta gramatyka, zdefiniowana za pomocą notacji wyrażeń regularnych i rozszerzonej formy Backusa–Naura . Opisuje składnię wyrażeń S , składnię danych języka programowania Lisp , który definiuje produkcje dla wyrażeń kategorii składniowych , atom , liczba , symbol i lista :

expression = atom   | list
atom       = number | symbol    
number     = [+-]?['0'-'9']+
symbol     = ['A'-'Z']['A'-'Z''0'-'9'].*
list       = '(', expression*, ')'

Ta gramatyka określa, co następuje:

  • ekspresja jest albo atom lub lista ;
  • atom jest albo numer lub symbol ;
  • liczba jest nieprzerwany ciąg jednego lub większej liczby cyfr, ewentualnie poprzedzone znakiem plus lub minus;
  • symbolem jest litera następuje zero lub więcej dowolnych znaków (bez spacji); oraz
  • lista jest dopasowana para nawiasów, z zero lub więcej wyrażeń wewnątrz niego.

Tutaj cyfry dziesiętne, duże i małe litery oraz nawiasy są symbolami terminala.

Oto przykłady dobrze uformowanych sekwencji tokenów w tej gramatyce: ' 12345', ' ()', ' (A B C232 (1))'

Gramatyki złożone

Gramatykę potrzebną do określenia języka programowania można sklasyfikować według jego pozycji w hierarchii Chomsky'ego . Gramatyka fraz większości języków programowania może być określona za pomocą gramatyki typu 2, tj. są to gramatyki bezkontekstowe , chociaż ogólna składnia jest zależna od kontekstu (ze względu na deklaracje zmiennych i zakresy zagnieżdżone), stąd typ-1. Istnieją jednak wyjątki, a dla niektórych języków gramatyka fraz to Type-0 (Turing-complete).

W niektórych językach, takich jak Perl i Lisp, specyfikacja (lub implementacja) języka pozwala na konstrukcje, które są wykonywane w fazie parsowania. Co więcej, języki te mają konstrukcje, które pozwalają programiście zmieniać zachowanie parsera. Ta kombinacja skutecznie zaciera różnicę między analizowaniem a wykonaniem i sprawia, że ​​analiza składni jest nierozstrzygalnym problemem w tych językach, co oznacza, że ​​faza parsowania może się nie zakończyć. Na przykład w Perlu możliwe jest wykonanie kodu podczas parsowania za pomocą BEGINinstrukcji, a prototypy funkcji Perla mogą zmienić interpretację składniową, a być może nawet ważność składniową pozostałego kodu. Potocznie określa się to jako „tylko Perl może parsować Perla” (ponieważ kod musi być wykonywany podczas parsowania i może modyfikować gramatykę) lub silniej „nawet Perl nie może parsować Perla” (ponieważ jest nierozstrzygnięty). Podobnie makra Lisp wprowadzone przez defmacroskładnię są również wykonywane podczas parsowania, co oznacza, że ​​kompilator Lisp musi mieć cały system wykonawczy Lisp. W przeciwieństwie do tego, makra C są jedynie zamiennikami ciągów i nie wymagają wykonania kodu.

Składnia a semantyka

Składnia języka opisuje formę poprawnego programu, ale nie dostarcza żadnych informacji o znaczeniu programu ani o wynikach wykonania tego programu. Znaczenie nadane kombinacji symboli jest obsługiwane przez semantykę ( formalną lub zakodowaną na stałe w implementacji referencyjnej ). Nie wszystkie programy poprawne składniowo są poprawne semantycznie. Wiele programów poprawnych składniowo jest jednak źle sformułowanych, zgodnie z regułami języka; i może (w zależności od specyfikacji językowej i rzetelności implementacji) skutkować błędem w tłumaczeniu lub wykonaniu. W niektórych przypadkach takie programy mogą wykazywać niezdefiniowane zachowanie . Nawet jeśli program jest dobrze zdefiniowany w języku, może nadal mieć znaczenie, które nie jest zamierzone przez osobę, która go napisała.

Używając języka naturalnego jako przykładu, nadanie znaczenia poprawnemu gramatycznie zdaniu może nie być możliwe lub zdanie może być fałszywe:

  • Bezbarwne zielone idee śpią wściekle ”. jest dobrze uformowany gramatycznie, ale nie ma ogólnie przyjętego znaczenia.
  • „John jest żonatym kawalerem”. jest dobrze uformowany gramatycznie, ale wyraża znaczenie, które nie może być prawdziwe.

Poniższy fragment języka C jest poprawny składniowo, ale wykonuje operację, która nie jest zdefiniowana semantycznie (ponieważ jest wskaźnikiem null , operacje i nie mają znaczenia): pp->realp->im

 complex *p = NULL;
 complex abs_p = sqrt (p->real * p->real + p->im * p->im);

Jako prostszy przykład

 int x;
 printf("%d", x);

jest poprawna składniowo, ale nie jest zdefiniowana semantycznie, ponieważ używa niezainicjowanej zmiennej . Mimo że kompilatory dla niektórych języków programowania (np. Java i C#) wykrywałyby tego rodzaju błędy niezainicjowanych zmiennych, należy je traktować raczej jako błędy semantyczne niż błędy składniowe.

Zobacz też

Aby szybko porównać składnię różnych języków programowania, spójrz na listę "Hello, World!" przykłady programów :

Bibliografia

Zewnętrzne linki