stringtranslate.com

Compilador de una sola pasada

En programación de computadoras , un compilador de un solo paso 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 pasos 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 paso 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 el almacenamiento temporal, pero luego esa copia podría escanearse muchas veces. El compilador IBM 1130 Fortran almacenó la fuente en la memoria y utilizó muchas pasadas; por el contrario, el ensamblador, en sistemas que carecían de una unidad de almacenamiento en disco, requería que la baraja de cartas original 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 varias pasadas. [1]

Los compiladores de una sola pasada no pueden generar programas tan eficientes como los compiladores de múltiples 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 por un programa completo. Algunos lenguajes de programación simplemente no se pueden compilar de una sola vez debido a 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, de modo que no se pueda generar ningún 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: es seguro que se realicen varias pasadas. Por el contrario, muchos lenguajes de programación se han diseñado específicamente para compilarse con compiladores de un solo paso e incluyen construcciones especiales para permitir la compilación de un solo paso.

Dificultades

El problema básico es el de las referencias adelantadas. 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 producir 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

Supongamos que el símbolo < se reconoce como para una comparación "menor que", a diferencia de "mayor que", por ejemplo. Debido a limitaciones de codificación de caracteres, es posible que el glifo ≤ no esté disponible en una codificación estándar, por lo que se permitirá 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 más en la que un código de carácter para el glifo ¬ ("no") no está disponible es "<>" para "¬=" o "no igual"; algunos sistemas emplean ~ o ! para ¬ como una variación aún mayor. 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 cual debe evitarse. De hecho, el archivo fuente puede provenir de un dispositivo que no admite la operación de retroceder y releer, como un lector de tarjetas. En lugar de tomar una decisión temprana que tal vez tenga que deshacer más tarde, el analizador léxico puede mantener múltiples interpretaciones, algo así como la noción de superposición cuántica, colapsando en una elección específica sólo al observar más tarde el símbolo determinante. En particular, los compiladores COBOL dedican un vistazo a distinguir entre puntos que aparecen en constantes decimales y puntos que aparecen al final de las declaraciones. Este esquema no está disponible para un compilador de un solo paso.

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. La tarea del analizador léxico es separar los elementos del flujo fuente secuencial en tokens del idioma. No solo palabras, porque "<" y "<=" también son tokens. Los nombres normalmente 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álida. Es habitual permitir una cantidad arbitraria de caracteres de espacio entre tokens, y Fortran es inusual al permitir (e ignorar) espacios dentro de tokens aparentes, 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 otro modo podrían indicarse mediante Begin ... End o marcadores similares.

Contexto dentro de las 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 cargar x, sumar y, porque x no se suma a y. Si se utiliza un esquema de pila para aritmética, el código puede comenzar con Load x, pero el código que corresponde al siguiente token + no sigue. En su lugar, se genera el código para (u - v), seguido de la multiplicación por y, y sólo entonces se suma x. El analizador de expresiones aritméticas no avanza y retrocede 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 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 de optimización puede analizar la forma de una expresión aritmética para identificar y eliminar repeticiones o realizar otras posibles mejoras. Considerar

