Sintaxe (linguagens de programação) - Syntax (programming languages)

O realce de sintaxe e o estilo de recuo são freqüentemente usados ​​para ajudar os programadores a reconhecer os elementos do código-fonte. Este código Python usa realce codificado por cores.

Na ciência da computação , a sintaxe de uma linguagem de computador é o conjunto de regras que define as combinações de símbolos que são consideradas declarações ou expressões corretamente estruturadas nessa linguagem. Isso se aplica tanto às linguagens de programação , onde o documento representa o código-fonte , quanto às linguagens de marcação , nas quais o documento representa os dados.

A sintaxe de uma linguagem define sua forma de superfície. As linguagens de computador baseadas em texto são baseadas em sequências de caracteres , enquanto as linguagens de programação visual são baseadas no layout espacial e nas conexões entre os símbolos (que podem ser textuais ou gráficos). Diz-se que os documentos que são sintaticamente inválidos contêm um erro de sintaxe . Ao projetar a sintaxe de uma linguagem, um designer pode começar escrevendo exemplos de strings legais e ilegais , antes de tentar descobrir as regras gerais a partir desses exemplos.

A sintaxe, portanto, se refere à forma do código e é contrastada com a semântica - o significado . No processamento de linguagens de computador, o processamento semântico geralmente vem depois do processamento sintático; entretanto, em alguns casos, o processamento semântico é necessário para a análise sintática completa, e estes são feitos juntos ou simultaneamente . Em um compilador , a análise sintática compreende o frontend , enquanto a análise semântica compreende o backend (e o middle end, se esta fase for diferenciada).

Níveis de sintaxe

A sintaxe da linguagem de computador geralmente é diferenciada em três níveis:

  • Palavras - o nível léxico, determinando como os personagens formam fichas ;
  • Frases - o nível gramatical, estritamente falando, determinando como os tokens formam as frases;
  • Contexto - determinar a quais objetos ou nomes de variáveis ​​se referem, se os tipos são válidos, etc.

Distinguir dessa forma produz modularidade, permitindo que cada nível seja descrito e processado separadamente e, muitas vezes, de forma independente. Primeiro, um lexer transforma a sequência linear de caracteres em uma sequência linear de tokens; isso é conhecido como " análise lexical " ou "lexing". Em segundo lugar, o analisador transforma a sequência linear de tokens em uma árvore de sintaxe hierárquica; isso é conhecido como " análise " estritamente falando. Em terceiro lugar, a análise contextual resolve nomes e verifica tipos. Essa modularidade às vezes é possível, mas em muitas linguagens do mundo real uma etapa anterior depende de uma etapa posterior - por exemplo, o hack lexer em C ocorre porque a tokenização depende do contexto. Mesmo nesses casos, a análise sintática costuma ser vista como uma aproximação desse modelo ideal.

O estágio de análise em si pode ser dividido em duas partes: a árvore de análise , ou "árvore de sintaxe concreta", que é determinada pela gramática, mas geralmente é muito detalhada para uso prático, e a árvore de sintaxe abstrata (AST), que simplifica isso em uma forma utilizável. As etapas de análise contextual e AST podem ser consideradas uma forma de análise semântica, pois estão adicionando significado e interpretação à sintaxe ou, alternativamente, como implementações manuais informais de regras sintáticas que seriam difíceis ou estranhas de descrever ou implementar formalmente.

Os níveis geralmente correspondem aos níveis na hierarquia de Chomsky . As palavras estão em uma linguagem regular , especificada na gramática lexical , que é uma gramática do Tipo 3, geralmente fornecida como expressões regulares . As frases estão em uma linguagem livre de contexto (CFL), geralmente uma linguagem livre de contexto determinística (DCFL), especificada em uma gramática de estrutura de frase , que é uma gramática Tipo 2, geralmente fornecida como regras de produção na forma Backus-Naur (BNF ) As gramáticas de frase são frequentemente especificadas em gramáticas muito mais restritas do que em gramáticas totalmente livres de contexto , para torná-las mais fáceis de analisar; enquanto o analisador LR pode analisar qualquer DCFL em tempo linear, o analisador LALR simples e o analisador LL ainda mais simples são mais eficientes, mas só podem analisar gramáticas cujas regras de produção são restritas. Em princípio, a estrutura contextual pode ser descrita por uma gramática sensível ao contexto e analisada automaticamente por meio de gramáticas de atributos , embora, em geral, essa etapa seja feita manualmente, por meio de regras de resolução de nome e verificação de tipo , e implementada por meio de uma tabela de símbolos que armazena nomes e tipos para cada escopo.

