Intérprete (informática)

Compartir Imprimir Citar

En informática, un intérprete es un programa informático que ejecuta directamente instrucciones escritas en un lenguaje de programación o scripting, sin necesidad de que hayan sido compiladas previamente en un programa de lenguaje máquina. Un intérprete generalmente usa una de las siguientes estrategias para la ejecución del programa:

  1. Analice el código fuente y realice su comportamiento directamente;
  2. Traduzca el código fuente a alguna representación intermedia eficiente o código objeto y ejecútelo inmediatamente;
  3. Ejecutar explícitamente el código de bytes precompilado almacenado creado por un compilador y emparejado con la máquina virtual del intérprete.

Las primeras versiones del lenguaje de programación Lisp y los dialectos BASIC de minicomputadoras y microcomputadoras serían ejemplos del primer tipo. Perl, Raku, Python, MATLAB y Ruby son ejemplos del segundo, mientras que UCSD Pascal es un ejemplo del tercer tipo. Los programas fuente se compilan con anticipación y se almacenan como código independiente de la máquina, que luego se vincula en tiempo de ejecución y lo ejecuta un intérprete y/o compilador (para sistemas JIT). Algunos sistemas, como Smalltalk y las versiones contemporáneas de BASIC y Java también pueden combinar dos y tres. También se han construido intérpretes de varios tipos para muchos lenguajes tradicionalmente asociados con la compilación, como Algol, Fortran, Cobol, C y C++.

Si bien la interpretación y la compilación son los dos medios principales por los que se implementan los lenguajes de programación, no se excluyen mutuamente, ya que la mayoría de los sistemas de interpretación también realizan algún trabajo de traducción, al igual que los compiladores. Los términos "lenguaje interpretado" o "lenguaje compilado" significan que la implementación canónica de ese lenguaje es un intérprete o un compilador, respectivamente. Un lenguaje de alto nivel es idealmente una abstracción independiente de implementaciones particulares.

Historia

Los intérpretes se utilizaron ya en 1952 para facilitar la programación dentro de las limitaciones de las computadoras en ese momento (por ejemplo, escasez de espacio de almacenamiento de programas o falta de soporte nativo para números de punto flotante). También se utilizaron intérpretes para traducir entre lenguajes de máquina de bajo nivel, lo que permitió escribir código para máquinas que aún estaban en construcción y probarse en computadoras que ya existían. El primer lenguaje de alto nivel interpretado fue Lisp. Lisp fue implementado por primera vez en 1958 por Steve Russell en una computadora IBM 704. Russell había leído el artículo de John McCarthy y se dio cuenta (para sorpresa de McCarthy) de que la función de evaluación de Lisp podía implementarse en código de máquina.El resultado fue un intérprete de Lisp que funcionaba y que podía usarse para ejecutar programas de Lisp, o más correctamente, "evaluar expresiones de Lisp".

Operación general

Un intérprete generalmente consta de un conjunto de comandos conocidos que puede ejecutar y una lista de estos comandos en el orden en que el programador desea ejecutarlos. Cada comando (también conocido como Instrucción) contiene los datos que el programador desea mutar e información sobre cómo mutar los datos. Por ejemplo, un intérprete podría leerlo ADD Wikipedia_Users, 5e interpretarlo como una solicitud para agregar cinco a la Wikipedia_Usersvariable.

Los intérpretes tienen una amplia variedad de instrucciones que están especializadas para realizar diferentes tareas, pero comúnmente encontrará instrucciones de intérprete para operaciones matemáticas básicas, bifurcación y administración de memoria, lo que hace que la mayoría de los intérpretes de Turing sean completos. Muchos intérpretes también están estrechamente integrados con un recolector de basura y un depurador.

Compiladores versus intérpretes

Los programas escritos en un lenguaje de alto nivel son ejecutados directamente por algún tipo de intérprete o convertidos en código de máquina por un compilador (y ensamblador y enlazador) para que la CPU los ejecute.

