Resumo
Num programa com um while top-level muito grande no __rtsn_main (~350 statements: dezenas de locais por iteração, chamadas a app.*/egui.*/input.*, métodos de singleton, picking aninhado, seção de UI enorme), a parte final do corpo do loop deixa de executar silenciosamente. No caso concreto (editor do rts-game, main.ts), a seção de render (setCam/drawGPU) nunca rodava → objetos 3D invisíveis, enquanto o resto do frame (ws poll, scene.update, animação) rodava normal.
Assinatura de corrupção de codegen sensível a layout: adicionar/remover statements inócuos (ex.: um S.markA = N;) muda se a seção de render roda ou não. Um contador escrito no fim do loop fica em 0 (nunca setado) mesmo com o loop iterando milhares de vezes.
Contorno (funciona 100%)
Envolver o corpo do frame numa função e chamá-la do loop:
// ANTES (bug): render não roda
while (app.running()) {
const goOn = app.beginFrame();
if (!goOn) break;
/* ...350 statements... render... */ // <- seção final não executa
app.endFrame();
}
// DEPOIS (ok): render roda sempre
function frame(): void { /* ...350 statements... render...*/ app.endFrame(); }
while (app.running()) {
if (!app.beginFrame()) break;
frame();
}
Em função o layout de codegen é diferente e o corpo roda inteiro, de forma estável. (rts-game commit 16d86b2.)
Impacto
Silencioso e perigoso: nenhum panic, nenhum erro de compilação, rts ir compila limpo. Statements simplesmente não rodam. Difícil de diagnosticar (só apareceu porque instrumentamos um contador lido por fora, via WebSocket).
O que já foi tentado / descartado
- Teoria "método de singleton importado não despacha no top-level do
while" → FALSA. Repro mínimo (singleton importado com array, scene.update()/computeWorld() + iteração no top-level do while) roda correto. Métodos/campos de singleton importado despacham OK tanto em função quanto no top-level.
- Repro pequeno denso (loop top-level de 200 iterações com ~30 locais +
scene.update/computeWorld + iteração) → passa (200/200). Não reproduz.
- Repro GUI (
createAppAt + app.beginFrame + scene + ~15 locais, 40 frames) → passa. Não reproduz.
- Fix de aliasing de gcell (PR/commit
af57caae, block_depth no lower_let): consertou um bug REAL e separado (um let/const aninhado em loop no __rtsn_main colidindo de nome com um global promovido escrevia na mesma gcell em vez de shadowar). NÃO resolve este heisenbug — o main.ts continua com render=0 sem o function-wrap mesmo depois desse fix.
Ou seja: o bug só reproduz na escala/complexidade completa do __rtsn_main real — é um efeito de limiar (contagem de basic-blocks / Variables / slots numa única função). Cada chamada emite emit_post_call_error_check (2 blocks), então um loop com dezenas de chamadas gera centenas–milhares de blocks num só __rtsn_main.
Investigação de codegen (feita)
Ponteiros levantados (arquivos em crates/rts-codegen-new/src/front/run/):
lower.rs — lower_function (383+), flag is_main (func.name == "__rtsn_main"), lower_block (bump de block_depth).
lower.rs:311-321 — nota de que reusar resultado de call efetivo entre blocks já fez Cranelift panicar antes (caching de immutable-gcell como Variable def'd uma vez na entry).
trycatch.rs:94-118 — emit_post_call_error_check cria 2 blocks por chamada.
- Reads de gcell (
Tagged, sem shape/class) num __rtsn_main com milhares de blocks — vale confirmar se a construção de φ (SSA) do Cranelift permanece correta nessa escala.
Hipótese
Overflow de algum limite implícito (basic-blocks / Variables / stack-slots) numa única função __rtsn_main, OU um problema de SSA/φ-construção do Cranelift no __rtsn_main gigante — que a função-wrap evita por dividir o mesmo código em duas funções menores.
Repro
Executar o editor rts-game/main.ts sem a função-wrap (corpo inline no while): a seção de render não roda (validável via a porta de controle WebSocket embutida: drawnLast=0 mesmo com wouldDraw=8). Com a função-wrap: drawnLast=8 estável.
Resumo
Num programa com um
whiletop-level muito grande no__rtsn_main(~350 statements: dezenas de locais por iteração, chamadas aapp.*/egui.*/input.*, métodos de singleton, picking aninhado, seção de UI enorme), a parte final do corpo do loop deixa de executar silenciosamente. No caso concreto (editor dorts-game,main.ts), a seção de render (setCam/drawGPU) nunca rodava → objetos 3D invisíveis, enquanto o resto do frame (ws poll,scene.update, animação) rodava normal.Assinatura de corrupção de codegen sensível a layout: adicionar/remover statements inócuos (ex.: um
S.markA = N;) muda se a seção de render roda ou não. Um contador escrito no fim do loop fica em0(nunca setado) mesmo com o loop iterando milhares de vezes.Contorno (funciona 100%)
Envolver o corpo do frame numa função e chamá-la do loop:
Em função o layout de codegen é diferente e o corpo roda inteiro, de forma estável. (rts-game commit
16d86b2.)Impacto
Silencioso e perigoso: nenhum panic, nenhum erro de compilação,
rts ircompila limpo. Statements simplesmente não rodam. Difícil de diagnosticar (só apareceu porque instrumentamos um contador lido por fora, via WebSocket).O que já foi tentado / descartado
while" → FALSA. Repro mínimo (singleton importado com array,scene.update()/computeWorld()+ iteração no top-level dowhile) roda correto. Métodos/campos de singleton importado despacham OK tanto em função quanto no top-level.scene.update/computeWorld+ iteração) → passa (200/200). Não reproduz.createAppAt+app.beginFrame+ scene + ~15 locais, 40 frames) → passa. Não reproduz.af57caae,block_depthnolower_let): consertou um bug REAL e separado (umlet/constaninhado em loop no__rtsn_maincolidindo de nome com um global promovido escrevia na mesma gcell em vez de shadowar). NÃO resolve este heisenbug — omain.tscontinua com render=0 sem o function-wrap mesmo depois desse fix.Ou seja: o bug só reproduz na escala/complexidade completa do
__rtsn_mainreal — é um efeito de limiar (contagem de basic-blocks / Variables / slots numa única função). Cada chamada emiteemit_post_call_error_check(2 blocks), então um loop com dezenas de chamadas gera centenas–milhares de blocks num só__rtsn_main.Investigação de codegen (feita)
Ponteiros levantados (arquivos em
crates/rts-codegen-new/src/front/run/):lower.rs—lower_function(383+), flagis_main(func.name == "__rtsn_main"),lower_block(bump deblock_depth).lower.rs:311-321— nota de que reusar resultado de call efetivo entre blocks já fez Cranelift panicar antes (caching de immutable-gcell comoVariabledef'd uma vez na entry).trycatch.rs:94-118—emit_post_call_error_checkcria 2 blocks por chamada.Tagged, sem shape/class) num__rtsn_maincom milhares de blocks — vale confirmar se a construção de φ (SSA) do Cranelift permanece correta nessa escala.Hipótese
Overflow de algum limite implícito (basic-blocks /
Variables / stack-slots) numa única função__rtsn_main, OU um problema de SSA/φ-construção do Cranelift no__rtsn_maingigante — que a função-wrap evita por dividir o mesmo código em duas funções menores.Repro
Executar o editor
rts-game/main.tssem a função-wrap (corpo inline nowhile): a seção de render não roda (validável via a porta de controle WebSocket embutida:drawnLast=0mesmo comwouldDraw=8). Com a função-wrap:drawnLast=8estável.