Clause
← Toute la documentation

Traduit de l'original anglais · 06042fa · 2026-10-09 · Lire en anglais

ABI du code généré (révision 4)

Contrat privé entre la sortie du compilateur et le runtime. Il s'agit de C++23 interne au projet, non compatible avec BEAM et qui n'est pas une FFI générale. Les objets et le runtime doivent provenir du même build ; les révisions de descripteur plus anciennes sont rejetées avant toute utilisation.

En-têtes : v1.hpp (types de terme, de contexte et de fonction), term.hpp (codec des entiers immédiats), status.hpp, builtins.hpp, startup.hpp (démarrage du programme).

Termes

Un terme est un mot non signé de la largeur d'un pointeur de la cible (32 ou 64 bits). Les dispositions dérivent de la cible LLVM configurée, si bien que le code multicible utilise les largeurs de la cible.

Fonctions et symboles

Signature d'entrée (convention d'appel C native, liaison C non requise) :

TermWord function(ProcessContext *context, const TermWord *arguments);

Les arguments forment un tableau emprunté, aligné sur les mots, dans l'ordre du source (null pour l'arité 0). Le contexte est vivant et transmis inchangé à travers les appels. Un mot retourné n'est utilisable qu'après vérification du canal d'échec.

Symboles : clausev1_<hex module>_<hex function>_<arity>, hexadécimal en minuscules des octets UTF-8, arité décimale canonique ; réversibles et indépendants de l'hôte. Les entrées exportées sont externes, les autres internes.

Enregistrement des modules

Chaque module émet clausev1_<hex module>__0.descriptor et .register. Le descripteur contient la version de l'ABI, la largeur des termes, la table des exports (nom, arité, entrée hôte et le FrameDescriptor dans lequel entrent les appels dynamiques), les graphies des atomes (paires pointeur/taille UTF-8), les descripteurs de native records (emplacements d'atomes du module, du nom et des champs, indicateur d'export ; native records) dans une table externe <prefix>.records à laquelle se réfèrent les autres modules d'un lot, et les descripteurs de funs dans une table privée <prefix>.funs (funs). L'enregistrement appelle CLAUSE_register_module_v4(Runtime*), qui valide la version, la largeur et tous les exports, interne les atomes, construit un registre figé et le publie avec l'image du code en une seule transaction. Les modules en double ne remplacent jamais le code ; tout échec ne publie rien (les atomes déjà internés restent dans la table bornée).

Canal d'échec (révision 2)

Les erreurs ne sont pas encodées dans les bits des termes. Après chaque appel généré qui n'est pas terminal, l'appelant vérifie CLAUSE_call_failed_v2(context) avant d'utiliser le résultat ou d'évaluer l'argument suivant. En cas d'échec, l'appelé retourne un mot zéro invalide.

IssueTransport
Non-correspondance de motif, rejet par un guardPoursuite vers le candidat suivant ; canal intact
Clauses épuiséeserror:function_clause ; error:{case_clause, Value} avec une charge utile possédée pour un case ; error:if_clause ; error:{try_clause, Value} pour les clauses of d'un try ; error:{else_clause, Value} pour les clauses else d'un maybe
Échec de correspondance dans le corpserror:{badmatch, Value} avec une charge utile possédée
Comprehensionserror:{bad_generator, Tail}, error:{bad_filter, Value}, error:{bad_generators, Inputs} (ErrorReason 16-18) ; le rejet par un générateur strict donne {badmatch, Element}
Accès aux records, mauvais arguments, arithmétique, mapsbadrecord, badarg, badarith, badmap/badkey
Champ de native record manquantErrorReason::badfield (20), charge utile {{Module, Name}, Field}
Appel d'une valeur (F(Args))ErrorReason::badfun (22, la charge utile étant la valeur), badarity (23, charge utile {Fun, Args}), undef (24), enregistrés par CLAUSE_apply_v1 (funs)
Appels dynamiques (M:F(Args), apply/2,3, fun M:F/A avec des variables)badarg pour un module ou une fonction qui n'est pas un atome, une liste d'arguments impropre ou une arité invalide ; undef lorsqu'aucun module du programme n'exporte la fonction ; badfun/badarity comme ci-dessus (funs)
Construction native externe sans valeurErrorReason::novalue (21), charge utile {{Module, Name}, Field}
Résultat entier au-delà de la limite de tailleIssue de service ValueOutcome::system_limit (3) : un guard rejette, un corps lève error:system_limit (ErrorReason 19)
Opérande gauche paresseux invalide{badarg, Value}
Infrastructure (OOM, limites, propriété, interne)CallError::runtime_failure avec le Status exact
erlang:error/1,2,3, exit/1, throw/1, erlang:raise/3raised_error/raised_exit/raised_throw : la classe provient de l'identifiant, la charge utile possédée est la raison entière
erlang:halt/0,1CallError::halted avec halt_status (et le slogan)

