Pular para o conteúdo

46 · Partida, pilha e capacidade

1 exercícios: .

bash
python3 exercicios/run_all.py 46

Os exercícios#

#TítuloEnunciado
264a partida, a pilha e a fronteira de capacidade

264 · a partida, a pilha e a fronteira de capacidade#

exercicios/46-partida/264_partida_pilha_e_capacidade.df
// Um programa nao comeca na primeira linha: antes dela o comptime
// rodou, os adopt carregaram e as declaracoes foram icadas. Nada disso
// era VISIVEL — e "por que a partida demora 400 ms?" nao tinha como ser
// respondido sem cronometrar a mao.

adopt Arcane.Inicio as I
adopt Arcane.Capacidade as Cap
adopt Arcane.Math as M

// tres modulos adotados: e o que a cronometragem da partida vai contar
assert M.sqrt(9) is 3.0

// ── as sete fases, nomeadas e em ordem ──
fases := I.fases()
assert [f["fase"] cycle f in fases] is
       ["lexer", "parser", "comptime", "hoisting", "adopt", "programa", "defer"]

// cada uma tem o que faz escrito ao lado — uma lista de nomes sem
// explicacao apodrece
assert len(fases[0]["o_que"]) bigger 20

// ── onde a partida foi gasta ──
// Sao dois floats por import, num lugar que roda UMA VEZ. Liga-la por
// opcao faria a medida existir so para quem ja desconfiava.
registro := I.adocoes()
assert len(registro) bigger_eq 3
assert "modulo" in registro[0] and "ms" in registro[0]

r := I.relatorio()
assert r["adocoes"] bigger_eq 3
assert r["total_ms"] bigger_eq 0.0
assert r["mais_caro"]["modulo"] isnt ""

// ── a pilha: ela tinha limite e nao tinha como ser perguntada ──
action folha():
    yield I.pilha()

action tronco():
    yield folha()

p := tronco()
assert p["profundidade"] bigger_eq 2
assert p["limite"] bigger 100
assert p["restante"] is p["limite"] - p["profundidade"]

// os quadros trazem quem chamou quem
action quem():
    yield [q["acao"] cycle q in I.quadros()]

nomes := quem()
assert "quem" in nomes

// o teto se ajusta, e BAIXA-LO vale de verdade
antes := I.limite_da_pilha()
I.limite_da_pilha(300)
assert I.pilha()["limite"] is 300
I.limite_da_pilha(antes)
assert I.pilha()["limite"] is antes

// subir demais e RECUSADO: cada chamada da linguagem gasta varios
// quadros do CPython, e um teto alto demais troca uma mensagem clara
// por um RecursionError cru que nao fala desta linguagem
monitor:
    I.limite_da_pilha(10000000)
    assert no
handle RuntimeError as e:
    assert "Python" in e.message or "recursion" in e.message

// ── uma variavel por thread ──
// 'inicial' e uma ACAO que constroi o valor, e nao o valor: um valor
// seria compartilhado por todas as threads, que e exatamente o que a
// variavel por thread existe para evitar.
contador := I.local(lambda => {"n": 0})
caixa := I.meu(contador)
caixa["n"] := caixa["n"] + 1
assert I.meu(contador)["n"] is 1
assert I.threads_com_valor(contador) bigger_eq 1

// limpar esquece, e o proximo 'meu' nasce de novo
I.limpar(contador)
assert I.meu(contador)["n"] is 0

// passar um VALOR em vez de uma acao e recusado, com o motivo
monitor:
    I.local({"n": 0})
    assert no
handle RuntimeError as e:
    assert "thread" in e.message

// ── a fronteira de capacidade ──
// O comptime ja recusava out, adopt e thread: e uma fronteira escrita a
// mao para um caso so. E o --plugin= roda um .df arbitrario do projeto
// com TODOS os poderes — uma regra de lint que pode abrir soquete.

// o que e puro roda sem permissao nenhuma
action formula():
    adopt Arcane.Math as Mat
    yield Mat.sqrt(16) + len("abc")

assert Cap.executar(formula, []) is 7.0

// o adopt ambiente e recusado pelo NOME da capacidade que falta
action ler_disco():
    adopt Arcane.IO as IO
    yield IO.exists(".")

monitor:
    Cap.executar(ler_disco, [])
    assert no
handle Error as e:
    assert "arquivos" in e.message
    assert "Arcane.IO" in e.message

// com a permissao, o MESMO codigo passa
assert Cap.executar(ler_disco, ["arquivos"])

// a ponte para o Python e capacidade PROPRIA, e nunca vem junto: ela
// alcanca tudo o que o Python alcanca, e deixa-la com outra faria o
// resto da lista virar enfeite
action escapar():
    adopt Python.os as so
    yield so.getcwd()

monitor:
    Cap.executar(escapar, ["arquivos", "rede", "processo", "banco",
                             "threads", "nativo", "ambiente"])
    assert no
handle Error as e:
    assert "python" in e.message

// uma capacidade inventada e recusada COM A LISTA: um erro de digitacao
// concederia silenciosamente nada, e o cofre pareceria mais aberto
monitor:
    Cap.executar(formula, ["superpoderes"])
    assert no
handle Error as e:
    assert "superpoderes" in e.message

// descobrir de que uma acao precisa, sem adivinhar
action tenta():
    monitor:
        adopt Arcane.OS as OS
        yield OS.name()
    handle Error:
        yield "negado"

visto := Cap.observar(tenta, [])
assert visto["resultado"] is "negado"
assert visto["negados"][0]["capacidade"] is "processo"

