En programación de computadoras , 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 en 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 de origen se puede leer una vez en el almacenamiento temporal, pero esa copia se puede escanear muchas veces. El compilador IBM 1130 Fortran almacenó la fuente en la memoria y usó muchas pasadas; por el contrario, el ensamblador, en los sistemas que carecen de una unidad de almacenamiento de discos, requería que la baraja de tarjetas de origen se presentara dos veces al lector / perforador de tarjetas.
Propiedades
Los compiladores de una pasada son más pequeños y más rápidos que los compiladores de varias pasadas.
Los compiladores de una pasada no pueden generar programas tan eficientes como los compiladores de varias pasadas debido al alcance limitado de la información disponible. Muchas optimizaciones efectivas del compilador requieren múltiples pasadas sobre un bloque básico , bucle (especialmente bucles anidados), subrutina o módulo completo. Algunos requieren pases 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 ser compilado: múltiples pasadas son ciertas. Por el contrario, muchos lenguajes de programación se han diseñado específicamente para ser compilados con compiladores de una sola pasada e incluyen construcciones especiales para permitir la compilación de una sola pasada.
Dificultades
El problema básico son las referencias futuras. 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 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 arbitrariamente grandes de texto fuente.
Contexto local
Suponga que el símbolo
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 fuente secuencial en los tokens del idioma. No solo palabras, porque "<" y "<=" también son tokens. Por lo general, los nombres comienzan 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 como lo son "<=" 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 expresiones
Los lenguajes que permiten expresiones aritméticas suelen seguir la sintaxis de 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 sin problemas ya 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 la carga x, suma y, porque x no se suma a y. Si se usa un esquema de pila para aritmética, el código puede comenzar con una Carga x, pero el código que corresponde al siguiente token + no lo sigue. En su lugar, se genera el código para (u - v), seguido de la multiplicación por y, y solo entonces se agrega x. El analizador de expresiones aritméticas no se mueve hacia adelante y hacia atrás a lo largo de la fuente durante su análisis, emplea una pila local de operaciones diferidas impulsadas por las reglas de precedencia. Este baile puede evitarse 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á estrictamente de izquierda a derecha.
Un compilador de optimización puede analizar la forma de una expresión aritmética para identificar y eliminar la repetición o realizar otras mejoras potenciales. Considerar
a * sin (x) + b * sin (x)
Algunos lenguajes, como Algol, 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 aparte del esfuerzo requerido para hacerlo, la forma del enunciado resultante es confusa y ya no se podrá comparar fácilmente con la expresión matemática que se codifica. Se cometerían errores fácilmente. En cambio, el compilador podría representar la forma de la expresión completa (normalmente usando una estructura de árbol), analizar y modificar esa estructura y luego emitir el código para la forma mejorada. 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 descartado cualquier comentario), la interpretación de estos tokens de acuerdo con la sintaxis del lenguaje puede depender del contexto. Considere las siguientes declaraciones en pseudocódigo de fortran:
si ( expresión ) = etc .if ( expresión ) label1 , label2 , label3 if ( expresión ) entonces
La primera es la asignación del valor de alguna expresión aritmética ( etc. ) a un elemento de una matriz unidimensional llamada "si". Fortran es inusual, ya que contiene no hay palabras reservadas, por lo que un token "escritura" no significa necesariamente que hay una escritura comunicado en curso. Los otros enunciados son de hecho enunciados si, el segundo es aritmético, si examina el signo del resultado de la expresión y, basándose en que es negativo, cero o positivo, salta a la etiqueta 1, 2 o 3; el tercero es un lógico-Si, y requiere que el resultado de su expresión sea boolean - por lo tanto, la correcta interpretación de la señal de "si" que emerge del analizador léxico no se puede hacer hasta después de la expresión ha sido escaneado y siguiendo el corchete de cierre aparece un signo igual, un dígito (que es el texto de label1 : fortran usa solo números enteros como etiquetas, aunque si se permitieran las letras, el escaneo tendría que basarse en encontrar las comas) o algo que comience con una letra (que debe ser "entonces "), por lo que ahora, el contexto abarca una cantidad arbitraria de texto fuente porque la expresión es arbitraria. Sin embargo, en los tres casos, el compilador puede generar el código para evaluar la expresión a medida que avanza su exploración. 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, por lo que el análisis de sintaxis 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 Algol, por ejemplo, era conocido por mensajes de error como "punto y coma esperado" junto con una lista de la línea de origen más un marcador que muestra la ubicación del problema, que a menudo marca un punto y coma. En ausencia de un punto y coma, si realmente se colocó uno como se indica, en la recopilación podría aparecer un mensaje "punto y coma inesperado" para él. A menudo, solo valdrá 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 de origen 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, "si", "entonces" y "si no" siempre son parte de una declaración if y no pueden ser nombres de variables, sino un número sorprendentemente grande de por tanto, las palabras pueden dejar de estar disponibles. Otro enfoque es "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 if
son tokens diferentes, siendo este último un nombre ordinario, pero proporcionar todos esos apóstrofos pronto se vuelve molesto. Para muchos idiomas, el espaciado proporciona suficiente información, aunque esto puede ser complejo. A menudo, no es solo un espacio (o tabulación, etc.) sino un carácter que no es una letra o un dígito lo que termina el texto de un posible token. En el ejemplo anterior, la expresión de la instrucción if debe estar entre corchetes para que "(" definitivamente termine la identificación de "si" y, de manera similar, ")" habilite la identificación de "entonces"; además, otras partes de una sentencia if compuesta deben aparecer en nuevas líneas: "else" y "endif" (o "endif") y "else if". Por el contrario, con Algol y otros, los corchetes no son necesarios y todas las partes de una declaración if pueden estar en una línea. Con Pascal, si una o b continuación, etc es válida, pero si un y b son expresiones, entonces deben estar encerrados entre paréntesis.
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 o en negrita o cursiva , pero ha habido críticas: "Algol es el único lenguaje que distingue entre cursiva y punto normal". . En realidad, esto no es un asunto de broma. En fortran, el inicio de una instrucción do como DO 12 I = 1,15
se 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 una lista impresa no pueden estar bien formado.
Una atención cuidadosa al diseño de un lenguaje puede promover la claridad y simplicidad de expresión con el fin de 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 ', que no tiene excepciones y sigue de cerca el uso matemático. 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 usa apóstrofos para esto. Aunque Octave usa comillas dobles para cadenas de texto, también se esfuerza por aceptar declaraciones de Matlab, por lo que el problema se extiende a otro sistema.
Expansiones de preprocesador
Es en esta etapa que se ejercen las opciones de preprocesador, llamadas así porque se ejercen antes de que el compilador procese adecuadamente la fuente entrante. Se hacen eco de las opciones de "macro expansión" 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 la condición entonces esta fuente otra fuente fi
a menudo con alguna disposición 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
definir esto = eso
Pero se necesita precaución, como en
definir SumXY = (x + y)suma: = 3 * SumXY;
Dado que sin los corchetes, el resultado sería la 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. Considerar
# definir tres = 3;#define point =.;# definir uno = 1;x: = tres punto uno;
Aquí, la instrucción define termina con un punto y coma, y el punto y coma no es en sí mismo una parte del reemplazo. La invocación no puede x:=threepointone;
deberse a que es un nombre diferente, pero three point one
lo sería 3 . 1
y el escaneo posterior puede o no considerarlo como un solo token.
Algunos sistemas permiten que se compile la definición de procedimientos de preprocesador cuya salida es el texto fuente, e incluso pueden permitir que dicha fuente defina aún más elementos de preprocesador. El uso hábil de tales opciones permite que las constantes reciban nombres explicativos, que los detalles recónditos sean reemplazados por mnemónicos fáciles, la aparición de nuevas formas de declaración 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, el número de combinaciones necesarias crece exponencialmente.
Además, la misma sintaxis de preprocesador podría usarse para múltiples idiomas diferentes, incluso lenguajes naturales como en la generación de una historia a partir de una plantilla de historia usando el nombre de una persona, apodo, nombre de perro mascota, etc. y la tentación sería la de Diseñe un programa de preprocesador que acepte el archivo fuente, realice las acciones del preprocesador y muestre el resultado listo para la siguiente etapa, la compilación. Pero esto constituye claramente al menos un paso adicional a través de la fuente y, por lo tanto, 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 enfrenta el problema de la referencia de reenvío, más directamente en los gustos de Ir a la 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 llegar a la ubicación de esa etiqueta implica una etiqueta desconocida. distancia a través del código aún por generar. Algunos diseños de lenguaje, influenciados quizás por "GOTO considerados dañinos" , no tienen una declaración GOTO, pero esto no evita el problema ya que hay muchos equivalentes GOTO implícitos en un programa. Considerar
si la condición entonces codifica verdadero de lo contrario codifica falso fi
Como se mencionó anteriormente, el código para evaluar la condición se puede generar de inmediato. Pero cuando el entonces se encuentra modo, un código de operación JumpFalse debe colocarse cuya dirección de destino es el comienzo del código de los códigos falsas declaraciones, y de manera similar, cuando la persona se encuentra modo, el código que acaba de concluir para el código de verdaderas declaraciones debe ir seguido de una operación de salto de estilo GOTO cuyo destino es el código que sigue al final de la instrucción if, aquí marcado 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 enunciado cuyas partes abarquen cantidades arbitrarias de fuente, como el enunciado de caso .
Un compilador de descendencia recursiva 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 del código verdadero y codificar partes falsas de su declaración y de manera similar para el otras declaraciones de acuerdo con su sintaxis. En su almacenamiento local, realizaría un seguimiento de la ubicación del campo de dirección de su operación JumpFalse incompleta, y al encontrar su token de entonces , 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. verdadero código. La instrucción GoTo difiere en que el código que se va a saltar no está dentro de su forma de instrucción, por lo que se necesita una entrada en una tabla auxiliar de "arreglos" que se usaría cuando finalmente se encuentre su etiqueta. Esta noción podría ampliarse. Todos los saltos de destino desconocido se pueden realizar 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 se desconoce hasta el final de la compilación.
Una solución a esto es que el compilador emita la fuente del 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 (una versión de) el archivo de origen y, por lo tanto, no está permitido para compiladores de paso único.
Decisiones desafortunadas
Aunque la descripción anterior ha empleado la noción de que el código puede generarse dejando ciertos campos para ser arreglados más tarde, había una suposición implícita de que el tamaño de tales secuencias de código era estable. Puede que este no sea el caso. Muchas computadoras tienen provisión para operaciones que ocupan diferentes cantidades de almacenamiento, en particular el direccionamiento relativo, por lo que si el destino está dentro de, digamos, -128 o +127 pasos de direccionamiento, se puede usar un campo de dirección de ocho bits; de lo contrario, se requiere un campo de dirección mucho más grande para alcanzar . Por lo tanto, si el código se generó con un campo de dirección corto esperanzador, más adelante puede ser necesario volver atrás y ajustar el código para usar un campo más largo, con la consecuencia de que las ubicaciones de referencia de código anteriores después del cambio también tendrán que ajustarse. Del mismo modo, las referencias posteriores que retrocedan a través del cambio deberán corregirse, incluso aquellas que hayan sido a direcciones conocidas. Además, la información de reparación deberá corregirse correctamente. Por otro lado, las direcciones largas podrían usarse para todos los casos en los que la cercanía no sea segura, pero el código resultante ya no será el ideal.
Entrada secuencial de una pasada, salida de secuencia irregular
Ya se mencionaron algunas posibilidades de optimización dentro de una sola declaración. Las optimizaciones en múltiples declaraciones requerirían que el contenido de dichas declaraciones se mantenga en algún tipo de estructura de datos que pueda analizarse y manipularse antes de que se emita el código. En tal caso, producir un código provisional, incluso con las 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 todo el programa en una forma interna, pero se podría agarrar una pajita y hacer la afirmación de 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 notable que un compilador no pueda generar su código en una única secuencia de reenvío implacable, y menos aún de forma inmediata a medida que se lee cada parte de la fuente. La salida aún podría escribirse secuencialmente, pero solo si la salida de una sección se aplaza 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 números enteros o variables de punto flotante (y 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 El enfoque simple sería transferir una cantidad adecuada de palabras de almacenamiento, pero, para las cadenas, esto podría no ser adecuado ya que el destinatario 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. por mil caracteres, pero actualmente contiene diez. Luego hay construcciones más complejas, como los ofrecidos por COBOL y PL / I, como A:=B by name;
en este caso, A y B son agregados (o estructuras) con A que tiene por ejemplo partes A.x
, A.y
y A.other
mientras que B tiene partes B.y
, B.c
y 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.c
no tiene contraparte en A y A.other
no tiene contraparte en B, no están involucrados.
Todo esto puede manejarse con el requisito de que los artículos se declaren antes de que se utilicen. Algunos lenguajes no requieren declaraciones explícitas, lo que genera una declaración implícita al encontrar por primera vez un nuevo nombre. Si un compilador de fortran encuentra un nombre previamente desconocido cuya primera letra es una de I, J, ..., N, entonces la variable será un número entero, de lo contrario una variable de punto flotante. Por tanto, un nombre DO12I
sería una variable de coma flotante. Esto es conveniente, pero después de algunas experiencias con nombres mal escritos, la mayoría de los programadores están de acuerdo en que se debe usar la opción del compilador "implícita ninguna".
Otros sistemas utilizan la naturaleza del primer encuentro para decidir el tipo, como una cadena o una matriz, etc. Los idiomas interpretados pueden ser particularmente flexibles, y la decisión se toma en tiempo de ejecución, de la siguiente manera
si condición entonces pi: = "3.14" si no pi: = 3.14 fi ;imprimir pi;
Si hubiera un compilador para dicho lenguaje, tendría que crear una entidad compleja para representar la variable pi, que contenga una indicación de cuál es 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 en la resolución de 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
Asimismo, la declaración antes del uso es un requisito fácil de cumplir para los procedimientos y funciones, y esto se aplica también al anidamiento de procedimientos dentro de los 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 contenedora, pero estos sistemas requieren que se definan una vez finalizado el procedimiento de contención.
Pero cuando se permite la recursividad, surge un problema. Dos procedimientos, cada uno invocando al otro, no pueden declararse ambos antes de su uso. Uno debe ser el primero en el archivo fuente. Esto no tiene por qué importar si, como en el encuentro con una variable desconocida, se puede deducir lo suficiente del encuentro que el compilador podría generar código adecuado para la invocación del procedimiento desconocido, con el aparato de "reparación" en su lugar para volver y complete 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 por la invocación de una función puede ser de un tipo discernible a partir de la invocación, pero puede que no siempre sea correcto: una función puede devolver un resultado de punto flotante pero su valor se asigna a un número entero.
Pascal resuelve este problema al exigir una "declaración previa". Primero se debe dar 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 . Entonces se puede declarar el otro procedimiento o función y definir su cuerpo. En algún momento se redeclara el procedimiento o función "adelante", 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 (se declarará 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 una dificultad inmediata para generar el código (como siempre, con las direcciones reales que se arreglarán más adelante), 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 significa sólo en la definición del procedimiento, que se desconoce 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 ser recibido por referencia, su ausencia significa por valor. En el primer caso el compilador debe generar código que pasa la dirección del parámetro, mientras que en el segundo debe generar código diferente que pasa una copia del valor, generalmente vía una pila. Como siempre, se podría invocar un mecanismo de "reparación" para lidiar con esto, pero sería muy complicado. Los compiladores de varias pasadas pueden, por supuesto, recopilar toda la información requerida mientras se desplazan de un lado a otro, 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 mantendrán en el almacenamiento interno) hasta el momento en que se encuentre la entidad necesaria, y esto podría no considerarse como resultado de una segunda pasada a través de la fuente porque la etapa de generación de código lo hará. pronto se puso al día, simplemente se detuvo por un tiempo. Pero esto sería complejo. En su lugar, se introduce una construcción especial, mediante la cual la definición del procedimiento del uso de parámetros se declara "adelante" de su definición completa posterior para que el compilador pueda conocerla antes de usarla, según lo requiera.
Desde First Fortran (1957) en adelante, ha sido posible la compilación separada de partes de un programa, apoyando 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 dicha 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 enteros y variables de punto flotante, la elección podía dejarse a las reglas para la declaración implícita, pero con la proliferación de tamaños y también tipos, el procedimiento de invocación necesitará una declaración de tipo para la función. Esto no es especial, 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 producir el código correcto para ella ahora, si se corrigen las direcciones más adelante. 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 es proporcionada por algún protocolo aquí.
El hecho de que todas las invocaciones de un procedimiento (o función) se verifiquen o no para verificar su compatibilidad entre sí y sus definiciones es un asunto aparte. En los lenguajes que descienden de la inspiración tipo Algol, esta comprobación suele ser rigurosa, pero otros sistemas pueden resultar 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 causarán que un programa se bloquee. Los sistemas que permiten la compilación separada de partes de un programa completo que luego se "vinculan" entre sí también deben verificar 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 hacen. Algunos lenguajes (como Algol) tienen una noción formal de "actualización" o "ampliación" o "promoción", mediante la cual un procedimiento que espera decir un parámetro de doble precisión puede invocarse con él como una variable de precisión única, y en este caso el compilador genera código que almacena la variable de precisión simple en una variable temporal de doble precisión que se convierte en el parámetro real. Sin embargo, esto cambia el mecanismo de paso de parámetros a copia de entrada y salida, lo que puede provocar diferencias sutiles en el comportamiento. Mucho menos sutiles son las consecuencias cuando un procedimiento recibe la dirección de una sola variable de precisión cuando espera un parámetro de doble precisión u otras variaciones de tamaño. Cuando dentro del procedimiento se lee el valor del parámetro, se leerá más almacenamiento que el de su 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 gastar mucha paciencia para encontrar y corregir estos descuidos.
Ejemplo de Pascal
Un ejemplo de tal construcción es la declaración directa en Pascal . Pascal requiere que los procedimientos se declaren o definan completamente antes de su uso. Esto ayuda a un compilador de un solo paso con su verificación de tipo : llamar a un procedimiento que no ha sido declarado en ninguna parte es un claro error. Las declaraciones de reenvío ayudan a que los procedimientos recursivos se llamen entre sí directamente, a pesar de la regla de declarar antes de usar:
función impar ( n : entero ) : booleano ; empezar si n = 0 entonces impar : = false otro si n < 0 entonces impar : = incluso ( n + 1 ) {Compiler error: 'incluso' no está definido} más extraño : = incluso ( n - 1 ) final ;función par ( n : entero ) : booleano ; comenzar si n = 0 entonces par : = verdadero en caso contrario si n < 0 entonces par : = impar ( n + 1 ) si no par : = impar ( n - 1 ) fin ;
Al agregar una declaración de avance para la función even
antes de la función odd
, se le dice al compilador de una pasada que habrá una definición de even
más adelante en el programa.
función par ( 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, los parámetros se omiten o deben ser absolutamente idénticos a la declaración directa original, o se marcará un error.
Recursividad preprocesador
Al declarar agregados de datos complejos, podría surgir un posible uso de las funciones pares e impares. Quizás 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 elemento de un solo byte bajo el control de una prueba en Odd (ByteSize (X)) para hacer un número par. Dadas las declaraciones equivalentes de Odd e Even como las anteriores, una declaración "hacia adelante" probablemente no sería necesaria porque el preprocesador conoce el uso de los parámetros, 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 que, por supuesto, el preprocesador participe en varias pasadas de su archivo fuente.
Reenviar declaraciones consideradas perjudiciales
Cualquiera que haya intentado mantener la coherencia entre las declaraciones y usos de procedimientos en un programa grande y su uso de bibliotecas de rutinas, especialmente uno que esté experimentando cambios, habrá tenido problemas con el uso de declaraciones adicionales avanzadas o similares para procedimientos invocados pero no definidos en la compilación actual. Mantener la sincronía entre ubicaciones muy separadas, especialmente en diferentes archivos de origen, requiere diligencia. Esas declaraciones que usan 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 un solo paso eliminaría esta imposición.