Muchas, muchas respuestas geniales aquí. Solo puedo agregar una cosa:
Compilador – un programa
Intérprete: un programa que ejecuta un programa
Además, como muchos han dicho que ha sido confundido por la tecnología moderna. Usemos el camino de regreso a la máquina para dirigirnos hacia 1980.
Las primeras “computadoras domésticas” a menudo arrancaban y comenzaban lo que es un intérprete BÁSICO. BASIC era un lenguaje fácil de aprender, por lo que fue elegido como una forma de proporcionar a los usuarios domésticos un sistema que pudieran programar. Significa Código de Instrucción Simbólico para todo uso para principiantes y puede leerlo todo en la página BÁSICA de Wikipedia.
Desmonté el compilador BASIC en aproximadamente tres de estos primeros sistemas hasta que entiendo absolutamente lo que estaban haciendo. Tenían una tabla de búsqueda para convertir nombres de variables en ubicaciones de memoria. Tenían una o más pilas para realizar un seguimiento de los bucles anidados y los parámetros, etc. Se reservaron memoria para las matrices cuando usó la palabra clave DIM. Lo que hicieron fue esto:
1) Tokenize la fuente a tokens internos que representan todas las palabras clave BASIC. Algunos tokenizaron todo el programa y lo almacenaron en algún lugar de la memoria, otros tokenizaron sobre la marcha y fueron bastante lentos. Algunos convirtieron cada línea tal como se ingresó en la versión tokenizada y cuando escribiste LIST la destokenizó de nuevo a una salida legible (la mayoría hizo esto).
2) Cuando se utilizaron variables, insertaron un desplazamiento desde el comienzo de la memoria de variables o la dirección absoluta de la variable.
3) Cada palabra clave BASIC tenía (generalmente) una pequeña pieza de código de ensamblaje que realizaba las acciones necesarias para hacer esa palabra clave. Algunos fueron directos. Cuando dijiste PRINT “HELLO WORLD” (mi primera computadora no tenía minúsculas), saltó a una parte de la memoria que contenía el lenguaje ensamblador para el comando PRINT (más tarde) y lo ejecutó para generar el literal “HELLO WORLD” .
4) Cuando escribió RUN, un programa escrito en ensamblador (el intérprete) cargó el primer token de su programa, saltó a la ubicación de memoria adecuada para realizar esa acción, lo ejecutó hasta que regresó, avanzó al siguiente token, etc. .
Mucho trabajo realmente sucedió cuando ingresó una línea de código. Tenía una rutina que decía así:
- espera un personaje
- si ese carácter es la tecla “ENTER”, salte a la rutina del analizador
- compruebe para asegurarse de que el lugar actual en el búfer de comando no sea más de lo que una línea BÁSICA podría ser larga (tenía un máximo, generalmente, aproximadamente 255 caracteres)
- si no lo coloca en la ubicación actual en el búfer de comandos
- agregue 1 al lugar actual en el búfer de comando (moviéndose a la siguiente ubicación)
- saltar a 1
Cuando escribí esto y presioné ENTER al final:
10 IMPRIME “HOLA MUNDO”
el intérprete saltó al analizador y analizó la línea, haciendo algo como lo siguiente:
- leer un personaje
- verifique si ese carácter es un carácter especial como un operador o el “:” que dividió los comandos.
- Si es así, maneja ese personaje especial
- Si no lo considera parte del comando actual
- ir a 1
Entonces sería:
lee “10” y convierte que el número 10 ($ 0A en ese entonces $ significaba hexadecimal) y ponlo en una cola FIFO.
Analice “IMPRIMIR” y compare esto uno por uno con una lista de cadenas en la memoria que contenía las palabras clave. Si lo encontró, lo convirtió en un número, digamos 14 ($ 0e) y lo puso en la cola. El dispositivo predeterminado es 0 (la pantalla), por lo que lo coloca en la cola a continuación.
Luego vio la comilla doble (no tenía una comilla simple) y saltó a una rutina que leería y almacenaría una cadena literal. Esta rutina a menudo contaba los caracteres y luego saltaba a una rutina que encontraría espacio en la memoria reservada para contener variables y devolver esta dirección. Ahora copió los caracteres que en realidad eran sus códigos ASCII, uno por uno, desde el búfer de comando a la ubicación de almacenamiento variable hasta que llegó a la siguiente comilla doble. Produce un carácter especial al final de la cadena para denotar “STOP”. Luego empujó la dirección de este literal a la cola
Todo el tiempo hacía un seguimiento de cuán grande era la línea tokenizada.
Ahora, dado que se veía con un número de línea, significaba que era parte del programa actual en la memoria y no algo para ejecutar en este momento (llamado modo inmediato). Comenzó a leer el programa tokenizado en la memoria para ver si había una línea 10 y si se verificó la longitud. Si era más corto, copiaba los tokens en cola a la ubicación y movía el resto del programa hacia atrás en la memoria para cerrar el espacio. Si era más largo, entonces la línea actual 10 encontró espacio para ella en la memoria reservada para el programa y la puso allí cambiando una tabla en la memoria que almacenaba la ubicación de cada línea y luego simplemente reemplazó la nueva dirección con la dirección anterior en una tabla que muestra el orden de las líneas por número de línea y la dirección de cada línea en el espacio de código tokenizado.
Finalmente se hizo y devolvió el control a esa primera rutina. En general, el espacio del programa, espacio para los tokens, creció en una dirección (hacia adelante en la memoria) mientras que los datos del programa crecieron en el sentido opuesto (hacia atrás en la memoria). De esta manera, se puede usar la misma memoria para almacenar tanto el programa como los datos antes de que se quede sin memoria (lo que a menudo hacía). Un programa pequeño que usa muchos datos era tan malo como un programa grande que usa pocos datos.
Cuando escribí “EJECUTAR” hizo lo mismo que antes pero como no comenzó con un número de línea, saltó a la rutina de ejecución que ejecutó la cola que saltó al ensamblado que contiene el comando “EJECUTAR”.
Aquí es donde realmente ejecutó el programa. Fue a la tabla de números de línea y cargó la dirección de la primera línea, que contenía la forma simbólica de la primera línea. Este era un token de $ 0e del comando de impresión. En la memoria había una matriz de todas las direcciones de los comandos BASIC que respaldan las rutinas de ensamblaje almacenadas por número de token como índice. Saltó a la tabla [$ oe], que era el comando de impresión.
El ensamblaje para el comando de impresión, cargó el siguiente token que le indicó el número de dispositivo que debía generar. Las rutinas del vector de salida del dispositivo estaban en una matriz con el número de dispositivo como índice, lo cargó en un registro específico. El siguiente token le dijo que había literalmente el siguiente. El siguiente “token” no era en realidad un token sino la dirección de la cadena “HELLO WORLD” en la tabla literal. Comenzó un bucle en el ensamblaje que cargó la dirección del personaje en un registro y saltó a la dirección de la rutina de salida del dispositivo almacenada en ese registro específico que en este caso era el carácter fuera (“chrout”), una rutina de ensamblaje que puso el Código ASCII del personaje en el búfer de visualización de video en la ubicación actual, y avanzó la posición del personaje. Siguió haciendo esto hasta que llegó al carácter STOP en el que le dijo a la rutina chrout que comenzara una nueva línea.
Luego volvió a la primera rutina en la parte superior porque no había más tokens en el área de almacenamiento del programa.
El programa BASIC era simplemente información utilizada por otro programa para realizar las acciones que se escribieron en ensamblador. Ese programa base (el intérprete) tenía estructuras de datos adicionales que necesitaba, como tablas de vectores, tablas de cadenas, memoria variable y código para administrarlo, etc. Como puede ver, el análisis del lenguaje es leer el código y convertirlo en datos que el comando EJECUTAR puede usar para realizar el trabajo.
Un compilador analiza el código, lo tokeniza y hace algo similar al intérprete al convertirlo en ensamblador. Pero en lugar de ser un programa que utiliza estructuras de datos para “ejecutar” otro programa, convierte el programa a otra forma, generalmente ensamblado para el chip nativo y el código necesario para iniciar e inicializar un programa para el sistema operativo. Muchas veces se trata de un archivo de texto de ensamblaje (código fuente) que se ensambla usando un ensamblador y finalmente se convierte en código de máquina nativo para la CPU y el sistema operativo. Esto crea un archivo en algún dispositivo. Cuando le dice al sistema operativo que desea ejecutar ese archivo, lo abre, lo carga en la memoria y luego lo ejecuta. Ahora, al principio, puede parecer que el sistema operativo es un programa que también ejecuta un programa, pero en realidad no lo es. El código que escribe el compilador es nativo de la CPU y se ejecuta igual que el propio sistema operativo. Dado que el sistema operativo controla el hardware, puede detener su programa después de un momento, guardar todos los registros e iniciar otro programa cargando sus registros, pero su programa tiene el control directo de la CPU por un momento.
Eso es un compilador. O eso parece.
Ahora, durante mucho tiempo, hemos tenido lo que se llama un lenguaje de código de byte como Java, Perl, Inferno y muchos otros. Estos lenguajes son un híbrido entre un compilador y un intérprete. En este caso, su Java está tokenizado y compilado en códigos de bytes muy similares al programa BASIC anterior, pero en su lugar escribe tokens que son como ensamblados pero para otro programa (llamado máquina virtual) que lee y ejecuta esos tokens como si fueran instrucciones de lenguaje ensamblador a algún hardware virtual. Puede ver todos los de Java en el Capítulo 6. El conjunto de instrucciones de la máquina virtual Java. Entonces, este es un programa que, como un intérprete, ejecuta datos como código pero lo hace en un método mucho más eficiente que un intérprete y el lenguaje está diseñado para compilarse, mientras que muchos lenguajes interpretados no son muy fáciles de compilar. Compilar estos códigos de bytes en el ensamblado nativo suele ser mucho más fácil. Lo bueno de esto es que el mismo archivo que se compila en un sistema que se ejecuta sobre Linux en un ARM y se ejecuta en un sistema que ejecuta Windows en un procesador x86 ya que la máquina virtual hace toda la funcionalidad del hardware virtual en el hardware real .
Entonces, este sigue siendo un programa que ejecuta un programa que es un intérprete. O eso parece.
Se compilan muchos lenguajes de códigos de bytes en tiempo de ejecución hasta el ensamblado nativo antes de que se ejecuten. Esto se conoce como compilación justo a tiempo o JIT. En este caso, ese archivo de códigos de bytes para una máquina virtual determinada se lee y se convierte en ensamblado nativo para el chip y el sistema operativo actuales y se ejecuta como código nativo para la CPU. Este es un compilador.
Si llegaste hasta aquí en esta larga respuesta, debes estar interesado. Aquí hay algunos enlaces:
Comparación del software de virtualización de aplicaciones.
Compilación justo a tiempo
BASIC
¿Qué probar un poco BÁSICO? Muchas, muchas, muchas personas aprendieron a programar de esta manera
QuiteBasic: diversión, aprendizaje y nostalgia