3.1    Sintaxis de lenguajes de programación.

La sintaxis describe una secuencia de símbolos que generan programas válidos. Por ejemplo la instrucción C, x=y+z representa una secuencia de símbolos válida, mientras que xy+- no es una secuencia válida en un programa C.

La sintaxis proporciona información significativa para entender un programa y proporciona información imprescindible para la traducción del programa frente a un programa objeto.

Por ejemplo 2+3*4 normalmente se interpreta como el valor 14 y no el 20. Es decir, se interpreta como 2+(3*4) y no como (2+3)*4; por sintaxis se puede especificar una u otra interpretación y con ello se guía al traductor en la evaluación correcta de la expresión.

Pero la sintaxis no es suficiente para especificar sin ambigüedad una instrucción. Por ejemplo en la instrucción x= 2.45+3.67, la sintaxis (o gramática) no nos puede decir si se declaró la variable x, o si se declaró como tipo real; los resultados x=5, x=6, x=6.12 son todos posibles si x y + son enteros, si x es entero y + es suma de reales,y si x y + son reales respectivamente.

Entonces se necesita algo más que puras estructuras sintácticas para la descripción completa del lenguaje. Bajo el término de semántica se describen otros atributos como el uso de declaraciones, operaciones, control de secuencias y referencias.

3.1.1 Criterios generales de sintaxis.

La elección de estructuras sintácticas se puede representar de muchas maneras, por ejemplo, el hecho de que una variable tenga el tipo real, en FORTRAN se declara implícitamente. Los detalles de sintaxis se eligen en gran medida en base a criterios secundarios, como la legibilidad, que no tienen relación con el objetivo central de comunicar información al traductor.

Existen muchos criterios secundarios, per4o los más comunes los podemos clasificar en los objetivos de hacer programas fáciles de leer, de escribir , de traducir y no ambiguos.

Consideraremos a continuación algunos diseños que los diseñadores toman en cuenta para elegir las estructuras sintácticas:

Legibilidad: Se suele decir que un programa legible es autodocumentable (aunque no siempre se logra en la práctica). La legibilidad se mejora usando formatos más naturales para las instrucciones, comandos estructurados, uso de muchas palabras claves, recursos para incrustar comentarios, identificadores sin restricción de longitud, símbolos mnemotécnicos en los operadores, declaraciones completas de datos, etc. Pero, finalmente, por más que el lenguaje presente las facilidades, la legibilidad de los programas depende del programador. Generalmente las facilidades en legibilidad van en desmedro de las facilidades de escritura y traducción.

La legibilidad mejora cuando se tienen estructuras sintácticas muy diferenciadas para actividades diferentes y estructuras similares para actividades similares.

El lenguaje COBOL tiene características para hacer programas legibles, pero esto cae en una escritura tediosa.

Facilidad de escritura: Las características sintácticas que facilitan la escritura, suelen hallarse en conflicto con las que facilitan la lectura. La escritura del programa es facilitada a través del uso de estructuras sintácticas concisas y regulares, por ejemplo, C.

Las convenciones sintácticas implícitas que permiten no hacer tantas declaraciones, hacen a los programas más cortos y fáciles de escribir.

Otras características favorecen ambos objetivos: comandos estructurados, símbolos mnemotécnicos de operaciones e identificadores no restringidos; ya que permiten que se represente en el programa la estructura natural de los algoritmos.

Una sintaxis es redundante si tiene más de un formato para un mismo elemento de información, lo que facilita la legibilidad y la búsqueda de errores durante la traducción, pero dificulta la escritura.

Por otro lado, omisiones busca reducir la redundancia eliminando la expresión explícita pero puede dificultar la búsqueda de errores. Por ejemplo, la declaración de variables enteras y reales en FORTRAN es implícita; las que comienzan con I-N son consideradas enteras y el resto reales; pero el compilador no es capaz de detectar un error en el nombre de la variable, veamos. Si el programa usa Índice como variable y en algún punto se referencia como ÍNDCE, el compilador tomará a la segunda como una nueva variable entera y no como un error.

