Pular para o conteúdo

HIR — a árvore sem açúcar

O que é açúcar de escrita na linguagem, o que só parece, e a prova de que abrir um não muda o resultado.

A árvore que o parser entrega tem cento e quarenta formas de nó. Parte delas é conveniência de escrita: orif é um given deitado, soma += 1 é soma := soma + 1. Cada forma é um caso a mais em todo analisador que percorre a árvore — e é assim que uma análise fica certa num caminho e errada no outro sem ninguém ver.

O HIR é a mesma árvore com menos formas. Cinco açúcares são abertos:

NomeO que abre
orif-aninhadoa corrente de orif vira given/otherwise aninhado
composta-simplesx += 1 vira x := x + 1 quando o alvo é um nome
pertence-negadox not in xs vira not (x in xs)
perform-para-persistperform … persist c vira a primeira volta mais o laço
sinal-de-literal-5, hoje um UnaryOp sobre 5, vira o literal -5
dataforge
adopt Arcane.Compilador as K

fonte := "soma := 0\ncycle i in [1, 2, 3]:\n    soma += i\nout -3 not in [1, 2]\n"

assert K.acucares(fonte) is {"composta-simples": 1, "pertence-negado": 1,
                             "sinal-de-literal": 1}

// e no HIR o 'orif' deixou de existir: virou um 'given' dentro do 'otherwise'
com_orif := "given a:\n    out 1\norif b:\n    out 2\n"
de_fora := K.hir(com_orif)["corpo"][0]

assert de_fora["tipo"] is "GivenBlock"
assert de_fora["otherwise_body"][0]["tipo"] is "GivenBlock"
assert len(de_fora["orif_blocks"]) is 0

A prova não é a forma da árvore: é a saída#

Um desaçucaramento errado não levanta erro — ele muda o resultado. Por isso a garantia é medida do único jeito que vale: exercícios do repositório rodam nas duas formas, e a saída é comparada caractere por caractere.

O perform, e o que ele vira
n := 0
perform:
    n += 1
persist n smaller 4
assert n is 4
O HIR equivalente, escrito à mão
m := yes                    // a marca garante a primeira volta
n := 0
persist m or n smaller 4:   // 'or' curto-circuita: na 1a volta 'n' nao e lido
    m := no
    n += 1
assert n is 4

O que só PARECE açúcar#

Esta lista vale mais que a de cima: é o que impede alguém de "simplificar" a árvore e mudar a linguagem sem notar. Cada entrada tem o motivo ao lado, e há teste cobrando o motivo.

FormaPor que não é açúcar
cycle i from 0 to 3range materializa a lista: um laço de um milhão de voltas viraria uma lista de um milhão de itens, e o laço de contador existe justamente para não pagar isso
a given c otherwise bo ternário é expressão e o given é instrução; trocar um pelo outro exigiria uma temporária, que muda o escopo
a ?? ba forma com ternário avaliaria a duas vezes, e o lado esquerdo de um ?? costuma ser uma chamada
x?.yo mesmo: f()?.campo chamaria f duas vezes
$"{x:.2f}"o formato depois dos dois-pontos não existe como operador na linguagem
[e cycle x in xs]compreensão é expressão com escopo próprio; virar laço exigiria instrução, e o valor teria de sair por uma variável
mark @fg := f(g) seria errado: um decorador que devolve void não substitui o alvo, e é isso que deixa @Rota("/x") só anotar
xs >> morph …os estágios são preguiçosos e os verbos de quadro pedem o quadro, não a lista

Resolução de nomes#

A outra metade do HIR: de onde vem cada nome que um corpo menciona — parâmetro, local, livre (vem de fora) ou embutido.

dataforge
adopt Arcane.Compilador as K

fonte := "fora := 10\naction somar(a, b):\n    local := a + b\n    yield local + fora + sqrt(4)\n"

somar := [c cycle c in K.resolucao(fonte) given c["nome"] is "somar"][0]
assert somar["parametros"] is ["a", "b"]
assert somar["locais"] is ["local"]
assert somar["livres"] is ["fora"]
assert somar["embutidos"] is ["sqrt"]

Um nome livre é o que a ação lê e não cria. É a mesma pergunta que a travessia de processo faz para saber o que levar para outro núcleo, e a que o aviso de escrita concorrente faz para saber o que uma thread alcança.