Skip to content

Repository files navigation

MINI-CLOJURE-TS

Um interpretador e transpilador de Clojure robusto e modular escrito em TypeScript.

CI license last-commit repo-top-language repo-language-count


Table of Contents

Table of Contents

Overview

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.


Features

Componente Detalhes
⚙️ Arquitetura
  • Design modular que separa Tokenizador, Analisador Sintático, Avaliador, Ambiente e Transpilador
  • Sistema de tratamento de erros tipado com classes de erro personalizadas
  • Separação clara da Biblioteca Padrão (stdlib)
⚡️ Desempenho
  • Otimização de Chamada de Cauda (TCO): Implementa a técnica de Trampolim para lidar com recursão infinita sem estouro de pilha
  • Processamento e avaliação eficientes da AST
🧠 Metaprogramação
  • Suporte completo a macros (defmacro, quasiquote, unquote)
  • Expansão de macros em tempo de execução
  • Capacidade de estender a sintaxe da linguagem dinamicamente
🌐 Interoperabilidade com JS
  • Acesso direto a globalThis via js/Namespace
  • Instanciação de classes JS (new)
  • Encadeamento de métodos e acesso a propriedades (operador .)
📦 Estruturas de Dados
  • Suporte para listas (), vetores [] e mapas de hash {}
  • Palavras-chave (:key), átomos (estado mutável) e tipos primitivos
  • Operações no estilo imutável via funções da stdlib
🛡️ Tratamento de Erros
  • Tratamento de exceções Try/Catch
  • Tipos de erro personalizados (ClojureError, InvalidParamError, ClojureReferenceError)
  • Relatórios de erro detalhados com contexto
🔄 Gerenciamento de Estado
  • Átomos para estado mutável com atom, deref, reset!, swap!
  • Atualizações de estado thread-safe
🎯 Desestruturação
  • Suporte completo à desestruturação em vinculações let e parâmetros de função
  • Suporte para parâmetros rest &
  • Padrões de desestruturação aninhados
⚙️ Transpilador
  • Compila código Clojure para JavaScript executável
  • Suporte à transpilação pela linha de comando
  • Gera código JS limpo e executável
💻 REPL
  • Loop interativo de leitura-avaliação-impressão com realce de sintaxe
  • Estado de ambiente persistente
  • Relatório de erros detalhado

Project Structure

└── 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.yaml

Project Index

MINI-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 JavaScript
core
⦿ src.core
File Name Summary
Environment.ts - Gerencia escopo de variáveis e closures
- Implementa cadeia de escopos (scope chain)
- Suporte a destructuring em bindings
Evaluator.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 TCO
Parser.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úmeros
Trampoline.ts - Implementa padrão Trampoline para Tail Call Optimization (TCO)
- Permite recursão infinita sem stack overflow
Transpiler.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 otimizado
errors
⦿ 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ções
types
⦿ 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

Getting Started

Prerequisites

This project requires the following dependencies:

  • Runtime: Node.js (v18+)
  • Package Manager: pnpm (recommended) or npm

Installation

  1. Clone the repository:
❯ git clone https://github.com/BrunoL28/mini-clojure-ts.git
  1. Navigate to the project directory:
❯ cd mini-clojure-ts
  1. Install the dependencies: Using pnpm:
❯ pnpm install

Using npm:

❯ npm install

Usage

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

CLI

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

Testing

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

Documentação

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

Biblioteca Padrão

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 false e nil sã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).


Módulos

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

Compilador

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.


Interop e Sandbox

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, use node:vm com contexto separado, um Worker ou um processo. O código compilado não é sandboxado.


No Browser

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";

Desempenho e Limites

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 anterior

Para 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 aninhamento

Para 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 formato

O 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 — --timeout só interrompe quem está avaliando formas.


Preguiça e Transdutores

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. transduce custa o mesmo e evita as coleções intermediárias.


API Pública (Embed)

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"] ]

Roadmap

  • 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

Contributing

Contributing Guidelines
  1. Fork the Repository: Start by forking the project repository to your github account.
  2. Clone Locally: Clone the forked repository to your local machine.
  3. Create a New Branch: Always work on a new branch.
git checkout -b feature/my-new-feature
  1. Make Your Changes: Develop and test your changes locally.
  2. Commit Your Changes: Commit with a clear message.
  3. Push to github: Push the changes to your forked repository.
  4. Submit a Pull Request: Create a PR against the original project repository.

License

Distributed under the MIT License. See LICENSE for more information.


Acknowledgments

  • 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.

Releases

Packages

Contributors

Languages