Clause
← Toda a documentação

O original em inglês mudou desde esta tradução, pelo que partes dela podem estar desatualizadas. Ler em inglês

Traduzido do original em inglês · 1e3a39c8-dirty · 2026-10-26

Modelo de execução: frames e continuações

Como as funções Erlang geradas chamam, retornam, se suspendem e falham. O modelo foi escolhido após a comparação de protótipos em alternativas comparadas; o lado do código gerado da sua ABI está em frames e transferências.

Decisão

Cada processo Erlang corre na sua própria pilha plana de frames explícitos, e o código gerado só passa de uma função para outra através de transferências de cauda garantidas (musttail do LLVM). Uma chamada que não é de cauda guarda a continuação do chamador no frame do chamador e salta para a função chamada; um retorno salta de volta para essa continuação. A pilha nativa fica, portanto, com uma única chamada de profundidade acima do escalonador, seja qual for a profundidade da recursão Erlang, e um processo pode parar em qualquer transferência e retomar mais tarde em qualquer thread.

Estado do processo

abi::v1::FrameState está no início de cada contexto, pelo que o código gerado o lê e atualiza inline:

CampoSignificado
stack, top, limitO bloco da pilha (desloca-se quando cresce), a sua primeira palavra livre e as palavras que um push pode alcançar sem crescer nem ultrapassar o limite da pilha
frameDeslocamento, em palavras, do cabeçalho do frame atual
registers, liveOs 256 registos de argumentos/resultados; as primeiras live palavras são raízes numa transferência
reductionsEntradas de funções que restam na fatia de tempo
slow, failedBits que enviam as transferências para os serviços (slow_collect, slow_profile); diferente de zero enquanto o canal de falhas contém uma falha

heap_top/heap_limit, runtime e self servem a alocação inline, os carregamentos de átomos e self/0 (frames e transferências). Um processo suspenso regista a entrada na qual continua em ProcessStack::resume_.

Frames

Um frame é um cabeçalho de 4 palavras seguido dos slots da função (registos Y do BEAM): slots de termos, que são raízes, e depois slots em bruto para valores nativos guardados (spilled).

Palavra do cabeçalhoSignificado
previousDeslocamento do cabeçalho do chamador (os frames ligam-se por deslocamentos, nunca por apontadores)
functionO FrameDescriptor da função
resumeÍndice de continuação sobre o qual o corpo faz o switch quando o controlo regressa aqui
handlerÍndice de handler; sempre 0, já que as falhas retornam através dos chamadores

O descritor <symbol>.frame indica o módulo, a função e a aridade e contém o apontador de código do corpo e o número de slots (abi). As palavras do cabeçalho não são termos; quem percorre a pilha segue previous e lê o número de slots nos descritores. Um frame de fundo pertencente ao runtime fica por baixo da primeira chamada de cada processo e de cada invocação pelo anfitrião: retornar para ele guarda o resultado no primeiro registo e termina a chamada nativa.

Operações

Cada chamada faz push de um frame; as funções folha não o dispensam, pelo que cada função mantém a sua entrada no stack trace.

Visibilidade das raízes

Em cada transferência e em cada safepoint, as raízes de um processo são: os slots de termos de todos os frames da sua pilha, x[0..live), e o payload, a lista de argumentos e o termo de pilha do canal de falhas (raízes).

Os valores nunca sobrevivem a uma transferência em registos nativos ou em valores SSA, exceto os parâmetros da própria transferência. Cada corpo recarrega o endereço do seu frame a partir de stack + frame depois de entrar, e lê os valores vivos dos slots. Dentro de uma continuação, um serviço que possa fazer push de um frame ou deslocar o heap invalida todos os apontadores de slots e de heap mantidos em valores SSA; a regra de recarregamento para as recolhas está fixada em regra de recarregamento.

O bloco da pilha cresce duplicando e deslocando-se enquanto o limite global do runtime o permitir, nunca encolhe durante a execução e é separado do bloco de heap. Não tem limite de número de frames; as palavras da pilha contam para o limite de memória do runtime.

Alvos

musttail com uma única assinatura de código uniforme é aceite por todos os alvos exigidos em O0 e O2 (protótipo abaixo, clang 23.1.2):

AlvoPalavraResultado
x86_64-pc-windows-msvc, x86_64-unknown-linux-gnu64saltos de cauda
i686-pc-windows-msvc, i686-unknown-linux-gnu32saltos de cauda
aarch64-unknown-linux-gnu, arm64-apple-macosx14.064saltos de cauda
armv7-unknown-linux-gnueabihf32saltos de cauda

O backend indica um erro quando não consegue honrar uma chamada musttail, pelo que uma compilação bem-sucedida é a garantia. Alternativa para um alvo futuro que a rejeite: um trampolim, em que cada código devolve o apontador de código seguinte ao ciclo do escalonador em vez de saltar; os frames, as raízes e os índices de continuação ficam inalterados. Nenhum alvo exigido precisa dele.

Implementação

Alternativas comparadas

tests/prototypes/execution_model converte à mão as mesmas funções Erlang (sum/1 com recursão no corpo, loop/2 com recursão de cauda, fail/1 a lançar à profundidade N, catcher/1 a capturá-lo) de três formas e compila-as para todos os alvos acima em O0 e O2 (python tests/prototypes/execution_model/run.py). Em Windows x64 com O2, o modelo escolhido executou uma recursão no corpo com 1M de profundidade em 17 ms com 5 palavras por frame e 10M chamadas de cauda em pilha constante; a sua variante com trampolim passou as mesmas execuções. Rejeitadas:

Custos aceites: um salto indireto por retorno mais um despacho por switch; os valores vivos através das chamadas são recarregados dos slots; os backtraces dos depuradores nativos mostram apenas a função atual, enquanto os stack traces Erlang provêm da cadeia de frames (depuração).