Facilidad de verificación: tiene relación con la legibilidad y la facilidad de escritura el concepto de corrección de programa o verificación de programas. Entender cada sentencia del lenguaje es relativamente fácil pero crear programas correctos es en extremo difícil; por eso se necesitan técnicas que permitan probar que el programa está matemáticamente correcto. (Ej: programas estructurados facilitan la verificación.)

Facilidad de traducción: La legibilidad y facilidad de escritura son criterios que están dirigidos a las necesidades del programador humano. La facilidad de traducción se relaciona con las necesidades del traductor que procesa el programa.

La clave para una traducción fácil es la regularidad de la estructura. Por ejemplo Lisp no es fácil de leer ni escribir, pero es fácil de traducir; su sintaxis a causa de la regularidad, se puede describir en unas cuantas reglas sencillas.

Carencia de ambigüedad: Una definición de lenguaje, idealmente proporciona un significado único para cada construcción sintáctica. Una construcción ambigua permite dos o más interpretaciones distintas. El problema de la ambigüedad surge normalmente en la traducción entre diferentes estructuras. El caso típico es cuando se permiten dos formas distintas para el if condicional:

1.- if <expresión booleana> then <instrucción1>
else <instrucción2>
2.- if <expresión booleana> then <instrucción1> 

La interpretación de cada una no es ambigua, está absolutamente clara, pero al combinar estas dos estructuras surge la ambigüedad:

if <expresión booleana1> then if <expresión booleana2>
then <instrucción1> else <instrucción2>

Es ambigua porque no se sabe si <instrucción2> se ejecuta cuando <expresión booleana1> es falsa  o cuando <expresión booleana1> es cierta y <expresión booleana2> es falsa.

Para solucionar este problema, algunos lenguajes introducen los delimitadores begin-end en el comando if interno; así cualquiera de las dos interpretaciones  queda explícitamente especificada:

1.- if <expresión booleana1> then begin if <expresión booleana2>
then <instrucción1> end else <instrucción2>
2.- if <expresión booleana1> then begin if <expresión booleana2>
then <instrucción1> else <instrucción2> end

En Ada, cada instrucción if debe terminar con el delimitador endif; y por último en otros lenguajes el else final se parea con el then más próximo; es decir, el enunciado original tendría la interpretación 2 de la solución dada por Algol.

La sintaxis de FORTRAN ofrece otro ejemplo: A(i,j) podría ser  una referencia a un arreglo bidimensional A, o una llamada al subprograma (function) A. Esta ambigüedad está resuelta suponiendo que A(i,j) es una llamada a una función si no hay en el programa una declaración para el arreglo A. Sólo que esta suposición no se puede comprobar hasta el tiempo de carga, cuando todas las funciones externas han sido encadenadas en el ejecutable final. Esta misma ambigüedad en Pascal se solucionó poniendo paréntesis cuadrados a los subíndices de los arreglos, así A[i,j] es una referencia a un elemento del arreglo a y A(i,j) es una llamada al subprograma A con parámetros i y j.

3.1.2 Elementos sintácticos de un lenguaje.

El estilo sintáctico general de un lenguaje está dado por la selección de distintos elementos sintácticos básicos; veamos en forma breve los principales:

Conjunto de caracteres: La elección del conjunto de caracteres es importante en la determinación del equipo de entrada y salida que se pueda usar al implementar el lenguaje. Por ejemplo, el conjunto de caracteres de C está disponible en casi todos los equipos de E/S, mientras que no es así con el conjunto de caracteres de APL, ya que usa muchos caracteres especiales.

