stringtranslate.com

Compilador de una sola pasada

En programación informática , un compilador de una sola pasada es un compilador que pasa por las partes de cada unidad de compilación solo una vez, traduciendo inmediatamente cada parte a su código de máquina final. Esto contrasta con un compilador de múltiples pasadas que convierte el programa en una o más representaciones intermedias en pasos entre el código fuente y el código de máquina, y que reprocesa toda la unidad de compilación en cada pasada secuencial.

Esto se refiere al funcionamiento lógico del compilador, no a la lectura real del archivo fuente una sola vez. Por ejemplo, el archivo fuente podría leerse una vez en un almacenamiento temporal, pero esa copia podría escanearse luego muchas veces. El compilador Fortran IBM 1130 almacenaba el código fuente en la memoria y utilizaba muchos pases; por el contrario, el ensamblador, en sistemas que carecían de una unidad de almacenamiento en disco, requería que la baraja de cartas de origen se presentara dos veces al lector/perforador de tarjetas.

Propiedades

Los compiladores de una sola pasada son más pequeños y más rápidos que los compiladores de múltiples pasadas. [1]

Los compiladores de una sola pasada no pueden generar programas tan eficientes como los compiladores de varias pasadas debido al alcance limitado de la información disponible. Muchas optimizaciones de compiladores efectivas requieren múltiples pasadas sobre un bloque básico , bucle (especialmente bucles anidados), subrutina o módulo completo. Algunas requieren pasadas sobre un programa completo. Algunos lenguajes de programación simplemente no se pueden compilar en una sola pasada, como resultado de su diseño. Por ejemplo, PL/I permite que las declaraciones de datos se coloquen en cualquier lugar dentro de un programa, específicamente, después de algunas referencias a los elementos aún no declarados, por lo que no se puede generar código hasta que se haya escaneado todo el programa. La definición del lenguaje también incluye declaraciones de preprocesador que generan código fuente para compilar: las múltiples pasadas son seguras. Por el contrario, muchos lenguajes de programación se han diseñado específicamente para compilarse con compiladores de una sola pasada e incluyen construcciones especiales para permitir la compilación de una sola pasada.

Dificultades

El problema básico es el de las referencias hacia delante. La interpretación correcta de un símbolo en algún punto del archivo fuente puede depender de la presencia o ausencia de otros símbolos más adelante en el archivo fuente y, hasta que no se encuentren, no se puede generar el código correcto para el símbolo actual. Este es el problema de la dependencia del contexto, y el intervalo puede ser desde símbolos adyacentes hasta cantidades arbitrarias de texto fuente.

Contexto local

Supongamos que el símbolo < se reconoce como una comparación de "menor que", en lugar de "mayor que", por ejemplo. Debido a las limitaciones de codificación de caracteres, el glifo ≤ puede no estar disponible en una codificación estándar, por lo que se permite una representación compuesta, "<=". Aunque este contexto está determinado por el siguiente símbolo, se desconoce cuándo se encuentra "<". De manera similar, el símbolo "=" no siempre significa "=", como cuando es parte de un símbolo compuesto. Otros símbolos compuestos pueden incluir ".lt." para el caso en que el carácter especial "<" no está disponible. Otra posibilidad en la que no está disponible un código de carácter para el glifo ¬ ("no") es "<>" para "¬=" o "no igual"; algunos sistemas emplean ~ o ! para ¬ como una variación adicional. Un enfoque es avanzar el escaneo después de "<" y al encontrar el "=", retroceder. Por supuesto, esto significa que habrá dos pasadas sobre esa parte del texto, lo que se debe evitar. En este sentido, el archivo fuente puede proceder de un dispositivo que no admita una operación de lectura y retroceso, como un lector de tarjetas. En lugar de tomar una decisión temprana que luego puede tener que deshacerse, el analizador léxico puede mantener múltiples interpretaciones, de forma similar a la noción de superposición cuántica, y reducirse a una opción específica solo al observar más tarde el símbolo determinante. Cabe destacar que los compiladores COBOL dedican una pasada a distinguir entre los puntos que aparecen en las constantes decimales y los puntos que aparecen al final de las declaraciones. Un esquema de este tipo no está disponible para un compilador de una sola pasada.