Si bien los compiladores (y ensambladores) generalmente producen código de máquina ejecutable directamente por el hardware de la computadora, a menudo (opcionalmente) pueden producir una forma intermedia llamada código objeto. Este es básicamente el mismo código específico de la máquina pero aumentado con una tabla de símbolos con nombres y etiquetas para hacer que los bloques (o módulos) ejecutables sean identificables y reubicables. Los programas compilados generalmente usarán bloques de construcción (funciones) guardados en una biblioteca de dichos módulos de código de objeto. Se utiliza un enlazador para combinar archivos de biblioteca (prefabricados) con los archivos de objeto de la aplicación para formar un solo archivo ejecutable. Los archivos de objeto que se utilizan para generar un archivo ejecutable se producen a menudo en diferentes momentos y, a veces, incluso en diferentes idiomas (capaces de generar el mismo formato de objeto).

Un intérprete simple escrito en un lenguaje de bajo nivel (por ejemplo, ensamblador) puede tener bloques de código de máquina similares que implementan funciones del lenguaje de alto nivel almacenadas y ejecutadas cuando la entrada de una función en una tabla de búsqueda apunta a ese código. Sin embargo, un intérprete escrito en un lenguaje de alto nivel generalmente usa otro enfoque, como generar y luego recorrer un árbol de análisis, o generar y ejecutar instrucciones intermedias definidas por software, o ambos.

Por lo tanto, tanto los compiladores como los intérpretes generalmente convierten el código fuente (archivos de texto) en tokens, ambos pueden (o no) generar un árbol de análisis y ambos pueden generar instrucciones inmediatas (para una máquina de pila, código cuádruple o por otros medios). La diferencia básica es que un sistema de compilación, que incluye un enlazador (incorporado o separado), genera un programa de código de máquina independiente, mientras que un sistema de interpretación realiza las acciones descritas por el programa de alto nivel.

Por lo tanto, un compilador puede realizar casi todas las conversiones de la semántica del código fuente al nivel de la máquina de una vez por todas (es decir, hasta que se deba cambiar el programa), mientras que un intérprete tiene que realizar parte de este trabajo de conversión cada vez que se ejecuta una instrucción o función.. Sin embargo, en un intérprete eficiente, gran parte del trabajo de traducción (incluido el análisis de tipos y similares) se excluye y se realiza solo la primera vez que se ejecuta un programa, módulo, función o incluso declaración, por lo tanto, es bastante similar a cómo se ejecuta un programa. funciona el compilador. Sin embargo, un programa compilado todavía se ejecuta mucho más rápido, en la mayoría de las circunstancias, en parte porque los compiladores están diseñados para optimizar el código y se les puede dar suficiente tiempo para esto. Esto es especialmente cierto para lenguajes de alto nivel más simples sin (muchas) estructuras de datos dinámicas, verificaciones o verificación de tipos.

En la compilación tradicional, la salida ejecutable de los enlazadores (archivos.exe o.dll o una biblioteca, vea la imagen) generalmente se puede reubicar cuando se ejecuta en un sistema operativo general, al igual que los módulos de código objeto, pero con la diferencia de que esta reubicación se realiza de forma dinámica en tiempo de ejecución, es decir, cuando se carga el programa para su ejecución. Por otro lado, los programas compilados y vinculados para pequeños sistemas integrados generalmente se asignan de forma estática, a menudo codificados de forma rígida en una memoria flash NOR, ya que a menudo no hay almacenamiento secundario ni sistema operativo en este sentido.

Históricamente, la mayoría de los sistemas de interpretación han tenido incorporado un editor autónomo. Esto se está volviendo más común también para los compiladores (entonces a menudo llamado IDE), aunque algunos programadores prefieren usar un editor de su elección y ejecutar el compilador, el enlazador y otros. herramientas manualmente. Históricamente, los compiladores son anteriores a los intérpretes porque el hardware en ese momento no podía admitir tanto el intérprete como el código interpretado y el entorno por lotes típico de la época limitaba las ventajas de la interpretación.

Ciclo de desarrollo