a*pecado(x) + b*pecado(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 aparte del esfuerzo requerido para hacerlo, la forma de la declaración resultante es confusa y ya no será fácil de comparar 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 usando 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 un segundo repaso 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 idioma aún puede depender del contexto. Considere las siguientes declaraciones 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 porque no contiene palabras reservadas, por lo que una "escritura" simbólica no significa necesariamente que haya una declaración de escritura en progreso. Las otras declaraciones son de hecho declaraciones if - la segunda es una aritmética - if que examina el signo del resultado de la expresión y, en función de que sea negativo, cero o positivo, salta a la etiqueta 1, 2 o 3; el tercero es un si lógico y requiere que el resultado de su expresión sea booleano ("lógico" 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 "verdadero"). " al no ser una palabra reservada, el significado especial se indica mediante los puntos que la separan); y el cuarto 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 símbolo "si" que emerge del analizador léxico no se puede realizar hasta que la expresión haya sido escaneada y después del corchete de cierre aparezca un signo igual, un dígito (siendo el texto de etiqueta1 : mientras que fortran usa solo números enteros como etiquetas, las etiquetas se pueden nombrar mediante la instrucción ASSIGN, por lo que el escaneo debe basarse en encontrar las dos comas), el inicio de una instrucción de asignación (o una instrucción de escritura, etc.), o un "entonces" ( que no debe ir seguido de otro texto), por lo que 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, por lo que el análisis sintáctico debe mantener una superposición de estados posibles si se quiere evitar retroceder.

Con el análisis de sintaxis a la deriva en una niebla de estados superpuestos, si se encontrara un error (es decir, se encontrara un token que no se 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 "se esperaba punto y coma" junto con una lista de la línea fuente más un marcador que mostraba la ubicación del problema, a menudo marcando un punto y coma. En ausencia de un punto y coma, si realmente se colocara uno como se indica, al volver a compilarlo bien podría aparecer un mensaje de "punto y coma inesperado". A menudo, sólo 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 fuente tiene un error y, por lo tanto, los mensajes posteriores no son útiles. Por supuesto, se abandona la producción adicional de código.

Este problema se puede reducir mediante el empleo de palabras reservadas, de modo que, por ejemplo, "si", "entonces" y "si no" siempre sean partes de una declaración if y no puedan ser nombres de variables, sino un número sorprendentemente grande de elementos útiles. Por lo tanto, es posible que las palabras dejen de estar disponibles. Otro método es el "stropping", mediante el cual las palabras reservadas se marcan, por ejemplo colocándolas entre caracteres especiales como puntos o apóstrofes, como en algunas versiones de Algol. Esto significa que 'if'y ifson símbolos diferentes, siendo este último un nombre común, pero proporcionar todos esos apóstrofes pronto se vuelve molesto. Para muchos idiomas, el espaciado proporciona información suficiente, aunque esto puede resultar complejo. A menudo no es sólo un espacio (o tabulación, etc.) sino un carácter distinto de una letra o un dígito lo que termina el texto de un posible token. En el ejemplo anterior, la expresión de la declaración if debe estar entre corchetes para que "(" definitivamente termine la identificación de "si" y de manera similar, ")" permita la identificación de "entonces"; Además, otras partes de una declaración 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 declaración if pueden estar en una línea. Con Pascal, si aob entonces etc. es válido , pero si ayb son expresiones , entonces deben estar entre corchetes.

Los listados de archivos fuente producidos por el compilador pueden ser más fáciles de leer presentando las palabras reservadas que identifica 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 una broma. En fortran, el comienzo de una declaración de hacer 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) sólo por la diferencia entre una coma y un punto, y los glifos de un listado impreso pueden no estar bien formado.

Una atención cuidadosa al diseño de un lenguaje puede promover la claridad y la simplicidad de expresión con miras a crear un compilador confiable cuyo comportamiento sea fácilmente comprensible. Sin embargo, las malas decisiones son comunes. Por ejemplo, Matlab denota transposición de matrices mediante el uso de un apóstrofo como en A', que es intachable 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óstrofes 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  condición  entonces  esta fuente  otra  fuente  fi

a menudo con algún arreglo para distinguir las declaraciones fuente del preprocesador de las declaraciones fuente "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 = aquello

Pero es necesario tener precaución, como en

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

Ya que sin los corchetes, el resultado sería sum:=3*x + y; De manera similar, se necesita precaución al determinar los límites del texto de reemplazo y cómo se escaneará el texto resultante. Considerar

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

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

Algunos sistemas permiten la definición de procedimientos de preprocesador cuya salida es el texto fuente a compilar, e incluso pueden permitir que dicha fuente defina aún más elementos de preprocesador. El uso hábil de tales opciones permite dar nombres explicativos a las constantes, reemplazar detalles recónditos por mnemónicos sencillos, 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 del 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, etc., y la tentación sería idee un programa de preprocesador que acepte el archivo fuente, realice las acciones del preprocesador y genere el resultado listo para la siguiente etapa, la compilación. Pero esto claramente constituye al menos un paso adicional a través del código fuente y, por lo tanto, dicha solución 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 bien 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 directa, más directamente en el caso de Ir a la etiqueta , donde la etiqueta de destino está a una distancia desconocida más adelante en el archivo fuente y, por lo tanto, la instrucción de salto para llegar a la ubicación de esa etiqueta implica una referencia 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  codifique verdadero,  de lo contrario  codifique falso  fi

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 es el inicio del código para las declaraciones false del código , y de manera similar, cuando se encuentra el token else , el código recién completado para las declaraciones true del código debe ir seguido de una operación de salto estilo GOTO cuyo destino es el código que sigue al final de la declaración if, aquí marcada por el token fi . Estos destinos sólo se pueden conocer después de que se genera una cantidad arbitraria de código para la fuente aún no explorada. Surgen problemas similares para cualquier declaración cuyas partes abarquen cantidades arbitrarias de fuente, como la declaración de caso .

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 del código verdadero y codificar las partes falsas de su declaración y de manera similar para las partes de código verdadero y falso de su declaración. otras declaraciones según 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, 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. código verdadero . La declaración GoTo se diferencia 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 "arreglos" que se usará cuando finalmente se encuentre su etiqueta. Esta noción podría ampliarse. Todos los saltos a destinos desconocidos podrían realizarse mediante una entrada en una tabla de saltos (cuyas direcciones se completan posteriormente 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 el código 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 fuente 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 se puede generar código dejando ciertos campos para corregirlos más tarde, había una suposición implícita de que el tamaño de dichas secuencias de código era estable. Puede que este no sea el caso. Muchas computadoras tienen provisiones para operaciones que ocupan diferentes cantidades de almacenamiento, en particular direccionamiento relativo, por lo que si el destino está dentro de, digamos, -128 o +127 pasos de direccionamiento, entonces 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 llegar. . Por lo tanto, si el código se generó con un campo de dirección corto, 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 el código anterior que hace referencia a ubicaciones después del cambio también tendrá que ajustarse. Asimismo, se deberán corregir las referencias posteriores que retrocedan en el cambio, incluso aquellas que hubieran sido a direcciones conocidas. Y además, la información de reparación deberá corregirse correctamente. Por otro lado, se podrían utilizar direcciones largas para todos los casos en los que la cercanía no sea segura, pero el código resultante ya no será ideal.

Entrada secuencial de una pasada, salida de secuencia irregular

Ya se han mencionado 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 emitir 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 representa todo el programa en una forma interna, pero se podría agarrar una pajita 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 al compilador.

Por lo tanto, en particular, un compilador no puede generar su código en una única secuencia implacable, y menos aún inmediatamente cuando 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 difiere hasta que se hayan realizado todas las correcciones pendientes para esa sección.

Declaración antes del uso

Al generar código para las distintas 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 de qué tamaño: precisión simple, doble o cuádruple) o números complejos, matrices, cadenas, tipos definidos por el programador, etc. Un enfoque simple sería transferir una cantidad adecuada de palabras de almacenamiento, pero, para las cadenas, esto podría resultar inadecuado 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. 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), donde A tiene, 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 como B.cno tiene contraparte en A y A.otherno tiene contraparte en B, no están involucrados.