Lo mismo ocurre con los nombres de los elementos. Pocos idiomas se limitan a nombres de un solo carácter, por lo que el carácter "x" como nombre de un solo carácter es bastante diferente del carácter "x" dentro de un nombre como "texto" - ahora el contexto se extiende más allá de los caracteres inmediatamente adyacentes. Es tarea del analizador léxico separar los elementos del flujo de origen secuencial en los tokens del idioma. No sólo palabras, porque "<" y "<=" también son tokens. Los nombres suelen empezar con una letra y continúan con letras y dígitos, y quizás algunos símbolos adicionales como "_". La sintaxis permitida para especificar números es sorprendentemente compleja, por ejemplo +3.14159E+0 puede ser válido. Es habitual permitir un número arbitrario de caracteres de espacio entre tokens, y Fortran es inusual al permitir (e ignorar) espacios dentro de tokens aparentes también, de modo que "GO TO" y "GOTO" son equivalentes, al igual que "<=" y "< =". Sin embargo, algunos sistemas pueden requerir espacios para delimitar ciertos tokens, y otros, como Python, usan espacios iniciales para indicar el alcance de los bloques de programa que de otra manera podrían estar indicados por Begin ... End o marcadores similares.

Contexto dentro de las expresiones

Los lenguajes que permiten expresiones aritméticas suelen seguir la sintaxis de la notación infija con reglas de precedencia. Esto significa que la generación de código para la evaluación de una expresión no se realiza de forma fluida a medida que los tokens de la expresión se obtienen del texto fuente. Por ejemplo, la expresión x + y*(u - v) no conduce al equivalente de cargar x, agregar y, porque x no se suma a y. Si se utiliza un esquema de pila para la aritmética, el código puede comenzar con un Load x, pero el código que corresponde al token + siguiente no sigue. En su lugar, se genera el código para (u - v), seguido de la multiplicación por y, y solo entonces se suma x. El analizador sintáctico de expresiones aritméticas no se mueve de un lado a otro a lo largo de la fuente durante su análisis, sino que emplea una pila local de operaciones diferidas impulsadas por las reglas de precedencia. Esta danza se puede evitar exigiendo que las expresiones aritméticas se presenten en notación polaca inversa o similar; para el ejemplo anterior algo como uv - y * x + y que se escanearía estrictamente de izquierda a derecha.

Un compilador optimizador puede analizar la forma de una expresión aritmética para identificar y eliminar repeticiones o realizar otras mejoras potenciales. Considere

a*sin(x) + b*sin(x)

Algunos lenguajes permiten asignaciones dentro de una expresión aritmética, por lo que el programador podría haber escrito algo como

a*(t:=sin(x)) + b*t

pero además del esfuerzo que se requiere para hacerlo, la forma de la declaración resultante es desordenada y ya no será fácil compararla con la expresión matemática que se está codificando. Se cometerían errores fácilmente. En cambio, el compilador podría representar la forma completa de la expresión (normalmente utilizando una estructura de árbol), analizar y modificar esa estructura y luego emitir el código para la forma mejorada, tal vez algo como (a + b)*sin(x). Habría una extensión obvia a bloques de declaraciones de asignación sucesivas. Esto no implica una segunda pasada por el texto fuente, como tal.

Contexto de rango medio

Aunque el analizador léxico ha dividido el flujo de entrada en un flujo de tokens (y ha descartado cualquier comentario), la interpretación de estos tokens según la sintaxis del lenguaje puede depender del contexto. Considere las siguientes afirmaciones en pseudocódigo Fortran:

si ( expresión ) = etc.si ( expresión ) etiqueta1 , etiqueta2 , etiqueta3
si ( expresión ) x = .true.si ( expresión ) entonces

La primera es la asignación del valor de alguna expresión aritmética (el etc ) a un elemento de una matriz unidimensional llamada "if". Fortran es inusual en el sentido de que no contiene palabras reservadas, por lo que un token "write" no significa necesariamente que haya una declaración de escritura en curso. Las otras declaraciones son de hecho declaraciones if - la segunda es una if aritmética que examina el signo del resultado de la expresión y, basándose en que sea negativo, cero o positivo, salta a la etiqueta 1, 2 o 3; la tercera es una if lógica, y requiere que el resultado de su expresión sea booleano ("logical" en la terminología de fortran) que, si es verdadero significa que se ejecutará la siguiente parte de la declaración (aquí, asignando verdadero a x, y "true" no siendo una palabra reservada, el significado especial se indica mediante los puntos que la marcan); y la cuarta es el comienzo de una secuencia if ... then ... else ... end if que también requiere que la expresión produzca un resultado booleano.

