Clause
← Toute la documentation

L'original anglais a changé depuis cette traduction ; certaines parties peuvent donc être obsolètes. Lire en anglais

Traduit de l'original anglais · 1e3a39c8-dirty · 2026-10-26

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

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, les descripteurs de funs dans une table privée <prefix>.funs (funs), les littéraux encodés du module et le cache de littéraux (zones), les caches d'atomes et de funs inscriptibles que le code généré lit en ligne, et la code_convention de ses corps. 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 le canal avant d'utiliser le résultat ou d'évaluer l'argument suivant : il lit le mot failed du FrameState du contexte, que le runtime maintient égal à CLAUSE_call_failed_v2(context). En cas d'échec, l'appelé retourne un mot zéro invalide.

IssueTransport
Non-correspondance de motif, rejet par une 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) : une 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}), qui construit le résultat en une seule réservation de tas ; les générateurs de listes testent et lisent les cellules cons en ligne ; 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 prennent MapOperation::entries (11, {Map}) une seule fois : une cellule contenant les clés et les valeurs aux mots 1 + 2i et 2 + 2i dans l'ordre canonique des clés (une map classique elle-même, ou une copie en forme de tuple des entrées d'une map hors tas que seul le code généré lit), et lisent chaque entrée en ligne ; key_at/value_at ({Map, Position}) restent pour les autres appelants. L'entrée restante d'un zip est exposée avec MapOperation::iterator, la chaîne {K, V, Next} d'OTP se terminant par none. MapOperation::update_owned (10) prend les opérandes de update pour une mise à jour de map dont le compilateur a prouvé que la source n'est plus lue ensuite : une map source hors tas est mise à jour sur place et son talon (stub) est laissé consommé (vide). 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 ; 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 abi::v1::Code, void(void *context, TermWord x0, ...) avec abi::v1::register_arguments paramètres de termes ; avec un runtime construit par Clang sur x86-64 et AArch64, les corps et chaque transfert utilisent la convention preserve_none, comme abi::v1::Code sur ces cibles : les corps ne retournent jamais normalement et ne sauvegardent donc aucun registre préservé par l'appelé ; l'enregistrement rejette un module dont la code_convention (0 aucune, 1 C, 2 preserve_none) n'est pas celle du runtime. 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 (pour d'autres consommateurs ; le code généré teste et lit les cellules de tuple et de cons en ligne), 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.

Formes en ligne (codegen/lowering_inline.cpp) : les tests de type (is_atom, is_integer, is_number, is_boolean, is_tuple, is_list, is_bitstring, is_float, is_map, is_pid, is_port, is_reference, is_function/1, is_record/1 natif, is_binary/1 par le mot de longueur en bits) lisent les étiquettes et, après un test d'étiquette boxed, le genre de l'en-tête (abi::v1::HeaderKind) ; ils n'appellent jamais de service. Les comparaisons de deux immédiats (==, /=) ou de deux petits entiers (<, =<, >, >=), element/2, tuple_size/1, bit_size/1 et byte_size/1 (depuis le mot de longueur en bits, tant que la taille est un petit entier), hd/1, tl/1, les opérateurs booléens, band, bor, bxor, bsr, bsl (tant que le résultat reste petit) et div/rem sur petits entiers s'exécutent en ligne ; self/0 lit FrameState::self. Ils s'exécutent en ligne là où leurs opérandes le permettent et appellent sinon CLAUSE_immediate_v1, qui conserve tous les cas d'erreur et de bignum. setelement/3 d'un petit indice dans un tuple boxed copie le tuple dans les mots libres du bloc de tas et remplace l'emplacement en ligne ; sinon, ou sans place, il entre dans le builtin de pont.

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).

Un builtin direct (BuiltinCall::direct : conversions pures comme integer_to_list/1, atom_to_list/1, list_to_tuple/1, setelement/3, phash2/1) n'est jamais intercepté, n'attend jamais et n'agit jamais sur un autre processus. Le code généré l'appelle avec CLAUSE_builtin_call_v1(context, builtin, arguments), qui exécute son corps immédiatement et retourne le résultat, sans transfert de cadre, sans point de reprise et sans point sûr d'entrée ; il dépense une réduction, et un échec va au canal vérifié comme pour un service. Un builtin rapide (BuiltinCall::quick : length/1, binary_to_list/1, list_to_binary/1, iolist_to_binary/1) est d'abord appelé de la même façon ; son chemin rapide borné renvoie 0 lorsqu'il refuse (entrées plus longues, autres formes, toute erreur, et systématiquement lorsque les appels sont tracés), et le code généré entre alors dans le cadre du builtin comme pour tout builtin de pont.

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
9Chaque contexte commence par abi::v1::FrameState ; transferts en ligne
10Premiers registres X passés comme paramètres du code
11Mots fixes de false et true
12Champs de tas en ligne heap_top/heap_limit dans FrameState
13undefined, ok, error fixes ; ModuleDescriptor::atom_cache et FrameState::runtime
14Zones de littéraux de module : ModuleDescriptor::literals, literal_words
15FrameState::self, le mot pid du contexte
16Code utilise preserve_none sur x86-64 et AArch64 avec un runtime construit par Clang ; ModuleDescriptor::code_convention
17Six registres X comme paramètres de Code
18Cellules sub_binary (HeaderKind::sub_binary, sub_*_word) et extraction de Rest en ligne
19Mots de fin et de limite d'ajout dans les cellules refc_binary (refc_append_*_word, 8 mots de charge utile) et ajouts en ligne
20MapOperation::entries et générateurs de maps en ligne
21ModuleDescriptor::fun_cache et fermetures en ligne
22Builtins directs : BuiltinName::direct, CLAUSE_builtin_call_v1
23Builtins rapides : BuiltinName::call (BuiltinCall), 0 de CLAUSE_builtin_call_v1 signifie un refus
24RecordDescriptor::definition (la liste des champs sous forme d'octets External Term Format, pour records:get_definition/2) et ModuleDescriptor::features (emplacements d'atomes des fonctionnalités activées, pour erl_features:used/1)
25Littéraux encodés du module et tas de littéraux : ModuleDescriptor::literals contient les octets encodés (literal_bytes), literal_cache, literal_count ; CLAUSE_literal_v1