Memória
A redefinição: a memória é colorless, e o gerenciador viaja com o objeto, não com o contexto. E não há borrow checker.
O conceito
Seção intitulada “O conceito”Toda linguagem de sistemas precisa responder duas perguntas: de onde vêm os bytes e quem os
libera. Rust responde com ownership + lifetimes + borrow checker (e &T/&mut T/*const T/*mut T
como tipos distintos). Go responde com um GC e ponto. Zig responde com allocators passados como
parâmetro. Cada resposta colore o código de um jeito.
Como Makoto redefine
Seção intitulada “Como Makoto redefine”Cada valor alocado no heap carrega um ponteiro para o seu próprio MemoryManager (@mm). Quando
o objeto cresce ou realoca, ele usa o @mm dele, não o da função que o está manipulando. A função
muta um Buffer sem saber nem se importar se ele é arena-backed, GC ou manual:
@mm(gc)fn append_log(buf: mut Buffer, line: string) { buf.push(line) // se push realocar, usa o @mm do buf (a arena), NÃO o @mm(gc) da função}var buf: Buffer @mm(arena) = Buffer.new()append_log(mut buf, "erro") // muta in-place na arena; o GC nem encostaIsto é o “colorless” em ação: o @mm(gc) de uma função governa só as alocações novas que ela
origina, nunca a estratégia dos objetos que chegam até ela. A estratégia viaja com o dado.
Por baixo, duas camadas ortogonais: MemoryManager (a estratégia, gc/arena/none/c,
“como gerencio”) sobre MemorySource (de onde vêm as páginas brutas, mmap/VirtualAlloc/
memory.grow do WASM/buffer fixo bare-metal, “de onde vem a RAM”). Qualquer combinação serve:
GC-sobre-WASM, arena-sobre-nativo. É o mesmo desacoplamento do colorless, descido um nível.
O raciocínio
Seção intitulada “O raciocínio”Por que dá para abandonar o borrow checker? Porque o motivo dele desaparece dentro de um processo. O borrow checker existe para impedir aliasing mutável concorrente, dois caminhos mutando o mesmo dado ao mesmo tempo. Mas em Makoto o processo é a unidade de concorrência, com uma única linha de execução; o scheduler só o suspende em pontos explícitos (IO, send/recv), e nenhum outro processo toca o heap dele (shared-nothing, cap. 04). Sem aliasing concorrente, a justificativa principal do borrow checker some. Restam só dois perigos, e cada um tem uma defesa local:
- Aliasing bug (mutar uma struct enquanto se segura outra referência a ela): resolvido por
semântica de valor por default +
mutpara mutação.muté modo de passagem, não tipo (como oinoutdo Swift): exclusivo, write-back, não escapa (morre no fim da chamada). A checagem é puramente local (“este lugar já estámutaqui?”), sem lifetimes. - Use-after-free: só existe sob
@mm(none)/arena, território explicitamenteunsafe. Sob o GC default, um ponteiro*Tvivo é uma referência que o trace enxerga, então o alvo fica vivo, seguro por construção.
A chave que fecha o modelo: ponteiro nasce de identidade persistente, seja uma alocação no heap (que
já carrega seu @mm), um campo de estrutura ou FFI. Você não consegue pegar *T de um lvalue de
stack local; para mutar um int na stack, só há mut. Logo, dangling de stack é impossível: o & de
um local não te dá um ponteiro armazenável.
Por que não as alternativas
Seção intitulada “Por que não as alternativas”- Lifetimes / borrow checker (Rust). O
&mut Tempacota três coisas ao mesmo tempo (pode aliasar, é exclusivo, tem lifetime), e é isso que obriga o borrow checker a existir. Makoto desmonta o pacote:muté o “exclusivo + write-back” sem o “pode aliasar”;*Té o “pode aliasar” sem o resto. Separados, nenhum dos dois exige rastreamento global. - Ponteiros nullable por default (C/Go). Tornariam o
Optional[*T]redundante e reabririam a ambiguidade do null. Em Makoto o ponteiro é sempre válido; ausência éOptional[*T], explícita. - Allocator como parâmetro (Zig). Quebra o colorless: a função passaria a saber qual memória usa,
e passar um buffer de arena para uma função GC viraria um descompasso. Com o
@mmviajando no objeto, isso é seguro e invisível. - Smart pointers /
Cell/RefCell(Rust). Embarcariam no core um maquinário que contradiz “não embarcar o que não se usa”. As interfaces de@mmcobrem o mesmo terreno mais limpo. - Um marcador
@copyexplícito. Descartado: cópia simples é o default não-marcado, e marcar o caso comum violaria o opt-in (você pagaria sintaxe pelo que mais acontece). O que ganha marca é o contrário: mover (@transfer, invalida a origem) e copiar-para-outro-MM (@promote(mm), corta o cordão com a arena de origem).
Houve um medo real e nomeado: o de que mut morresse para o atalho do ponteiro (como o ? virou
escape-hatch universal em Rust/Zig). A resposta de design não foi tornar mut mais atraente. Foi
fazer ponteiro não ser substituto: ele resolve problema diferente (referência persistente), nasce
só de heap-identity (não dá para “preferir” no caso comum), e cobra deref explícito (*p) em cada
acesso, então quem pegou um ponteiro “para não digitar mut” digita * o dia inteiro.
A dor concreta
Seção intitulada “A dor concreta”O programador de Rust que escreve uma função de mutação simples e precisa anotar lifetimes que não têm
nada a ver com o problema. O de Zig que precisa threadar allocator: *Allocator por dez camadas de
chamada. O de Go que não tem escolha de estratégia quando o GC não serve. Cada dor é uma cor de
memória que vazou para onde não devia.
O modelo mental
Seção intitulada “O modelo mental”O objeto carrega sua estratégia de memória. Você muta o valor; o runtime usa o
@mmdele para qualquer crescimento. Sem cor, sem lifetime.
A fricção aceita
Seção intitulada “A fricção aceita”Sob @mm(none)/arena, use-after-free volta a ser sua responsabilidade, e o compilador não cobre
(é o preço explícito de descer ao metal, e fica em ilha unsafe). Ponteiros não-anuláveis forçam
Optional[*T] onde C teria um simples nullable. E o deref *p obrigatório é ruído em código
legítimo de ponteiro, aceito de propósito, porque é justamente o que derrota a preguiça.
Próximo: 03 · Async e IO