Por lo tanto, la interpretación correcta del token "if" que emerge del analizador léxico no se puede hacer hasta que se haya escaneado la expresión y después del corchete de cierre aparezca un signo igual, un dígito (que es el texto de la etiqueta1 : mientras que Fortran usa solo números enteros como etiquetas, las etiquetas se pueden nombrar mediante la declaración ASSIGN, y por lo tanto el escaneo debe basarse en encontrar las dos comas), el comienzo de una declaración de asignación (o una declaración de escritura, etc.), o un "then" (que no debe ir seguido de otro texto), y por lo tanto ahora, el contexto abarca una cantidad arbitraria de texto fuente porque la expresión es arbitraria. Sin embargo, en los cuatro casos, el compilador puede generar el código para evaluar la expresión a medida que avanza su escaneo. Por lo tanto, el análisis léxico no siempre puede determinar el significado de los tokens que acaba de identificar debido a los caprichos de la sintaxis permitida, y por lo tanto el análisis sintáctico debe mantener una superposición de estados posibles si se quiere evitar el retroceso.

Con el análisis de sintaxis a la deriva en una niebla de estados superpuestos, si se encuentra un error (es decir, se encuentra un token que no puede encajar en ningún marco sintáctico válido), la producción de un mensaje útil puede resultar difícil. El compilador B6700 de Algol, por ejemplo, era conocido por sus mensajes de error como "se esperaba un punto y coma" junto con una lista de la línea de origen más un marcador que mostraba la ubicación del problema, que a menudo marcaba un punto y coma. En ausencia de un punto y coma, si de hecho se hubiera colocado uno como se indica, al volver a compilar bien podría surgir un mensaje "punto y coma inesperado" para él. A menudo, solo vale la pena prestar atención al primer mensaje de error de un compilador, porque los mensajes posteriores salieron mal. Cancelar la interpretación actual y luego reanudar el escaneo al comienzo de la siguiente declaración es difícil cuando el archivo fuente tiene un error, por lo que los mensajes posteriores no son útiles. Por supuesto, se abandona la producción de código adicional.

Este problema se puede reducir mediante el empleo de palabras reservadas, de modo que, por ejemplo, "if", "then" y "else" siempre sean partes de una sentencia if y no puedan ser nombres de variables, pero una cantidad sorprendentemente grande de palabras útiles pueden no estar disponibles. Otro enfoque es el "stropping", mediante el cual las palabras reservadas se marcan, por ejemplo, colocándolas entre caracteres especiales como puntos o apóstrofos como en algunas versiones de Algol. Esto significa que 'if'y ifson tokens diferentes, siendo el último un nombre común, pero proporcionar todos esos apóstrofos pronto se vuelve molesto. Para muchos idiomas, el espaciado proporciona información suficiente, aunque esto puede ser complejo. A menudo no es solo un espacio (o tabulación, etc.) sino un carácter distinto de una letra o dígito lo que termina el texto de un posible token. En el ejemplo anterior, la expresión de la sentencia if debe estar entre corchetes para que "(" finalice definitivamente la identificación de "if" y, de manera similar, ")" permita la identificación de "then"; Además, otras partes de una sentencia if compuesta deben aparecer en nuevas líneas: "else" y "end if" (o "endif") y "else if". Por el contrario, con Algol y otros, los corchetes no son necesarios y todas las partes de una sentencia if pueden estar en una línea. Con Pascal, si a o b entonces etc. es válido, pero si a y b son expresiones, entonces deben estar encerradas entre corchetes.

Los listados de archivos fuente producidos por el compilador pueden ser más fáciles de leer si las palabras reservadas que identifica se presentan subrayadas , en negrita o en cursiva , pero ha habido críticas: "Algol es el único lenguaje que distingue entre un punto en cursiva y uno normal". En realidad, esto no es una broma. En Fortran, el inicio de una sentencia do como DO 12 I = 1,15se distingue de DO 12 I = 1.15(una asignación del valor 1.15 a una variable llamada DO12I; recuerde que los espacios son irrelevantes) solo por la diferencia entre una coma y un punto, y los glifos de un listado impreso pueden no estar bien formados.