Les raisons sont des identifiants typés enregistrés par CLAUSE_raise_v2 ; les trois identifiants raised_* sélectionnent la classe exit ou throw (sinon error) et portent n'importe quel terme comme raison. error/2,3 lèvent via CLAUSE_error_v1(context, reason, args), qui conserve aussi une liste args pour le cadre de pile du sommet. Le premier échec l'emporte ; les invocations imbriquées partagent le canal. GeneratedInvocation est la portée hôte : elle vérifie les échecs en attente avant l'entrée et après le retour, copie le résultat ou l'erreur, et ne nettoie qu'à la sortie la plus externe (y compris en cas d'exceptions C++). Aucune exception ne traverse les entrées générées. Les appelants d'entrées brutes doivent ouvrir une GeneratedInvocation ; les hôtes normaux utilisent ResolvedFunction::call.

catch Expr redirige chaque vérification d'échec et chaque levée à l'intérieur d'Expr vers un bloc gestionnaire qui appelle CLAUSE_catch_v1(context, slot). Pour une exception Erlang en attente, il écrit la valeur du catch dans l'emplacement racine et nettoie le canal : le terme lancé, {'EXIT', Reason} pour un exit, ou {'EXIT', {Reason, []}} pour une erreur (les raisons typées deviennent leurs termes OTP, comme {badmatch, V} ; la pile est décrite plus bas). Les arrêts (halts) et les échecs d'infrastructure restent en attente, et la vérification propre au gestionnaire continue vers le gestionnaire englobant ou la sortie de la fonction. Les liaisons créées à l'intérieur d'Expr ne sont plus sûres ensuite, si bien que la jonction ne fusionne que la valeur.

try Body of ... catch ... end protège uniquement Body de la même manière. Son gestionnaire appelle CLAUSE_exception_v2(context, class_slot, reason_slot, stack_slot), qui écrit l'atome de classe (error, exit ou throw), la raison et le terme de trace de pile dans des emplacements racines et nettoie le canal (les arrêts et les échecs d'infrastructure restent en attente comme pour catch). Les clauses catch font ensuite correspondre Class:Reason avec des motifs et des guards ordinaires ; une classe omise correspond à throw, et une variable de pile nommée se lie au terme de pile. Si aucune ne correspond, CLAUSE_reraise_v2(context, class, reason, stack) enregistre de nouveau l'exception avec une raison raised_* et la même pile, qui est signalée et capturée exactement comme l'originale. Les clauses of sélectionnent sur la valeur du corps et lèvent {try_clause, Value} (ErrorReason::try_clause = 14) ; les exceptions à l'intérieur des clauses of et des gestionnaires vont au gestionnaire englobant.

maybe n'a besoin d'aucun service : chaque ?= est une correspondance ordinaire dont l'arête d'échec quitte le corps vers la sortie du maybe avec la valeur non appariée (déjà enracinée). Sans else, cette valeur est le résultat ; sinon les clauses else sélectionnent sur elle comme des clauses case et lèvent {else_clause, Value} (ErrorReason::else_clause = 15) si aucune ne correspond.

Les comprehensions utilisent les services existants avec quelques opérations : les éléments accumulés sont inversés par la construction reverse (ContainerConstruction::reverse = 2, valeurs {List, Tail}) ; une comprehension de binary joint ses morceaux avec BitOperation::concat ({List}) et une comprehension de map construit sa map avec MapOperation::from_list ({Pairs}, les clés ultérieures l'emportent). Les générateurs de maps lisent MapOperation::key_at/value_at ({Map, Position} dans l'ordre canonique des clés) et exposent l'entrée restante d'un zip avec MapOperation::iterator, la chaîne {K, V, Next} d'OTP se terminant par none. Les générateurs de bitstrings utilisent l'extraction de motif ordinaire plus un segment final binary/all pour le reste.

try ... after A end ajoute une seconde protection autour du corps et de toutes les clauses of et catch. Sur le chemin normal, A s'exécute après que la valeur sélectionnée a été enracinée, et sa valeur est ignorée. Le gestionnaire after prend l'exception avec CLAUSE_exception_v2, exécute une seconde copie de A et relève l'exception avec CLAUSE_reraise_v2 ; une exception ou un échec à l'intérieur de A sort plutôt par le gestionnaire englobant, en remplaçant l'original. Les arrêts et les échecs d'infrastructure sautent A. Les emplacements racines appartiennent au cadre de la fonction, si bien que chaque chemin les libère à la sortie de la fonction.

Traces de pile

