Eval of Julia code
Una de las partes más difíciles de aprender cómo el lenguaje Julia ejecuta código es entender cómo todas las piezas trabajan juntas para ejecutar un bloque de código.
Cada fragmento de código típicamente pasa por muchos pasos con nombres potencialmente desconocidos, tales como (sin un orden particular): flisp, AST, C++, LLVM, eval
, typeinf
, macroexpand
, sysimg (o imagen del sistema), arranque, compilar, analizar, ejecutar, JIT, interpretar, empaquetar, desempaquetar, función intrínseca y función primitiva, antes de convertirse en el resultado deseado (con suerte).
Julia Execution
La vista de 10,000 pies de todo el proceso es la siguiente:
- El usuario inicia
julia
. - La función C
main()
decli/loader_exe.c
se llama. Esta función procesa los argumentos de la línea de comandos, llenando la estructurajl_options
y configurando la variableARGS
. Luego inicializa Julia (llamando ajulia_init
ininit.c
, que puede cargar un sysimg previamente compilado. Finalmente, pasa el control a Julia llamando aBase._start()
. - Cuando
_start()
toma el control, la secuencia subsiguiente de comandos depende de los argumentos de la línea de comandos proporcionados. Por ejemplo, si se suministró un nombre de archivo, procederá a ejecutar ese archivo. De lo contrario, comenzará un REPL interactivo. - Omitiendo los detalles sobre cómo el REPL interactúa con el usuario, digamos simplemente que el programa termina con un bloque de código que desea ejecutar.
- Si el bloque de código a ejecutar está en un archivo,
jl_load(char *filename)
se invoca para cargar el archivo y parse lo. Cada fragmento de código se pasa luego aeval
para ejecutarlo. - Cada fragmento de código (o AST) se entrega a
eval()
para convertirlo en resultados. eval()
toma cada fragmento de código e intenta ejecutarlo enjl_toplevel_eval_flex()
.jl_toplevel_eval_flex()
decide si el código es una acción "toplevel" (comousing
omodule
), lo cual sería inválido dentro de una función. Si es así, pasa el código al intérprete de nivel superior.jl_toplevel_eval_flex()
luego expands el código para eliminar cualquier macro y "bajar" el AST para hacerlo más simple de ejecutar.jl_toplevel_eval_flex()
luego utiliza algunas heurísticas simples para decidir si compilar JIT el AST o interpretarlo directamente.- La mayor parte del trabajo para interpretar el código es manejado por
eval
ininterpreter.c
. - Si en su lugar, el código se compila, la mayor parte del trabajo es manejado por
codegen.cpp
. Cada vez que se llama a una función de Julia por primera vez con un conjunto dado de tipos de argumentos, type inference se ejecutará en esa función. Esta información es utilizada por el paso codegen para generar código más rápido. - Eventualmente, el usuario sale del REPL, o se alcanza el final del programa, y el método
_start()
devuelve. - Justo antes de salir,
main()
llama ajl_atexit_hook(exit_code)
. Esto llama aBase._atexit()
(que llama a cualquier función registrada enatexit()
dentro de Julia). Luego llama ajl_gc_run_all_finalizers()
. Finalmente, limpia de manera ordenada todos los manejadores delibuv
y espera a que se vacíen y cierren.
Parsing
El analizador de Julia es un pequeño programa lisp escrito en femtolisp, cuyo código fuente se distribuye dentro de Julia en src/flisp.
Las funciones de interfaz para esto están definidas principalmente en jlfrontend.scm
. El código en ast.c
maneja esta transferencia en el lado de Julia.
Los otros archivos relevantes en esta etapa son julia-parser.scm
, que maneja la tokenización del código Julia y lo convierte en un AST, y julia-syntax.scm
, que se encarga de transformar representaciones complejas de AST en representaciones de AST más simples y "reducidas" que son más adecuadas para el análisis y la ejecución.
Si deseas probar el analizador sin reconstruir Julia en su totalidad, puedes ejecutar el frontend por su cuenta de la siguiente manera:
$ cd src
$ flisp/flisp
> (load "jlfrontend.scm")
> (jl-parse-file "<filename>")
Macro Expansion
Cuando eval()
encuentra un macro, expande ese nodo AST antes de intentar evaluar la expresión. La expansión de macros implica una transferencia desde 4d61726b646f776e2e436f64652822222c20226576616c28292229_40726566
(en Julia), a la función de análisis jl_macroexpand()
(escrita en flisp
) al macro de Julia en sí (escrito en - qué más - Julia) a través de fl_invoke_julia_macro()
, y de vuelta.
Típicamente, la expansión de macros se invoca como un primer paso durante una llamada a Meta.lower()
/jl_expand()
, aunque también se puede invocar directamente mediante una llamada a macroexpand()
/jl_macroexpand()
.
Type Inference
La inferencia de tipos se implementa en Julia mediante typeinf()
in compiler/typeinfer.jl
. La inferencia de tipos es el proceso de examinar una función de Julia y determinar los límites para los tipos de cada una de sus variables, así como los límites sobre el tipo del valor de retorno de la función. Esto permite muchas optimizaciones futuras, como el desboxing de valores inmutables conocidos y la elevación en tiempo de compilación de varias operaciones en tiempo de ejecución, como el cálculo de desplazamientos de campos y punteros de función. La inferencia de tipos también puede incluir otros pasos, como la propagación de constantes y la inlining.
JIT Code Generation
Codegen es el proceso de convertir un AST de Julia en código de máquina nativo.
El entorno JIT se inicializa mediante una llamada temprana a jl_init_codegen
in codegen.cpp
.
A demanda, un método de Julia se convierte en una función nativa mediante la función emit_function(jl_method_instance_t*)
. (nota, al usar el MCJIT (en LLVM v3.4+), cada función debe ser JIT en un nuevo módulo). Esta función llama recursivamente a emit_expr()
hasta que toda la función ha sido emitida.
Gran parte del resto de este archivo está dedicado a varias optimizaciones manuales de patrones de código específicos. Por ejemplo, emit_known_call()
sabe cómo inyectar muchas de las funciones primitivas (definidas en builtins.c
) para varias combinaciones de tipos de argumentos.
Otras partes de codegen son manejadas por varios archivos auxiliares:
Maneja las trazas de retroceso para funciones JIT
Maneja el ccall y llvmcall FFI, junto con varios archivos
abi_*.cpp
Maneja la emisión de varias funciones intrínsecas de bajo nivel.
System Image
La imagen del sistema es un archivo comprimido precompilado de un conjunto de archivos de Julia. El archivo sys.ji
distribuido con Julia es una de estas imágenes del sistema, generada al ejecutar el archivo sysimg.jl
, y serializando el entorno resultante (incluyendo Tipos, Funciones, Módulos y todos los demás valores definidos) en un archivo. Por lo tanto, contiene una versión congelada de los módulos Main
, Core
y Base
(y cualquier otra cosa que estuviera en el entorno al final del arranque). Este serializador/deserializador está implementado por jl_save_system_image
/jl_restore_system_image
in staticdata.c
.
Si no hay un archivo sysimg (jl_options.image_file == NULL
), esto también implica que se dio --build
en la línea de comandos, por lo que el resultado final debería ser un nuevo archivo sysimg. Durante la inicialización de Julia, se crean los módulos mínimos Core
y Main
. Luego, se evalúa un archivo llamado boot.jl
desde el directorio actual. Julia luego evalúa cualquier archivo dado como un argumento de línea de comandos hasta que llega al final. Finalmente, guarda el entorno resultante en un archivo "sysimg" para usarlo como punto de partida para una futura ejecución de Julia.