Una atención cuidadosa al diseño de un lenguaje puede promover la claridad y simplicidad de expresión con vistas a crear un compilador confiable cuyo comportamiento sea fácilmente comprensible. Sin embargo, las malas decisiones son comunes. Por ejemplo, Matlab denota la transposición de matrices mediante el uso de un apóstrofo como en A', lo cual es intachable y sigue de cerca el uso matemático. Muy bien, pero para los delimitadores de una cadena de texto Matlab ignora la oportunidad que presenta el símbolo de comillas dobles para cualquier propósito y también utiliza apóstrofos para esto. Aunque Octave utiliza comillas dobles para cadenas de texto, se esfuerza por aceptar también las declaraciones de Matlab y, por lo tanto, el problema se extiende a otro sistema.

Expansiones del preprocesador

Es en esta etapa en la que se ejercen las opciones del preprocesador, llamadas así porque se ejercen antes de que el compilador procese correctamente la fuente entrante. Son un eco de las opciones de "expansión de macro" de los sistemas ensambladores, con suerte con una sintaxis más elegante. La disposición más común es una variación de

Si  condición  entonces  esta fuente  de lo contrario  otra fuente  fi

A menudo con algún arreglo para distinguir las declaraciones de origen del preprocesador de las declaraciones de origen "ordinarias", como la declaración que comienza con un símbolo % en pl/i, o un #, etc. Otra opción simple es una variación de

define  esto = aquello

Pero es necesario tener precaución, ya que

definir SumXY = (x + y)suma:=3*SumXY;

Dado que sin los corchetes, el resultado sería suma:=3*x + y; De manera similar, se debe tener cuidado al determinar los límites del texto de reemplazo y cómo se escaneará el texto resultante. Considere

#define tres = 3; #define punto = .; #define uno = 1;x:=tres punto uno;

Aquí, la declaración define termina con un punto y coma, y ​​el punto y coma en sí no es parte del reemplazo. La invocación no puede ser x:=threepointone;porque ese es un nombre diferente, pero three point onelo sería 3 . 1y el escaneo posterior puede o no considerarlo como un solo token.

Algunos sistemas permiten la definición de procedimientos de preprocesamiento cuya salida es texto fuente que se va a compilar, e incluso pueden permitir que dicha fuente defina aún más elementos de preprocesamiento. El uso hábil de tales opciones permite dar nombres explicativos a las constantes, reemplazar detalles recónditos por mnemotécnicos fáciles, la aparición de nuevas formas de instrucciones y la generación de código en línea para usos específicos de un procedimiento general (como la clasificación), en lugar de idear procedimientos reales. Con una proliferación de parámetros y tipos de parámetros, la cantidad de combinaciones necesarias crece exponencialmente.

Además, la misma sintaxis de preprocesador podría utilizarse para varios idiomas diferentes, incluso idiomas naturales, como en la generación de una historia a partir de una plantilla de historia utilizando el nombre de una persona, su apodo, el nombre de su perro, etc., y la tentación sería idear un programa de preprocesador que aceptara el archivo fuente, realizara las acciones del preprocesador y generara el resultado listo para la siguiente etapa, la compilación. Pero esto constituye claramente al menos un paso adicional a través del código fuente, por lo que una solución de este tipo no estaría disponible para un compilador de un solo paso. Por lo tanto, el progreso a través del archivo fuente de entrada real puede avanzar a trompicones, pero sigue siendo unidireccional.

Contexto de largo alcance

La generación de código por parte del compilador también se enfrenta al problema de la referencia hacia delante, más directamente en casos como Ir a etiqueta, donde la etiqueta de destino está a una distancia desconocida más adelante en el archivo de origen y, por lo tanto, la instrucción de salto para alcanzar la ubicación de esa etiqueta implica una distancia desconocida a través del código que aún no se ha generado. Algunos diseños de lenguaje, influenciados quizás por "GOTO considerados dañinos" , no tienen una declaración GOTO, pero esto no evade el problema ya que hay muchos equivalentes GOTO implícitos en un programa. Considere

