En informática , la sintaxis de un lenguaje informático son las reglas que definen las combinaciones de símbolos que se consideran enunciados o expresiones correctamente estructurados en ese lenguaje. Esto se aplica tanto a los lenguajes de programación , donde el documento representa el código fuente , como a los lenguajes de marcado , donde el documento representa los datos.
La sintaxis de un lenguaje define su forma superficial. [1] Los lenguajes informáticos basados en texto se basan en secuencias de caracteres , mientras que los lenguajes de programación visual se basan en la disposición espacial y las conexiones entre símbolos (que pueden ser textuales o gráficos). Se dice que los documentos que no son válidos desde el punto de vista sintáctico tienen un error de sintaxis . Al diseñar la sintaxis de un lenguaje, un diseñador podría comenzar escribiendo ejemplos de cadenas legales e ilegales , antes de intentar deducir las reglas generales a partir de estos ejemplos. [2]
Por lo tanto, la sintaxis se refiere a la forma del código y se contrasta con la semántica , el significado . En el procesamiento de lenguajes informáticos, el procesamiento semántico generalmente viene después del procesamiento sintáctico; sin embargo, en algunos casos, el procesamiento semántico es necesario para un análisis sintáctico completo, y estos se realizan juntos o simultáneamente . En un compilador , el análisis sintáctico comprende el frontend , mientras que el análisis semántico comprende el backend (y el middle end, si se distingue esta fase).
La sintaxis del lenguaje informático se distingue generalmente en tres niveles:
Distinguir de esta manera produce modularidad, permitiendo que cada nivel se describa y procese por separado y a menudo de forma independiente.
En primer lugar, un analizador léxico convierte la secuencia lineal de caracteres en una secuencia lineal de tokens; esto se conoce como " análisis léxico " o "lexing". [3]
En segundo lugar, el analizador convierte la secuencia lineal de tokens en un árbol sintáctico jerárquico; esto se conoce como " análisis sintáctico " en sentido estricto. Esto garantiza que la línea de tokens se ajuste a las gramáticas formales del lenguaje de programación. La etapa de análisis sintáctico en sí se puede dividir en dos partes: el árbol sintáctico o "árbol sintáctico concreto", que está determinado por la gramática, pero que generalmente es demasiado detallado para su uso práctico, y el árbol sintáctico abstracto (AST), que lo simplifica en una forma utilizable. Los pasos de AST y análisis contextual se pueden considerar una forma de análisis semántico, ya que agregan significado e interpretación a la sintaxis, o alternativamente como implementaciones manuales informales de reglas sintácticas que serían difíciles o incómodas de describir o implementar formalmente.
En tercer lugar, el análisis contextual resuelve nombres y comprueba tipos. Esta modularidad es posible a veces, pero en muchos lenguajes del mundo real un paso anterior depende de un paso posterior; por ejemplo, el truco del analizador léxico en C se debe a que la tokenización depende del contexto. Incluso en estos casos, el análisis sintáctico suele considerarse como una aproximación a este modelo ideal.
Los niveles generalmente corresponden a niveles en la jerarquía de Chomsky . Las palabras están en un lenguaje regular , especificado en la gramática léxica , que es una gramática de tipo 3, generalmente dada como expresiones regulares . Las frases están en un lenguaje libre de contexto (CFL), generalmente un lenguaje libre de contexto determinista (DCFL), especificado en una gramática de estructura de frase , que es una gramática de tipo 2, generalmente dada como reglas de producción en forma Backus-Naur (BNF). Las gramáticas de frase a menudo se especifican en gramáticas mucho más restringidas que las gramáticas completamente libres de contexto , para que sean más fáciles de analizar; mientras que el analizador LR puede analizar cualquier DCFL en tiempo lineal, el analizador LALR simple e incluso el analizador LL más simple son más eficientes, pero solo pueden analizar gramáticas cuyas reglas de producción estén restringidas. En principio, la estructura contextual se puede describir mediante una gramática sensible al contexto y analizar automáticamente por medios como las gramáticas de atributos , aunque, en general, este paso se realiza manualmente, a través de reglas de resolución de nombres y verificación de tipos , y se implementa mediante una tabla de símbolos que almacena nombres y tipos para cada ámbito.
Se han escrito herramientas que generan automáticamente un analizador léxico a partir de una especificación léxica escrita en expresiones regulares y un analizador sintáctico a partir de la gramática de frases escrita en BNF: esto permite utilizar programación declarativa , en lugar de tener que tener programación procedimental o funcional. Un ejemplo notable es el par lex - yacc . Estos producen automáticamente un árbol de sintaxis concreto ; el escritor del analizador sintáctico debe escribir manualmente el código que describe cómo se convierte esto en un árbol de sintaxis abstracto . El análisis contextual también se implementa generalmente de forma manual. A pesar de la existencia de estas herramientas automáticas, el análisis sintáctico a menudo se implementa manualmente, por varias razones: tal vez la estructura de la frase no está libre de contexto, o una implementación alternativa mejora el rendimiento o el informe de errores, o permite cambiar la gramática más fácilmente. Los analizadores sintácticos a menudo se escriben en lenguajes funcionales, como Haskell , o en lenguajes de script, como Python o Perl , o en C o C++ .
A modo de ejemplo, (add 1 1)
se trata de un programa Lisp sintácticamente válido (suponiendo que existe la función 'add', de lo contrario la resolución de nombres falla), que suma 1 y 1. Sin embargo, los siguientes no son válidos:
(_ 1 1) error léxico: '_' no es válido(añadir 1 1 error de análisis: falta el cierre ')'
El analizador léxico no puede identificar el primer error; todo lo que sabe es que, después de producir el token LEFT_PAREN, '(' el resto del programa no es válido, ya que ninguna regla de palabra comienza con '_'. El segundo error se detecta en la etapa de análisis: el analizador ha identificado la regla de producción "lista" debido al token '(' (como la única coincidencia) y, por lo tanto, puede dar un mensaje de error; en general, puede ser ambiguo .
Los errores de tipo y los errores de variables no declaradas a veces se consideran errores de sintaxis cuando se detectan en tiempo de compilación (que suele ser el caso cuando se compilan lenguajes fuertemente tipados), aunque es común clasificar este tipo de errores como errores semánticos . [4] [5] [6]
Como ejemplo, el código Python
'un' + 1
contiene un error de tipo porque añade un literal de cadena a un literal entero. Los errores de tipo de este tipo se pueden detectar en tiempo de compilación: se pueden detectar durante el análisis sintáctico (análisis de frase) si el compilador utiliza reglas independientes que permiten "integerLiteral + wholeLiteral" pero no "stringLiteral + wholeLiteral", aunque es más probable que el compilador utilice una regla de análisis sintáctico que permita todas las expresiones de la forma "LiteralOrIdentifier + LiteralOrIdentifier" y, entonces, el error se detecte durante el análisis contextual (cuando se produce la comprobación de tipo). En algunos casos, el compilador no realiza esta validación y estos errores solo se detectan en tiempo de ejecución.
En un lenguaje tipado dinámicamente, donde el tipo solo se puede determinar en tiempo de ejecución, muchos errores de tipo solo se pueden detectar en tiempo de ejecución. Por ejemplo, el código Python
a + b
es sintácticamente válido a nivel de frase, pero la exactitud de los tipos de a y b solo se puede determinar en tiempo de ejecución, ya que las variables no tienen tipos en Python, solo los valores los tienen. Si bien existe un desacuerdo sobre si un error de tipo detectado por el compilador debe llamarse un error de sintaxis (en lugar de un error semántico estático ), los errores de tipo que solo se pueden detectar en el momento de la ejecución del programa siempre se consideran errores semánticos en lugar de errores de sintaxis.
La sintaxis de los lenguajes de programación textual se define generalmente utilizando una combinación de expresiones regulares (para la estructura léxica ) y la forma Backus–Naur (un metalenguaje para la estructura gramatical ) para especificar inductivamente categorías sintácticas ( no terminales ) y símbolos terminales . [7] Las categorías sintácticas se definen mediante reglas llamadas producciones , que especifican los valores que pertenecen a una categoría sintáctica particular. [1] Los símbolos terminales son los caracteres concretos o cadenas de caracteres (por ejemplo, palabras clave como define , if , let o void ) a partir de los cuales se construyen programas sintácticamente válidos.
La sintaxis se puede dividir en sintaxis libre de contexto y sintaxis sensible al contexto. [7] La sintaxis libre de contexto son reglas dirigidas por el metalenguaje del lenguaje de programación. Estas no estarían limitadas por el contexto que rodea o hace referencia a esa parte de la sintaxis, mientras que la sintaxis sensible al contexto sí lo estaría.
Un idioma puede tener diferentes gramáticas equivalentes, como expresiones regulares equivalentes (en los niveles léxicos), o diferentes reglas de frases que generan el mismo idioma. El uso de una categoría más amplia de gramáticas, como las gramáticas LR, puede permitir gramáticas más cortas o simples en comparación con categorías más restringidas, como la gramática LL, que puede requerir gramáticas más largas con más reglas. Gramáticas de frases diferentes pero equivalentes producen diferentes árboles de análisis, aunque el idioma subyacente (conjunto de documentos válidos) sea el mismo.
A continuación se muestra una gramática simple, definida utilizando la notación de expresiones regulares y la forma Backus–Naur extendida . Describe la sintaxis de S-expresiones , una sintaxis de datos del lenguaje de programación Lisp , que define producciones para las categorías sintácticas expresión , átomo , número , símbolo y lista :
expresión = átomo | lista átomo = número | símbolo número = [ + - ] ? [ '0' - '9' ] + símbolo = [ 'A' - 'Z' ][ 'A' - 'Z''0' - '9' ]. * lista = '(' , expresión * , ')'
Esta gramática especifica lo siguiente:
Aquí los dígitos decimales, los caracteres mayúsculas y minúsculas y los paréntesis son símbolos terminales.
Los siguientes son ejemplos de secuencias de tokens bien formadas en esta gramática: ' 12345
', ' ()
', ' (A B C232 (1))
'
La gramática necesaria para especificar un lenguaje de programación se puede clasificar por su posición en la jerarquía de Chomsky . La gramática de frases de la mayoría de los lenguajes de programación se puede especificar utilizando una gramática de Tipo 2, es decir, son gramáticas libres de contexto , [8] aunque la sintaxis general es sensible al contexto (debido a las declaraciones de variables y los ámbitos anidados), por lo tanto, de Tipo 1. Sin embargo, hay excepciones, y para algunos lenguajes la gramática de frases es de Tipo 0 (Turing-completa).
En algunos lenguajes como Perl y Lisp, la especificación (o implementación) del lenguaje permite construcciones que se ejecutan durante la fase de análisis. Además, estos lenguajes tienen construcciones que permiten al programador alterar el comportamiento del analizador. Esta combinación difumina efectivamente la distinción entre análisis y ejecución, y hace que el análisis de sintaxis sea un problema indecidible en estos lenguajes, lo que significa que la fase de análisis puede no terminar. Por ejemplo, en Perl es posible ejecutar código durante el análisis utilizando una BEGIN
declaración, y los prototipos de funciones de Perl pueden alterar la interpretación sintáctica, y posiblemente incluso la validez sintáctica del código restante. [9] [10] Coloquialmente, esto se conoce como "solo Perl puede analizar Perl" (porque el código debe ejecutarse durante el análisis y puede modificar la gramática), o más fuertemente "incluso Perl no puede analizar Perl" (porque es indecidible). De manera similar, las macros de Lisp introducidas por la defmacro
sintaxis también se ejecutan durante el análisis, lo que significa que un compilador de Lisp debe tener presente un sistema de ejecución completo de Lisp. En cambio, las macros de C son simplemente reemplazos de cadenas y no requieren la ejecución de código. [11] [12]
La sintaxis de un lenguaje describe la forma de un programa válido, pero no proporciona ninguna información sobre el significado del programa o los resultados de la ejecución de ese programa. El significado dado a una combinación de símbolos es manejado por la semántica (ya sea formal o codificada en una implementación de referencia ). Se debe establecer una sintaxis válida antes de que la semántica pueda darle significado. [7] No todos los programas sintácticamente correctos son semánticamente correctos. Muchos programas sintácticamente correctos están, sin embargo, mal formados, según las reglas del lenguaje; y pueden (dependiendo de la especificación del lenguaje y la solidez de la implementación) dar como resultado un error en la traducción o ejecución. En algunos casos, dichos programas pueden exhibir un comportamiento indefinido . Incluso cuando un programa está bien definido dentro de un lenguaje, aún puede tener un significado que no es el previsto por la persona que lo escribió.
Usando el lenguaje natural como ejemplo, puede que no sea posible asignar un significado a una oración gramaticalmente correcta o la oración puede ser falsa:
El siguiente fragmento del lenguaje C es sintácticamente correcto, pero realiza una operación que no está definida semánticamente (porque p
es un puntero nulo , las operaciones y no tienen significado):p->real
p->im
complejo * p = NULL ; complejo abs_p = sqrt ( p -> real * p -> real + p -> im * p -> im );
Como ejemplo más simple,
int x ; printf ( "%d" , x );
es sintácticamente válido, pero no semánticamente definido, ya que utiliza una variable no inicializada . Aunque los compiladores de algunos lenguajes de programación (por ejemplo, Java y C#) detectarían errores de variables no inicializadas de este tipo, deberían considerarse errores semánticos en lugar de errores de sintaxis. [6] [13]
Para comparar rápidamente la sintaxis de varios lenguajes de programación, eche un vistazo a la lista de ejemplos del programa "¡Hola, mundo!" :