assert Cap.exige("Arcane.OS") is "processo"
assert Cap.exige("Arcane.Math") is void      // inofensivo
assert len(Cap.modulos_de("rede")) bigger 5

// ── o limite HONESTO, e ele e o modelo ──
// Capacidade e o que o codigo ALCANCA, nao o que lhe foi entregue. Um
// cofre que prometesse o contrario mentiria.
adopt Arcane.IO as IO

action com_o_que_recebeu(ferramenta):
    yield ferramenta.exists(".")

assert Cap.executar(com_o_que_recebeu, [], [IO])

// e o modulo diz na cara o que NAO e — se a doc prometesse contencao,
// alguem o usaria onde nao pode, e descobriria por incidente
limites := Cap.limites()
assert len(limites) bigger_eq 3
assert "hostil" in join(" ", limites)

// a fronteira se desfaz SEMPRE, inclusive quando o corpo falha
action quebra():
    yield 1 / 0        // df: permitir division-by-zero

monitor:
    Cap.executar(quebra, [])
handle Error:
    void

// fora da fronteira, o adopt volta a funcionar
adopt Arcane.IO as IO2
assert IO2.exists(".")

out "264 ok"

Um programa não começa na primeira linha. Antes dela o comptime rodou numa caixa sem E/S, os adopt carregaram módulos e as declarações de topo foram içadas.

Nada disso era visível — e "por que a partida demora 400 ms?" não tinha como ser respondido sem cronometrar à mão.

As sete fases#

text
lexer → parser → comptime → hoisting → adopt → programa → defer

Cada uma tem o que faz escrito ao lado: uma lista de nomes sem explicação apodrece, e há teste cobrando o tamanho da descrição.

E cada adopt é cronometrado. São dois floats por import, num lugar que roda uma vez — ligar a medida por opção faria ela existir só para quem já desconfiava, e a pergunta aparece justamente quando ninguém desconfiava.

A pilha#

O teto é mil quadros, e recursão legítima o atinge: uma travessia de árvore de cinco mil nós não tem nada de infinita. Quem a escreve precisa saber de quanto é o teto antes de bater nele.

I.pilha() dá profundidade, limite e quanto falta; I.quadros() dá quem chamou quem.

Subir o teto é recusado além do que o Python aguenta: cada chamada desta linguagem gasta vários quadros do CPython, e um teto alto demais troca uma mensagem clara ("a recursão passou de mil quadros, e aqui estão as duas saídas") por um RecursionError cru — que não fala desta linguagem e não diz o que fazer.

As duas saídas continuam sendo as certas: yield f(…) como retorno inteiro vira salto e não tem teto, ou um cycle com pilha explícita.

Uma variável por thread#

Um armazém por thread resolve metade do problema. A outra metade é o que costuma faltar: a inicialização declarada num lugar só, e o finalizador quando a thread acaba — sem ele, uma conexão aberta por thread fica aberta depois que ela morre, e o sintoma aparece no servidor, não no código.

`inicial` é uma ação, não um valor. Um valor seria compartilhado por todas as threads — que é exatamente o que a variável por thread existe para evitar. Passar um valor é recusado, com esse motivo na mensagem.

E o finalizador roda quando a thread é coletada, não no instante em que ela termina: é o que o Python garante, e prometer precisão maior seria prometer um gancho que não existe. Para liberar num instante exato, Arcane.Posse.

A fronteira de capacidade#

O comptime já recusava out, adopt e thread: é uma fronteira de capacidade escrita à mão, para um caso só. E o --plugin= roda um .df arbitrário do projeto com todos os poderes — uma regra de lint que pode abrir soquete.

Cap.executar(acao, permissoes) recusa o adopt de um módulo fora da lista pelo nome da capacidade que falta, e não por um erro genérico.

A ponte para o Python é capacidade própria, e nunca vem junto: ela alcança tudo o que o Python alcança, e deixá-la com outra faria o resto da lista virar enfeite.

Um nome inventado é recusado com a lista — um erro de digitação concederia silenciosamente nada, e a fronteira pareceria mais aberta do que é.

E Cap.observar responde "de que esta ação precisa?": rode com a lista vazia e leia os negados.

O limite honesto — e ele é o modelo#

A fronteira não tira o que foi ENTREGUE. Se você passa o módulo IO como argumento, o código tem IO.

Isso não é um furo: é o modelo. Numa linguagem de capacidade, poder é o que se passa, não o que está no ar — e é por isso que bloquear o adopt (a autoridade ambiente) é a fronteira certa.

O módulo diz na cara o que não é, e Cap.limites() devolve essa lista em tempo de execução:

  • não é uma caixa contra programa hostil;
  • a lista de módulos é escrita à mão, e um erro nela é um furo;
  • contra código malicioso: processo separado, contêiner, ou o sistema

operacional.

Escrito assim de propósito. Um módulo chamado Sandbox que prometesse contenção seria usado onde não pode ser usado, e a descoberta viria por incidente.

E a fronteira se desfaz sempre, inclusive quando o corpo falha: um cofre que não se desfizesse travaria o programa inteiro.

O que NÃO se aplica#

  • boot, stack probes, stack guards, stack growth: quem faz o boot e

gerencia a pilha é o CPython.

  • thread-local allocators: quem aloca é o CPython.
  • global constructors no sentido do C++: não há.
  • overflow de inteiro: não existe aqui — o inteiro é de precisão

arbitrária, e uma classe inteira de bug não acontece. O preço é a conta ser mais lenta que uma de 64 bits.


Rode um isolado com dataforge run exercicios/46-partida/264_partida_pilha_e_capacidade.df.