Inicialmente, al comienzo de los 60', se pensó que la representación de caracteres con 8 bits (256 posibilidades) serían suficientes para representar 52 letras minúsculas y mayúsculas, más 10 dígitos y algunos caracteres especiales. Pero cada idioma tiene letras y acentos distintos (ö, ü, ß, ä, â, à, á, ñ, etc.); ahora si pensamos en los idiomas Griego, Hebreo, que tienen alfabetos diferentes; o idiomas como el Japonés o el Chino, donde cada símbolo representa una palabra o frase, se requieren alrededor de 10000 símbolos. Es evidente que hoy los implementadores de lenguajes tienen que considerar cada vez más las representaciones de 16 bits (65536 posibilidades) para el conjunto de caracteres.

Identificadores: La sintaxis básica es que comienzan por una letra seguida por un string de letras y/o dígitos. Las variaciones están en si aceptan los símbolos "-" ó "." para mejorar la legibilidad.

Operadores: Casi todos los lenguajes usan "+" y "-" para representar suma y resta aritmética, pero más allá de esto, casi no existe uniformidad. Ejemplo: PLUS, TIMES en Lisp.

Palabras clave y palabras reservadas: Una palabra clave es un identificador que se usa como parte fija de la sintaxis de un comando, por ejemplo: "IF". Una palabra clave es palabra reservada si no se puede usar también como identificador elegido por el programador.

El análisis sintáctico se facilita durante la traducción, usando palabras reservadas.

En FORTRAN no hay palabras reservadas, por lo tanto, el programador puede usar como variables "DO", "IF", "READ". Otros lenguajes como COBOL tienen tantas palabras reservadas, que es difícil recordarlas todas.

Palabras opcionales: Son palabras que se insertan en los comandos para mejorar la legibilidad. COBOL ofrece abundancia de estas palabras, por ejemplo, en el comando GO TO <rótulo>, sólo "GO" es palabra clave, perfectamente podría escribirse GO <rótulo>.

Comentarios: La inclusión de comentarios en un programa es una parte importante de su documentación. Los comentarios pueden hacerse de varias maneras:

  1. Líneas de comentario como en FORTRAN.
  2. Delimitados por marcadores especiales como en C ( /* y */ ) sin que importen los límites de la línea y;
  3. Comenzando en cualquier punto de la línea pero terminan con ella: "-" en Ada, // en C++; ! en FORTRAN 90.

Espacios en blanco: El uso de los espacios en blanco varía de un lenguaje a otro. Por ejemplo, en FORTRAN no son significativos, excepto, dentro de los literales; otros lenguajes los usan como separadores; en SNOBOL 4 el blanco representa la concatenación de caracteres y también como separador de los elementos de un comando.

Delimitadores: Se usan simplemente para señalar el principio y el fin de una unidad sintáctica, como un comando o una expresión. Se pueden usar para mejorar la legibilidad, simplificar el análisis sintáctico, pero también para eliminar ambigüedad definiendo explícitamente los límites de la unidad sintáctica. Ejemplos: begin-end; (-); [-]; etc.

Formatos de campo libre y fijos: Una sintaxis es de campo libre si el comando se puede escribir en cualquier parte de la entrada. Una sintaxis de campo fijo usa una posición fija en la línea de entrada para transmitir información. El campo fijo es más común en lenguajes ensambladores, en los de alto nivel es más común el uso de un formato fijo parcial o formato libre; por ejemplo FORTRAN usa los 5 primeros caracteres para rótulo; en SNOBOL 4 los rótulos, comentarios y líneas de continuación se distinguen por un carácter en la primera posición de la línea.

Expresiones: En los lenguajes imperativos como C, las expresiones constituyen operaciones básicas que permiten que el comando modifique el estado de la máquina. En lenguajes aplicativos como ML o Lisp, las expresiones forman el control básico que dirige la ejecución del programa.

Comandos: Constituyen el componente sintáctico más destacado de los lenguajes imperativos, que son la clase dominante en la actualidad. Su sintaxis tiene un efecto des¡civo sobre la regularidad, legibilidad y facilidad de escritura del lenguaje. Algunos lenguajes tienen formato único para todos sus comandos, otros tienen diferente sintaxis para cada uno. Ejemplo de los primeros es SNOBOL 4 y de los segundos COBOL.