Foram escritas ferramentas que geram automaticamente um lexer a partir de uma especificação lexical escrita em expressões regulares e um analisador da gramática de frase escrita em BNF: isso permite usar a programação declarativa , ao invés de precisar de programação procedural ou funcional. Um exemplo notável é o par lex - yacc . Eles produzem automaticamente uma árvore de sintaxe concreta ; o escritor do analisador deve, então, escrever manualmente o código que descreve como isso é convertido em uma árvore de sintaxe abstrata . A análise contextual também é geralmente implementada manualmente. Apesar da existência dessas ferramentas automáticas, a análise é frequentemente implementada manualmente, por vários motivos - talvez a estrutura da frase não seja livre de contexto, ou uma implementação alternativa melhore o desempenho ou o relatório de erros, ou permita que a gramática seja alterada mais facilmente. Os analisadores geralmente são escritos em linguagens funcionais, como Haskell , ou em linguagens de script, como Python ou Perl , ou em C ou C ++ .

Exemplos de erros

Como exemplo, (add 1 1)é um programa Lisp sintaticamente válido (assumindo que a função 'adicionar' exista, caso contrário, a resolução do nome falhará), adicionando 1 e 1. No entanto, os seguintes são inválidos:

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

Observe que o lexer é incapaz de identificar o primeiro erro - tudo que ele sabe é que, após produzir o token LEFT_PAREN, '(' o restante do programa é inválido, pois nenhuma regra de palavra começa com '_'. O segundo erro é detectado no estágio de análise: O analisador identificou a regra de produção "lista" devido ao token '(' (como a única correspondência) e, portanto, pode fornecer uma mensagem de erro; em geral, pode ser ambíguo .

Erros de tipo e erros de variável não declarada às vezes são considerados erros de sintaxe quando são detectados em tempo de compilação (o que geralmente é o caso ao compilar linguagens fortemente tipadas), embora seja comum classificar esses tipos de erro como erros semânticos .

Por exemplo, o código Python

'a' + 1

contém um erro de tipo porque adiciona um literal de string a um literal de inteiro. Erros de tipo deste tipo podem ser detectados em tempo de compilação: Eles podem ser detectados durante a análise (análise de frase) se o compilador usa regras separadas que permitem "integerLiteral + integerLiteral", mas não "stringLiteral + integerLiteral", embora seja mais provável que o compilador usará uma regra de análise que permite todas as expressões da forma "LiteralOrIdentifier + LiteralOrIdentifier" e então o erro será detectado durante a análise contextual (quando ocorrer a verificação de tipo). Em alguns casos, essa validação não é feita pelo compilador e esses erros são detectados apenas em tempo de execução.

Em uma linguagem digitada dinamicamente, em que o tipo só pode ser determinado em tempo de execução, muitos erros de tipo só podem ser detectados em tempo de execução. Por exemplo, o código Python

a + b

é sintaticamente válido no nível da frase, mas a exatidão dos tipos de a e b só pode ser determinada em tempo de execução, já que as variáveis ​​não têm tipos em Python, apenas os valores. Embora haja desacordo sobre se um erro de tipo detectado pelo compilador deve ser chamado de erro de sintaxe (em vez de erro semântico estático ), erros de tipo que só podem ser detectados no tempo de execução do programa são sempre considerados como erros de semântica em vez de erros de sintaxe.

Definição de sintaxe

Analise a árvore do código Python com tokenização de inserção

A sintaxe das linguagens de programação textual é geralmente definida usando uma combinação de expressões regulares (para estrutura lexical ) e forma Backus – Naur (para estrutura gramatical ) para especificar indutivamente categorias sintáticas (não terminais) e símbolos terminais . As categorias sintáticas são definidas por regras chamadas produções , que especificam os valores que pertencem a uma determinada categoria sintática. Os símbolos terminais são os caracteres concretos ou cadeias de caracteres (por exemplo, palavras-chave como define , if , let ou void ) a partir dos quais os programas sintaticamente válidos são construídos.

Uma linguagem pode ter gramáticas equivalentes diferentes, como expressões regulares equivalentes (nos níveis lexicais), ou regras de frase diferentes que geram a mesma linguagem. Usar uma categoria mais ampla de gramáticas, como gramáticas LR, pode permitir gramáticas mais curtas ou mais simples em comparação com categorias mais restritas, como a gramática LL, que pode exigir gramáticas mais longas com mais regras. Gramáticas de frase diferentes, mas equivalentes, produzem diferentes árvores de análise, embora o idioma subjacente (conjunto de documentos válidos) seja o mesmo.

