From 6b372b26138cb82025a554bca42029335db7a388 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Wed, 4 Feb 2026 09:18:04 +0000 Subject: [PATCH] pr 01 --- crates/prometeu-analysis/src/file_db.rs | 102 ++++++ crates/prometeu-analysis/src/ids.rs | 2 + crates/prometeu-analysis/src/lib.rs | 34 +- crates/prometeu-analysis/src/span.rs | 8 + .../tests/file_db_line_index.rs | 69 ++++ files/PRs para Junie.md | 340 ------------------ 6 files changed, 187 insertions(+), 368 deletions(-) create mode 100644 crates/prometeu-analysis/src/file_db.rs create mode 100644 crates/prometeu-analysis/src/ids.rs create mode 100644 crates/prometeu-analysis/src/span.rs create mode 100644 crates/prometeu-analysis/tests/file_db_line_index.rs diff --git a/crates/prometeu-analysis/src/file_db.rs b/crates/prometeu-analysis/src/file_db.rs new file mode 100644 index 00000000..911d3de0 --- /dev/null +++ b/crates/prometeu-analysis/src/file_db.rs @@ -0,0 +1,102 @@ +use std::collections::HashMap; +use crate::ids::FileId; + +#[derive(Default)] +pub struct FileDB { + files: Vec, + uri_to_id: HashMap, +} + +struct FileData { + uri: String, + text: String, + line_index: LineIndex, +} + +pub struct LineIndex { + line_starts: Vec, + total_len: u32, +} + +impl FileDB { + pub fn new() -> Self { + Self { + files: Vec::new(), + uri_to_id: HashMap::new(), + } + } + + pub fn upsert(&mut self, uri: &str, text: String) -> FileId { + if let Some(&id) = self.uri_to_id.get(uri) { + let line_index = LineIndex::new(&text); + self.files[id.0 as usize] = FileData { + uri: uri.to_string(), + text, + line_index, + }; + id + } else { + let id = FileId(self.files.len() as u32); + let line_index = LineIndex::new(&text); + self.files.push(FileData { + uri: uri.to_string(), + text, + line_index, + }); + self.uri_to_id.insert(uri.to_string(), id); + id + } + } + + pub fn file_id(&self, uri: &str) -> Option { + self.uri_to_id.get(uri).copied() + } + + pub fn uri(&self, id: FileId) -> &str { + &self.files[id.0 as usize].uri + } + + pub fn text(&self, id: FileId) -> &str { + &self.files[id.0 as usize].text + } + + pub fn line_index(&self, id: FileId) -> &LineIndex { + &self.files[id.0 as usize].line_index + } +} + +impl LineIndex { + pub fn new(text: &str) -> Self { + let mut line_starts = vec![0]; + for (offset, c) in text.char_indices() { + if c == '\n' { + line_starts.push((offset + 1) as u32); + } + } + Self { + line_starts, + total_len: text.len() as u32, + } + } + + pub fn offset_to_line_col(&self, offset: u32) -> (u32, u32) { + let line = match self.line_starts.binary_search(&offset) { + Ok(line) => line as u32, + Err(line) => (line - 1) as u32, + }; + let col = offset - self.line_starts[line as usize]; + (line, col) + } + + pub fn line_col_to_offset(&self, line: u32, col: u32) -> Option { + let start = *self.line_starts.get(line as usize)?; + let offset = start + col; + + let next_start = self.line_starts.get(line as usize + 1).copied().unwrap_or(self.total_len); + if offset < next_start || (offset == next_start && offset == self.total_len) { + Some(offset) + } else { + None + } + } +} diff --git a/crates/prometeu-analysis/src/ids.rs b/crates/prometeu-analysis/src/ids.rs new file mode 100644 index 00000000..9d6b6c42 --- /dev/null +++ b/crates/prometeu-analysis/src/ids.rs @@ -0,0 +1,2 @@ +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] +pub struct FileId(pub u32); diff --git a/crates/prometeu-analysis/src/lib.rs b/crates/prometeu-analysis/src/lib.rs index 76bcdaad..a70494c1 100644 --- a/crates/prometeu-analysis/src/lib.rs +++ b/crates/prometeu-analysis/src/lib.rs @@ -1,29 +1,7 @@ -//! Tipos e utilitários de análise usados pelo servidor LSP e pelo pipeline. -//! -//! Este crate expõe o `FileDB`, repositório de textos do projeto. -//! Ele será expandido nos próximos PRs para suportar snapshots e índices. -//! -//! Exemplo básico: -//! ``` -//! use prometeu_analysis::FileDB; -//! let db = FileDB::default(); -//! assert_eq!(db.len_files(), 0); -//! ``` -use serde::{Deserialize, Serialize}; +pub mod ids; +pub mod span; +pub mod file_db; -/// Banco de arquivos do projeto. -/// -/// Responsável por armazenar textos, URIs/paths e permitir criação de snapshots. -/// Implementação mínima no PR-00; funcionalidades serão adicionadas nos PRs seguintes. -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct FileDB { - // TODO(PR-03+): armazenar textos, URIs/paths, índices de linha e snapshots - #[serde(skip)] - _placeholder: (), -} - -impl FileDB { - /// Retorna a quantidade de arquivos conhecidos. - /// Implementação temporária para manter a API estável. - pub fn len_files(&self) -> usize { 0 } -} +pub use ids::FileId; +pub use span::Span; +pub use file_db::{FileDB, LineIndex}; diff --git a/crates/prometeu-analysis/src/span.rs b/crates/prometeu-analysis/src/span.rs new file mode 100644 index 00000000..e9d91378 --- /dev/null +++ b/crates/prometeu-analysis/src/span.rs @@ -0,0 +1,8 @@ +use crate::ids::FileId; + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Span { + pub file: FileId, + pub start: u32, // byte offset + pub end: u32, // byte offset, exclusive +} diff --git a/crates/prometeu-analysis/tests/file_db_line_index.rs b/crates/prometeu-analysis/tests/file_db_line_index.rs new file mode 100644 index 00000000..db505f9b --- /dev/null +++ b/crates/prometeu-analysis/tests/file_db_line_index.rs @@ -0,0 +1,69 @@ +use prometeu_analysis::{FileDB, LineIndex}; + +#[test] +fn test_line_index_roundtrip() { + let text = "line 1\nline 2\nline 3"; + let index = LineIndex::new(text); + + // Roundtrip for each character + for (offset, _) in text.char_indices() { + let (line, col) = index.offset_to_line_col(offset as u32); + let recovered_offset = index.line_col_to_offset(line, col).expect("Should recover offset"); + assert_eq!(offset as u32, recovered_offset, "Offset mismatch at line {}, col {}", line, col); + } +} + +#[test] +fn test_line_index_boundaries() { + let text = "a\nbc\n"; + let index = LineIndex::new(text); + + // "a" -> (0, 0) + assert_eq!(index.offset_to_line_col(0), (0, 0)); + assert_eq!(index.line_col_to_offset(0, 0), Some(0)); + + // "\n" -> (0, 1) + assert_eq!(index.offset_to_line_col(1), (0, 1)); + assert_eq!(index.line_col_to_offset(0, 1), Some(1)); + + // "b" -> (1, 0) + assert_eq!(index.offset_to_line_col(2), (1, 0)); + assert_eq!(index.line_col_to_offset(1, 0), Some(2)); + + // "c" -> (1, 1) + assert_eq!(index.offset_to_line_col(3), (1, 1)); + assert_eq!(index.line_col_to_offset(1, 1), Some(3)); + + // "\n" (second) -> (1, 2) + assert_eq!(index.offset_to_line_col(4), (1, 2)); + assert_eq!(index.line_col_to_offset(1, 2), Some(4)); + + // EOF (after last \n) -> (2, 0) + assert_eq!(index.offset_to_line_col(5), (2, 0)); + assert_eq!(index.line_col_to_offset(2, 0), Some(5)); + + // Out of bounds + assert_eq!(index.line_col_to_offset(2, 1), None); + assert_eq!(index.line_col_to_offset(3, 0), None); +} + +#[test] +fn test_file_db_upsert_and_access() { + let mut db = FileDB::new(); + let uri = "file:///test.txt"; + let text = "hello\nworld".to_string(); + + let id = db.upsert(uri, text.clone()); + assert_eq!(db.file_id(uri), Some(id)); + assert_eq!(db.uri(id), uri); + assert_eq!(db.text(id), &text); + + let index = db.line_index(id); + assert_eq!(index.offset_to_line_col(6), (1, 0)); // 'w' is at offset 6 + + // Update existing file + let new_text = "new content".to_string(); + let same_id = db.upsert(uri, new_text.clone()); + assert_eq!(id, same_id); + assert_eq!(db.text(id), &new_text); +} diff --git a/files/PRs para Junie.md b/files/PRs para Junie.md index 3f6b8f11..0cb4ce4a 100644 --- a/files/PRs para Junie.md +++ b/files/PRs para Junie.md @@ -101,346 +101,6 @@ Workspace (Archive.zip) tem crates: # PRs (detalhados) -## PR-00 — Infra: crates de análise + lsp (estrutura do workspace) - -**Branch:** `pr-00-analysis-lsp-foundations` - -### Decisão travada (LSP) - -* **Biblioteca escolhida:** `tower-lsp` -* **Motivo:** reduzir boilerplate, acelerar MVP e minimizar vai-e-volta. Cancelamento/incremental será feito **por cima** (tasks async + cancel tokens). - -### Objetivo - -Adicionar crates novas sem mexer no pipeline existente. - -### Entregas obrigatórias - -1. **Novo crate:** `prometeu-analysis/` - - * `Cargo.toml` (library) - * `src/lib.rs` -2. **Novo crate:** `prometeu-lsp/` - - * `Cargo.toml` (bin) - * `src/main.rs` -3. Workspace root: ajustar `Cargo.toml` (se existir no root) para incluir members. - -### Dependências (fixas) - -* `prometeu-analysis`: `serde`, `serde_json` -* `prometeu-lsp`: - - * `tower-lsp = "0.20"` (ou versão estável atual) - * `tokio` (full) - * `prometeu-analysis` (path) - -### Esqueleto obrigatório (`prometeu-lsp/src/main.rs`) - -```rust -use tower_lsp::{LspService, Server}; -use tokio::sync::RwLock; -use std::sync::Arc; - -#[derive(Default)] -struct AnalysisDb { - // FileDB, AstArena, SymbolArena, TypeArena, Diagnostics -} - -struct Backend { - db: Arc>, -} - -#[tower_lsp::async_trait] -impl tower_lsp::LanguageServer for Backend { - async fn initialize(&self, _: tower_lsp::lsp_types::InitializeParams) - -> tower_lsp::jsonrpc::Result { - Ok(tower_lsp::lsp_types::InitializeResult { - capabilities: tower_lsp::lsp_types::ServerCapabilities { - text_document_sync: Some(tower_lsp::lsp_types::TextDocumentSyncCapability::Kind( - tower_lsp::lsp_types::TextDocumentSyncKind::FULL, - )), - // Declaramos capacidades desde já para evitar churn posterior. - definition_provider: Some(true.into()), - document_symbol_provider: Some(true.into()), - workspace_symbol_provider: Some(true.into()), - hover_provider: Some(true.into()), - references_provider: Some(true.into()), - rename_provider: Some(tower_lsp::lsp_types::OneOf::Left(true)), - completion_provider: Some(tower_lsp::lsp_types::CompletionOptions { - resolve_provider: Some(false), - trigger_characters: Some(vec![".".into(), ":".into()]), - ..Default::default() - }), - semantic_tokens_provider: Some( - tower_lsp::lsp_types::SemanticTokensServerCapabilities::SemanticTokensOptions( - tower_lsp::lsp_types::SemanticTokensOptions { - legend: tower_lsp::lsp_types::SemanticTokensLegend { - // preenchido no PR-12 - token_types: vec![], - token_modifiers: vec![], - }, - full: Some(tower_lsp::lsp_types::SemanticTokensFullOptions::Bool(true)), - range: None, - ..Default::default() - }, - ), - ), - ..Default::default() - }, - ..Default::default() - }) - } - - async fn initialized(&self, _: tower_lsp::lsp_types::InitializedParams) {} -} - -#[tokio::main] -async fn main() { - let stdin = tokio::io::stdin(); - let stdout = tokio::io::stdout(); - let (service, socket) = LspService::new(|_| Backend { db: Arc::new(RwLock::new(AnalysisDb::default())) }); - Server::new(stdin, stdout, socket).serve(service).await; -} -``` - -### Contrato exato do AnalysisDb (travado) - -> **Regra:** o LSP nunca recalcula segurando `RwLock` por muito tempo. Toda análise pesada roda fora do lock e depois faz swap. - -Criar arquivo: `prometeu-lsp/src/analysis_db.rs` - -```rust -use std::sync::Arc; -use tokio::sync::RwLock; -use tokio_util::sync::CancellationToken; - -use prometeu_analysis::FileDB; - -#[derive(Default)] -pub struct AnalysisDb { - pub file_db: FileDB, - - // Os campos abaixo serão conectados conforme PR-03/04/05 (podem começar como None) - // pub ast: Option, - // pub symbols: Option, - // pub types: Option, - // pub diagnostics: Vec, - - /// Incrementa a cada rebuild concluído com sucesso - pub revision: u64, - - /// Cancel token do último rebuild em progresso (se houver) - pub active_rebuild: Option, -} - -pub type SharedDb = Arc>; -``` - -No `main.rs`, **substituir** o `AnalysisDb` local pelo import acima (ou manter local e delegar, mas sem duplicar). - -### Modelo de cancelamento e coalescing (travado) - -> Objetivo: quando chegam múltiplos `didChange` em sequência, cancelar o rebuild anterior e rodar apenas o último. - -Adicionar dependência fixa em `prometeu-lsp/Cargo.toml`: - -* `tokio-util` - -Criar arquivo: `prometeu-lsp/src/rebuild.rs` - -```rust -use tokio_util::sync::CancellationToken; -use crate::analysis_db::SharedDb; - -/// Solicita rebuild do projeto (coarse). Cancela rebuild anterior se em progresso. -/// Implementação inicial: apenas cria task e retorna. -pub async fn request_rebuild(db: SharedDb) { - // 1) lock curto: cancelar token anterior e instalar token novo - // 2) spawn task: roda análise fora do lock - // 3) lock curto: se token não cancelado, swap estado + revision++ -} -``` - -Regras obrigatórias: - -1. `request_rebuild` deve: - - * cancelar `active_rebuild` anterior (se existir) - * instalar um token novo - * `tokio::spawn` uma task de rebuild -2. A task deve checar `token.is_cancelled()` em pontos seguros: - - * antes de começar - * após parsing - * após resolver - * após typecheck -3. O lock (`RwLock`) deve ser segurado apenas: - - * para trocar `active_rebuild` - * para aplicar o resultado final - -### Critérios de aceite - -* `cargo build -p prometeu-lsp` ok -* LSP inicializa em VS Code/Neovim sem crash -* Nenhuma feature além do declarado - ---- - -## Política fixa de cancelamento e rebuild (tower-lsp) - -### Objetivo - -Garantir que edições rápidas não gerem backlog de análises e evitar race conditions. - -### Decisão travada - -* Usar `tokio_util::sync::CancellationToken` -* **Um token por rebuild** -* Qualquer `didChange`: - - 1. Cancela o rebuild anterior - 2. Cria novo token - 3. Agenda nova task de análise - -### Estruturas obrigatórias - -```rust -use tokio_util::sync::CancellationToken; - -struct AnalysisController { - current: Option, -} -``` - -### Regras - -* Nunca segurar `RwLock` durante parsing/resolver/typecheck. -* Fluxo correto: - - 1. Clonar snapshot dos textos (FileDB) - 2. Soltar lock - 3. Rodar análise pesada - 4. Re-adquirir lock e substituir estado -* Tasks devem checar `token.is_cancelled()` entre fases (parse / bind / type). - -### Critérios de aceite - -* Digitar rapidamente não acumula tasks. -* Apenas o último estado publica diagnostics. - ---- - -## Contrato fechado do AnalysisDb (fonte de verdade do LSP) - -### Objetivo - -Eliminar ambiguidade sobre o que vive no estado compartilhado. - -### Estrutura obrigatória - -```rust -#[derive(Default)] -pub struct AnalysisDb { - pub files: FileDB, - pub ast: Option, - pub symbols: Option, - pub types: Option, - pub type_facts: Option, - pub diagnostics: Vec, -} -``` - -### Regras - -* `AnalysisDb` **nunca** armazena estado parcialmente válido. -* Se uma fase falhar: - - * `ast/symbols/types` podem ficar `None` - * `diagnostics` deve estar preenchido -* LSP handlers **não** rodam análise: apenas leem `AnalysisDb`. - -### Leitura segura - -* Todos os handlers (`definition`, `hover`, `references`, etc.) devem: - - * adquirir `db.read()` - * lidar com `Option::None` retornando `null` / resposta vazia - -### Escrita segura - -* Apenas a task de rebuild escreve no `AnalysisDb`. -* Escrita sempre substitui o estado inteiro (swap lógico). - -### Critérios de aceite - -* Nenhum handler pode panic por `None`. -* Rebuild substitui estado de forma atômica (do ponto de vista do LSP). - ---- - - -# Série PR-00.X — Reestruturação de crates (Arena-Driven + LSP-ready) - -> **Objetivo da série PR-00.X:** reorganizar o workspace em crates com fronteiras claras, **sem alterar comportamento**, preparando o terreno para o Prometeu Arena-Driven (compiler/analysis) e um LSP completo depois. -> -> **Novo layout alvo (travado):** -> -> * `prometeu-bytecode` (fica) -> * `prometeu-abi` (novo nome para o atual `prometeu-core` depurado) = **types + model + protocols** (sem execução) -> * `prometeu-vm` (novo) = execução da VM -> * `prometeu-kernel` (novo) = OS + FS + syscalls (implementa interface para a VM) -> * `prometeu-runtime-desktop` (fica) = host desktop -> * `prometeu-compiler` (fica) -> * `prometeu` (CLI) (fica) -> -> **Regra de ouro:** 1 PR = 1 fronteira. Nada de mover tudo de uma vez. -> -> **Regras para Junie (serie 00.X):** -> -> 1. **Não mudar lógica**; apenas mover, ajustar imports, e deixar testes verdes. -> 2. Se algo exigir mudança de API pública, criar `TODO(PR-00.Y)` e parar. -> 3. Sempre manter o workspace compilável a cada PR. - ---- -## PR-00.4 — Depurar `prometeu-core`: remover execução e virar apenas compat layer - -**Branch:** `pr-00-4-deprecate-prometeu-core` - -### Objetivo - -Eliminar o conteúdo executável restante do `prometeu-core`, deixando-o como reexport/compat por um curto período. - -### Passos - -* `hardware` e `firmware/hub` ficam onde estiverem por enquanto (podem virar PR-00.5/00.6 se necessário) -* `prometeu-core` passa a reexportar: - - * `prometeu-abi` - * `prometeu-vm` - * `prometeu-kernel` - -### Critérios - -* Zero imports internos `crate::virtual_machine` etc. fora da facade. - ---- - -## PR-00.6 — Remover `prometeu-core` e migrar consumidores - -**Branch:** `pr-00-6-remove-prometeu-core` - -### Objetivo - -Apagar `prometeu-core` após migração total. - -### Critérios - -* Nenhum crate depende de `prometeu-core`. - ---- - ## PR-01 — FileDB + LineIndex (base do LSP e spans) **Branch:** `pr-01-filedb`