3

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:

CategoriaExemplos
KeywordsCRIA, SE LIGA SO, REPETE NA MORAL, CHAMA ESSE CARA
Operadores 1-char+, -, *, /, =, <, >, !, %
Operadores 2-char==, !=, <=, >=, +=, -=, *=, /=, ++, --, =>
Literaisnumeros (42, 3.14), strings ("texto"), templates
Simbolos(, ), {, }, [, ], :, ;, ,, .
Identificadoresnomes 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:

NivelOperadoresAssociatividade
1OUesquerda
2Eesquerda
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

XanaScriptJavaScript
CRIA x = 1let x = 1
SE LIGA SOif
REPETE NA MORALfor
CHAMA ESSE CARAfunction
SOLTA O GRITOconsole.log
E / OU / NAO&& / `
VERDADEIRO / FALSOtrue / false
NULOnull
ISTOthis
NOVAnew
VOLTAreturn
VOAbreak
CONTINUAcontinue

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:

  1. Coleta todas as funcoes e seus tipos
  2. Atribui indices para cada funcao, tipo, tabela e memoria
  3. Emite as secoes na ordem correta
  4. 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:

CategoriaInstrucoes
Controlunreachable, nop, block, loop, if/else/end, br, br_if, return
Parametricdrop, select
Variablelocal.get, local.set, local.tee, global.get, global.set
Memoryi32.load, i32.store, i32.load8_s, i32.store8
Numerici32.const, i64.const, f32.const, f64.const
Comparisoni32.eq, i32.ne, i32.lt_s, i32.gt_s, i32.le_s, i32.ge_s
Arithmetici32.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:

  1. Localiza a definicao do macro na tabela de simbolos
  2. Substitui os parametros formais pelos argumentos na AST do corpo do macro
  3. 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:

  1. Descobre arquivos *.xs no diretorio atual
  2. Executa cada um em um contexto isolado
  3. Coleta resultados de AFIRMA e TESTE
  4. Gera relatorio com TAP (Test Anything Protocol)

CLI

O binario xs suporta 20+ subcomandos:

ComandoDescricao
xs runExecuta um arquivo .xs
xs buildCompila para JS ou Wasm
xs testExecuta suite de testes
xs replREPL interativo com historico
xs formatFormata codigo fonte
xs docsGera documentacao
xs lspInicia servidor LSP
xs initInicializa projeto
xs installInstala pacote
xs publishPublica pacote
xs watchWatcher com hot reload
xs benchmarkBenchmark 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()


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

Carregando publicação patrocinada...
4

Projeto interessante, mas o nome descredibiliza todo o projeto, fica parecendo meme.

Por mais que você possa dizer que esse X tenha som de Z, para falantes de português do Brasil a primeira leitura será com Ch, aí vira piada pronta e ofusca todo o objetivo do projeto. Minha recomendação e trocar o nome do projeto.


Os links do seu post estão referenciados errados.

3

Valeu pelo feedback! Na verdade, essa foi uma decisão consciente. A proposta da Xanascript sempre foi ter um lado "meme", mesmo sendo um projeto grande e tecnicamente sério.

A ideia nunca foi competir diretamente com linguagens de produção como Rust, Go ou C#. Ela nasceu para ser uma linguagem divertida de explorar, testar ideias e, principalmente, aprender como linguagens de programação e compiladores funcionam por baixo dos panos.

Curiosamente, durante o desenvolvimento ela acabou ficando muito mais robusta do que eu imaginava. Hoje já tem bastante funcionalidade e muitos conceitos para estudar, então acabou virando uma ótima porta de entrada para quem quer entender parsing, AST, análise semântica, compilação, gerenciamento de memória e outros aspectos do desenvolvimento de uma linguagem.

Então sim, o nome é propositalmente descontraído. Faz parte da identidade do projeto. Se ele chama a atenção de alguém por ser diferente e essa pessoa acaba aprendendo algo sobre como uma linguagem funciona, para mim ele cumpriu exatamente o objetivo.