Table of Contents
Mini-Clojure-TS é um interpretador e transpilador de Lisp moderno inspirado em Clojure, construído inteiramente em TypeScript. Este projeto foi desenvolvido com foco em arquitetura modular, performance e extensibilidade. Ele suporta recursos avançados como otimização de chamada de cauda (TCO), metaprogramação via Macros, interoperabilidade direta com JavaScript e agora também transpilação para código JavaScript executável.
| Componente | Detalhes | |
|---|---|---|
| ⚙️ | Arquitetura |
|
| ⚡️ | Desempenho |
|
| 🧠 | Metaprogramação |
|
| 🌐 | Interoperabilidade com JS |
|
| 📦 | Estruturas de Dados |
|
| 🛡️ | Tratamento de Erros |
|
| 🔄 | Gerenciamento de Estado |
|
| 🎯 | Desestruturação |
|
| ⚙️ | Transpilador |
|
| 💻 | REPL |
|
└── mini-clojure-ts/
├── .github
│ └── workflows
├── src/
│ ├── core/
│ │ ├── Environment.ts
│ │ ├── Evaluator.ts
│ │ ├── Parser.ts
│ │ ├── Tokenizer.ts
│ │ ├── Trampoline.ts
│ │ └── Transpiler.ts
│ ├── errors/
│ │ ├── ClojureError.ts
│ │ ├── InvalidParamError.ts
│ │ └── ReferenceError.ts
│ ├── stdlib/
│ │ └── index.ts
│ ├── types/
│ │ └── index.ts
│ └── index.ts
├── tests/
│ ├── atomos.clj
│ ├── compilador.clj
│ ├── destructuring.clj
│ ├── erros.clj
│ ├── estouro.clj
│ ├── filtro.clj
│ ├── final.clj
│ ├── interop.clj
│ ├── listas.clj
│ ├── macros.clj
│ ├── main.clj
│ ├── map.clj
│ ├── multiplo.clj
│ └── soma.clj
├── README.md
├── LICENSE
├── package.json
├── tsconfig.json
├── .eslintrc.json
├── .prettierrc
└── pnpm-lock.yamlMINI-CLOJURE-TS/
src
⦿ src
File Name Summary index.ts - Ponto de entrada principal da aplicação
- Gerencia argumentos CLI para executar arquivos, iniciar REPL ou transpilar código
- Implementa REPL interativo com highlighting de sintaxe
- Suporte a transpilação para JavaScriptcore
⦿ src.core
File Name Summary Environment.ts - Gerencia escopo de variáveis e closures
- Implementa cadeia de escopos (scope chain)
- Suporte a destructuring em bindingsEvaluator.ts - Cérebro do interpretador
- Processa AST, lida com forms especiais (`def`, `if`, `fn`, `let`, `try/catch`)
- Expansão de macros e execução de código
- Suporte a destructuring e TCOParser.ts - Converte tokens em Abstract Syntax Tree (AST)
- Lida com estruturas recursivas (Lists, Vectors, Maps)
- Suporte a reader macros (`'`, `` ` ``, `~`, `@`)Tokenizer.ts - Análise léxica usando Regex
- Lida com comentários, strings, símbolos e caracteres especiais
- Suporte a keywords e númerosTrampoline.ts - Implementa padrão Trampoline para Tail Call Optimization (TCO)
- Permite recursão infinita sem stack overflowTranspiler.ts - NOVO: Compila AST Clojure para código JavaScript executável
- Suporte a forms básicos, funções, condicionais e interop JS
- Gera código limpo e otimizadoerrors
⦿ src.errors
File Name Summary ClojureError.ts - Classe base para todos os erros do interpretador InvalidParamError.ts - Erro para parâmetros inválidos em funções e forms especiais ReferenceError.ts - Erro para símbolos não encontrados no ambiente stdlib
⦿ src.stdlib
File Name Summary index.ts - Biblioteca padrão com funções essenciais (`map`, `filter`, `+`, `str`, etc.)
- Funções de interoperação JavaScript
- Operações com átomos (`atom`, `deref`, `reset!`, `swap!`)
- Funções para manipulação de coleçõestypes
⦿ src.types
File Name Summary index.ts - Tipos de dados fundamentais do Clojure
- `ClojureVector`, `ClojureKeyword`, `ClojureMap`, `ClojureAtom`, `ClojureMacro`
- Interfaces e tipos para AST e funções de usuário
tests
⦿ tests
File Name Summary atomos.clj - Testes de átomos e estado mutável compilador.clj - Programa de exemplo para transpilação destructuring.clj - Testes de destructuring em let e funções erros.clj - Testes de try/catch e tratamento de erros estouro.clj - Testes de Tail Call Optimization (TCO) filtro.clj - Implementação da função filter final.clj - Teste final integrado interop.clj - Testes de interoperabilidade JavaScript listas.clj - Manipulação básica de listas macros.clj - Testes de metaprogramação com macros main.clj - Programa principal de exemplo map.clj - Testes da função map multiplo.clj - Testes de blocos do e strings soma.clj - Testes de recursão básica
This project requires the following dependencies:
- Clone the repository:
❯ git clone https://github.com/BrunoL28/mini-clojure-ts.git
- Navigate to the project directory:
❯ cd mini-clojure-ts
- Install the dependencies: Using pnpm:
❯ pnpm install
Using npm:
❯ npm install
Start the REPL (Interactive Mode):
❯ pnpm start
Execute a Clojure file:
❯ pnpm start -- tests/final.clj
Transpile a Clojure file to JavaScript:
❯ pnpm start -- -t tests/compilador.clj
or
❯ pnpm start -- --transpile tests/compilador.clj
mini-clj # REPL
mini-clj app.clj # executa um arquivo
mini-clj --sandbox app.clj # executa código não confiável
mini-clj -e '(reduce + [1 2 3])' # avalia e imprime
mini-clj -t app.clj # compila para app.mjs
mini-clj -t app.clj --target cjs --out-dir build -s -w
mini-clj --help # todas as opções| Opção | Descrição |
|---|---|
-e, --eval <código> |
Avalia uma expressão e imprime o resultado |
-f, --file <arq> |
Executa um arquivo .clj |
--sandbox |
Interop restrito: sem IO, sem módulos, whitelist |
--allow <a,b> |
Libera globais extras no sandbox |
--timeout <ms> |
Interrompe a execução depois de N ms |
--print-length <n> |
Máximo de itens por coleção ao imprimir |
--trace-eval |
Imprime cada forma avaliada (stderr) |
--trace-macroexpand |
Imprime cada expansão de macro (stderr) |
--trace-depth <n> |
Profundidade máxima impressa no trace |
--profile |
Conta formas e mede o tempo ao final |
fmt <arquivos> |
Formata código-fonte (--write, --check) |
--repl |
Força o REPL |
-h, --help |
Ajuda |
-v, --version |
Versão |
Compilação (com -t):
| Opção | Descrição |
|---|---|
-t, --transpile |
Compila em vez de executar |
--target <alvo> |
esm (padrão), cjs ou iife |
-o, --out-file <arq> |
Arquivo de saída |
--out-dir <dir> |
Diretório de saída (nome derivado da entrada) |
--runtime-global <n> |
Global de onde o iife lê o runtime |
-s, --source-map |
Gera o .map e linka no arquivo compilado |
-w, --watch |
Recompila a cada mudança |
The project includes a comprehensive suite of .clj files to test various features:
# Test Tail Call Optimization
❯ pnpm start -- tests/estouro.clj
# Test Macros
❯ pnpm start -- tests/macros.clj
# Test Atoms and State Management
❯ pnpm start -- tests/atomos.clj
# Test Destructuring
❯ pnpm start -- tests/destructuring.clj
# Test Error Handling
❯ pnpm start -- tests/erros.clj
# Test JavaScript Interop
❯ pnpm start -- tests/interop.clj
# Test Transpilation
❯ pnpm start -- -t tests/compilador.clj
As suítes de aceitação (executadas no CI) ficam em tests/fixtures/ e rodam via node:test:
❯ pnpm test
| Fixture | Cobre |
|---|---|
semantics_suite.clj |
Macros, destructuring, atoms, try/catch, TCO |
stdlib_seq_suite.clj |
Sequências, helpers funcionais e mapas |
predicates_suite.clj |
Predicados e tipos |
core_macros_suite.clj |
defn when and or cond -> ->> |
io_util_suite.clj |
assert time slurp spit |
| Documento | Sobre |
|---|---|
| docs/semantics.md | Especificação do subset e diferenças vs Clojure |
| docs/stdlib.md | Referência completa do core |
| docs/modules.md | require, load-file e a política de módulos |
| docs/compiler.md | Pipeline, targets, source maps e watch |
| docs/interop.md | Contrato de interop e sandbox |
| docs/browser.md | Bundles e limitações no browser |
| docs/performance.md | Benchmarks, limites e observabilidade |
| docs/formatting.md | pprint e o formatador de código |
| docs/lazy-and-transducers.md | Sequências preguiçosas e transdutores |
O core do Mini-Clojure-TS está documentado em docs/stdlib.md — a referência completa de aritmética, predicados, coleções, sequências, mapas, macros utilitárias e IO.
Resumo do que existe hoje:
| Grupo | Formas |
|---|---|
| Aritmética | + - * / rem mod quot inc dec max min abs |
| Comparação/lógica | = not= identical? < > <= >= not |
| Predicados | nil? some? true? false? boolean? number? string? keyword? symbol? fn? macro? map? vector? list? seq? coll? atom? zero? pos? neg? even? odd? empty? contains? |
| Coleções | list vector hash-map first second last rest count nth cons conj concat |
| Sequências | map filter remove reduce some every? not-any? take drop take-while drop-while range repeat iterate cycle reverse seq into |
| Helpers funcionais | identity apply comp partial |
| Transdutores | transduce sequence reduced reduced? unreduced, e a aridade sem coleção de map, filter, take, … |
| Mapas | get assoc dissoc keys vals merge update get-in assoc-in update-in |
| Macros utilitárias | defn when when-not and or cond -> ->> |
| IO/util | print println prn pr-str str read-string assert time slurp¹ spit¹ |
| Átomos/interop | atom deref/@ reset! swap! new . js/… throw |
¹ Node-only (usa fs).
Truthiness: apenas
falseenilsão falsos —0,""e[]são verdadeiros.and,or,cond,when,when-not,->e->>são formas especiais com avaliação preguiçosa (short-circuit garantido).
Referência completa em docs/modules.md.
Um módulo é só um arquivo .clj. Não há namespaces (ns/in-ns): cada
módulo roda num ambiente isolado e é exposto por alias.
;; math.clj
(def pi 3.14)
(defn soma [a b] (+ a b))
;; main.clj
(require "./math.clj" :as math)
(math/soma 1 2) ;=> 3
math/pi ;=> 3.14
;; sem :as, os nomes públicos entram no ambiente atual
(require "./math.clj")
(soma 1 2) ;=> 3
;; load-file: env atual, sempre reexecuta
(load-file "./setup.clj")| Regra | Comportamento |
|---|---|
| Isolamento | def do módulo não vaza; o alias não expõe a stdlib herdada |
| Cache | Um arquivo executa no máximo uma vez por sessão |
| Caminhos | Relativos ao arquivo que requer; extensão .clj opcional |
| Superfície | Tudo que o módulo define é público (sem export) |
| Ciclos | Detectados e rejeitados com erro explícito |
load-file |
Env atual, sem cache, devolve a última expressão |
Referência completa em docs/compiler.md.
O compilador gera um módulo ESM que importa um runtime — sem globalThis:
mini-clj -t app.clj -o build/app.js
node build/app.js// Gerado por Mini-Clojure-TS. Não edite à mão.
import * as $rt from "mini-clojure-ts/runtime";
const println = $rt.core["println"];
let total;
total = $rt.core["+"](1, 2);
println("total:", total);Pipeline: parse → macroexpand → desugar → codegen.
O runtime reusa a stdlib do interpretador em vez de reimplementá-la em JS —
é o que torna a paridade real. A suíte
tests/integration/compiler-parity.test.ts roda dezenas de programas
interpretados e compilados e exige saída idêntica.
Compila: let com destructuring, mapas, keywords, try/catch, atoms,
and/or com short-circuit, cond, when, threading macros, quote,
quasiquote e macros (expandidas em compile-time).
Não compila (falha com erro explícito): require, load-file, macroexpand.
Targets: esm (padrão, .mjs), cjs (.cjs) e iife (.js).
globalThis aparece só no iife. O runtime é publicado nos dois formatos,
então import e require funcionam de verdade.
Source maps: --source-map gera um .map v3 autocontido. Com
node --enable-source-maps, o stack trace aponta a linha do .clj.
Watch: --watch recompila a cada mudança e não morre em erro de
compilação.
Contrato completo em docs/interop.md.
js/Math.PI ;=> 3.14159... (caminho com ponto)
(. "repeat" "ab" 3) ;=> "ababab" (. chama quando é função)
(.- "toUpperCase" "abc") ;=> #<Function> (.- nunca chama)
(new js/Date 2020 0 1)Para rodar código não confiável:
mini-clj --sandbox app.clj
mini-clj --sandbox --allow Intl app.clj| No sandbox | Comportamento |
|---|---|
js/... |
Só a whitelist (Math, Date, JSON, console, …) |
slurp / spit |
Bloqueados |
require / load-file |
Bloqueados |
constructor, __proto__ |
Bloqueados — são a rota para Function/eval |
⚠️ O sandbox roda no mesmo realm do host. Ele eleva o custo de um escape e cobre as rotas conhecidas, mas não é uma fronteira de segurança contra código adversário, e não protege contra laço infinito. Para isolamento real, usenode:vmcom contexto separado, um Worker ou um processo. O código compilado não é sandboxado.
Guia completo em docs/browser.md. Demo em
examples/browser/index.html.
<script src="dist/mini-clojure.global.js"></script>
<script>
console.log(MiniClojure.runSource("(reduce + [1 2 3])")); // 6
</script>Interpretador, macros, estruturas persistentes, sandbox e compilador
funcionam igual. Só o que depende de sistema de arquivos não vai: slurp,
spit, require e load-file — e falham com mensagem explícita.
Via bundler, a condição browser do package.json escolhe a variante certa
sozinha:
import { runSource } from "mini-clojure-ts/browser";Guia completo em docs/performance.md.
pnpm bench # benchmarks do interpretador
pnpm bench --save antes.json # grava para comparar depois
pnpm bench --baseline antes.json # compara com a medição anteriorPara não travar em código com bug ou hostil:
mini-clj --timeout 5000 app.clj # interrompe com erro explicando o motivo
mini-clj --print-length 20 app.clj # trunca coleções ao imprimir(set-print-length! 10) ; itens por coleção; nil = sem limite
(set-print-level! 3) ; profundidade de aninhamentoPara entender o que o avaliador está fazendo (tudo em stderr):
mini-clj --trace-eval --trace-depth 3 app.clj
mini-clj --trace-macroexpand app.clj
mini-clj --profile app.clj— perfil —
formas avaliadas: 37.625
tempo total: 33.69 ms
formas por segundo: 1.116.674
mais avaliadas:
fib 8.361 22.2%
if 8.361 22.2%
Para ler dado grande sem virar uma linha só, e para formatar código:
(pprint {:nome "ana" :tags [:admin :dev] :endereco {:cidade "sp"}})mini-clj fmt --write src/*.clj # formata
mini-clj fmt --check src/*.clj # falha se algo estiver fora do formatoO formatador preserva comentários e é verificado contra duas propriedades
em todo .clj do repo: não altera o programa, e é idempotente. Detalhes em
docs/formatting.md.
Não há limite de memória. Um programa que aloca sem parar continua capaz de derrubar o processo —
--timeoutsó interrompe quem está avaliando formas.
Guia completo em docs/lazy-and-transducers.md.
(take 5 (map (fn [x] (* x x)) (range))) ;=> (0 1 4 9 16)
(first (range)) ;=> 0
(take 4 (cycle [:a :b])) ;=> (:a :b :a :b)range, repeat, iterate, cycle, map, filter, remove, take,
drop, take-while e drop-while produzem sob demanda, com memoização.
Sequências infinitas existem.
Sem a coleção, essas funções devolvem um transdutor:
(def xf (comp (map inc) (filter even?)))
(transduce xf + 0 (range 10)) ;=> 30
(into [] xf (range 10)) ;=> [2 4 6 8 10]
(into [] (comp (filter odd?) (take 4)) (range)) ;=> [1 3 5 7]A preguiça não é de graça. Terminação antecipada ficou 7400× mais rápida, mas um pipeline eager sobre coleção já materializada ficou ~25% mais lento.
transducecusta o mesmo e evita as coleções intermediárias.
O Mini-Clojure-TS pode ser usado como uma biblioteca em outros projetos TypeScript/JavaScript.
import { runSource, createGlobalEnv, parse } from "./src/index.js";
// 1. Execução Simples
const code = "(+ 10 20)";
const result = runSource(code);
console.log(result); // 30
// 2. Ambiente Personalizado
const env = createGlobalEnv();
runSource("(def x 42)", { env });
const x = runSource("x", { env });
console.log(x); // 42
// 3. Acesso à AST
const ast = parse('(print "Ola")');
console.log(ast);
// [ ['print', "Ola"] ]- v3.0: Rastreabilidade de Erros, CI e testes automatizados, Separação Engine/CLI, Multiline + Histórico no REPL
- v4.0: Escapes e erros melhores,
identical(para ponteiros), Printing legível, Ferramentas de Macro, Destructuring de Mapas - v5.0: Sec/Core Functions, Predicados e Tipos, Macros Utilitárias, Utilitários e IO básicos para uso no Node
- v6.0: Loader e Cache, Namespaces (decisão: sem
ns), Empacotamento - v7.0: Transpiler como Compilador Útil, Runtime de Suporte, Macroexpand em Compile-Time
- v7.1: Output e Targets (
esm/cjs/iife), Source Maps, Watch Mode - v8.0: Sandbox/Whitelist, Política de Interop, Build para Browser
- v9.0: Performance do Evaluator, Observabilidade, Limites, Higiene do Repo
- 💬 Join the Discussions: Share your insights, provide feedback, or ask questions.
- 🐛 Report Issues: Submit bugs found or log feature requests.
- 💡 Submit Pull Requests: Review open PRs, and submit your own PRs.
Contributing Guidelines
- Fork the Repository: Start by forking the project repository to your github account.
- Clone Locally: Clone the forked repository to your local machine.
- Create a New Branch: Always work on a new branch.
git checkout -b feature/my-new-feature
- Make Your Changes: Develop and test your changes locally.
- Commit Your Changes: Commit with a clear message.
- Push to github: Push the changes to your forked repository.
- Submit a Pull Request: Create a PR against the original project repository.
Distributed under the MIT License. See LICENSE for more information.
- Inspired by Rich Hickey's Clojure.
- Built with TypeScript for type safety and developer experience.
- Thanks to all contributors and testers who helped shape this project.