
En informática , un intérprete es un software que ejecuta código fuente sin compilarlo previamente a código máquina . Un entorno de ejecución interpretado se diferencia de uno que procesa código ejecutable nativo de la CPU , el cual requiere la traducción del código fuente antes de su ejecución. Un intérprete puede traducir el código fuente a un formato intermedio, como el bytecode . Un entorno híbrido puede traducir el bytecode a código máquina mediante compilación justo a tiempo , como en el caso de .NET y Java , en lugar de interpretar el bytecode directamente.
Antes de la adopción generalizada de intérpretes, la ejecución de programas informáticos solía depender de compiladores , que traducen y compilan el código fuente a código máquina. Los primeros entornos de ejecución para Lisp y BASIC podían analizar el código fuente directamente. Posteriormente, se desarrollaron entornos de ejecución para lenguajes (como Perl , Raku , Python , MATLAB y Ruby ), que traducían el código fuente a un formato intermedio antes de su ejecución para mejorar el rendimiento .
El código que se ejecuta en un intérprete puede ejecutarse en cualquier plataforma que cuente con un intérprete compatible . El mismo código puede distribuirse a cualquiera de estas plataformas, en lugar de tener que compilar un ejecutable para cada una. Si bien cada lenguaje de programación suele asociarse con un entorno de ejecución específico, un lenguaje puede utilizarse en diferentes entornos. Se han creado intérpretes para lenguajes tradicionalmente asociados con la compilación , como ALGOL , Fortran , COBOL , C y C++ .
Historia
En los inicios de la informática, los compiladores eran más comunes y se utilizaban más que los intérpretes porque el hardware de la época no podía soportar tanto el intérprete como el código interpretado, y el entorno de procesamiento por lotes típico de la época limitaba las ventajas de la interpretación. [ 1 ]
Los intérpretes se utilizaron ya en 1952 para facilitar la programación dentro de las limitaciones de las computadoras de la época (por ejemplo, la escasez de espacio de almacenamiento de programas o la falta de soporte nativo para números de coma flotante). Los intérpretes también se utilizaron 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 probarlo en computadoras ya existentes. [ 2 ] El primer lenguaje de alto nivel interpretado fue Lisp . Lisp fue implementado por primera vez por Steve Russell en una computadora IBM 704. Russell había leído el artículo de John McCarthy , "Funciones recursivas de expresiones simbólicas y su computación por máquina, Parte I", y se dio cuenta (para sorpresa de McCarthy) de que la función eval de Lisp podía implementarse en código máquina. [ 3 ] El resultado fue un intérprete de Lisp funcional que podía usarse para ejecutar programas Lisp, o más propiamente, "evaluar expresiones Lisp".
El desarrollo de intérpretes de edición estuvo influenciado por la necesidad de la computación interactiva. En la década de 1960, la introducción de los sistemas de tiempo compartido permitió que varios usuarios accedieran a una computadora simultáneamente, y los intérpretes de edición se volvieron esenciales para administrar y modificar código en tiempo real. Es probable que los primeros intérpretes de edición se desarrollaran para computadoras centrales, donde se utilizaban para crear y modificar programas sobre la marcha. Uno de los primeros ejemplos de un intérprete de edición es el sistema EDT (Editor y Depurador para TECO), desarrollado a finales de la década de 1960 para la computadora PDP-1. EDT permitía a los usuarios editar y depurar programas mediante una combinación de comandos y macros, allanando el camino para los editores de texto modernos y los entornos de desarrollo interactivos.
Usar
Entre los usos más destacados de los intérpretes se incluyen:
- Virtualización
- Un intérprete actúa como una máquina virtual para ejecutar código máquina para una arquitectura de hardware diferente a la que ejecuta el intérprete.
- Emulación
- Un intérprete (máquina virtual) puede emular otro sistema informático para ejecutar código escrito para ese sistema.
- Sandboxing
- Si bien algunos tipos de entornos aislados (sandboxes) dependen de las protecciones del sistema operativo, un intérprete (máquina virtual) puede ofrecer un control adicional, como el bloqueo de código que infrinja las normas de seguridad .
- Código automodificable
- El código automodificable puede implementarse en un lenguaje interpretado. Esto guarda relación con los orígenes de la interpretación en Lisp y la investigación en inteligencia artificial .
Eficiencia
La sobrecarga interpretativa es el costo de ejecución del código a través de un intérprete en lugar de como código nativo (compilado). La interpretación es más lenta porque el intérprete ejecuta múltiples instrucciones de código máquina para la funcionalidad equivalente en el código nativo. En particular, el acceso a las variables 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 . [ 4 ] Pero un desarrollo más rápido (debido a factores como un ciclo de edición-compilación-ejecución más corto) puede compensar el valor de una mayor velocidad de ejecución, especialmente al prototipar y probar, cuando el ciclo de edición-compilación-ejecución es frecuente. [ 4 ] [ 5 ]
Un intérprete puede generar una representación intermedia (RI) del programa a partir del código fuente para lograr objetivos como un rendimiento rápido en tiempo de ejecución. Un compilador también puede generar una RI, pero el compilador genera código máquina para su posterior ejecución, mientras que el intérprete se prepara para ejecutar el programa. Estos objetivos diferentes dan lugar a diseños de RI diferentes. 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 saltos . [ 4 ] Algunos intérpretes, como el intérprete PBASIC , logran niveles aún mayores de compactación del programa al usar una estructura de memoria de programa orientada a bits en lugar de a bytes, donde los tokens de comandos ocupan quizás 5 bits, las constantes nominalmente de "16 bits" se almacenan 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 de BASIC pueden almacenar y leer su propia representación interna tokenizada.
Existen diversas compensaciones entre la velocidad de desarrollo al usar un intérprete y la velocidad de ejecución al usar un compilador. Algunos sistemas (como algunos lenguajes Lisp ) permiten que el código interpretado y el compilado se llamen entre sí y compartan variables. Esto significa que, una vez que una rutina ha sido probada y depurada bajo el intérprete, puede compilarse y, por lo tanto, beneficiarse de una ejecución más rápida mientras se desarrollan otras rutinas. [ 6 ]
Implementación
Dado que las primeras etapas de la interpretación y la compilación son similares, un intérprete podría utilizar el mismo analizador léxico y sintáctico que un compilador y luego interpretar el árbol de sintaxis abstracta resultante .
Ejemplo
Un intérprete de expresiones escrito en C++ .
importar std ;using std :: runtime_error ; using std :: unique_ptr ; using std :: variant ;// tipos de datos para el árbol de sintaxis abstracta enum class Kind : char { VAR , CONST , SUM , DIFF , MULT , DIV , PLUS , MINUS , NOT };// declaración anticipada de la clase Nodo ;clase Variable { público : int * memoria ; };clase Constante { público : int valor ; };clase UnaryOperation { public : unique_ptr <Node> right ; } ;clase BinaryOperation { público : unique_ptr <Node> izquierda ; unique_ptr <Node> derecha ; } ;usando Expresión = variante < Variable , Constante , OperaciónBinaria , OperaciónUnaria > ;clase Nodo { público : Tipo tipo ; Expresión e ; };// procedimiento del intérprete [[ nodiscard ]] int executeIntExpression ( const Node & n ) { int leftValue ; int rightValue ; switch ( n -> kind ) { case Kind :: VAR : return std :: get < Variable > ( n . e ). memory ; case Kind :: CONST : return std :: get < Constant > ( n . e ). value ; case Kind :: SUM : case Kind :: DIFF : case Kind :: MULT : case Kind :: DIV : const BinaryOperation & bin = std :: get < BinaryOperation > ( n . e ); leftValue = executeIntExpression ( bin . left . get ()); rightValue = executeIntExpression ( bin . right . get ()); switch ( n . kind ) { case Kind :: SUM : return leftValue + rightValue ; case Kind :: DIFF : return leftValue - rightValue ; case Kind :: MULT : return leftValue * rightValue ; case Kind :: DIV : if ( rightValue == 0 ) { throw runtime_error ( "División por cero" ); } return leftValue / rightValue; } case Kind :: PLUS : case Kind :: MINUS : case Kind :: NOT : const UnaryOperation & un = std :: get < UnaryOperation > ( n . e ); rightValue = executeIntExpression ( un . right . get ()); switch ( n . kind ) { case Kind :: PLUS : return + rightValue ; case Kind :: MINUS : return - rightValue ; case Kind :: NOT : return ! rightValue ; } default : std :: unreachable (); } }Compilación justo a tiempo
La compilación justo a tiempo (JIT) es el proceso de convertir un formato intermedio (es decir, bytecode) a código nativo en tiempo de ejecución. Dado que esto da como resultado la ejecución de código nativo, es un método para evitar el costo computacional de usar un intérprete, manteniendo al mismo tiempo algunas de las ventajas que propiciaron su desarrollo.
Variaciones
- Intérprete de la tabla de control
- La lógica se especifica como datos formateados en forma de tabla.
- Intérprete de código de bytes
- Algunos intérpretes procesan bytecode, que es un formato intermedio de lógica compilada a partir de un lenguaje de alto nivel. Por ejemplo, Emacs Lisp se compila a bytecode, que es interpretado por un intérprete. Podría decirse que este código compilado es código máquina para una máquina virtual , implementada por el intérprete. A este tipo de intérprete se le denomina a veces compreter . [ 7 ] [ 8 ]
- Intérprete de código multihilo
- Un intérprete de código multihilo es similar a un intérprete de bytecode, pero en lugar de bytes, utiliza 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 multihilo puede iterar obteniendo instrucciones y llamando a las funciones a las que apuntan, o bien obtener la primera instrucción y saltar a ella, y cada secuencia de instrucciones finaliza con una obtención y un salto a la siguiente instrucción. Un ejemplo de código multihilo es el código Forth utilizado en los sistemas Open Firmware . El lenguaje fuente se compila en "código F" (un bytecode), que luego es interpretado por una máquina virtual .
- Intérprete de árbol de sintaxis abstracta
- Un intérprete de árbol de sintaxis abstracta transforma el código fuente en un árbol de sintaxis abstracta (AST), luego lo interpreta directamente o lo usa para generar código nativo mediante compilación JIT. [ 9 ] En este enfoque, cada oración necesita ser analizada solo una vez. Como ventaja sobre el código de bytes, el AST conserva la estructura global del programa y las relaciones entre las instrucciones (que se pierden en una representación de código de bytes) y, cuando se comprime, proporciona una representación más compacta. [ 10 ] Por lo tanto, se ha propuesto el uso de AST como un mejor formato intermedio que el código de bytes. Sin embargo, para los intérpretes, el AST resulta en una mayor sobrecarga que un intérprete de código de bytes, debido a que los nodos relacionados con la sintaxis no realizan trabajo útil, a una representación menos secuencial (que requiere recorrer más punteros) y a la sobrecarga de visitar el árbol. [ 11 ]
- Intérprete de plantillas
- En lugar de implementar la ejecución del código mediante una gran sentencia switch que contiene todos los posibles bytecode, mientras opera en una pila de software o un recorrido de árbol, un intérprete de plantillas mantiene una gran matriz de bytecode (o cualquier representación intermedia eficiente) mapeada directamente a las instrucciones de máquina nativas correspondientes que pueden ejecutarse en el hardware del host como pares clave-valor (o en diseños más eficientes, direcciones directas a las instrucciones nativas), [ 12 ] [ 13 ] conocida como "Plantilla". Cuando se ejecuta el segmento de código particular, el intérprete simplemente carga o salta al mapeo de opcode en la plantilla y lo ejecuta directamente en el hardware. [ 14 ] [ 15 ] Debido a su diseño, el intérprete de plantillas se asemeja mucho a un compilador JIT en lugar de a un intérprete tradicional; sin embargo, técnicamente no es un JIT debido a que simplemente traduce el código del lenguaje a llamadas nativas un opcode a la vez en lugar de crear secuencias optimizadas de instrucciones ejecutables por la CPU a partir de todo el segmento de código. Debido al diseño simple del intérprete, que consiste en pasar las llamadas directamente al hardware en lugar de implementarlas directamente, es mucho más rápido que cualquier otro tipo, incluso que los intérpretes de bytecode, y hasta cierto punto menos propenso a errores, pero como contrapartida es más difícil de mantener debido a que el intérprete debe admitir la traducción a múltiples arquitecturas diferentes en lugar de una máquina virtual/pila independiente de la plataforma. Hasta la fecha, las únicas implementaciones de intérpretes de plantillas de lenguajes ampliamente conocidos que existen son el intérprete dentro de la implementación de referencia oficial de Java, la máquina virtual Java Sun HotSpot [ 12 ] y el intérprete Ignition en el motor de ejecución JavaScript V8 de Google .
- Microcódigo
- El microcódigo proporciona una capa de abstracción que actúa como intérprete de hardware, implementando el código máquina en un lenguaje de bajo nivel. [ 16 ] Separa las instrucciones de máquina de alto nivel de la electrónica subyacente , permitiendo así que dichas instrucciones se diseñen y modifiquen con mayor libertad. Además, facilita la implementación de instrucciones complejas de varios pasos, a la vez que reduce la complejidad de los circuitos informáticos.
Véase también
- Compilación dinámica
- Homoiconicidad – Característica de un lenguaje de programación
- Evaluador metacircular : tipo de intérprete en informática.
- Evaluación parcial : técnica para la optimización de programas.
- Bucle de lectura-evaluación-impresión : entorno de programación informática
Referencias
- ↑ "¿Por qué se escribió el primer compilador antes que el primer intérprete?" . Ars Technica . 8 de noviembre de 2014 . Consultado el 9 de noviembre de 2014 .
- ↑ Bennett, JM; Prinz, DG; Woods, ML (1952). "Subrutinas interpretativas". Actas de la Conferencia Nacional de la ACM, Toronto .
- ↑ Según lo que Paul Graham relata en Hackers & Painters , pág. 185, McCarthy dijo: «Steve Russell me dijo: “Mira, ¿por qué no programo esta función eval …?”, y yo le respondí: “Ja, ja, estás confundiendo la teoría con la práctica; esta función eval está pensada para leer, no para computar”. Pero él siguió adelante y lo hizo. Es decir, compiló la función eval de mi artículo encódigo máquina IBM 704 , corrigiendo un error , y luego la promocionó como un intérprete de Lisp, que sin duda lo era. Así que en ese momento Lisp tenía esencialmente la forma que tiene hoy en día…»
- 1 2 3 Este artículo se basa en material tomado de Interpreter en el Free On-line Dictionary of Computing antes del 1 de noviembre de 2008 e incorporado bajo los términos de "relicencia" de la GFDL , versión 1.3 o posterior.
- ↑ "Compiladores vs. intérpretes: explicación y diferencias" . Guía digital de IONOS . Consultado el 16 de septiembre de 2022 .
- ↑ Nanz, Sebastian; Furia, Carlo A. (mayo de 2015). «Estudio comparativo de lenguajes de programación en código Rosetta». 2015 IEEE/ACM 37.ª Conferencia Internacional IEEE sobre Ingeniería de Software . págs. 778–788 . arXiv : 1409.0252 . doi : 10.1109/icse.2015.90 . ISBN 978-1-4799-1934-5.
- ↑ Kühnel, Claus (1987) [1986]. "4. Kleincomputer - Eigenschaften und Möglichkeiten" [ 4. Microordenador - Propiedades y posibilidades ] . En Erlekampf, Rainer; Mönk, Hans-Joachim (eds.). Mikroelektronik in der Amateurpraxis [ Microelectrónica para el aficionado práctico ] (en alemán) (3 ed.). Berlín: Militärverlag der Deutschen Demokratischen Republik , Leipzig. pag. 222.ISBN 3-327-00357-2. 7469332.
- ^ Heyne, R. (1984). "Compredor básico para U880" [ Compredor BÁSICO para U880 (Z80) ] . radio-fernsehn-elektronik (en alemán). 1984 (3): 150-152 .
- ↑ Representaciones intermedias de AST , Lambda the Ultimate forum
- ↑ Kistler, Thomas; Franz, Michael (febrero de 1999). "Una alternativa basada en árboles a los códigos de bytes de Java" (PDF) . International Journal of Parallel Programming . 27 (1): 21– 33. CiteSeerX 10.1.1.87.2257 . doi : 10.1023/A:1018740018601 . ISSN 0885-7458 . S2CID 14330985. Recuperado el 20 de diciembre de 2020 .
- ↑ Surfin' Safari - Archivo del blog » Anunciando SquirrelFish . Webkit.org (2 de junio de 2008). Consultado el 10 de agosto de 2013.
- ^ " openjdk/jdk" . GitHub . 18 de noviembre de 2021.
- ↑ "Descripción general del entorno de ejecución de HotSpot" . Openjdk.java.net . Consultado el 6 de agosto de 2022 .
- ↑ "Desmitificando la JVM: Variantes de la JVM, Cppinterpreter y TemplateInterpreter" . metebalci.com .
- ↑ "Intérprete de plantillas JVM" . ProgrammerSought .
- ↑ Kent, Allen; Williams, James G. (5 de abril de 1993). Enciclopedia de Ciencias de la Computación y Tecnología: Volumen 28 - Suplemento 13. Nueva York: Marcel Dekker, Inc. ISBN 0-8247-2281-7Consultado el 17 de enero de 2016 .
Fuentes
Enlaces externos
- Página de intérpretes de tarjetas IBM en la Universidad de Columbia.
- Fundamentos teóricos para la "programación totalmente funcional" práctica (especialmente el capítulo 7). Tesis doctoral que aborda el problema de formalizar qué es un intérprete.
- Intérpretes (informática)
- Implementación del lenguaje de programación