Estágio 01 · 01-13
LockedVocê usa V8, Babel, TypeScript Compiler, esbuild, Webpack, Tailwind CLI, Prisma generator, GraphQL codegen, ESLint, prettier todos os dias. Cada uma dessas ferramentas é um compiler ou interpreter sob algum aspecto, recebe texto, parseia, transforma, emite. Entender as fases (lex → parse → analyze → optimize → emit) é o que permite ler bug em parser de TS, escrever um plugin de Babel, depurar source maps, ou desenhar uma DSL pra um problema do seu domínio.
Este módulo é a base prática de compiladores e interpretadores: o que é tokenizer, o que é AST, como recursive descent parsing funciona, o que é semantic analysis e tipo inferência, o que é IR (intermediate representation), o que é codegen, e como JITs (V8, JVM HotSpot, .NET) escalam. Ao final você consegue escrever um interpreter completo para uma linguagem de brinquedo e entender por que TypeScript demora 30s pra typecheck repo grande.
source text
→ lexer (tokenizer)
→ parser → AST
→ semantic analyzer (resolve names, type-check) → annotated AST
→ IR (intermediate representation)
→ optimizer
→ backend (codegen) → bytecode/asm/JS/binário
Compilers AOT (gcc, rustc, javac) executam tudo antes da execução. Interpreters podem parar em AST (interpretadores tree-walking) ou bytecode (Python, Ruby antigo). JITs combinam: parse → bytecode → execute, e em hot paths recompila pra código nativo.
Pega texto bruto e produz tokens (categoria + lexema): IDENT("foo"), NUMBER(42), KEYWORD("if"), OP("+"). Whitespace e comments geralmente são ignorados (com exceções: Python indent é significant; JS ASI olha newline).
Implementação típica: state machine ou regex tabulado. DFAs vêm de regex via Thompson construction + subset construction. Lex/flex geram tabelas. Hand-written wins em flexibility (TypeScript usa hand-written).
Edge cases comuns: string escapes, comments aninhados, números em multiple bases, identifiers Unicode, JSX em meio de expressão.
Linguagens de programação são tipicamente context-free (CFG) na sintaxe (com pequenas violações resolvidas em semantic). Notação BNF/EBNF.
Classes:
Operator precedence (Pratt parsing): técnica elegante pra parsear expressions com precedence/associativity. V8 e muitos parsers modernos usam.
Estrutura central. Nodes representam constructs (BinaryOp, If, FunctionDecl, Ident). Cada node carrega children + span (file/line/col) pra erros.
Diferente de CST (Concrete Syntax Tree, parse tree completo com tokens). AST descarta details sintáticos. Linters/formatters preferem CST.
Visitors (visitor pattern, ou pattern matching em Rust/OCaml) percorrem AST. Babel plugins manipulam AST.
Após parse, percorra AST construindo scopes (mapas nome → declaration). Cada let, function, import adiciona symbol. Lookup respeita scope chain.
Hoisting em JS: declarations subem pro topo do scope (vars, function declarations). Let/const têm temporal dead zone.
Erros típicos: identifier not declared, redeclared, used before declaration.
Estático: tipo é determinado em compile time, sem rodar. Dinâmico: tipo só conhecido em runtime (Python, JS). Gradual: misto (TypeScript, Mypy).
Algoritmo central: type inference. Hindley-Milner (ML, OCaml, Haskell) infere principal type sem anotação. TypeScript usa flow-based + bidirectional. Subtyping vs structural typing distingue Java de TypeScript (TS é structural).
Variance: covariance (subtipo em posição de saída), contravariance (em entrada), invariance (mutável). Conhecer evita bugs em generics.
Transformação 1:1 com semantics, mas mais regular. Tipos comuns:
t1 = x + y; t2 = t1 * z.LLVM popularizou IR como contrato: front-ends emitem LLVM IR, optimizer trabalha em LLVM IR, back-ends emitem nativo. Rust, Swift, Julia, Zig usam LLVM. V8/JVM têm IRs próprios.
2 + 3 → 5.let x = 5; y = x + 1; → y = 6.Trade-off: tempo de compilação vs qualidade do código. -O0 rápido, -O3 lento. Profile-guided optimization (PGO) usa traces reais.
Backend emite código alvo: máquina, bytecode (JVM, .NET CLR, Python), JS, Wasm.
Para nativo: register allocation (graph coloring, linear scan), instruction selection (cobertura de patterns IR → asm), instruction scheduling (reorder pra pipelining).
ABIs (Application Binary Interface) ditam: como passar args (registros vs stack), calling convention, layout de structs, name mangling. Cross-language interop respeita ABI alvo (extern "C").
Linguagens managed precisam GC. Estratégias:
V8 GC: scavenge (young), mark-compact (old), incremental marking, write barriers.
Interpretador roda; profila hot paths; recompila em código nativo otimizado. Tiered:
Inline caches: lookups de propriedade ficam cached por shape; misses re-lookup. Hidden classes (V8) e shapes (SpiderMonkey) tornam objetos JS rápidos quando shape estabiliza.
Por isso obj.foo = 1; obj.bar = 2 consistente é mais rápido que if cond obj.foo = .... Adicionar property dinamicamente muda shape e invalida caches.
Toolchains modernos (TypeScript, Babel, esbuild, Vite, webpack) emitem source maps: mapeamento posição-no-output → posição-no-source. Permite debugger mostrar source original. Format VLQ (variable-length quantity) pra caber em string.
constexpr em C++, comptime em Zig): código rodando em compile time.Java generics usam erasure: List<String> em runtime é só List. Reflection não vê parameter type. C# generics são reified: type info preservada em runtime. TypeScript: erasure (compila pra JS sem tipos).
Implicação prática: em Java/TS, runtime checks não podem inspecionar generic type. Workaround: passar Class<T> ou t: new () => T.
TypeScript checker é caro porque:
Mitigação: split em projects, usar tsc --build, evitar tipos exóticos sem motivo, skipLibCheck quando seguro.
Internal DSL (chains de método, builders) vs external (parse separado). Casos onde external vence:
Antes de escrever DSL, considere: subset de JSON, YAML com schema, ou config em código host. DSL nova é maintenance burden.
Você precisa, sem consultar:
Construir uma linguagem de brinquedo "Mini" com lexer, parser, type-checker e interpreter, escrita em TypeScript.
Mini tem:
int, bool, string, T -> U, [T], { field: T, ... }.+ - * / == < && ||), if/else, let, lambda, function call, list literal, record literal, field access.fn name(args): T = expr), top-level let.Pipeline:
examples/quicksort.mini) com lista, recursion, lambda, records, type-checa e roda.