Eval of Julia code
L'une des parties les plus difficiles de l'apprentissage de la façon dont le langage Julia exécute du code est d'apprendre comment toutes les pièces fonctionnent ensemble pour exécuter un bloc de code.
Chaque morceau de code passe généralement par de nombreuses étapes avec des noms potentiellement inconnus, tels que (dans aucun ordre particulier) : flisp, AST, C++, LLVM, eval
, typeinf
, macroexpand
, sysimg (ou image système), bootstrap, compiler, analyser, exécuter, JIT, interpréter, box, unbox, fonction intrinsèque et fonction primitive, avant de se transformer en résultat souhaité (espérons-le).
Julia Execution
La vue d'ensemble à 10 000 pieds de l'ensemble du processus est la suivante :
- L'utilisateur démarre
julia
. - La fonction C
main()
decli/loader_exe.c
est appelée. Cette fonction traite les arguments de la ligne de commande, remplissant la structurejl_options
et définissant la variableARGS
. Elle initialise ensuite Julia (en appelantjulia_init
ininit.c
, ce qui peut charger un sysimg précédemment compilé. Enfin, elle passe le contrôle à Julia en appelantBase._start()
. - Lorsque
_start()
prend le contrôle, la séquence suivante de commandes dépend des arguments de ligne de commande fournis. Par exemple, si un nom de fichier a été fourni, il procédera à l'exécution de ce fichier. Sinon, il commencera un REPL interactif. - En sautant les détails sur la façon dont le REPL interagit avec l'utilisateur, disons simplement que le programme se retrouve avec un bloc de code qu'il souhaite exécuter.
- Si le bloc de code à exécuter se trouve dans un fichier,
jl_load(char *filename)
est invoqué pour charger le fichier et parse le. Chaque fragment de code est ensuite passé àeval
pour être exécuté. - Chaque fragment de code (ou AST) est transmis à
eval()
pour être transformé en résultats. eval()
prend chaque fragment de code et essaie de l'exécuter dansjl_toplevel_eval_flex()
.jl_toplevel_eval_flex()
décide si le code est une action "toplevel" (commeusing
oumodule
), ce qui serait invalide à l'intérieur d'une fonction. Si c'est le cas, il transmet le code à l'interpréteur de niveau supérieur.jl_toplevel_eval_flex()
puis expands le code pour éliminer toutes les macros et pour "abaisser" l'AST afin de le rendre plus simple à exécuter.jl_toplevel_eval_flex()
utilise ensuite quelques heuristiques simples pour décider s'il doit compiler à la volée l'AST ou l'interpréter directement.- La majeure partie du travail d'interprétation du code est gérée par
eval
ininterpreter.c
. - Si au lieu de cela, le code est compilé, la majeure partie du travail est gérée par
codegen.cpp
. Chaque fois qu'une fonction Julia est appelée pour la première fois avec un ensemble donné de types d'arguments, type inference sera exécuté sur cette fonction. Ces informations sont utilisées par l'étape codegen pour générer un code plus rapide. - Finalement, l'utilisateur quitte le REPL, ou la fin du programme est atteinte, et la méthode
_start()
retourne. - Juste avant de sortir,
main()
appellejl_atexit_hook(exit_code)
. Cela appelleBase._atexit()
(qui appelle toutes les fonctions enregistrées àatexit()
à l'intérieur de Julia). Ensuite, il appellejl_gc_run_all_finalizers()
. Enfin, il nettoie gracieusement tous les handleslibuv
et attend qu'ils se vident et se ferment.
Parsing
Le parseur Julia est un petit programme lisp écrit en femtolisp, dont le code source est distribué à l'intérieur de Julia dans src/flisp.
L'interface des fonctions pour cela est principalement définie dans jlfrontend.scm
. Le code dans ast.c
gère ce transfert du côté de Julia.
Les autres fichiers pertinents à ce stade sont julia-parser.scm
, qui gère la tokenisation du code Julia et le transforme en un AST, et julia-syntax.scm
, qui gère la transformation des représentations AST complexes en représentations AST plus simples, "abaissées", qui sont plus adaptées à l'analyse et à l'exécution.
Si vous souhaitez tester le parseur sans reconstruire entièrement Julia, vous pouvez exécuter le frontend de manière autonome comme suit :
$ cd src
$ flisp/flisp
> (load "jlfrontend.scm")
> (jl-parse-file "<filename>")
Macro Expansion
Lorsque eval()
rencontre un macro, il développe ce nœud AST avant d'essayer d'évaluer l'expression. L'expansion de macro implique un transfert de 4d61726b646f776e2e436f64652822222c20226576616c28292229_40726566
(en Julia), à la fonction de parsing jl_macroexpand()
(écrite en flisp
) au macro Julia lui-même (écrit en - quoi d'autre - Julia) via fl_invoke_julia_macro()
, et retour.
Typiquement, l'expansion de macro est invoquée comme première étape lors d'un appel à Meta.lower()
/jl_expand()
, bien qu'elle puisse également être invoquée directement par un appel à macroexpand()
/jl_macroexpand()
.
Type Inference
L'inférence de type est implémentée en Julia par typeinf()
in compiler/typeinfer.jl
. L'inférence de type est le processus d'examen d'une fonction Julia et de détermination des limites pour les types de chacune de ses variables, ainsi que des limites sur le type de la valeur de retour de la fonction. Cela permet de nombreuses optimisations futures, telles que le déboxing de valeurs immuables connues et le levage à la compilation de diverses opérations d'exécution telles que le calcul des décalages de champ et des pointeurs de fonction. L'inférence de type peut également inclure d'autres étapes telles que la propagation de constantes et l'inlining.
JIT Code Generation
Codegen est le processus de transformation d'un AST Julia en code machine natif.
L'environnement JIT est initialisé par un appel précoce à jl_init_codegen
in codegen.cpp
.
À la demande, une méthode Julia est convertie en une fonction native par la fonction emit_function(jl_method_instance_t*)
. (notez que lors de l'utilisation du MCJIT (dans LLVM v3.4+), chaque fonction doit être JIT dans un nouveau module.) Cette fonction appelle récursivement emit_expr()
jusqu'à ce que l'ensemble de la fonction ait été émis.
Une grande partie du reste de ce fichier est consacrée à diverses optimisations manuelles de modèles de code spécifiques. Par exemple, emit_known_call()
sait comment intégrer de nombreuses fonctions primitives (définies dans builtins.c
) pour diverses combinaisons de types d'arguments.
D'autres parties de codegen sont gérées par divers fichiers d'aide :
Gère les traces de retour pour les fonctions JIT
Gère le ccall et llvmcall FFI, ainsi que divers fichiers
abi_*.cpp
Gère l'émission de diverses fonctions intrinsèques de bas niveau
System Image
L'image système est une archive précompilée d'un ensemble de fichiers Julia. Le fichier sys.ji
distribué avec Julia est l'une de ces images système, générée en exécutant le fichier sysimg.jl
, et en sérialisant l'environnement résultant (y compris les Types, Fonctions, Modules, et toutes les autres valeurs définies) dans un fichier. Par conséquent, il contient une version figée des modules Main
, Core
, et Base
(et tout ce qui était dans l'environnement à la fin du démarrage). Ce sérialiseur/désérialiseur est implémenté par jl_save_system_image
/jl_restore_system_image
in staticdata.c
.
Si aucun fichier sysimg n'existe (jl_options.image_file == NULL
), cela implique également que --build
a été donné en ligne de commande, donc le résultat final devrait être un nouveau fichier sysimg. Lors de l'initialisation de Julia, des modules Core
et Main
minimaux sont créés. Ensuite, un fichier nommé boot.jl
est évalué à partir du répertoire courant. Julia évalue ensuite tout fichier donné comme argument de ligne de commande jusqu'à ce qu'elle atteigne la fin. Enfin, elle enregistre l'environnement résultant dans un fichier "sysimg" pour être utilisé comme point de départ pour une future exécution de Julia.