Otra diferencia importante entre los lenguajes es si permiten comandos estructurados o simples. APL y SNOBOL 4 sólo permiten comandos simples.

3.1.3 Estructura de programas y subprogramas.

La organización sintáctica de los programas y subprogramas es tan variada como los demás elementos sintácticos.

Definiciones individuales de subprogramas: En FORTRAN cada subprograma se trata como una unidad sintáctica individual. Cada subprograma se compila por separado y se encadenan durante la carga. Cada subprograma requiere declaraciones completas, incluso de variables globales declaradas en un área común (COMMON).

Definiciones individuales de datos: Un modelo alternativo consiste en agrupar todas las operaciones que manipulan un objeto de datos determinado. Por ejemplo, un subprograma puede consistir en todas las operaciones que se ocupan de un tipo de dato específico dentro del programa, operaciones para crear el dato, operaciones para imprimirlo, para realizar cómputos, etc. Este es el enfoque general de clases en lenguajes como C++ y Smalltalk.

Definiciones de subprogramas anidados: Pascal muestra este tipo de definiciones de subprogramas, donde cada subprograma aparece como una declaración dentro del programa principal, o dentro de otro subprograma. esto se relaciona con el énfasis que hace Pascal con los comandos estructurados, pero su propósito es distinto. Este tipo de definiciones de subprogramas sirven para proporcionar un entorno no local para las llamadas a subprogramas, que se define en tiempo de compilación, permitiendo así la verificación estática de datos y compilación de código ejecutable eficiente para subprogramas que contienen referencias no locales.

Sin anidamiento, se tendría que declarar las variables no locales, como en FORTRAN, o definir la verificación de tipos para referencias no locales para el tiempo de ejecución

Definiciones individuales de interfaz: La estructura de FORTRAN permite la compilación fácil de subprogramas, pero éstos pueden tener definiciones de datos diferentes que el compilador no podrá detectar este tipo de errores, pero para cambiar una sola instrucción hay que compilar todo el programa nuevamente. En C, ML y Ada, una implementación de lenguaje consiste en varios subprogramas que deben interactuar juntos. Todos éstos componentes (llamados módulos), como en FORTRAN, se encadenan juntos para formar un ejecutable, pero sólo es necesario volver a compilar los módulos que han sido cambiados. Por otra parte, los datos que se pasan entre los procedimientos de un módulo, deben tener declaraciones comunes, lo que permite la verificación eficiente por parte del compilador. Para pasar información entre dos componentes compilados por separado, se requieren datos adicionales. esto es manejado por un componente de especificación del programa. En C, los archivos ".h" constituyen los componentes de especificación, y los ".c" son los componentes de implementación.

En Ada, se integran estas características en el lenguaje, los programas se definen en componentes llamados paquetes (packages) que contienen las especificaciones de interfaz y las implementaciones (cuerpo del paquete).

Descripción de datos separada de comandos: En un programa COBOL la declaración de todos los datos va en una división de datos y los comandos ejecutables para todos los subprogramas van en una división de procedimientos. Una tercera división de ambiente consiste en declaraciones del entorno externo de operación. La división de procedimientos se organiza en subunidades, pero todos los datos son globales para todos los subprogramas, no hay datos locales. La ventaja es que separa los formatos de los datos, de los algoritmos.

Definiciones de subprogramas no separadas: En SNOBOL 4 no se hace distinción sintáctica entre los comandos del programa principal y los de subprograma, tampoco se distinguen sintácticamente los puntos donde un subprograma comienza o termina.

La ejecución de una llamada a función inicia un nuevo subprograma y la ejecución de un RETURN lo termina.

El comportamiento del programa es totalmente dinámico, cualquier instrucción puede ser parte del programa principal y de cualquier número de subprogramas al mismo tiempo. Esta organización caótica del programa es valiosa sólo en cuanto a que permite traducción en tiempo de ejecución y ejecución de nuevas instrucciones y subprogramas con mecanismos relativamente sencillos.