Exemplo: expressões S do Lisp

Abaixo está uma gramática simples, definida usando a notação de expressões regulares e a forma Extended Backus-Naur . Ele descreve a sintaxe das expressões S , uma sintaxe de dados da linguagem de programação Lisp , que define produções para a expressão de categorias sintáticas , átomo , número , símbolo e lista :

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

Esta gramática especifica o seguinte:

  • uma expressão é um átomo ou uma lista ;
  • um átomo é um número ou um símbolo ;
  • um número é uma sequência ininterrupta de um ou mais dígitos decimais, opcionalmente precedido por um sinal de mais ou menos;
  • um símbolo é uma letra seguida por zero ou mais de quaisquer caracteres (excluindo espaços em branco); e
  • uma lista é um par de parênteses correspondente, com zero ou mais expressões dentro dela.

Aqui, os dígitos decimais, caracteres maiúsculos e minúsculos e parênteses são símbolos terminais.

A seguir estão exemplos de sequências de token bem formadas nesta gramática: ' 12345', ' ()', ' (A B C232 (1))'

Gramáticas complexas

A gramática necessária para especificar uma linguagem de programação pode ser classificada por sua posição na hierarquia de Chomsky . A gramática de frases da maioria das linguagens de programação pode ser especificada usando uma gramática Tipo-2, ou seja, elas são gramáticas livres de contexto , embora a sintaxe geral seja sensível ao contexto (devido às declarações de variáveis ​​e escopos aninhados), portanto, Tipo-1. No entanto, há exceções e, para alguns idiomas, a gramática de frases é Tipo-0 (Turing-completo).

Em algumas linguagens como Perl e Lisp, a especificação (ou implementação) da linguagem permite construções que são executadas durante a fase de análise. Além disso, essas linguagens têm construções que permitem ao programador alterar o comportamento do analisador. Essa combinação efetivamente confunde a distinção entre análise e execução e torna a análise de sintaxe um problema indecidível nessas linguagens, o que significa que a fase de análise pode não terminar. Por exemplo, em Perl é possível executar código durante a análise usando uma BEGINinstrução, e os protótipos de função Perl podem alterar a interpretação sintática e, possivelmente, até a validade sintática do código restante. Coloquialmente, isso é referido como "apenas Perl pode analisar Perl" (porque o código deve ser executado durante a análise e pode modificar a gramática), ou mais fortemente "mesmo Perl não pode analisar Perl" (porque é indecidível). Da mesma forma, as macros Lisp introduzidas pela defmacrosintaxe também são executadas durante a análise, o que significa que um compilador Lisp deve ter um sistema de tempo de execução Lisp inteiro presente. Em contraste, as macros C são meramente substituições de strings e não requerem execução de código.

Sintaxe versus semântica

A sintaxe de uma linguagem descreve a forma de um programa válido, mas não fornece nenhuma informação sobre o significado do programa ou os resultados de sua execução. O significado dado a uma combinação de símbolos é tratado pela semântica ( formal ou codificada em uma implementação de referência ). Nem todos os programas sintaticamente corretos são semanticamente corretos. Muitos programas sintaticamente corretos são malformados, de acordo com as regras da linguagem; e pode (dependendo da especificação do idioma e da solidez da implementação) resultar em um erro na tradução ou execução. Em alguns casos, esses programas podem exibir um comportamento indefinido . Mesmo quando um programa está bem definido em uma linguagem, ele ainda pode ter um significado que não foi pretendido pela pessoa que o escreveu.

Usando a linguagem natural como exemplo, pode não ser possível atribuir um significado a uma frase gramaticalmente correta ou a frase pode ser falsa:

  • " Idéias verdes incolores dormem furiosamente ." é gramaticalmente bem formado, mas não tem um significado geralmente aceito.
  • "John é solteiro casado." é gramaticalmente bem formado, mas expressa um significado que não pode ser verdadeiro.

O seguinte fragmento de linguagem C é sintacticamente correcto, mas executa uma operação que não está definido semanticamente (porque é um ponteiro nulo , as operações e não têm qualquer significado): pp->realp->im

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

Como um exemplo mais simples,

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

é sintaticamente válido, mas não semanticamente definido, pois usa uma variável não inicializada . Embora os compiladores de algumas linguagens de programação (por exemplo, Java e C #) detectem erros de variáveis ​​não inicializadas desse tipo, eles devem ser considerados erros de semântica em vez de erros de sintaxe.

Veja também

Para comparar rapidamente a sintaxe de várias linguagens de programação, dê uma olhada na lista de "Hello, World!" exemplos de programas :

Referências

links externos