Chaque cadre racine nomme sa fonction générée avec un abi::v1::FrameDescriptor privé (descripteur de module, emplacements d'atomes du nom de module et de fonction, arité). Lorsqu'une exception Erlang est enregistrée, le canal copie les 8 cadres nommés les plus internes (la backtrace_depth par défaut de BEAM) ; le terme [{Module, Function, Arity, []}, ...] n'est construit que lorsqu'un gestionnaire, un catch ou un rapport le demande. Le cadre du sommet affiche la liste d'arguments de error/2,3 au lieu de l'arité lorsque cet argument est une liste.

erlang:raise(Class, Reason, Stack) (CLAUSE_reraise_v2) accepte les piles que BEAM accepte : une liste propre de {M, F, A} (complétés par une localisation []) ou de {M, F, A, Location} avec des atomes M, F et une liste Location, tronquée à 8 entrées ; la pile est alors conservée telle quelle et les cadres ne sont plus capturés. Une classe ou une pile invalide n'enregistre rien et l'appel s'évalue en badarg, comme dans OTP. erlang:get_stacktrace/0 est rejeté avec le texte de lint « removed » d'OTP 29.

Différences par rapport à OTP, toutes visibles uniquement dans le terme de pile :

Cadres et transferts

Les fonctions générées s'exécutent sur des cadres explicites (modèle d'exécution, step 19 ; services dans frames.hpp). Chaque fonction possède un descripteur <symbol>.frame (FrameDescriptor : descripteur de module, emplacements d'atomes du module et de la fonction, arité, code du corps, nombre d'emplacements, nombre d'emplacements de termes ; externe pour les fonctions exportées) et un <symbol>.body interne de type void(void *context). Un <symbol> exporté conserve la signature TermWord(Context *, const TermWord *) comme entrée hôte appelant CLAUSE_invoke_v1(context, frame, arguments).

Services du runtime

Le code généré appelle des services C++ vérifiés : CLAUSE_exact_v1 (égalité exacte), CLAUSE_immediate_v1 (prédicats et requêtes sur les immédiats), CLAUSE_construct_v1, CLAUSE_inspect_v1, CLAUSE_integer_v1, CLAUSE_float_v1, CLAUSE_map_v1, CLAUSE_bits_v1, CLAUSE_record_v1 (création, lecture, mise à jour, correspondance et test de native records sous un RecordCheck ; issues bad_record, bad_field, no_match), CLAUSE_make_fun_v1 (construction d'une fun à partir d'un FunDescriptor). Chacun retourne un succès, une erreur sémantique (badarg/badarith/...) ou un échec d'infrastructure et n'écrit sa sortie qu'en cas de succès. CLAUSE_display_v1 (output.hpp) affiche une ligne erlang:display/1 et produit true ; il n'a pas d'erreur sémantique. CLAUSE_halt_v1 ne réussit jamais : il enregistre une demande d'arrêt (CallError::halted avec le code de sortie) ou badarg, de sorte que l'appelant se déroule. Les graphies pour l'éditeur de liens suivent la décoration (mangling) C++ Itanium ou Microsoft de la cible.

CLAUSE_builtin_frame_v1(context, builtin) (builtins.hpp) retourne le FrameDescriptor du builtin de production d'indice builtin dans abi::v1::bridge_builtins (ajout seulement) ; le code généré y entre comme dans une fonction avec les arguments dans les registres, et les erreurs et les échecs vont au canal vérifié (builtins). Un FrameDescriptor dont le corps est null est un builtin : y entrer exécute le builtin sur les registres et retourne dans l'appelant, ou suspend le processus à la continuation du builtin lorsque celui-ci a été intercepté (trapped) (portions).

abi::v1::dispatch_builtin appelle les builtins enregistrés par l'hôte d'après les octets du module et de la fonction, le tableau d'arguments et l'arité, et retourne un Status ; la sortie n'est écrite qu'en cas de succès.

StatusValeurSignification
ok0Succès
not_implemented1Fonctionnalité différée connue, signalée une seule fois
invalid_argument2Entrée invalide
diagnostic_failure3Échec de la remise du rapport
out_of_memory4Échec de l'allocation, état restauré
busy5Contextes vivants ou dispatch en cours
wrong_owner6L'objet appartient à un autre runtime ou contexte
resource_limit7Plafond ou budget épuisé
stopped8Propriétaire arrêté
abi_mismatch9La version ou la largeur des termes diffère
internal_error10Échec inattendu contenu
unknown_builtin11Signature non enregistrée ni cataloguée
erlang_error12Erreur Erlang structurée enregistrée
output_failure13La sortie standard a refusé une écriture

Révisions

RévisionModification
1Descripteurs et entrées initiaux
2Canal d'échec vérifié
3Graphies et emplacements des atomes dans les descripteurs
4Portées racines générées obligatoires
5Cadres de processus explicites et transferts
6Descripteurs de native records dans les descripteurs de module, CLAUSE_record_v1
7Descripteurs de funs dans les descripteurs de module, CLAUSE_make_fun_v1, CLAUSE_apply_v1
8Les descripteurs d'export nomment leur FrameDescriptor ; services d'appel dynamique CLAUSE_call_v1, CLAUSE_apply_list_v1, CLAUSE_call_list_v1, CLAUSE_make_external_fun_v1