Durante el ciclo de desarrollo de software, los programadores realizan cambios frecuentes en el código fuente. Al usar un compilador, cada vez que se realiza un cambio en el código fuente, deben esperar a que el compilador traduzca los archivos fuente modificados y vincule todos los archivos de código binario antes de que se pueda ejecutar el programa. Cuanto mayor sea el programa, mayor será la espera. Por el contrario, un programador que usa un intérprete espera mucho menos, ya que el intérprete generalmente solo necesita traducir el código en el que está trabajando a una representación intermedia (o no traducirlo en absoluto), lo que requiere mucho menos tiempo antes de que los cambios puedan ser realizados. probado Los efectos son evidentes al guardar el código fuente y recargar el programa. El código compilado generalmente se depura con menos facilidad que la edición, compilación, y la vinculación son procesos secuenciales que deben llevarse a cabo en la secuencia adecuada con un conjunto adecuado de comandos. Por esta razón, muchos compiladores también tienen una ayuda ejecutiva, conocida como Makefile y programa. El Makefile enumera las líneas de comando del compilador y del enlazador y los archivos de código fuente del programa, pero puede tomar una entrada de menú de línea de comando simple (por ejemplo, "Make 3") que selecciona el tercer grupo (conjunto) de instrucciones y luego envía los comandos al compilador y al enlazador. alimentando los archivos de código fuente especificados.

Distribución

Un compilador convierte el código fuente en instrucciones binarias para la arquitectura de un procesador específico, lo que lo hace menos portátil. Esta conversión se realiza una sola vez, en el entorno del desarrollador, y luego el mismo binario se puede distribuir a las máquinas del usuario donde se puede ejecutar sin más traducción. Un compilador cruzado puede generar código binario para la máquina del usuario incluso si tiene un procesador diferente al de la máquina donde se compila el código.

Un programa interpretado se puede distribuir como código fuente. Necesita ser traducido en cada máquina final, lo que lleva más tiempo pero hace que la distribución del programa sea independiente de la arquitectura de la máquina. Sin embargo, la portabilidad del código fuente interpretado depende de que la máquina de destino tenga un intérprete adecuado. Si es necesario suministrar el intérprete junto con la fuente, el proceso de instalación general es más complejo que la entrega de un ejecutable monolítico, ya que el intérprete mismo es parte de lo que debe instalarse.

El hecho de que el código interpretado pueda ser leído y copiado fácilmente por humanos puede ser motivo de preocupación desde el punto de vista de los derechos de autor. Sin embargo, existen varios sistemas de encriptación y ofuscación. La entrega de código intermedio, como el código de bytes, tiene un efecto similar a la ofuscación, pero el código de bytes podría decodificarse con un descompilador o desensamblador.

Eficiencia

La principal desventaja de los intérpretes es que un programa interpretado normalmente se ejecuta más lento que si hubiera sido compilado. La diferencia de velocidades puede ser pequeña o grande; a menudo un orden de magnitud ya veces más. Por lo general, lleva más tiempo ejecutar un programa con un intérprete que ejecutar el código compilado, pero puede llevar menos tiempo interpretarlo que el tiempo total necesario para compilarlo y ejecutarlo. Esto es especialmente importante cuando se crean prototipos y se prueba el código cuando un ciclo de edición, interpretación y depuración a menudo puede ser mucho más corto que un ciclo de edición, compilación, ejecución y depuración.

Interpretar el código es más lento que ejecutar el código compilado porque el intérprete debe analizar cada declaración en el programa cada vez que se ejecuta y luego realizar la acción deseada, mientras que el código compilado solo realiza la acción dentro de un contexto fijo determinado por la compilación. Este análisis en tiempo de ejecución se conoce como "gastos generales interpretativos". El acceso a las variables también es más lento en un intérprete porque la asignación de identificadores a ubicaciones de almacenamiento debe realizarse repetidamente en tiempo de ejecución en lugar de en tiempo de compilación.

