Definicao De Linguagem - Linguagem Tipos De Linguagem
Linguagem Tipos De Linguagem

Por que a maior parte das definicoes de linguagem que circula pela internet ta errada

A primeira coisa que todo mundo confunde: definicao de linguagem nao e uma lista de tokens. E nao e um PDF de 200 paginas com sintaxe BNF. Quando alguem pede a definicao de linguagem de forma rigorosa, ele quer saber tres coisas simultaneas: a gramatica formal (o que pode ser dito), a semantica operacional (o que essa coisa faz quando executa), e o escopo de variaveis junto com as regras de visibilidade (o que fica acessivel em cada ponto do programa). Faltando qualquer uma dessas tres camadas, voce nao definiu a linguagem. Definiu metade dela. Ou um quarto.

Como na verdade se escreve uma definicao, na ordem que funciona

Contrario ao que a maioria dos textbooks sugere, voce nao parte da sintaxe e desce ate a semantica. A ordem que menos quebra as costuras e a seguinte: primeiro voce define os tipos fundamentais e as regras de inferencia. Depois a gramatica da expressao, que depende dos tipos. So depois a sintaxe de declaracao de escopo. Isso porque se voce comece pela sintaxe, vai perceber em duas semanas que precisa mexer no modelo de tipos, e ai volta tudo pro ponto zero. Eu trabalhei num projeto onde a equipe puxou a gramatica completa em LALR(1) antes de fechar a definicao de polimorfismo de tipo, e refizemos o parser inteiro tres vezes porque o typing rule mudou a cada iteracao. O workaround que usamos foi parar de gerar o parser automaticamente por uns meses, trabalhar com um reconhecedor recursivo-descente feito a mao que tolerava ambiguidade, e so gerar o parser estatico depois que o model de tipos estabilizou. Economizou uns dois meses de retrabalho, mas o codigo ficou muito mais bagacento.

👉 Clique no botão abaixo para saber mais sobre o assunto!

O que a definicao de linguagem exige de fato (e o que as especificacoes raramente entregam)

Uma definicao de linguagem completa precisa responder, para cada construcao sintatica: qual e a forma canonica dela, como ela compila para qual representacao intermediaria, qual a prioridade associativa quando mistura operadores, e o que acontece quando voce tenta aplicar uma regra de inferencia a um contexto onde o tipo ainda nao foi resolvido. A ultima parte e onde quase todo mundo tropeca. Se a sua linguagem tem lambda e inferencia de tipo, voce precisa definir explicitamente o que ocorre quando um lambda referencia uma variavel cujo tipo depende de um generico que ainda nao foi monomorfizado. As especificacoes da C++ resolvem isso com a clause 17 sobre dependent types, mas a maioria das linguagens que tentam misturar static typing com higher-kinded types nunca documenta isso. O resultado e que voces compilam diferente em cada compiler flag, e o comportamento em edge case fica implícito no behavior do backend. Outra coisa que as pessoas nao percebem: a definicao precisa incluir as regras de short-circuit evaluation. Parece bobo, mas voce tem que dizer explicitamente que a && b nao avalia b se a for false, e que isso significa que efeitos colaterais em b nao acontecem. Se voce nao escreve isso, o programador assume que e sequencial, e ai voce quebra o contract. Na pratica, quando fizemos a definicao de linguagem interna pro nosso DSL de pipeline de dados, descobrimos que dois times diferentes tinham implementado a avaliacao de condicoes de forma diferente: um seguia strict left-to-right, o outro fazia lazy evaluation com memoization. A output era identica para 95% dos casos. Nos outros 5%, um dava null pointer e o outro dava timeout. A fix foi padronizar no spec e adicionar testes de conformance com um suite de 340 casos, que levou uns tres dias pra escrever mas evitou um incidente em producao em novembro.

Erros que so aparecem quando voce escala

Numa linguagem pequena, tipo um shell scripting de 200 linhas de sintaxe, a definicao de linguagem cabe numa folha de papel. Voce lista os tokens, as regras de producao, as semanticas em pseudocodigo, e pronto. O problema comece quando voce adiciona concurrency. Ate ai, a semantica operacional era: passo um, passo dois, passo tres. Com threads ou coroutines, voce precisa definir a ordem de memoria (mem order model), quais variaveis sao volatile por regra, se existe happens-before guarantee entre dois tasks, e o que significa "atomico" no seu contexto. O Rust resolve isso de forma explicita no Reference, mas a maioria das linguagens com GC e thread pool so diz "nada acontece de inesperado", o que tecnicamente e falso porque GC pauses interagem com visibility de forma nao trivial. Um ponto que quase ninguem explica: a definicao precisa cobrir o comportamento em estado de erro. O que acontece quando voce tenta fazer type cast de inteiro pra ponteiro? O que faz um overflow de enteira signed? A C deixa isso undefined behavior, o que na pratica quer dizer que o optimizer pode assumir que nao aconteceu e reordenar instrucoes, o que e o opposto do que o desenvolvedor pensa. A Ada define que overflow so acontece em modo protected. A Go faz wrap-around. Cada escolha muda a complexidade do provedor de tipos e, consequentemente, o tempo de compile. Na minha experiencia, so decidir isso a fundo leva de uma a duas semanas porque voce tem que checar contra o hardware target e ver se a ISA suporta as operacoes que voce prometeu.

Onde a coisa quebra de verdade

Se a sua linguagem tem reflection runtime e a definicao de linguagem precisa cobrir isso, voce entra em territorio de auto-referencia. O metamodel descreve o model, mas o model precisa ter espaco pra guardar a referencia ao metamodel. Na pratica, voce ou corta features (nada de reflectir sobre generic classes) ou paga o custo em performance de startup, que num caso que vi aumentou o init time de 40ms pra 310ms porque o runtime precisava construir a tabela de type descriptors em memoria antes de aceitar a primeira chamada. A decisao foi manter a restrictao e documentar claramente que reflection so funciona com types concretas. Nao e elegante, mas evita que o sistema entre em loop de resolucao e trave. Resumindo a parte pratica: se voce ta escrevendo uma definicao de linguagem pro seu time, comece pelos tipos, nao pela sintaxe. Adicione uma secao inteira sobre evaluation strategy e error semantics. Escreva os casos de conformance como tests executaveis, nao como paragrafos. E aceite que nas primeiras versoes o documento vai ter lacunas, e marque-as explicitamente com "TBD" em vez de deixar implicito. O custo de descobrir a lacuna em producao e pelo menos vinte vezes maior que o de discutir a decisao numa chamada de quarenta minutos.