Todo esto puede solucionarse exigiendo que los artículos se declaren antes de su uso. Algunos idiomas no requieren declaraciones explícitas, generando una declaración implícita cuando encuentran por primera vez un nuevo nombre. Si un compilador de Fortran encuentra un nombre previamente desconocido cuya primera letra sea I,J,...,N, entonces la variable será un número entero; de lo contrario, será una variable de punto flotante. Por 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 usar la opción del compilador "ninguno implícito".

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

si  la condición  entonces pi:="3.14" else 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 contuviera 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 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 también es un requisito fácil de cumplir para procedimientos y funciones, y esto también se aplica al anidamiento de procedimientos dentro de procedimientos. Al igual que 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 sólo dentro de la función contenedora, pero estos sistemas requieren que se definan una vez finalizado el procedimiento contenedor.

Pero cuando se permite la recursividad, surge un problema. Dos procedimientos, cada uno de los cuales invoca 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 caso del encuentro con una variable desconocida, se puede deducir del encuentro lo suficiente como para que el compilador pueda generar un código adecuado para la invocación del procedimiento desconocido, con, por supuesto, el aparato de "reparación" instalado para regresar. y complete la dirección correcta para el destino cuando se encuentre la definición del procedimiento. Éste sería el caso, por ejemplo, de un procedimiento sin parámetros. El resultado devuelto por una invocación de función puede ser de un tipo discernible a partir de la invocación, pero esto puede no siempre ser correcto: una función podría devolver un resultado de punto flotante pero tener su valor asignado a un número entero.

