ATUALIZAÇÃO: Esta pergunta foi o assunto do meu blog em 8 de junho de 2012 . Obrigado pela ótima pergunta!
Ótima pergunta. Debatemos os problemas que você levanta por muito, muito tempo.
Gostaríamos de ter uma estrutura de dados com as seguintes características:
- Imutável.
- A forma de uma árvore.
- Acesso barato aos nós pai dos nós filhos.
- É possível mapear de um nó na árvore para um deslocamento de caractere no texto.
- Persistente .
Por persistência, quero dizer a capacidade de reutilizar a maioria dos nós existentes na árvore quando uma edição é feita no buffer de texto. Como os nós são imutáveis, não há barreira para reutilizá-los. Precisamos disso para desempenho; não podemos analisar novamente enormes wodges do arquivo toda vez que você pressiona uma tecla. Precisamos reexprimir e analisar novamente apenas as partes da árvore que foram afetadas pela edição.
Agora, quando você tenta colocar todas essas cinco coisas em uma estrutura de dados, você imediatamente encontra problemas:
- Como você constrói um nó em primeiro lugar? O pai e o filho se referem um ao outro e são imutáveis, então qual deles é construído primeiro?
- Supondo que você consiga resolver esse problema: como você o mantém persistente? Você não pode reutilizar um nó filho em um pai diferente, porque isso envolveria dizer ao filho que ele tem um novo pai. Mas a criança é imutável.
- Supondo que você consiga resolver esse problema: quando você insere um novo caractere no buffer de edição, a posição absoluta de cada nó mapeado para uma posição após esse ponto muda. Isso torna muito difícil criar uma estrutura de dados persistente, porque qualquer edição pode alterar os intervalos da maioria dos nós!
Mas na equipe de Roslyn, rotineiramente fazemos coisas impossíveis. Na verdade, fazemos o impossível mantendo duas árvores de análise. A árvore "verde" é imutável, persistente, não possui referências pai, é construída "de baixo para cima" e cada nó rastreia sua largura, mas não sua posição absoluta . Quando uma edição acontece, reconstruímos apenas as partes da árvore verde que foram afetadas pela edição, que geralmente são sobre O (log n) do total de nós de análise na árvore.
A árvore "vermelha" é uma fachada imutável construída ao redor da árvore verde; é construído "de cima para baixo" sob demanda e jogado fora em todas as edições. Ele calcula as referências dos pais, fabricando-as sob demanda conforme você desce pela árvore a partir do topo . Ele fabrica posições absolutas calculando-as a partir das larguras, novamente, à medida que você desce.
Você, o usuário, apenas vê a árvore vermelha; a árvore verde é um detalhe de implementação. Se você observar o estado interno de um nó de análise, verá que há uma referência a outro nó de análise de um tipo diferente; esse é o nó da árvore verde.
Aliás, essas são chamadas de "árvores vermelhas / verdes" porque essas eram as cores dos marcadores do quadro branco que usamos para desenhar a estrutura de dados na reunião de design. Não há outro significado para as cores.
O benefício dessa estratégia é que obtemos todas essas grandes coisas: imutabilidade, persistência, referências aos pais e assim por diante. O custo é que esse sistema é complexo e pode consumir muita memória se as fachadas "vermelhas" ficarem grandes. No momento, estamos fazendo experimentos para ver se podemos reduzir alguns dos custos sem perder os benefícios.