Si  la condición  es verdadera , entonces  el código es falso .   

Como se mencionó anteriormente, el código para evaluar la condición se puede generar de inmediato. Pero cuando se encuentra el token then , se debe colocar un código de operación JumpFalse cuya dirección de destino sea el inicio del código para las declaraciones de código falso y, de manera similar, cuando se encuentra el token else , el código recién completado para las declaraciones de código verdadero debe ir seguido de una operación de salto de estilo GOTO cuyo destino sea el código que sigue al final de la declaración if, aquí marcada por el token fi . Estos destinos se pueden conocer solo después de que se genere una cantidad arbitraria de código para la fuente aún no escaneada. Surgen problemas similares para cualquier declaración cuyas partes abarquen cantidades arbitrarias de fuente, como la declaración case .

Un compilador de descenso recursivo activaría un procedimiento para cada tipo de declaración, como una declaración if, invocando a su vez los procedimientos apropiados para generar el código para las declaraciones de las partes code true y code false de su declaración y de manera similar para las otras declaraciones de acuerdo con su sintaxis. En su almacenamiento local mantendría un registro de la ubicación del campo de dirección de su operación JumpFalse incompleta y, al encontrar su token then , colocaría la dirección ahora conocida y, de manera similar, al encontrar el token fi para el salto necesario después del código code true . La declaración GoTo difiere en que el código que se va a saltar no está dentro de su forma de declaración, por lo que se necesita una entrada en una tabla auxiliar de "correcciones" que se usaría cuando finalmente se encuentre su etiqueta. Esta noción podría extenderse. Todos los saltos de destino desconocido podrían realizarse a través de una entrada en una tabla de saltos (cuyas direcciones se completan más tarde a medida que se encuentran los destinos), sin embargo, el tamaño necesario de esta tabla es desconocido hasta el final de la compilación.

Una solución para esto es que el compilador emita el código fuente en ensamblador (con etiquetas generadas por el compilador como destinos para los saltos, etc.) y el ensamblador determinaría las direcciones reales. Pero esto claramente requiere un paso adicional a través de (una versión de) el archivo fuente y, por lo tanto, no está permitido para compiladores de un solo paso.

Decisiones desafortunadas

Aunque en la descripción anterior se ha empleado la idea de que el código puede generarse con ciertos campos que se deben corregir más tarde, se suponía implícitamente que el tamaño de esas secuencias de código era estable. Puede que no sea así. Muchas computadoras tienen disposiciones para operaciones que ocupan diferentes cantidades de almacenamiento, en particular el direccionamiento relativo, por el cual si el destino está dentro de, digamos, -128 o +127 pasos de direccionamiento, se puede utilizar un campo de dirección de ocho bits; de lo contrario, se requiere un campo de dirección mucho más grande para alcanzarlo. Por lo tanto, si el código se generó con un campo de dirección corto, es posible que más tarde sea necesario volver atrás y ajustar el código para utilizar un campo más largo, con la consecuencia de que también habrá que ajustar las ubicaciones de referencia de código anteriores después del cambio. Del mismo modo, las referencias posteriores que vayan hacia atrás a través del cambio tendrán que corregirse, incluso aquellas que hayan sido a direcciones conocidas. Y también, la información de corrección tendrá que corregirse correctamente. Por otro lado, se podrían utilizar direcciones largas para todos los casos en los que la proximidad no es segura, pero el código resultante ya no será ideal.

Entrada secuencial de una sola pasada, salida de secuencia irregular

Ya se han mencionado algunas posibilidades de optimización dentro de una única declaración. Las optimizaciones a través de múltiples declaraciones requerirían que el contenido de dichas declaraciones se mantuviera en algún tipo de estructura de datos que pudiera analizarse y manipularse antes de que se emitiera el código. En tal caso, producir código provisional, incluso con correcciones permitidas, sería un obstáculo. En el límite, esto significa que el compilador generaría una estructura de datos que representaría el programa completo en una forma interna, pero se podría agarrar a un clavo ardiendo y afirmar que no hay una segunda pasada real del archivo fuente de principio a fin. Posiblemente en el documento de relaciones públicas que anuncia el compilador.