Pascal resuelve este problema exigiendo 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 "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án 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 dificultades inmediatas para generar el código (como siempre, las direcciones reales se arreglarán más adelante), pero Pascal y otros lenguajes permiten que los parámetros se pasen mediante diferentes métodos. a elección del programador ( por referencia , por valor , o incluso quizás por "nombre" ) y esto se indica sólo en la definición del procedimiento, que se desconoce antes de encontrar 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 un 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 "solución" para solucionar este problema, pero sería muy complicado. Por supuesto, los compiladores de múltiples pasadas pueden recopilar 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 guardarán en el almacenamiento interno) hasta el momento en que se encuentre la entidad necesaria, y esto podría no considerarse como resultado de un segundo paso a través de la fuente porque la etapa de generación de código terminará. pronto lo alcanzó, 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 "antes" de su definición completa posterior para que el compilador pueda conocerla antes de su uso, según lo requiera.

Desde First Fortran (1957) en adelante, ha sido posible la compilación separada de partes de un programa, lo que respalda la creación de bibliotecas de procedimientos y funciones. Un procedimiento en el archivo fuente que se está compilando y 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 sólo había números enteros y variables de punto flotante, la elección podía dejarse en manos de las reglas para la declaración implícita, pero con la proliferación de tamaños y tipos, el procedimiento de invocación necesitará una declaración de tipo para la función. Esto no es especial, ya que tiene la misma forma que 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 ahora, si se realizan correcciones de dirección 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 se proporciona aquí mediante algún protocolo.

Si se verifica o no la compatibilidad entre todas las invocaciones de un procedimiento (o función) y sus definiciones es un asunto aparte. En lenguas descendientes de inspiración algol, esta verificació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 provocarán que un programa falle. 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 es aún más fácil cometer errores, pero a menudo no es así. Algunos lenguajes (como Algol) tienen una noción formal de "actualización", "ampliación" o "promoción", mediante la cual un procedimiento que espera, digamos, 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 precisión doble 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 de copia, lo que puede generar diferencias sutiles en el comportamiento. Mucho menos sutiles son las consecuencias cuando un procedimiento recibe la dirección de una única 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 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: seguramente algo se dañará. Se puede tener 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 una sola pasada con su verificación de tipos : llamar a un procedimiento que no ha sido declarado en ninguna parte es un claro error. Las declaraciones directas ayudan a que los procedimientos recursivos se llamen entre sí directamente, a pesar de la regla de declaración antes de usar:

función impar ( n : entero ) : booleano ; comience si n = 0 entonces impar : = falso de lo contrario si n < 0 entonces impar : = par ( n + 1 ) {Error del compilador: 'par' no está definido} más impar : = par ( n - 1 ) final ;                               función par ( n : entero ) : booleana ; comenzar si n = 0 entonces par := verdadero de lo contrario si n < 0 entonces par := impar ( n + 1 ) de lo contrario par : = impar ( n - 1 ) final ;                              

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

función par ( n : entero ) : booleana ; 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.

Recursión del preprocesador

Al declarar agregados de datos complejos, podría surgir un posible uso de 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 podría agregarle un elemento de un solo byte bajo el control de una prueba sobre Odd(ByteSize(X)) para obtener un número par. Dadas las declaraciones equivalentes de pares e impares como las anteriores, probablemente no sería necesaria una declaración "adelante" 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 conocer el resultado de la invocación. A menos, por supuesto, que el preprocesador haya realizado múltiples pases de su archivo fuente.

Declaraciones anticipadas consideradas perjudiciales

Cualquiera que haya intentado mantener 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á luchado por el uso de declaraciones avanzadas o similares agregadas para 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. El beneficio de una compilación supuestamente más rápida puede parecer insuficiente cuando simplemente abandonar el objetivo de la compilación en una sola pasada eliminaría esta imposición.

Ver también

Referencias

  1. ^ "Compiladores de una sola pasada, dos pasadas y varias pasadas". Geeks para Geeks . 2019-03-13 . Consultado el 15 de mayo de 2023 .