Rust

Gerenciamento de Dependências e Publicação de Crates — Do Projeto Local ao crates.io Já leu

14 min de leitura

Gerenciamento de Dependências e Publicação de Crates — Do Projeto Local ao crates.io
Rust — Artigo #39 Gerenciamento de Dependências e Publicação de Crates — Do Projeto Local ao crates.io Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1

Rust — Artigo #39

Gerenciamento de Dependências e Publicação de Crates — Do Projeto Local ao crates.io

Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano


Toda biblioteca que você escreveu nos artigos anteriores pode beneficiar outros desenvolvedores. O ecossistema Rust é construído por contribuições da comunidade — as mais de 150 mil crates disponíveis no crates.io foram criadas por pessoas que resolveram um problema e decidiram compartilhar a solução.

Neste artigo cobrimos o ciclo completo: gerenciar dependências com precisão, entender versionamento semântico, preparar uma crate para publicação, e manter uma biblioteca ao longo do tempo com compatibilidade retroativa.


Anatomia completa do Cargo.toml

[package]
name = "minha-crate"          # nome no crates.io (único, com hífens)
version = "1.2.3"             # semver obrigatório
edition = "2021"              # edição do Rust (2015, 2018, 2021)
authors = ["Ana Silva <ana@exemplo.com>"]
description = "Uma linha descrevendo o que a crate faz"
documentation = "https://docs.rs/minha-crate"
homepage = "https://github.com/usuario/minha-crate"
repository = "https://github.com/usuario/minha-crate"
readme = "README.md"
license = "MIT OR Apache-2.0"  # licença SPDX — dupla licença é convencional em Rust
keywords = ["parsing", "texto", "unicode"]   # até 5, para busca no crates.io
categories = ["text-processing", "parsing"]  # categorias do crates.io
exclude = ["tests/fixtures/", "benches/dados/"]  # exclui do pacote publicado
include = ["src/**/*", "Cargo.toml", "README.md"] # alternativa ao exclude

# Versão mínima do Rust suportada (MSRV)
rust-version = "1.70"

[lib]
name = "minha_crate"          # nome do crate (underscores)
path = "src/lib.rs"           # caminho padrão, opcional
crate-type = ["lib"]          # lib (padrão), cdylib, staticlib, proc-macro

[[bin]]
name = "minha-cli"
path = "src/bin/cli.rs"

[[example]]
name = "uso_basico"
path = "examples/basico.rs"

[[bench]]
name = "benchmark_principal"
path = "benches/principal.rs"
harness = false               # para criterion

[[test]]
name = "integracao"
path = "tests/integracao.rs"

# Perfis de compilação
[profile.dev]
opt-level = 0
debug = true
incremental = true