Por lo tanto, es de destacar que un compilador no puede generar su código en una única secuencia que avance incesantemente, y menos aún inmediatamente a medida que se lee cada parte del código fuente. La salida aún se podría escribir secuencialmente, pero solo si la salida de una sección se pospone hasta que se hayan realizado todas las correcciones pendientes para esa sección.

Declaración antes del uso

Al generar código para las diversas expresiones, el compilador necesita conocer la naturaleza de los operandos. Por ejemplo, una declaración como A:=B; podría producir un código bastante diferente dependiendo de si A y B son enteros o variables de punto flotante (y de qué tamaño: precisión simple, doble o cuádruple) o números complejos, matrices, cadenas, tipos definidos por el programador, etc. En este caso, un enfoque simple sería transferir una cantidad adecuada de palabras de almacenamiento, pero, para cadenas, esto podría no ser adecuado ya que el receptor puede ser más pequeño que el proveedor y, en cualquier caso, solo se puede usar una parte de la cadena; tal vez tenga espacio para mil caracteres, pero actualmente contiene diez. Luego hay construcciones más complejas, como las que ofrecen COBOL y pl/i, como A:=B by name;En este caso, A y B son agregados (o estructuras) con A teniendo, por ejemplo, partes A.x, A.yy A.othermientras que B tiene partes B.y, B.cy B.x, y en ese orden. La característica "por nombre" significa el equivalente de A.y:=B.y; A.x:=B.x;Pero debido a que B.cno tiene contraparte en A, y A.otherno tiene contraparte en B, no están involucrados.

Todo esto se puede solucionar con el requisito de que los elementos se declaren antes de su uso. Algunos lenguajes no requieren declaraciones explícitas, y generan una declaración implícita al encontrar por primera vez un nombre nuevo. Si un compilador de Fortran encuentra un nombre previamente desconocido cuya primera letra es una de las siguientes: I, J,..., N, entonces la variable será un entero, de lo contrario, una variable de punto flotante. Por lo tanto, un nombre DO12Isería una variable de punto flotante. Esto es una conveniencia, pero después de algunas experiencias con nombres mal escritos, la mayoría de los programadores están de acuerdo en que se debe utilizar la opción del compilador "implicit none".

Otros sistemas utilizan la naturaleza del primer encuentro para decidir el tipo, como una cadena, una matriz, etc. Los lenguajes interpretados pueden ser particularmente flexibles, y la decisión se toma en tiempo de ejecución, de forma similar a lo siguiente:

si  condición  entonces pi:="3.14" de lo contrario pi:=3.14 fi ; imprimir pi;

Si existiera un compilador para un lenguaje de este tipo, tendría que crear una entidad compleja para representar la variable pi, que contuviera una indicación de cuál es exactamente su tipo actual y el almacenamiento asociado para representar dicho tipo. Esto es ciertamente flexible, pero puede no ser útil para cálculos intensivos como resolver Ax = b donde A es una matriz de orden cien y, de repente, cualquiera de sus elementos puede ser de un tipo diferente.

Procedimientos y funciones

La declaración antes del uso es también un requisito fácil de cumplir para los procedimientos y funciones, y esto se aplica también a la anidación de procedimientos dentro de procedimientos. Al igual que con ALGOL, Pascal, PL/I y muchos otros, MATLAB y (desde 1995) Fortran permiten que una función (o procedimiento) contenga la definición de otra función (o procedimiento), visible solo dentro de la función que la contiene, pero estos sistemas requieren que se definan después del final del procedimiento que la contiene.

Pero cuando se permite la recursión, surge un problema. Dos procedimientos, cada uno invocando al otro, no pueden ser declarados antes de su uso. Uno debe ser el primero en el archivo fuente. Esto no tiene por qué importar si, como en el caso del encuentro con una variable desconocida, se puede deducir lo suficiente del encuentro como para que el compilador pueda generar código adecuado para la invocación del procedimiento desconocido, con, por supuesto, el aparato de "corrección" en su lugar para volver y completar la dirección correcta para el destino cuando se encuentre la definición del procedimiento. Este sería el caso de un procedimiento sin parámetros, por ejemplo. El resultado devuelto de una invocación de función puede ser de un tipo discernible de la invocación, pero esto puede no ser siempre correcto: una función puede devolver un resultado de punto flotante pero tener su valor asignado a un entero.