Hay varios compromisos entre la velocidad de desarrollo cuando se usa un intérprete y la velocidad de ejecución cuando se usa un compilador. Algunos sistemas (como algunos Lisps) permiten que el código interpretado y compilado se llame entre sí y comparta variables. Esto significa que una vez que una rutina ha sido probada y depurada bajo el intérprete, puede compilarse y así beneficiarse de una ejecución más rápida mientras se desarrollan otras rutinas.Muchos intérpretes no ejecutan el código fuente tal como está, sino que lo convierten en una forma interna más compacta. Muchos intérpretes de BASIC reemplazan las palabras clave con tokens de un solo byte que se pueden usar para encontrar la instrucción en una tabla de salto. Algunos intérpretes, como el intérprete PBASIC, logran niveles aún más altos de compactación del programa mediante el uso de una estructura de memoria de programa orientada a bits en lugar de orientada a bytes, donde los tokens de comandos ocupan quizás 5 bits, nominalmente se almacenan constantes de "16 bits". en un código de longitud variable que requiere 3, 6, 10 o 18 bits, y los operandos de dirección incluyen un "desplazamiento de bits". Muchos intérpretes BASIC pueden almacenar y leer su propia representación interna tokenizada.

Un intérprete bien podría usar el mismo analizador léxico y analizador sintáctico que el compilador y luego interpretar el árbol de sintaxis abstracta resultante. En el cuadro se muestran definiciones de tipo de datos de ejemplo para este último y un intérprete de juguete para árboles de sintaxis obtenidos de expresiones C.

Regresión

La interpretación no se puede usar como el único método de ejecución: aunque un intérprete se puede interpretar a sí mismo, etc., se necesita un programa ejecutado directamente en algún lugar al final de la pila porque el código que se interpreta no es, por definición, el mismo que el código de máquina que la CPU puede ejecutar.

Variaciones

Intérpretes de código de bytes

Hay un espectro de posibilidades entre interpretar y compilar, dependiendo de la cantidad de análisis realizado antes de ejecutar el programa. Por ejemplo, Emacs Lisp se compila en código de bytes, que es una representación altamente comprimida y optimizada de la fuente Lisp, pero no es un código de máquina (y, por lo tanto, no está vinculado a ningún hardware en particular). Este código "compilado" luego es interpretado por un intérprete de código de bytes (escrito en C). El código compilado en este caso es código de máquina para una máquina virtual, que no se implementa en hardware, sino en el intérprete de bytecode. Estos intérpretes de compilación a veces también se denominan compreters.En un intérprete de código de bytes, cada instrucción comienza con un byte y, por lo tanto, los intérpretes de código de bytes tienen hasta 256 instrucciones, aunque no se pueden usar todas. Algunos códigos de bytes pueden tomar múltiples bytes y pueden ser arbitrariamente complicados.

Las tablas de control, que no necesariamente tienen que pasar nunca por una fase de compilación, dictan el flujo de control algorítmico apropiado a través de intérpretes personalizados de manera similar a los intérpretes de bytecode.

Intérpretes de código enhebrado

Los intérpretes de código de subprocesos son similares a los intérpretes de código de bytes, pero en lugar de bytes, utilizan punteros. Cada "instrucción" es una palabra que apunta a una función o una secuencia de instrucciones, posiblemente seguida de un parámetro. El intérprete de código subproceso realiza un bucle para obtener instrucciones y llama a las funciones a las que apuntan, o obtiene la primera instrucción y salta a ella, y cada secuencia de instrucciones termina con una búsqueda y salta a la siguiente instrucción. A diferencia del código de bytes, no existe un límite efectivo en la cantidad de instrucciones diferentes que no sean la memoria disponible y el espacio de direcciones. El ejemplo clásico de código enhebrado es el código Forth que se usa en los sistemas Open Firmware: el lenguaje de origen se compila en "código F" (un código de bytes), que luego es interpretado por una máquina virtual.

Intérpretes de árboles de sintaxis abstracta

En el espectro entre la interpretación y la compilación, otro enfoque es transformar el código fuente en un árbol de sintaxis abstracta optimizado (AST), luego ejecutar el programa siguiendo esta estructura de árbol, o usarlo para generar código nativo justo a tiempo. En este enfoque, cada oración debe analizarse solo una vez. Como ventaja sobre el código de bytes, el AST mantiene la estructura del programa global y las relaciones entre declaraciones (que se pierde en una representación de código de bytes), y cuando se comprime proporciona una representación más compacta. Por lo tanto, se ha propuesto el uso de AST como un mejor formato intermedio para los compiladores justo a tiempo que el código de bytes. Además, permite que el sistema realice un mejor análisis durante el tiempo de ejecución.

