Pular para o conteúdo

Macros: a árvore como dado

Ler o corpo de uma ação, percorrer, transformar, gerar código e derivar métodos — com higiene explícita.

Um decorador troca o valor: recebe a ação pronta e devolve outra coisa. O que ele não alcança é o corpo — e metade da metaprogramação que vale a pena é sobre o corpo: instrumentar cada instrução, derivar um método a partir dos campos, gerar uma ação a partir de um esquema.

A árvore é um vault#

dataforge
adopt Arcane.Macro as M

action somar(a, b):
    yield a + b

arvore := M.arvore(somar)

assert arvore["tipo"] is "ActionDeclaration"
assert arvore["parametros"] is ["a", "b"]
assert arvore["corpo"][0]["tipo"] is "YieldStatement" 

Ela é um dado comum: percorre com cycle, casa com match, serializa em JSON e atravessa processo — sem que nada disso precise conhecer a classe do nó. É o mesmo raciocínio do Quadro e da ponte para o Python.

citar: texto vira árvore#

dataforge
adopt Arcane.Macro as M

arvore := M.citar("x * 2 + 1")
assert arvore["tipo"] is "BinaryOp"
assert M.texto(arvore) is "x * 2 + 1" 

Reescrever o corpo#

dataforge
adopt Arcane.Macro as M

action dobro(x):
    yield x * 2

action trocar(nodo):
    given nodo["tipo"] is "BinaryOp" and nodo["op"] is "*":
        yield M.citar("x + x")
    yield nodo

trocada := M.reescrever(dobro, trocar)

assert dobro(5) is 10
assert trocada(5) is 10        // o corpo é outro, o resultado é o mesmo

A árvore original não é tocada: transformar devolve uma nova. Uma macro que mutasse o que recebeu mudaria a ação de quem chamou.

Gerar código#

dataforge
adopt Arcane.Macro as M

corpo := M.citar("a * 10")
gerada := M.acao("dez_vezes", ["a"], corpo)

assert gerada(4) is 40
assert M.arvore(gerada)["nome"] is "dez_vezes"

// e o atalho, num passo só
pronta := M.compilar("a + b", "soma", ["a", "b"])
assert pronta(2, 3) is 5

Higiene: não capturar o nome de quem chamou#

A macro que gera um temporário chamado temp quebra o código de quem já tinha um temp. nome_fresco e renomear existem para isso, e são explícitos: fazer higiene sozinho exigiria saber o que é "de dentro", e essa decisão é de quem escreve a macro.

dataforge
adopt Arcane.Macro as M

corpo := M.citar("temporario + 1")
limpo := M.renomear(corpo, "temporario", M.nome_fresco("temporario"))

assert M.texto(limpo) isnt M.texto(corpo)
assert "__temporario_" in M.texto(limpo)

Macro de atributo: derivar#

Em vez de escrever __str__ e __eq__ à mão em cada blueprint, os campos que já existem geram os dois:

dataforge
adopt Arcane.Macro as M

mark @M.derivar("texto", "igualdade")
blueprint Ponto:
    x := 1
    y := 2

a := spawn Ponto()
b := spawn Ponto()

assert $"{a}" is "Ponto(x=1, y=2)"
assert a is b
DerivávelGera
texto__str__ com os campos, no formato Nome(campo=valor, …)
igualdade__eq__ comparando todos os campos
ordem__lt__ pelo primeiro campo
vaultpara_vault() com os campos

A superfície#

SímboloO que faz
M.arvore(acao)a árvore da ação (ou do blueprint) como dado
M.citar(texto)texto vira árvore
M.texto(arvore)árvore vira texto (aproximado)
M.percorrer(arvore, visitante)chama o visitante em cada nó
M.transformar(arvore, acao)árvore nova, com cada nó passado pela ação
M.substituir · M.renomeartroca por tipo de nó, ou nome de identificador
M.nome_fresco(base)um nome que o código de fora não tem
M.acao(nome, params, corpo) · M.compilar(texto, …)gera a ação
M.reescrever(acao, transformador)ação nova, corpo transformado
M.derivar(…)a macro de atributo