Pascal resuelve este problema al exigir una "predeclaración". Primero se debe proporcionar una de las declaraciones de procedimiento o función, pero, en lugar del cuerpo del procedimiento o función, se proporciona la palabra clave forward . Luego se puede declarar el otro procedimiento o función y definir su cuerpo. En algún momento, se vuelve a declarar el procedimiento o función "forward", junto con el cuerpo de la función.

Para la invocación de un procedimiento (o función) con parámetros, se conocerá su tipo (ya que se declaran antes de su uso), pero su uso en la invocación del procedimiento puede no serlo. Fortran, por ejemplo, pasa todos los parámetros por referencia (es decir, por dirección), por lo que no hay ninguna dificultad inmediata para generar el código (como siempre, con direcciones reales que se fijarán más tarde), pero Pascal y otros lenguajes permiten que los parámetros se pasen por diferentes métodos a elección del programador ( por referencia , o por valor , o incluso quizás por "nombre" ) y esto se indica solo en la definición del procedimiento, que es desconocida antes de que se haya encontrado la definición. Específicamente para Pascal, en la especificación de parámetros un prefijo "Var" significa que debe recibirse por referencia, su ausencia significa por valor. En el primer caso, el compilador debe generar código que pase la dirección del parámetro, mientras que en el segundo debe generar un código diferente que pase una copia del valor, generalmente a través de una pila. Como siempre, se podría invocar un mecanismo de "corrección" para lidiar con esto, pero sería muy complicado. Los compiladores de múltiples pasadas pueden, por supuesto, cotejar toda la información requerida mientras van y vienen, pero los compiladores de una sola pasada no pueden. La generación de código podría pausarse mientras avanza el escaneo (y sus resultados se guardan en el almacenamiento interno) hasta el momento en que se encuentre la entidad necesaria, y esto podría no considerarse como el resultado de una segunda pasada a través del código fuente porque la etapa de generación de código pronto se pondrá al día, simplemente se detuvo por un tiempo. Pero esto sería complejo. En su lugar, se introduce una construcción especial, por la cual la definición del uso de parámetros del procedimiento se declara "adelante" de su definición completa posterior para que el compilador pueda conocerla antes de su uso, como lo requiere.

Desde First Fortran (1957) en adelante, ha sido posible la compilación separada de partes de un programa, lo que permite la creación de bibliotecas de procedimientos y funciones. Un procedimiento en el archivo fuente que se está compilando que invoca una función de una colección externa debe conocer el tipo de resultado devuelto por la función desconocida, aunque solo sea para generar código que busque en el lugar correcto para encontrar el resultado. Originalmente, cuando solo había números enteros y variables de punto flotante, la elección podía dejarse en manos de las reglas de declaración implícita, pero con la proliferación de tamaños y también tipos, el procedimiento que invoca necesitará una declaración de tipo para la función. Esto no es especial, ya que tiene la misma forma que para una variable declarada dentro del procedimiento.

El requisito que se debe cumplir es que en el punto actual de una compilación de un solo paso se necesita información sobre una entidad para que se pueda generar el código correcto para ella ahora, si se realizan correcciones de dirección más tarde. Ya sea que la información requerida se encuentre más adelante en el archivo fuente o se encuentre en algún archivo de código compilado por separado, la información se proporciona aquí mediante algún protocolo.

El hecho de que se compruebe o no la compatibilidad de todas las invocaciones de un procedimiento (o función) entre sí y con sus definiciones es un asunto aparte. En lenguajes que descienden de una inspiración similar a Algol, esta comprobación suele ser rigurosa, pero otros sistemas pueden ser indiferentes. Dejando de lado los sistemas que permiten que un procedimiento tenga parámetros opcionales, los errores en el número y tipo de parámetros normalmente harán que un programa se bloquee. Los sistemas que permiten la compilación por separado de partes de un programa completo que luego se "vinculan" entre sí también deberían comprobar el tipo y número correctos de parámetros y resultados, ya que los errores son aún más fáciles de cometer, pero a menudo no lo son. Algunos lenguajes (como Algol) tienen una noción formal de "actualización", "ampliación" o "promoción", por la que un procedimiento que espera, por ejemplo, un parámetro de doble precisión puede ser invocado con él como una variable de precisión simple y, en este caso, el compilador genera código que almacena la variable de precisión simple en una variable de doble precisión temporal que se convierte en el parámetro real. Sin embargo, esto cambia el mecanismo de paso de parámetros a copia de entrada, copia de salida, lo que puede dar lugar a diferencias sutiles en el comportamiento. Mucho menos sutiles son las consecuencias cuando un procedimiento recibe la dirección de una variable de precisión simple cuando espera un parámetro de precisión doble u otras variaciones de tamaño. Cuando dentro del procedimiento se lee el valor del parámetro, se leerá más almacenamiento que el del parámetro dado y es poco probable que el valor resultante sea una mejora. Mucho peor es cuando el procedimiento cambia el valor de su parámetro: es seguro que algo se dañará. Se puede dedicar mucha paciencia a encontrar y corregir estos descuidos.