[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
strip = "symbols"             # remove símbolos de debug do binário

[profile.release-with-debug]  # perfil customizado para profiling
inherits = "release"
debug = true
strip = "none"

[profile.bench]
inherits = "release"
debug = true

Especificando versões de dependências

O versionamento semântico (semver) é o contrato entre a sua crate e seus usuários:

[dependencies]
# Compatível com 1.x.x (^1.0 = >=1.0.0 <2.0.0) — padrão do Cargo
serde = "1"
serde = "1.0"
serde = "1.0.0"
serde = "^1.0.0"   # explícito, mesmo que o anterior

# Compatível com patch (^0.8 = >=0.8.0 <0.9.0)
# Para 0.x, minor é breaking — semver especial
mio = "0.8"

# Compatível com qualquer patch (^0.0.3 = >=0.0.3 <0.0.4)
alguma-crate = "0.0.3"

# Tilde: compatível com patch, não minor
tokio = "~1.35"    # >=1.35.0 <1.36.0

# Exato: fixado a uma versão específica (raramente adequado)
alguma-crate = "=1.2.3"

# Intervalo wildcard
alguma-crate = "1.*"
alguma-crate = "1.2.*"

# Intervalo explícito
alguma-crate = ">=1.2.0, <2.0.0"

# Múltiplas restrições
alguma-crate = ">= 1.0, < 2.0, != 1.3.0"

# Dependência de path (desenvolvimento local)
minha-lib = { path = "../minha-lib" }

# Dependência de git
experimental = { git = "https://github.com/usuario/repo" }
experimental = { git = "https://github.com/usuario/repo", branch = "main" }
experimental = { git = "https://github.com/usuario/repo", tag = "v1.2.3" }
experimental = { git = "https://github.com/usuario/repo", rev = "abc1234" }

# Com features
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"], default-features = false }

# Apenas para desenvolvimento
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
tempfile = "3"
proptest = "1"
rstest = "0.18"

# Apenas para build scripts
[build-dependencies]
cc = "1"
bindgen = "0.69"

Cargo.lock — reprodutibilidade

# Cargo.lock: gerado automaticamente, registra versões exatas

# Para BINÁRIOS: commitar o Cargo.lock no git
# → garante builds reprodutíveis para todos os colaboradores

# Para BIBLIOTECAS: NÃO commitar (convenção)
# → permite que usuários usem versões mais novas de dependências transitivas

# Verificar se as versões no lock são as usadas
cargo verify-project

# Atualizar dependências (respeitando restrições do Cargo.toml)
cargo update

# Atualizar uma dependência específica
cargo update -p serde

# Atualizar para a versão mais nova disponível
cargo update -p serde --precise 1.0.196

# Ver árvore de dependências
cargo tree

# Apenas dependências diretas
cargo tree --depth 1

# Encontrar por que uma crate está sendo incluída
cargo tree -i openssl

# Detectar versões duplicadas
cargo tree --duplicates

Workspaces — projetos multi-crate

Para projetos grandes, workspaces permitem múltiplas crates com configuração compartilhada:

# Cargo.toml da raiz do workspace
[workspace]
members = [
    "core",
    "cli",
    "server",
    "macros",
]
resolver = "2"  # resolver de features (recomendado)

# Dependências compartilhadas — evita versões inconsistentes
[workspace.dependencies]
serde       = { version = "1", features = ["derive"] }
tokio       = { version = "1", features = ["full"] }
anyhow      = "1"
thiserror   = "1"
tracing     = "0.1"
clap        = { version = "4", features = ["derive"] }

[workspace.package]
version = "0.5.0"  # versão compartilhada
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/usuario/projeto"
# core/Cargo.toml
[package]
name = "meu-core"
version.workspace = true      # herda do workspace
edition.workspace = true
license.workspace = true

[dependencies]
serde.workspace = true        # herda versão e features do workspace
anyhow.workspace = true
tokio.workspace = true

# Dependência específica deste crate
uuid = { version = "1", features = ["v4"] }
# cli/Cargo.toml
[package]
name = "meu-cli"
version.workspace = true
edition.workspace = true

[dependencies]
meu-core = { path = "../core" }  # dependência interna
clap.workspace = true
anyhow.workspace = true

Preparando uma crate para publicação

1. Checklist de qualidade

# Verificar formatação
cargo fmt --check

# Rodar o linter
cargo clippy -- -D warnings

# Rodar todos os testes incluindo doc tests
cargo test --all-features

# Testar sem features padrão
cargo test --no-default-features

# Verificar documentação
cargo doc --all-features --no-deps

# Verificar se o pacote está correto
cargo package --list  # lista arquivos incluídos
cargo package         # gera o .crate sem publicar

2. README.md bem estruturado

# minha-crate

[![Crates.io](https://img.shields.io/crates/v/minha-crate)](https://crates.io/crates/minha-crate)
[![Docs.rs](https://docs.rs/minha-crate/badge.svg)](https://docs.rs/minha-crate)
[![CI](https://github.com/usuario/minha-crate/actions/workflows/ci.yml/badge.svg)](...)
[![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue)](...)

Uma linha descrevendo o que a crate faz.

## Uso

Adicione ao `Cargo.toml`:

[dependencies] minha-crate = "1"


Exemplo básico:

use minha_crate::MinhaStruct;

let s = MinhaStruct::nova("dados"); println!("{}", s.processar());


## Features

| Feature | Padrão | Descrição |
|---------|--------|-----------|
| `json`  | ✓      | Suporte a JSON |
| `async` |        | API assíncrona |

## MSRV

Rust 1.70 ou superior.

## Licença

MIT OR Apache-2.0

3. CI/CD com GitHub Actions

.github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1

jobs:
  test:
    name: Test (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]

    steps:
      - uses: actions/checkout@v4

      - name: Instalar Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Cache Cargo
        uses: Swatinem/rust-cache@v2

      - name: Build
        run: cargo build --all-features

      - name: Testes
        run: cargo test --all-features

      - name: Doc tests
        run: cargo test --doc --all-features

  msrv:
    name: MSRV (Rust 1.70)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@1.70
      - run: cargo build --all-features

  linting:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy

      - name: Formato
        run: cargo fmt --check

      - name: Clippy
        run: cargo clippy --all-features -- -D warnings

      - name: Documentação
        run: cargo doc --all-features --no-deps
        env:
          RUSTDOCFLAGS: "-D warnings"

  security:
    name: Auditoria de segurança
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: rustsec/audit-check@v1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

  publish-dry-run:
    name: Publicação (dry-run)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo publish --dry-run --all-features

Publicando no crates.io

# 1. Criar conta em https://crates.io

# 2. Gerar token de API em https://crates.io/settings/tokens

# 3. Autenticar localmente
cargo login <seu_token>

# 4. Verificar o pacote antes de publicar
cargo publish --dry-run

# 5. Publicar
cargo publish

# Publicar versão futura sem que seja a padrão de 'latest'
cargo publish --no-verify  # pula verificação de arquivos

# Para workspace com múltiplos crates (ordem importa)
cargo publish -p meu-core
cargo publish -p meu-cli

Versionamento semântico na prática

MAJOR.MINOR.PATCH

PATCH (1.0.0 → 1.0.1): correção de bug sem mudança de API
MINOR (1.0.0 → 1.1.0): nova funcionalidade compatível com versões anteriores
MAJOR (1.0.0 → 2.0.0): mudança incompatível com versões anteriores

Para 0.x.y: MINOR pode ser breaking (0.1.0 → 0.2.0 = breaking)

O que é breaking change?

// BREAKING — requer nova versão MAJOR:
// ✗ Remover função pública
// ✗ Mudar assinatura de função (tipos de parâmetros ou retorno)
// ✗ Remover variante de enum público
// ✗ Remover campo de struct público
// ✗ Adicionar método obrigatório a trait público
// ✗ Mudar tipo de campo público
// ✗ Aumentar MSRV além do que a versão especifica

// NÃO breaking — pode ir em MINOR ou PATCH:
// ✓ Adicionar nova função pública
// ✓ Adicionar nova variante a enum #[non_exhaustive]
// ✓ Adicionar novo campo com valor padrão em struct #[non_exhaustive]
// ✓ Implementar novo trait para tipo existente
// ✓ Adicionar implementação de trait para tipo existente
// ✓ Adicionar método com implementação padrão a trait

// O atributo #[non_exhaustive] é seu aliado:
#[non_exhaustive]  // previne match exaustivo externo
pub enum Evento {
    Conectado,
    Desconectado,
    Erro(String),
    // Podemos adicionar variantes em versões MINOR
}

#[non_exhaustive]  // previne construção direta externa
pub struct Config {
    pub host: String,
    pub porta: u16,
    // Podemos adicionar campos em versões MINOR
}

Depreciação e migração

// Depreciando uma API com guidance de migração
#[deprecated(
    since = "2.1.0",
    note = "Use `processar_v2()` que retorna Result em vez de Option. 
            Veja https://docs.rs/minha-crate/latest/minha_crate/fn.processar_v2.html"
)]
pub fn processar(dados: &str) -> Option<String> {
    processar_v2(dados).ok()
}

pub fn processar_v2(dados: &str) -> Result<String, ErroProcessamento> {
    if dados.is_empty() {
        return Err(ErroProcessamento::EntradaVazia);
    }
    Ok(dados.to_uppercase())
}

#[derive(Debug)]
pub enum ErroProcessamento {
    EntradaVazia,
}

// CHANGELOG.md bem mantido

CHANGELOG.md seguindo Keep a Changelog:

# Changelog

Todas as mudanças notáveis deste projeto são documentadas aqui.
Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/).

## [Unreleased]

## [2.1.0] - 2024-03-15

### Adicionado
- `processar_v2()` retorna `Result` em vez de `Option` para melhor tratamento de erros
- Suporte a feature `yaml` para serialização YAML

### Depreciado
- `processar()` — use `processar_v2()`. Será removida na v3.0.

### Corrigido
- Corrigido panic ao processar strings com caracteres Unicode (#42)

## [2.0.0] - 2024-01-10

### Mudanças incompatíveis
- `Config` agora usa `#[non_exhaustive]` — construção direta requer `.default()`
- Removido `antigo_metodo()` (depreciado desde 1.5.0)

### Adicionado
- Suporte a ARM64 (Apple Silicon, Raspberry Pi)

## [1.5.0] - 2023-11-20
...

Um projeto completo: do zero ao crates.io

Vamos simular o ciclo completo de uma crate pequena mas bem feita:

minha-crate/
├── Cargo.toml
├── README.md
├── CHANGELOG.md
├── LICENSE-MIT
├── LICENSE-APACHE
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── release.yml
├── src/
│   ├── lib.rs
│   └── error.rs
├── tests/
│   └── integracao.rs
├── examples/
│   └── uso_basico.rs
└── benches/
    └── principal.rs

src/lib.rs:

//! # slug-rs
//!
//! Converte texto em slugs URL-seguros.
//!
//! ## Uso
//!
//! ```rust
//! use slug_rs::slugify;
//!
//! assert_eq!(slugify("Olá, Mundo!"), "ola-mundo");
//! assert_eq!(slugify("Rust é Incrível"), "rust-e-incrivel");
//! ```

#![warn(missing_docs)]
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::must_use_candidate)]

mod error;
pub use error::ErroSlug;

/// Converte uma string em um slug URL-seguro.
///
/// Remove acentos, converte espaços em hífens, e elimina
/// caracteres não-alfanuméricos.
///
/// # Exemplos
///
/// ```rust
/// use slug_rs::slugify;
///
/// assert_eq!(slugify("Hello World"), "hello-world");
/// assert_eq!(slugify("  espaços extras  "), "espacos-extras");
/// assert_eq!(slugify("já-tem-hífen"), "ja-tem-hifen");
/// assert_eq!(slugify("!!!"), "");
/// ```
pub fn slugify(texto: &str) -> String {
    let sem_acentos = remover_acentos(texto);

    sem_acentos
        .to_lowercase()
        .chars()
        .map(|c| if c.is_alphanumeric() { c } else { '-' })
        .collect::<String>()
        .split('-')
        .filter(|s| !s.is_empty())
        .collect::<Vec<_>>()
        .join("-")
}

/// Converte com configurações personalizadas.
///
/// # Exemplos
///
/// ```rust
/// use slug_rs::{slugify_com_opcoes, OpcoesSlug};
///
/// let opcoes = OpcoesSlug {
///     separador: '_',
///     max_comprimento: Some(10),
/// };
///
/// assert_eq!(
///     slugify_com_opcoes("Hello Beautiful World", &opcoes),
///     "hello_beau"
/// );
/// ```
pub fn slugify_com_opcoes(texto: &str, opcoes: &OpcoesSlug) -> String {
    let sem_acentos = remover_acentos(texto);
    let separador = opcoes.separador;

    let mut slug = sem_acentos
        .to_lowercase()
        .chars()
        .map(|c| if c.is_alphanumeric() { c } else { separador })
        .collect::<String>()
        .split(separador)
        .filter(|s| !s.is_empty())
        .collect::<Vec<_>>()
        .join(&separador.to_string());

    if let Some(max) = opcoes.max_comprimento {
        slug.truncate(max);
        // Remove separador final se truncado no meio de uma palavra
        while slug.ends_with(separador) {
            slug.pop();
        }
    }

    slug
}

/// Opções para personalizar a geração de slugs.
#[derive(Debug, Clone)]
pub struct OpcoesSlug {
    /// Caractere usado como separador entre palavras.
    /// Padrão: `-`
    pub separador: char,

    /// Comprimento máximo do slug gerado.
    /// `None` significa sem limite.
    pub max_comprimento: Option<usize>,
}

impl Default for OpcoesSlug {
    /// Cria opções com valores padrão.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// use slug_rs::OpcoesSlug;
    ///
    /// let opcoes = OpcoesSlug::default();
    /// assert_eq!(opcoes.separador, '-');
    /// assert_eq!(opcoes.max_comprimento, None);
    /// ```
    fn default() -> Self {
        OpcoesSlug {
            separador: '-',
            max_comprimento: None,
        }
    }
}

fn remover_acentos(texto: &str) -> String {
    texto.chars().map(|c| match c {
        'á'|'à'|'â'|'ã'|'ä' => 'a',
        'Á'|'À'|'Â'|'Ã'|'Ä' => 'A',
        'é'|'è'|'ê'|'ë'     => 'e',
        'É'|'È'|'Ê'|'Ë'     => 'E',
        'í'|'ì'|'î'|'ï'     => 'i',
        'Í'|'Ì'|'Î'|'Ï'     => 'I',
        'ó'|'ò'|'ô'|'õ'|'ö' => 'o',
        'Ó'|'Ò'|'Ô'|'Õ'|'Ö' => 'O',
        'ú'|'ù'|'û'|'ü'     => 'u',
        'Ú'|'Ù'|'Û'|'Ü'     => 'U',
        'ç'                  => 'c',
        'Ç'                  => 'C',
        'ñ'                  => 'n',
        'Ñ'                  => 'N',
        outro                => outro,
    }).collect()
}

#[cfg(test)]
mod testes {
    use super::*;

    #[test]
    fn basico() {
        assert_eq!(slugify("Hello World"), "hello-world");
    }

    #[test]
    fn acentos_portugueses() {
        assert_eq!(slugify("Ação"), "acao");
        assert_eq!(slugify("São Paulo"), "sao-paulo");
        assert_eq!(slugify("Café"), "cafe");
    }

    #[test]
    fn caracteres_especiais() {
        assert_eq!(slugify("foo@bar.com"), "foo-bar-com");
        assert_eq!(slugify("preço: R$10,50"), "preco-r-10-50");
    }

    #[test]
    fn espacos_multiplos() {
        assert_eq!(slugify("  a  b  c  "), "a-b-c");
    }

    #[test]
    fn string_vazia() {
        assert_eq!(slugify(""), "");
        assert_eq!(slugify("   "), "");
        assert_eq!(slugify("!!!"), "");
    }

    #[test]
    fn max_comprimento() {
        let opcoes = OpcoesSlug {
            separador: '-',
            max_comprimento: Some(5),
        };
        assert_eq!(slugify_com_opcoes("hello world", &opcoes), "hello");
    }

    #[test]
    fn separador_customizado() {
        let opcoes = OpcoesSlug {
            separador: '_',
            max_comprimento: None,
        };
        assert_eq!(slugify_com_opcoes("Hello World", &opcoes), "hello_world");
    }
}

tests/integracao.rs:

// Testes de integração — testam a API pública como um usuário externo
use slug_rs::{slugify, slugify_com_opcoes, OpcoesSlug};

#[test]
fn casos_de_uso_reais() {
    // URLs de blog
    assert_eq!(
        slugify("Como aprender Rust em 1 ano"),
        "como-aprender-rust-em-1-ano"
    );

    // Nomes de arquivo
    assert_eq!(
        slugify("Relatório Q1 2024 — Versão Final"),
        "relatorio-q1-2024-versao-final"
    );

    // Títulos com emoji (removidos)
    assert_eq!(
        slugify("Rust 🦀 é incrível"),
        "rust-e-incrivel"
    );
}

#[test]
fn compatibilidade_urls() {
    let slug = slugify("Programação em Rust: Guia Completo");

    // Deve conter apenas caracteres seguros para URLs
    assert!(slug.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'));

    // Não deve começar ou terminar com hífen
    assert!(!slug.starts_with('-'));
    assert!(!slug.ends_with('-'));

    // Não deve ter hífens consecutivos
    assert!(!slug.contains("--"));
}

#[test]
fn opcoes_padrao_equivalente() {
    let texto = "Hello World";
    let opcoes = OpcoesSlug::default();

    assert_eq!(slugify(texto), slugify_com_opcoes(texto, &opcoes));
}

examples/uso_basico.rs:

//! Exemplo básico de uso da crate slug-rs.
//!
//! Execute com: cargo run --example uso_basico

use slug_rs::{slugify, slugify_com_opcoes, OpcoesSlug};

fn main() {
    println!("═══ slug-rs: Exemplos ═══
");

    let exemplos = [
        "Hello, World!",
        "Programação em Rust",
        "São Paulo — Brasil",
        "preço: R$ 49,90",
        "2024: O Ano do Rust 🦀",
        "   espaços   extras   ",
    ];

    println!("── Slugify padrão ──");
    for texto in &exemplos {
        println!("  {:35} → {}", texto, slugify(texto));
    }

    println!("
── Com underscore e limite de 20 ──");
    let opcoes = OpcoesSlug {
        separador: '_',
        max_comprimento: Some(20),
    };
    for texto in &exemplos {
        println!("  {:35} → {}", texto, slugify_com_opcoes(texto, &opcoes));
    }
}

Mantendo uma crate popular

À medida que sua crate ganha usuários, a manutenção torna-se um compromisso:

Responda issues rapidamente — mesmo que seja só para confirmar que você viu. Silêncio por semanas é desanimador para quem contribui.

Use labels no GitHubgood-first-issue atrai contribuidores novos, help-wanted para issues que você não tem tempo, breaking-change para sinalizar PRs que requerem nova versão major.

Automatize o que puderdependabot para atualização de dependências, cargo-release para automatizar o processo de release (bump de versão, tag, publicação).

Documente decisões — um arquivo DESIGN.md ou ADRs (Architecture Decision Records) explicando por que certas escolhas foram feitas economiza horas de discussão em issues futuras.


Fontes e leituras recomendadas

  • "The Cargo Book" — referência completa — https://doc.rust-lang.org/cargo/
  • "Semantic Versioning 2.0.0" — especificação semver — https://semver.org/lang/pt-BR/
  • "SemVer Compatibility" — The Cargo Book — o que constitui breaking change — https://doc.rust-lang.org/cargo/reference/semver.html
  • cargo-release — automação de releases — https://github.com/crate-ci/cargo-release
  • cargo-audit — auditoria de dependências — https://github.com/rustsec/rustsec
  • "API Guidelines" — convenções para crates públicas — https://rust-lang.github.io/api-guidelines/
  • crates.io — repositório central — https://crates.io
  • lib.rs — busca alternativa com melhor UX — https://lib.rs

Artigo #39 de 52 | Série: Dominando Rust em 1 Ano Próximo → Artigo #40: Rust para CLI — Construindo ferramentas de linha de comando profissionais com clap


Comentários

Mais em Rust

Banco de Dados com SQLx — Persistência Real para sua API
Banco de Dados com SQLx — Persistência Real para sua API

No artigo anterior construímos uma API REST completa — mas com um problema cr...

Por que Rust existe e o que ele quer de você
Por que Rust existe e o que ele quer de você

Toda linguagem de programa&ccedil;&atilde;o nasce de uma frustra&ccedil;&atil...

Compiladores e Interpretadores — Construindo uma Linguagem com Rust
Compiladores e Interpretadores — Construindo uma Linguagem com Rust

Rust — Artigo #44 Compiladores e Interpretadores — Construindo uma Linguagem...