Sin embargo, para los intérpretes, un AST genera más sobrecarga que un intérprete de código de bytes, debido a que los nodos relacionados con la sintaxis no realizan ningún trabajo útil, a una representación menos secuencial (que requiere el recorrido de más punteros) y a la sobrecarga que visita el árbol.

Compilación justo a tiempo

Para desdibujar aún más la distinción entre intérpretes, intérpretes de código de bytes y compilación está la compilación justo a tiempo (JIT), una técnica en la que la representación intermedia se compila en código de máquina nativo en tiempo de ejecución. Esto confiere la eficiencia de ejecutar código nativo, a costa del tiempo de inicio y un mayor uso de memoria cuando se compila por primera vez el código de bytes o AST. El primer compilador JIT publicado generalmente se atribuye al trabajo en LISP de John McCarthy en 1960. La optimización adaptativa es una técnica complementaria en la que el intérprete perfila el programa en ejecución y compila sus partes ejecutadas con mayor frecuencia en código nativo. La última técnica tiene algunas décadas y apareció en lenguajes como Smalltalk en la década de 1980.

La compilación justo a tiempo ha ganado la atención general entre los implementadores de lenguajes en los últimos años, con Java,.NET Framework, la mayoría de las implementaciones modernas de JavaScript y Matlab que ahora incluye compiladores JIT.

Intérprete de plantilla

Haciendo la distinción entre compiladores e intérpretes una vez más aún más vaga es un diseño de intérprete especial conocido como intérprete de plantilla. En lugar de implementar la ejecución del código en virtud de una declaración de cambio grande que contiene todos los códigos de bytes posibles, mientras opera en una pila de software o en un paseo en árbol, un intérprete de plantilla mantiene una gran variedad de códigos de bytes (o cualquier representación intermedia eficiente) mapeados directamente a correspondiente instrucciones de máquina nativas que se pueden ejecutar en el hardware del host como pares de valores clave (o en diseños más eficientes, direcciones directas a las instrucciones nativas), conocidas como "Plantilla". Cuando se ejecuta el segmento de código en particular, el intérprete simplemente carga o salta al mapeo de código de operación en la plantilla y lo ejecuta directamente en el hardware.Debido a su diseño, el intérprete de plantilla se parece mucho a un compilador justo a tiempo en lugar de a un intérprete tradicional; sin embargo, técnicamente no es un JIT debido al hecho de que simplemente traduce el código del lenguaje a las llamadas nativas, un código de operación a la vez. tiempo en lugar de crear secuencias optimizadas de instrucciones ejecutables de CPU a partir de todo el segmento de código. Debido al diseño simple del intérprete de simplemente pasar llamadas directamente al hardware en lugar de implementarlas directamente, es mucho más rápido que cualquier otro tipo, incluso los intérpretes de bytecode, y hasta cierto punto menos propenso a errores, pero como compensación es más difícil de mantener debido a que el intérprete tiene que admitir la traducción a múltiples arquitecturas diferentes en lugar de una máquina/pila virtual independiente de la plataforma. Hasta la fecha,

Autointérprete

Un autointérprete es un intérprete de lenguaje de programación escrito en un lenguaje de programación que puede interpretarse a sí mismo; un ejemplo es un intérprete BASIC escrito en BASIC. Los autointérpretes están relacionados con los compiladores autoalojados.

Si no existe un compilador para el lenguaje a interpretar, la creación de un autointérprete requiere la implementación del lenguaje en un lenguaje host (que puede ser otro lenguaje de programación o ensamblador). Al tener un primer intérprete como este, el sistema se arranca y se pueden desarrollar nuevas versiones del intérprete en el propio lenguaje. Fue así como Donald Knuth desarrolló el intérprete TANGLE para el lenguaje WEB del sistema de composición tipográfica TeX estándar industrial.