Ejemplo de Pascal

Un ejemplo de este tipo de construcción es la declaración adelantada en Pascal . Pascal requiere que los procedimientos se declaren o definan por completo antes de su uso. Esto ayuda a un compilador de una sola pasada con su comprobación de tipos : llamar a un procedimiento que no se ha declarado en ningún lugar es un error claro. Las declaraciones adelantadas ayudan a que los procedimientos recursivos mutuos se llamen entre sí directamente, a pesar de la regla de declarar antes de usar:

función odd ( n : entero ) : booleano ; comienza si n = 0 entonces odd := falso de lo contrario si n < 0 entonces odd := par ( n + 1 ) { Error del compilador: 'par' no está definido } de lo contrario odd := par ( n - 1 ) fin ;                               función par ( n : entero ) : booleano ; comienza si n = 0 entonces par := verdadero de lo contrario si n < 0 entonces par := impar ( n + 1 ) de lo contrario par := impar ( n - 1 ) fin ;                              

Al agregar una declaración adelantada para la función evenantes de la función odd, se le dice al compilador de una sola pasada que habrá una definición de evenmás adelante en el programa.

función even ( n : entero ) : booleano ; adelante ;      función impar ( n : entero ) : booleano ; { Etcétera }      

Cuando se realiza la declaración real del cuerpo de la función, se omiten los parámetros o deben ser absolutamente idénticos a la declaración original, o se marcará un error.

Recursión de preprocesador

Al declarar agregados de datos complejos, podría surgir un posible uso de las funciones Odd y Even. Tal vez si un agregado de datos X tiene un tamaño de almacenamiento que es un número impar de bytes, se le podría agregar un solo elemento de byte bajo el control de una prueba sobre Odd(ByteSize(X)) para obtener un número par. Dadas las declaraciones equivalentes de Odd y Even como las anteriores, probablemente no se necesitaría una declaración "adelante" porque el uso de los parámetros es conocido por el preprocesador, lo que es poco probable que presente oportunidades para elegir entre por referencia y por valor. Sin embargo, no podría haber invocaciones de estas funciones en el código fuente (fuera de sus definiciones) hasta después de su definición real, porque se requiere que se conozca el resultado de la invocación. A menos, por supuesto, que el preprocesador haya realizado múltiples pasadas de su archivo fuente.

Declaraciones anticipadas consideradas perjudiciales

Cualquiera que haya intentado mantener la coherencia entre las declaraciones y los usos de los procedimientos en un programa grande y su uso de bibliotecas de rutinas, especialmente uno que está sufriendo cambios, habrá tenido problemas con el uso de declaraciones adicionales de tipo forward o similares para los procedimientos invocados pero no definidos en la compilación actual. Mantener la sincronía entre ubicaciones muy separadas, especialmente entre diferentes archivos fuente, requiere diligencia. Las declaraciones que utilizan la palabra reservada son fáciles de encontrar, pero si las declaraciones útiles no se distinguen de las declaraciones ordinarias, la tarea se vuelve problemática. La ganancia de una compilación supuestamente más rápida puede parecer insuficiente cuando simplemente abandonar el objetivo de la compilación de una sola pasada eliminaría esta imposición.

Véase también

Referencias

  1. ^ "Compiladores de una sola pasada, dos pasadas y múltiples pasadas". GeeksforGeeks . 2019-03-13 . Consultado el 2023-05-15 .