Pitch: XanaScript — Uma linguagem com sintaxe em portugues que compila para WebAssembly nativo
Meu nome e Flavio(flazo0), sou desenvolvedor e fundador da ESC Software.
Nos ultimos meses eu construi uma linguagem de programacao do zero — lexer, parser, otimizador, codegen JS e emissor WebAssembly — tudo em portugues. O nome e XanaScript.
instagram.com/flazo0
Visao geral
O XanaScript e uma linguagem de programacao compilada com sintaxe em portugues. O compilador foi projetado e implementado do zero — lexer, parser, AST, otimizador, codegen JS e codegen Wasm — sem dependencia de toolchains externas como Emscripten, LLVM ou wabt.
O resultado e uma linguagem de proposito geral que leva codigo em portugues do texto ate .wasm utilizando infraestrutura propria.
Arquitetura do compilador
O pipeline e dividido em estagios bem definidos:
Fonte (.xs)
-> Lexer (automato finito deterministico)
-> Parser (descida recursiva com precedencia)
-> AST (arvore com loc tracking por token)
-> Analise semantica (resolucao de escopo)
-> Otimizador (passes sobre a AST)
-> Codegen JS (emissao de string JS)
-> Codegen Wasm (emissao binaria .wasm)
Cada estagio opera sobre a saida do anterior, e a AST e a representacao central que todos os passes consomem e produzem.
Lexer
O lexer e implementado como um automato finito deterministico que percorre o codigo-fonte caractere por caractere. O estado atual determina como cada caractere e interpretado.
Estados principais
- DEFAULT: leitura de tokens comuns (identificadores, numeros, operadores)
- STRING: leitura de strings delimitadas por
"ou' - TEMPLATE: leitura de template strings (crases
\...${expr}...``) com estado de interporlacao - COMMENT: leitura de comentarios de linha (
//) ou bloco (/* */) - OPERATOR: leitura de operadores de 1 ou 2 caracteres
Tokenizacao de multi-palavras
Um dos desafios mais interessantes foi o tratamento de keywords de multi-palavras como tokens atomicos:
CHAMA ESSE CARA -> function
SE LIGA SO -> if
REPETE NA MORAL -> for
SOLTA O GRITO -> print
AGORA VAI -> await
O lexer implementa lookahead com backtracking controlado: ao encontrar CHAMA, ele verifica se os proximos tokens sao ESSE e CARA. Se sim, emite um token unico CHAMA_ESSE_CARA. Se nao, retrocede e trata CHAMA como identificador. Isso permite que palavras reservadas em composicoes tambem sejam usadas como identificadores isoladamente.
Tabela de tokens
O lexer reconhece as seguintes categorias:
| Categoria | Exemplos |
|---|---|
| Keywords | CRIA, SE LIGA SO, REPETE NA MORAL, CHAMA ESSE CARA |
| Operadores 1-char | +, -, *, /, =, <, >, !, % |
| Operadores 2-char | ==, !=, <=, >=, +=, -=, *=, /=, ++, --, => |
| Literais | numeros (42, 3.14), strings ("texto"), templates |
| Simbolos | (, ), {, }, [, ], :, ;, ,, . |
| Identificadores | nomes de variaveis, funcoes, tipos |
Tratamento de Unicode e acentuacao
O lexer aceita caracteres Unicode em identificadores, permitindo acentuacao grafica em nomes de variaveis (criacao, funcao, publica). Isso e feito aceitando qualquer caractere na categoria Unicode Letter (Lu, Ll, Lt, Lm, Lo) alem de _ e digitos apos o primeiro caractere.
Parser
O parser e implementado como descida recursiva com uma tabela de precedencia para operadores infixos.
Estrutura
Cada no da gramatica tem uma funcao de parse dedicada:
parseProgram() // raiz: lista de statements
parseStmt() // dispatch por keyword inicial
parseVarDecl() // CRIA x = expr
parseFunction() // CHAMA ESSE CARA nome(params) { body }
parseBlock() // { stmt* }
parseIf() // SE LIGA SO (expr) block SENAO?
parseWhile() // REPETE AI (expr) block
parseFor() // REPETE NA MORAL (init; test; update) block
parseSwitch() // ESCOLHE (expr) { case* }
parseMatch() // COMBINA (expr) { case* }
parseClass() // CLASSE nome HERDA? { members }
parseExpr() // ponto de entrada para expressoes
Tratamento de precedencia
Expressoes sao parseadas com o algoritmo de precedencia de operadores (inspirado no Pratt parsing). Cada operador infixo tem um nivel de precedencia:
| Nivel | Operadores | Associatividade |
|---|---|---|
| 1 | OU | esquerda |
| 2 | E | esquerda |
| 3 | == != | esquerda |
| 4 | < > <= >= | esquerda |
| 5 | + - | esquerda |
| 6 | * / % | esquerda |
| 7 | ++ -- (postfix) | direita |
| 8 | . [ ( (call) | esquerda |
A funcao parseExpr(prec) consome operadores infixos enquanto a precedencia do proximo operador for >= prec. Operadores prefixos (-, !, NAO, ++/-- prefix) sao tratados antes da chamada a parseExpr.
Pattern Matching (COMBINA)
O COMBINA e uma das construcoes mais complexas do parser. Diferente de um switch tradicional, cada CASO pode conter padroes com desestruturacao:
COMBINA (valor) {
CASO 0 => "zero"
CASO [a, b] => "par"
CASO { nome, idade } => nome
CASO _ => "outro"
}
O parser precisa distinguir entre:
- Literal:
0,"texto",VERDADEIRO - Array pattern:
[a, b]— desestrutura, nao cria array - Object pattern:
{ nome }— desestrutura, nao cria objeto - Wildcard:
_— coringa que aceita qualquer valor
Isso exigiu introduzir um no de AST especifico (MatchPattern) que e parseado por uma funcao separada (parsePattern()), distinta de parseExpr(), para evitar ambiguidades.
Error recovery
O parser implementa recuperacao de erros em modo panic: ao encontrar um erro sintatico, ele consome tokens ate encontrar um sincronizador (;, }, keyword de statement) e continua o parse. Isso permite relatar multiplos erros em uma unica execucao, em vez de parar no primeiro.
AST (Abstract Syntax Tree)
A AST e a representacao intermediaria central. Cada no armazena:
{
type: "BinaryExpr",
op: "+",
left: { type: "NumberLiteral", value: 2, loc: { line, col } },
right: { type: "NumberLiteral", value: 3, loc: { line, col } },
loc: { line: 1, col: 1 }
}
Tipos de nos
A AST possui aproximadamente 40 tipos de nos, entre statements e expressoes:
Statements: Program, VarDecl, FunctionDecl, Block, If, While, For, Switch, Match, ClassDecl, TryCatch, Return, Break, Continue, ExprStmt, MacroDecl, TableDecl, Import, Export, TaskDecl
Expressions: NumberLiteral, StringLiteral, BoolLiteral, NullLiteral, Identifier, BinaryExpr, UnaryExpr, UpdateExpr (++/--), CallExpr, MemberExpr, IndexExpr, ArrowFunction, TernaryExpr, ArrayLiteral, ObjectLiteral, TemplateLiteral, MatchExpr
Loc tracking
Cada no carrega informacao de localizacao (loc: { line, col }), que e usada para:
- Gerar mensagens de erro no estilo Rust com seta apontando o local exato
- Mapear erros em runtime para o codigo fonte
- Suportar source maps no futuro
Otimizador
O otimizador implementa passes que transformam a AST antes da geracao de codigo. Os passes sao executados em ordem, e cada um pode ser habilitado ou desabilitado individualmente.
Passes implementados
Constant Folding: Avalia expressoes constantes em tempo de compilacao:
CRIA x = 2 + 3 // folding: vira CRIA x = 5
CRIA y = "a" + "b" // folding: vira CRIA y = "ab"
Implementado como uma walk recursiva na AST: se um BinaryExpr tem ambos os lados como literais, substitui o no pelo resultado da operacao.
Loop Unrolling: Detecta loops REPETE NA MORAL com iteracoes pequenas e constantes e os desenrola:
REPETE NA MORAL (CRIA i = 0; i < 3; i += 1) {
SOLTA O GRITO(i)
}
// Apos unrolling:
// SOLTA O GRITO(0)
// SOLTA O GRITO(1)
// SOLTA O GRITO(2)
O limiar atual e 8 iteracoes. Loops com corpo muito grande ou chamadas de funcao no teste nao sao desenrolados.
Integer Hinting: Detecta variaveis que recebem apenas valores inteiros e marcam-nas para uso de Int32Array no codegen JS. A analise e feita por propagacao de tipos intra-procedural.
Inlining: Funcoes marcadas como INLINE ou funcoes muito pequenas (1-2 statements) sao inlineadas no ponto de chamada.
Trade-offs de otimizacao
Cada pass envolve trade-offs:
- Constant folding aumenta o tamanho do binario se aplicado agressivamente
- Loop unrolling aumenta o tamanho do codigo em troca de velocidade
- Integer hinting melhora performance JS mas nao tem efeito no Wasm
- Inlining reduz custo de call mas aumenta uso de memoria
O compilador expoe flags para controlar cada pass individualmente.
Codegen JS
O codegen JS percorre a AST e emite codigo JavaScript como string. O alvo e JavaScript moderno (ES2020+), e o codigo gerado e formatado com indentacao correta para legibilidade.
Mapeamento de keywords
| XanaScript | JavaScript |
|---|---|
CRIA x = 1 | let x = 1 |
SE LIGA SO | if |
REPETE NA MORAL | for |
CHAMA ESSE CARA | function |
SOLTA O GRITO | console.log |
E / OU / NAO | && / ` |
VERDADEIRO / FALSO | true / false |
NULO | null |
ISTO | this |
NOVA | new |
VOLTA | return |
VOA | break |
CONTINUA | continue |
Geracao de TypedArrays
Se o otimizador identifica que um array contem apenas numeros inteiros e e acessado indexadamente dentro de um loop, o codegen JS emite:
// Codigo xs: arr[i] = arr[i] * 2
// Codigo JS gerado:
arr[i] = arr[i] * 2;
// Se detectado como array inteiro:
if (!(arr instanceof Int32Array)) {
arr = new Int32Array(arr);
}
arr[i] = arr[i] * 2;
Isso pode gerar ganhos significativos de performance em operacoes numericas intensivas, aproveitando o JIT dos motores JavaScript modernos (V8, SpiderMonkey).
COMBINA (pattern matching)
O COMBINA e emitido como uma cadeia de if/else com comparacoes:
// COMBINA (valor) { CASO 0 => "zero"; CASO _ => "outro" }
let _match = valor;
if (_match === 0) { "zero"; }
else { "outro"; }
Para padroes com desestruturacao:
// CASO [a, b] => "par"
if (Array.isArray(_match) && _match.length === 2) {
let a = _match[0], b = _match[1];
"par";
}
Codegen Wasm
O codegen Wasm e o componente mais complexo do compilador. Diferente do codegen JS, que emite texto, o codegen Wasm emite bytes binarios que seguem o formato especificado pela especificacao oficial do WebAssembly.
Formato binario .wasm
Um arquivo .wasm e composto por secoes encadeadas, cada uma com um ID, tamanho e conteudo:
| magic (4 bytes: 00 61 73 6D) |
| version (4 bytes: 01 00 00 00) |
| section ID (1 byte) |
| section size (LEB128) |
| section content |
| ... |
O codegen implementa um pequeno linker que:
- Coleta todas as funcoes e seus tipos
- Atribui indices para cada funcao, tipo, tabela e memoria
- Emite as secoes na ordem correta
- Calcula offsets para instrucoes de controle (br, br_if)
Secoes emitidas
- Type section (1): Declaracao de todos os tipos de funcao
- Function section (3): Mapeamento de funcoes para seus tipos
- Export section (7): Funcoes exportadas (incluindo
_start) - Code section (10): Corpo das funcoes em instrucoes Wasm
- Data section (11): Strings e dados estaticos
Instrucoes Wasm emitidas
O codegen suporta um subconjunto significativo de instrucoes Wasm:
| Categoria | Instrucoes |
|---|---|
| Control | unreachable, nop, block, loop, if/else/end, br, br_if, return |
| Parametric | drop, select |
| Variable | local.get, local.set, local.tee, global.get, global.set |
| Memory | i32.load, i32.store, i32.load8_s, i32.store8 |
| Numeric | i32.const, i64.const, f32.const, f64.const |
| Comparison | i32.eq, i32.ne, i32.lt_s, i32.gt_s, i32.le_s, i32.ge_s |
| Arithmetic | i32.add, i32.sub, i32.mul, i32.div_s, i32.rem_s |
Gerenciamento de memoria
Para strings e arrays, o codegen Wasm implementa um allocador linear simples:
- Um bloco de memoria linear e reservado na inicializacao
- Strings sao armazenadas como (length, bytes)
- Alocacao e feita por bump allocation (incremento de ponteiro)
- Nao ha garbage collector — a memoria e liberada apenas ao final da execucao
Exemplo de emisao
CRIA x = 42
SOLTA O GRITO(x)
Emite Wasm equivalente a:
i32.const 42
local.set 0
local.get 0
call $print_i32
Onde $print_i32 e uma funcao importada do runtime Wasm.
ORM embutido
O sistema de TABELA e implementado como uma transformacao em tempo de compilacao. Quando o parser encontra:
TABELA Usuario {
nome: TEXTO
idade: NUMERO
}
Ele gera uma estrutura de dados interna que e usada pelo codegen para emitir funcoes CRUD:
- inserir(objeto): valida campos, persiste em JSON
- buscar(filtro): percorre registros, aplica filtros (gt, lt, $eq)
- atualizar(filtro, modificacao): busca e modifica registros
- remover(filtro): busca e remove registros
O armazenamento e feito em JSON local (via fs no CLI ou localStorage no browser). Cada TABELA e mapeada para um arquivo separado.
Macros (compile-time)
Macros sao expandidas durante a compilacao, antes da geracao de codigo:
MACRO quadrado(x) {
x * x
}
CRIA r = quadrado(5)
// Apos expansao: CRIA r = 5 * 5
O expansor de macros:
- Localiza a definicao do macro na tabela de simbolos
- Substitui os parametros formais pelos argumentos na AST do corpo do macro
- Substitui o no de chamada pelo corpo expandido
Macros podem conter qualquer codigo valido, incluindo outros macros (expansao recursiva com limite de profundidade).
LSP (Language Server Protocol)
O LSP do XanaScript implementa o protocolo padrao do Microsoft Language Server Protocol sobre stdio. As capacidades incluem:
- textDocument/completion: autocomplete de keywords e identificadores no escopo
- textDocument/hover: documentacao da keyword ou simbolo sob o cursor
- textDocument/definition: go-to-definition (navega para a declaracao do simbolo)
- textDocument/diagnostic: erros em tempo real (conectado ao parser)
- textDocument/formatting: formatacao do codigo fonte
O servidor LSP e stateless: cada requisicao executa o parser sobre o documento atual e responde com base na AST resultante. Isso simplifica a implementacao mas significa que documentos muito grandes podem ter latencia.
Test runner
O test runner e nativo — nao depende de Jest, Mocha ou qualquer biblioteca externa:
ASSUNTO Matematica
TESTE "soma" {
AFIRMA(2 + 3 == 5)
}
TESTE "subtracao" {
AFIRMA(10 - 4 == 6)
}
O comando xs test:
- Descobre arquivos
*.xsno diretorio atual - Executa cada um em um contexto isolado
- Coleta resultados de
AFIRMAeTESTE - Gera relatorio com TAP (Test Anything Protocol)
CLI
O binario xs suporta 20+ subcomandos:
| Comando | Descricao |
|---|---|
xs run | Executa um arquivo .xs |
xs build | Compila para JS ou Wasm |
xs test | Executa suite de testes |
xs repl | REPL interativo com historico |
xs format | Formata codigo fonte |
xs docs | Gera documentacao |
xs lsp | Inicia servidor LSP |
xs init | Inicializa projeto |
xs install | Instala pacote |
xs publish | Publica pacote |
xs watch | Watcher com hot reload |
xs benchmark | Benchmark de execucao |
O binario e compilado com bun --compile em um unico executavel (~30 MB) sem dependencias externas.
Erros estilo Rust
O compilador emite mensagens de erro no formato:
ERROR: Variavel `z` nao foi definida
Code: E002
--> input.xs:2:11
1 | CRIA x = 10
> 2 | CRIA y = z
| ^ Nao definido
Hint: Esqueceu de declarar "z" com CRIA?
Help: Adicione `CRIA z = valor` antes de usar
Cada erro inclui:
- Codigo (ex:
E002) para referencia rapida - Localizacao exata (arquivo, linha, coluna)
- Codigo fonte com a linha destacada
- Seta apontando a posicao exata
- Hint com sugestao do que pode estar errado
- Help com instrucao de como corrigir
Exemplos
HTTP Server
IMPORTA { Servidor } de "http"
CRIA app = Servidor()
app.rota("/", (req) => {
VOLTA { corpo: "Bem-vindo ao XanaScript!" }
})
app.rota("/api/dados", (req) => {
VOLTA {
corpo: PARSEIA('{"mensagem": "Ola", "status": 200}'),
tipo: "application/json"
}
})
app.escutar(3000)
Fibonacci recursivo
CHAMA ESSE CARA fib(n) {
SE LIGA SO (n <= 1) { VOLTA n }
VOLTA fib(n - 1) + fib(n - 2)
}
REPETE NA MORAL (CRIA i = 0; i <= 10; i += 1) {
SOLTA O GRITO("fib(" + i + ") = " + fib(i))
}
Pattern Matching
CRIA descrever = (valor) => COMBINA (valor) {
CASO 0 => "zero"
CASO 1 => "um"
CASO [a, b] => "par ordenado: " + a + ", " + b
CASO { nome } => "objeto: " + nome
CASO _ => "outro"
}
ORM
TABELA Produto {
nome: TEXTO
preco: NUMERO
estoque: NUMERO
}
CRIA repo = criarRepositorio(Produto)
repo.inserir({ nome: "Teclado", preco: 199.90, estoque: 10 })
CRIA produtos = repo.buscar({ preco: { $gt: 100 } })
repo.atualizar({ nome: "Teclado" }, { preco: 179.90 })
repo.remover({ nome: "Teclado" })
Classes
CLASSE Animal {
CONSTRUTOR(nome) {
CRIA ISTO.nome = nome
}
METODO falar() {
SOLTA O GRITO(ISTO.nome + " faz barulho")
}
}
CLASSE Cachorro HERDA Animal {
METODO falar() {
SOLTA O GRITO(ISTO.nome + " faz: Au au!")
}
}
CRIA rex = NOVA Cachorro("Rex")
rex.falar()
Links
- Site: xanascript.xyz
- Repositorio: github.com/xanascr/xs
- GitHub (dev): github.com/flazo0
- Instagram: instagram.com/flazo0
- ESC Software: esc-software.com
Consideracoes finais
Construir uma linguagem de programacao do zero envolve projetar uma gramatica, implementar passes de otimizacao, emitir codigo para alvos diferentes (JS e Wasm), construir ferramentas de produtividade (LSP, test runner, package manager), e garantir que tudo funcione de forma coerente.
O XanaScript faz tudo isso em portugues, com codigo aberto e infraestrutura propria.
O codigo ta la. O playground ta no ar. O LSP ta funcionando. O Wasm ta compilando.
O resto e questao de adocao.
by flazo0