La definición de un lenguaje informático suele hacerse en relación con una máquina abstracta (la llamada semántica operativa) o como una función matemática (semántica denotacional). Un idioma también puede ser definido por un intérprete en el que se da la semántica del idioma anfitrión. La definición de un idioma por un autointérprete no está bien fundamentada (no puede definir un idioma), pero un autointérprete le dice al lector sobre la expresividad y la elegancia de un idioma. También permite al intérprete interpretar su código fuente, el primer paso hacia la interpretación reflexiva.

Una dimensión de diseño importante en la implementación de un autointérprete es si una característica del idioma interpretado se implementa con la misma característica en el idioma anfitrión del intérprete. Un ejemplo es si un cierre en un lenguaje similar a Lisp se implementa usando cierres en el lenguaje del intérprete o se implementa "manualmente" con una estructura de datos que almacena explícitamente el entorno. Cuantas más funciones implemente la misma función en el idioma anfitrión, menos control tiene el programador del intérprete; no se puede realizar un comportamiento diferente para tratar con desbordamientos de números si las operaciones aritméticas se delegan a las operaciones correspondientes en el idioma anfitrión.

Algunos lenguajes como Lisp y Prolog tienen autointérpretes elegantes. Se ha realizado mucha investigación sobre los autointérpretes (particularmente los intérpretes reflexivos) en el lenguaje de programación Scheme, un dialecto de Lisp. En general, sin embargo, cualquier lenguaje completo de Turing permite la escritura de su propio intérprete. Lisp es uno de esos lenguajes, porque los programas Lisp son listas de símbolos y otras listas. XSLT es uno de esos lenguajes, porque los programas XSLT están escritos en XML. Un subdominio de la metaprogramación es la escritura de lenguajes específicos de dominio (DSL).

Clive Gifford introdujo una calidad de medida del autointérprete (la relación propia), el límite de la relación entre el tiempo de computadora dedicado a ejecutar una pila de N autointérpretes y el tiempo dedicado a ejecutar una pila de N − 1 autointérpretes a medida que N va a infinito. Este valor no depende del programa que se esté ejecutando.

El libro Estructura e interpretación de programas informáticos presenta ejemplos de interpretación metacircular para Scheme y sus dialectos. Otros ejemplos de lenguajes con autointérprete son Forth y Pascal.

Microcódigo

El microcódigo es una técnica muy utilizada "que impone un intérprete entre el hardware y el nivel arquitectónico de una computadora". Como tal, el microcódigo es una capa de instrucciones a nivel de hardware que implementa instrucciones de código de máquina de nivel superior o secuenciación de máquina de estado interna en muchos elementos de procesamiento digital. El microcódigo se utiliza en unidades de procesamiento central de uso general, así como en procesadores más especializados, como microcontroladores, procesadores de señales digitales, controladores de canal, controladores de disco, controladores de interfaz de red, procesadores de red, unidades de procesamiento de gráficos y en otro hardware.

El microcódigo generalmente reside en una memoria especial de alta velocidad y traduce las instrucciones de la máquina, los datos de la máquina de estado u otras entradas en secuencias de operaciones detalladas a nivel de circuito. Separa las instrucciones de la máquina de la electrónica subyacente para que las instrucciones puedan diseñarse y modificarse más libremente. También facilita la creación de instrucciones complejas de varios pasos, al tiempo que reduce la complejidad de los circuitos informáticos. Escribir microcódigo a menudo se denomina microprogramación y el microcódigo en una implementación de procesador particular a veces se denomina microprograma.

Una microcodificación más amplia permite que las microarquitecturas pequeñas y simples emulen arquitecturas más poderosas con una longitud de palabra más amplia, más unidades de ejecución, etc., que es una forma relativamente simple de lograr la compatibilidad de software entre diferentes productos en una familia de procesadores.

Procesador de computadora

Incluso un procesador de computadora sin microcodificación puede considerarse un intérprete de ejecución inmediata de análisis sintáctico que está escrito en un lenguaje de descripción de hardware de propósito general como VHDL para crear un sistema que analiza las instrucciones del código de máquina y las ejecuta inmediatamente.

Aplicaciones