This commit is contained in:
bQUARKz 2026-02-04 09:18:04 +00:00
parent 97c6ccb6f2
commit 6b372b2613
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
6 changed files with 187 additions and 368 deletions

View File

@ -0,0 +1,102 @@
use std::collections::HashMap;
use crate::ids::FileId;
#[derive(Default)]
pub struct FileDB {
files: Vec<FileData>,
uri_to_id: HashMap<String, FileId>,
}
struct FileData {
uri: String,
text: String,
line_index: LineIndex,
}
pub struct LineIndex {
line_starts: Vec<u32>,
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<FileId> {
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<u32> {
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
}
}
}

View File

@ -0,0 +1,2 @@
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub struct FileId(pub u32);

View File

@ -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};

View File

@ -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
}

View File

@ -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);
}

View File

@ -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<RwLock<AnalysisDb>>,
}
#[tower_lsp::async_trait]
impl tower_lsp::LanguageServer for Backend {
async fn initialize(&self, _: tower_lsp::lsp_types::InitializeParams)
-> tower_lsp::jsonrpc::Result<tower_lsp::lsp_types::InitializeResult> {
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<AstArena>,
// pub symbols: Option<SymbolArena>,
// pub types: Option<TypeArena>,
// pub diagnostics: Vec<Diagnostic>,
/// Incrementa a cada rebuild concluído com sucesso
pub revision: u64,
/// Cancel token do último rebuild em progresso (se houver)
pub active_rebuild: Option<CancellationToken>,
}
pub type SharedDb = Arc<RwLock<AnalysisDb>>;
```
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<CancellationToken>,
}
```
### Regras
* Nunca segurar `RwLock<AnalysisDb>` 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<AstArena>,
pub symbols: Option<SymbolArena>,
pub types: Option<TypeArena>,
pub type_facts: Option<TypeFacts>,
pub diagnostics: Vec<Diagnostic>,
}
```
### 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`