Cópia estática
Copiado de docs/decisions/adr-001-canonical-syntax.md em https://github.com/poppy-team/aipo-lang (MIT). Fixado na revisão ce47c4e070404fc41722ccd8e2b7b808b99062a8, blob 265d4be950a6f2435e6ac188342a8c90107cabbb. O repositório de origem permanece canônico; esta cópia é atualizada por um pull request de sincronização, não em tempo real.
Status: aceito Data: 2026-10-06 Substitui: fn Type.name(...) associated syntax, satisfy, impl, self!, end, divAlvo: Aipo V1
Aipo é projetada para pessoas neurodivergentes (TDAH, dislexia, sobrecarga cognitiva). Quatro regras governam toda decisão de superfície:
and sobre &&, not sobre !. Símbolos densos colados têm silhueta parecida e causam releitura.Não-objetivos explícitos: brevity deExpert, densidade deexpressão, compatibilidade com outras linguagens.
Toda abertura usa {, todo fechamento usa }.
if cond {
io.println("ok")
}A keyword end é removida. Ela era ambígua quando três blocos aninhados terminavam em sequência, e chaves têm contorno visual que editores highlightam como par.
Aplica-se a: fn, async fn, struct, interface, if/elif/else, match (cada braço when), loop, while, repeat, each, attempt/failed, await do, bloco trailer.
then then.then. É a única forma sem bloco.let rotulo = if idade >= 18 then "maior" else "menor"Regra: tem {}? Sem then. Não tem {}? Com then.
struct Servidor {
id: Int # imutável por padrão
var status: String = "online" # `var` = mutável
var carga: Float = 0.0
}A anotação de tipo no campo é opcional. Sem ela, o campo não promete nada ao compilador — útil em protótipo, oneroso evitado.
invariant() verifica regras de valor, não tipos: roda em runtime, após a construção e nas fronteiras mutáveis estáveis.
interface Drawable {
draw() -> Int
}Não contém corpos, não contém fn, não contém self.
Um tipo satisfaz uma interface quando possui todos os métodos com assinatura compatível. A satisfação é automática e não requer declaração.
Comparação de assinatura inclui o tipo de retorno e os tipos dos parâmetros. Para mutabilidade do receptor, a regra é covariante:
draw() (imutável) aceita implementação draw(var self) — mais forte, seguro.move(var self) exige implementação mutável.: Um método pertence a um tipo pelo prefixo Tipo:.
Point:init(x, y) {
self.x = x
self.y = y
}
Point:invariant {
self.x >= 0
}
Point:dist() -> Float {
return math.sqrt(self.x * self.x + self.y * self.y)
}
Point:move(var self, dx, dy) {
self.x += dx
self.y += dy
}init e invariant são hooks reconhecidos pelo nome; os demais são métodos.self implícito: não se escreve em método imutável. O parser injeta, como já faz com init e invariant na gramática atual.var self explícito: obrigatório para mutação. Uma única regra em toda a linguagem: var significa mutável.fn não aparece. A associação Tipo: já declara que é uma função.A chamada usa ., igual hoje:
let p = Point{x = 3, y = 4}
let d = p.dist() # passa
let m = p.move(var dx = 1, dy = 1): marca associação na declaração; . marca acesso no uso. Um operador, cada emprego.
:: Funções livres podem ser promovidas a métodos de um tipo sem reescrever o corpo. Exigência: o primeiro parâmetro deve ser self (ou var self).
fn calcular_area(self) {
return self.largura * self.altura
}
Retangulo::[calcular_area]
fn ajustar(var self, delta) { # continua mutável
self.largura += delta
}
Retangulo::[ajustar] # método mutável promovidoO :: é o token de associação em lote. Mutabilidade vem da assinatura de origem: self → método read-only, var self → método mutável.
Funções sem self como primeiro parâmetro são rejeitadas pelo compilador: batch existe para promover comportamento do tipo, não para namespacing.
Semântica: o binding é uma promoção. A função permanece chamável como função livre e ganha o método associado.
Nota:
:é o token canônico de associação.::existe apenas para o lote.
fn fn fica reservado a funções livres.
fn soma(a, b) -> Int { return a + b } # livre → usa fn
Point:dist() -> Float { ... } # associado → sem fnO contraste torna fn mais significativo, não menos: ele marca o que não pertence a um tipo.
self explícito é aceito A forma implícita é a canônica e a que o formatter produz. A forma explícita é aceita sem erro nem warning:
Point:dist() -> Float { ... } # canônico — implícito
Point:dist(self) -> Float { ... } # aceito — explícito
Point:move(var self, dx, dy) { ... } # mutação — `var self` obrigatórioO parser normaliza as duas para a mesma forma interna (o receptor é sempre o primeiro slot do frame), então não há custo em aceitar ambas. Não há diagnóstico: quem escreve self explicitamente sabe exatamente o que está fazendo, e a regra var continua sendo a única forma de declarar mutação.
Objetivo: quem erra vê o que fazer, não só o que está errado. Um erro ocupa no máximo uma linha de mensagem, uma linha de código com cursor, e uma dica.
Formato de renderização:
error [AIPO_SEM_TIPO_NAO_ESTRUTURA]: 'with' só funciona com struct
14 │ let novo = n with { y: 1 }
│ ^ sem struct aqui
dica: use `with` sobre uma struct, não sobre IntLimites rígidos: uma mensagem, um span primário, no máximo uma nota e uma sugestão. Cor carrega apenas estrutura (o código, o error, o cursor), nunca significado.
init e invariant são reconhecidos pelo nome. Um método chamado inicializar, initialize, constructor, validar ou validate parece querer ser hook e não é.
warning [AIPO_SEM_NOME_DE_HOOK]: 'validar' parece um hook, não um método
7 │ Point:validar() -> Bool { ... }
│ ^^^^^^^^ método comum; não roda após construção
dica: renomeie para `Point:invariant { ... }` se a intenção é validarMesmo formato para init:
warning [AIPO_SEM_NOME_DE_HOOK]: 'constructor' parece um hook, não um método
5 │ Point:constructor(x) { ... }
│ ^^^^^^^^^^^ método comum; não roda na construção
dica: use `Point:init(x) { ... }` para o hook de construçãoSeveridade Warning, não Error: o código funciona, é só a intenção que provavelmente não foi atingida. Um aviso sem ação possível é ruído; este tem ação.
warning [AIPO_SEM_HOOK_VAZIO]: 'invariant' sem condição não verifica nada
9 │ Point:invariant {
│ ^^^^^^^^^ nenhum predicado
dica: escreva ao menos uma condição, ou remova o hookerror [AIPO_SEM_HOOK_DUPLICADO]: 'Point' já declara 'invariant'
21 │ Point:invariant { self.x > 0 }
│ ^^^^^^^^^ já declarado na linha 9
dica: reúna as condições em um único `invariant`self mutável ausente error [AIPO_SEM_RECEPTOR_IMUTAVEL]: 'self' é somente leitura aqui
14 │ Point:avancar() { self.x += 1 }
│ ^^^ atribuição em receptor imutável
dica: declare `Point:avancar(var self)` para permitir mutaçãoEste é Error, não Warning: sem ele, o código não compila de forma alguma, e o motivo exato é o mais provável ponto de atrito de quem está migrando.
. acessa valor, : associa uma declaração, :: associa várias.
Erro ao chamar método sem receptor pelo . com valor solto:
error [AIPO_SEM_METODO_NAO_ASSOCIADO]: 'somar' não é método de 'Int'
9 │ let total = 3.somar(4)
│ ^^^^^ Int não tem esse método
dica: use a função livre `somar(3, 4)`, ou defina `Int:somar()`Erro ao usar :: sem lista:
error [AIPO_PARSE_LOTE_INVALIDO]: '::' espera uma lista entre colchetes
6 │ Point::desenhar()
│ ^^ use ':' para declarar um único método
dica: `Point:desenhar()` declara um método; `Point::[f1, f2]` agrupa funçõesEste par de mensagens é o que pagou a escolha de manter os três tokens: o erro nomeia o token, o motivo e a forma correta na mesma tela.
#! sem âncora Diretiva solta no fim do arquivo, sem item seguinte:
warning [AIPO_PARSE_DIRETIVA_ORFA]: '#!test' não tem função seguinte
42 │ #!test
│ ^^^^^ nenhuma declaração após a diretiva
dica: a diretiva aplica-se ao próximo `fn`; mova-a para logo acima do testeself explícito onde o implícito é canônico Aceito, sem diagnóstico (ver §4.5). Não entra na lista.
Aprovado em 2026-10-06, com o status registrado em ](development/decisions/surface-decisions).
#! Comentário é # até o fim da linha. Não existe comentário de bloco.
#! introduz uma diretiva: um comentário que o compilador lê.
| Diretiva | Onde | Efeito |
|---|---|---|
#!satisfies I1, I2 | imediatamente antes de struct | Exige que o tipo satisfaça as interfaces. Erro se falhar. |
#!test | imediatamente antes de fn livre | Marca a função como teste unitário. |
#!test[tag] | idem | Teste com etiqueta, filtrável por aipo test --filter tag. |
#!test("nome") | idem | Teste com nome descritivo explícito. |
Comentários comuns são descartados pelo parser. Diretivas passam: o lexer emite TokenKind::Directive, e o parser associa cada diretiva ao próximo item declarado.
#!satisfies mantém a lista de keywords curta, deixa a intenção visível a quem lê o arquivo, e não polui a assinatura do tipo. O contrato sobrevive a ferramentas que descartam comentários porque é verificado pelo compilador.
Duas vias, ambas ativas:
*_test.aipo, test_*.aipo (compatibilidade)#!test dentro de qualquer arquivo .aipo#!test
fn soma_basico() {
io.println(String(soma(1, 2))) # 3
}
#!test[parser]
fn parse_match_com_guard() {
...
}
#!test("divisão inteira trunca em direção a zero")
fn divide_trunca() {
...
}Um teste por arquivo deixa de ser obrigatório: funções pequenas ficam agrupadas no arquivo do que testam.
#!test por função.#!test marca funções livres. Um método Tipo:nome não é teste: precisa de self, logo não é unidade isolada.#!test não aceita conteúdo que não seja etiqueta entre [] ou string entre aspas.// e //= são divisão inteira truncada em direção a zero.
div e div= são removidos; div volta a ser identificador comum.
let metade = total // 2/ continua divisão que produz Float.
// é division, não comentário — o que colide com o hábito formado em JavaScript, Java, C, C++, Rust, Go, C#, PHP e Swift. O próprio piloto automático escreve comentário com //.
Mitigação: quando // aparece onde nenhuma expressão válida pode segui-lo, o diagnóstico sugere #.
// isto é um comentário
// ^ AIPO_PARSE_UNEXPECTED_TOKEN
// dica: use `#` para comentários; `//` é divisão inteirafn salvar(usuario: User, var contador: Int, modo = "rapido") { }Duas dimensões ortogonais:
var nome ou nome (imutável por padrão): T, opcionalnome! é removido. O ! era sutil demais para leitura e confundia com not.
Formas aceitas: nome · var nome · nome: T · var nome: T · nome = default · var nome: T = default · _.
Um arquivo .aipo é um módulo.
import math_util
import math_util as mu
import math_util: somar, dobro
export somar, dobroPrivado por padrão. Acesso a símbolo não exportado é AIPO_SEM_UNKNOWN_NAME.
. é o acesso a membro de módulo e a campo. :: é associação de tipo. Os três empregos ficam distintos: . para valor, :/:: para código.
fail "mensagem"
let v = ler_porta() or_else 8080
let w = risky()?
attempt { ... } failed erro { ... }? propaga. or_else é fallback. attempt/failed captura bloco.
Não existe try, catch, finally, throw, raise, defer.
async fn buscar(recurso) {
return http.get(recurso)
}
let v = await buscar("usuarios")
await do {
let a = await etapa_1()
let b = await etapa_2()
return a + b
}await é legal em statement, inicializador e valor de retorno. Subexpressão é AIPO_SEM_AWAIT_IN_SUBEXPRESSION.
| Forma | Nome |
|---|---|
"..." | normal |
f"..." | interpolada |
r"..." | raw |
fr"..." / rf"..." | raw interpolada |
"""...""" | multilinha |
r"""...""" / fr"""...""" | raw multilinha |
Escapes: \n \r \t \\ \" \0 \u{HEX}. Conteúdo é normalizado em NFC.
0x 0b 0o, _ como separador entre dígitos da mesma base. Int é inteiro assinado de 64 bits com faixa segura ±(2^53−1). Float é IEEE 754 sem NaN e sem Infinity.
# até o fim da linha. Único comentário da linguagem.
#! introduz diretiva (§5).
42 keywords. Removidas nesta decisão: end, div, self!, impl, satisfy.
Tokens não-keyword adicionados: : (associação), :: (lote), //= já existia.
enum — tipo de soma enum é promovido a V1 (ver §17 do SYNTAX.md para o desenho completo).
Um enum é um tipo fechado: o compilador conhece todas as variantes e o match sobre ele é verificado por exaustividade. Esse é o motivo de existir — sem enum, cobertura de casos é convenção; com ele, é obrigação do compilador.
Três formas de variante, sem sintaxe nova de payload:
enum Estado {
Inicial, # sem payload
Ativo { desde: Int }, # campos nomeados
Desligado(motivo: String), # um campo posicional
}Construção é o nome qualificado, sem new e sem :::
let e = Estado.Desligado("manutenção")O match nomeia tipo e variante, reusando o destructure de §6.1:
match e {
when Estado.Inicial { }
when Estado.Ativo { desde } { }
when Estado.Desligado(motivo) { }
}Nenhum conceito novo de pattern ou binding: enum compõe com fail (§7), Task (§9), hooks Tipo:invariant (§5.2) e #!satisfies (§5.4) exatamente como struct. Regras de Design Considerations da §1 valem: uma forma só para cada coisa (Tipo.Variante para construir e para casar), palavra completa sobre símbolo, e o erro de exaustividade nomeia as variantes faltantes.
Representação em runtime permanece decisão de backend (tagged union com heap vs int tag + payload inline); a superfície não depende dela.
defer · try · catch · finally · throw · raise · for · class · const · static · pub · private · unsafe · yield · lambda · module · void · case · where · generics · bitwise · ; · && · || · ++ · -- · ** · ?: · cast com as
Operadores não tokenizados: @ & ; && || ++. Um | isolado produz "did you mean '|>'?".