{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks","title":"Computación Incremental con los Frameworks Salsa y Adapton","description":"Análisis de la computación incremental demand-driven tal como se implementa en rust-analyzer, memoización con invalidación automática, seguimiento de dependencias y los patrones arquitectónicos que permiten respuestas sub-milisegundo en IDEs.","language":"es","datePublished":"2025-10-06","dateModified":null,"contentFormat":"text/markdown","content":"Compilers\n\n# Computación Incremental con los Frameworks Salsa y Adapton\n\n2025-10-069 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks>)\n\n## El problema: la recomputación está en todos lados\n\nConsiderar qué sucede al escribir un carácter en un IDE. El language server necesita:\n\n1. Re-lexear el archivo modificado (o al menos la región modificada)\n2. Re-parsear los nodos del syntax tree afectados\n3. Re-resolver nombres en el scope modificado\n4. Re-typecheckear expresiones que dependen de tipos que cambiaron\n5. Regenerar diagnósticos, completions, hover info\n\nPara una implementación ingenua, cada pulsación de tecla dispara una ejecución completa del pipeline. En un codebase de 100K líneas con type inference complejo, eso implica fácilmente 2-5 segundos. Los usuarios notan cualquier demora por encima de 100ms. Algo tiene que ceder.\n\nLa respuesta tradicional es \"almacenar todo en caché e invalidar con cuidado.\" Pero la invalidación manual de caché en un pipeline de compilador es una pesadilla. Las dependencias son profundas, circulares, y cambian de forma dependiendo del input. Si se omite un edge de invalidación, quedan resultados obsoletos. Si se sobre-invalida, se vuelve a recomputación completa.\n\nLos frameworks de computación incremental automatizan este proceso. Se describe la computación como un grafo de queries, el framework rastrea dependencias en runtime, y cuando un input cambia, recomputa exactamente lo necesario, nada más.\n\n## Salsa: computación incremental demand-driven\n\n[Salsa](<https://github.com/salsa-rs/salsa>) es un framework de Rust desarrollado por el equipo de rust-analyzer (originalmente por Niko Matsakis). Implementa computación incremental demand-driven con rastreo automático de dependencias.\n\nEl modelo central tiene tres conceptos:\n\n1. **Inputs**, valores mutables que se establecen desde fuera (contenido de archivos, configuración)\n2. **Funciones tracked**, computaciones puras que leen inputs y otras funciones tracked\n3. **La database**, mantiene todos los inputs, resultados almacenados en caché y metadatos de dependencias\n\nA continuación se muestra un ejemplo mínimo, un sistema que analiza archivos y computa un índice de símbolos:\n\n```rust\n#[salsa::input]\npub struct SourceFile {\n #[return_ref]\n pub name: String,\n #[return_ref]\n pub contents: String,\n}\n\n#[salsa::tracked]\npub struct ParsedFile<'db> {\n #[return_ref]\n pub ast: Ast,\n #[return_ref]\n pub errors: Vec<ParseError>,\n}\n\n#[salsa::tracked]\nfn parse_file<'db>(db: &'db dyn Db, file: SourceFile) -> ParsedFile<'db> {\n let contents = file.contents(db);\n let (ast, errors) = parser::parse(&contents);\n ParsedFile::new(db, ast, errors)\n}\n\n#[salsa::tracked]\nfn file_symbols<'db>(db: &'db dyn Db, file: SourceFile) -> Vec<Symbol> {\n let parsed = parse_file(db, file);\n let ast = parsed.ast(db);\n extract_symbols(ast)\n}\n\n#[salsa::tracked]\nfn all_symbols<'db>(db: &'db dyn Db, files: &[SourceFile]) -> SymbolIndex {\n let mut index = SymbolIndex::new();\n for &file in files {\n for symbol in file_symbols(db, file) {\n index.insert(symbol);\n }\n }\n index\n}\n```\n\nAl invocar ` all_symbols `, Salsa ejecuta la computación completa y registra que ` all_symbols ` depende de ` file_symbols ` para cada archivo, que depende de ` parse_file `, que depende del input ` contents ` del archivo.\n\nAl cambiar el contenido de un archivo y realizar la siguiente llamada a ` all_symbols `, Salsa:\n\n1. Nota que el input ` contents ` cambió para un archivo\n2. Verifica si el resultado de ` parse_file ` realmente cambió (quizá solo se modificó un comentario)\n3. Si el resultado del parse cambió, verifica si ` file_symbols ` cambió\n4. Solo recomputa ` all_symbols ` si la lista real de símbolos de ese archivo cambió\n\nEste es el insight clave: **early cutoff**. Incluso si un input cambia, si el resultado intermedio es igual al anterior, las computaciones downstream no necesitan re-correrse. En la práctica, esto significa que cambiar el cuerpo de una función no invalida el índice de símbolos, los símbolos no cambiaron.\n\n### El contador de revisiones\n\nInternamente, Salsa usa un contador global de revisiones. Cada vez que un input cambia, la revisión se incrementa. Cada valor almacenado en caché guarda dos cosas:\n\n- **verified\\_at**: la última revisión donde este valor fue confirmado válido\n- **changed\\_at**: la última revisión donde este valor realmente cambió\n\nAl acceder a un valor almacenado, Salsa verifica si ` verified_at == current_revision `. Si no es así, recorre el grafo de dependencias hacia atrás para determinar si algún input del que este valor depende (transitivamente) tiene ` changed_at > verified_at `. Si ninguna dependencia cambió, el valor sigue siendo válido, se actualiza ` verified_at ` y se devuelve el resultado almacenado sin recomputar.\n\n```\nRevisión 1: Computación inicial\n file.contents changed_at=1\n parse_file changed_at=1 verified_at=1\n file_symbols changed_at=1 verified_at=1\n all_symbols changed_at=1 verified_at=1\n\nRevisión 2: Editar archivo A (solo comentario)\n file_A.contents changed_at=2\n parse_file(A) → recomputar → AST idéntico → changed_at=1 verified_at=2\n file_symbols(A) → dependencia sin cambios → changed_at=1 verified_at=2\n all_symbols → dependencia sin cambios → changed_at=1 verified_at=2\n // Trabajo total: re-parseó un archivo. Todo lo demás fue gratis.\n\nRevisión 3: Editar archivo B (agregar función)\n file_B.contents changed_at=3\n parse_file(B) → recomputar → AST diferente → changed_at=3 verified_at=3\n file_symbols(B) → recomputar → nuevo símbolo → changed_at=3 verified_at=3\n all_symbols → recomputar (una dep cambió) → changed_at=3 verified_at=3\n // parse_file(A), file_symbols(A) intactos, siguen verified_at=2, válidos.\n```\n\n### Manejo de ciclos\n\nLos compiladores frecuentemente tienen dependencias circulares, una implementación de trait puede referenciar un tipo que referencia al trait. Salsa maneja ciclos explícitamente:\n\n```rust\n#[salsa::tracked(cycle_fn=recover_type_cycle)]\nfn infer_type<'db>(db: &'db dyn Db, expr: ExprId) -> Type {\n // ... puede llamar a infer_type en otras expresiones\n}\n\nfn recover_type_cycle<'db>(\n db: &'db dyn Db,\n _cycle: &salsa::Cycle,\n expr: ExprId,\n) -> Type {\n Type::Error // Romper el ciclo con un tipo error\n}\n```\n\nCuando Salsa detecta un ciclo (el mismo query se invoca recursivamente con la misma key), ejecuta la función de recovery. La función de recuperación determina el comportamiento, tipos de error, iteración de punto fijo, o cualquier estrategia apropiada para el dominio.\n\n## Adapton: la base académica\n\n[Adapton](<http://adapton.org/>) precede a Salsa y provee el framework teórico. El artículo fundamental (Hammer et al., 2014) introduce \"demanded computation graphs\", un modelo donde las computaciones son nodos, las dependencias de datos son edges, y el sistema propaga cambios lazily a través del grafo.\n\nLa contribución clave es la distinción entre dos estrategias:\n\n**From-scratch consistency**: verificar que el grafo entero es consistente antes de devolver cualquier resultado. Caro upfront pero garantiza corrección.\n\n**Demanded consistency**: solo verificar las porciones del grafo que el query actual toca. Lazy, eficiente, pero requiere ordenamiento cuidadoso.\n\nSalsa usa demanded consistency. Al consultar ` all_symbols `, no verifica cada valor almacenado en el sistema, solo los que están en la cadena de dependencias de ` all_symbols `.\n\nA continuación se presenta una implementación simplificada estilo Adapton en TypeScript para concretizar los conceptos:\n\n```typescript\ntype CellId = number;\ntype ThunkId = number;\n\nlet currentRevision = 0;\nlet activeThunk: ThunkId | null = null;\n\ninterface Cell<T> {\n value: T;\n changedAt: number;\n dependents: Set<ThunkId>;\n}\n\ninterface Thunk<T> {\n fn: () => T;\n value: T | undefined;\n verifiedAt: number;\n changedAt: number;\n dependencies: Set<CellId | ThunkId>;\n dependents: Set<ThunkId>;\n}\n\nconst cells = new Map<CellId, Cell<any>>();\nconst thunks = new Map<ThunkId, Thunk<any>>();\n\nfunction createCell<T>(value: T): CellId {\n const id = cells.size;\n cells.set(id, { value, changedAt: currentRevision, dependents: new Set() });\n return id;\n}\n\nfunction setCell<T>(id: CellId, value: T): void {\n const cell = cells.get(id)!;\n if (Object.is(cell.value, value)) return;\n currentRevision++;\n cell.value = value;\n cell.changedAt = currentRevision;\n}\n\nfunction readCell<T>(id: CellId): T {\n const cell = cells.get(id)!;\n if (activeThunk !== null) {\n const thunk = thunks.get(activeThunk)!;\n thunk.dependencies.add(id);\n cell.dependents.add(activeThunk);\n }\n return cell.value;\n}\n\nfunction createThunk<T>(fn: () => T): ThunkId {\n const id = thunks.size + 10000;\n thunks.set(id, {\n fn,\n value: undefined,\n verifiedAt: -1,\n changedAt: -1,\n dependencies: new Set(),\n dependents: new Set(),\n });\n return id;\n}\n\nfunction queryThunk<T>(id: ThunkId): T {\n const thunk = thunks.get(id)!;\n\n if (thunk.verifiedAt === currentRevision) {\n return thunk.value!;\n }\n\n let needsRecompute = thunk.value === undefined;\n if (!needsRecompute) {\n for (const dep of thunk.dependencies) {\n if (dep < 10000) {\n const cell = cells.get(dep)!;\n if (cell.changedAt > thunk.verifiedAt) {\n needsRecompute = true;\n break;\n }\n } else {\n queryThunk(dep);\n const depThunk = thunks.get(dep)!;\n if (depThunk.changedAt > thunk.verifiedAt) {\n needsRecompute = true;\n break;\n }\n }\n }\n }\n\n if (needsRecompute) {\n thunk.dependencies.clear();\n const prevActive = activeThunk;\n activeThunk = id;\n const newValue = thunk.fn();\n activeThunk = prevActive;\n\n if (!Object.is(newValue, thunk.value)) {\n thunk.changedAt = currentRevision;\n thunk.value = newValue;\n }\n }\n\n thunk.verifiedAt = currentRevision;\n return thunk.value!;\n}\n```\n\nSon ~80 líneas que capturan el algoritmo esencial. Los frameworks reales agregan garbage collection de entradas de cache sin usar, ejecución paralela, durabilidad y detección de ciclos apropiada, pero la idea core es tracking de dependencias más early cutoff.\n\n## Aplicando esto fuera de compiladores\n\nLos patrones de computación incremental se aplican efectivamente en contextos no-compilador. Consideremos un dashboard que agrega datos de 15 microservicios. Cada servicio se actualiza con diferente frecuencia, algunos cada segundo, otros cada hora. El dashboard computa métricas derivadas de combinaciones de estas fuentes.\n\nAntes: cada render fetchea las 15 fuentes y recomputa todas las métricas. Latencia: 800ms.\n\nDespués: al modelar cada fuente de datos como un input cell, cada métrica como una computación tracked, y usar un framework liviano estilo Adapton, en la mayoría de los renders solo 1-2 fuentes cambian, así que solo las métricas afectadas se recomputan. La latencia baja a 30-80ms dependiendo de qué cambió.\n\nEl patrón funciona en cualquier lugar donde tengas: inputs que cambian lento, computaciones derivadas caras, y consultas repetidas. Build systems (el grafo de acciones de Bazel), frameworks de UI reactivos (señales de Solid.js), motores de hojas de cálculo, todos usan variaciones de la misma idea.\n\n## Conclusiones prácticas\n\nPara cualquier sistema que derive datos repetidamente de inputs que cambian, vale la pena considerar si la computación incremental encaja. La inversión upfront en modelar la computación como un grafo de queries paga dividendos cuando el dataset crece. Salsa es un punto de partida natural para proyectos Rust, y los conceptos se portan a otros lenguajes (el sketch en TypeScript de arriba sirve como punto de partida). El modelo mental, inputs, funciones tracked, grafos de dependencias, early cutoff, es la parte valiosa, no ningún framework específico.\n\nComo ejemplo concreto, reestructurar un language server alrededor de límites apropiados de queries de Salsa llevó el autocompletado de 400ms a 8ms. La computación era la misma. La única diferencia fue definir dónde estaban los límites de queries.\n\n## Preguntas frecuentes\n\n### ¿Qué es la computación incremental?\n\nLa computación incremental es un paradigma de programación donde las computaciones reutilizan automáticamente resultados previos cuando los inputs cambian parcialmente. En lugar de recomputar todo desde cero, el sistema identifica qué cambió y solo recomputa las porciones afectadas del resultado.\n\n### ¿En qué se diferencia Salsa de la memoización regular?\n\nLa memoización regular almacena resultados por input pero no tiene mecanismo de invalidación, las entradas obsoletas persisten indefinidamente. Salsa rastrea dependencias entre computaciones, de modo que cuando un input cambia, determina exactamente qué resultados almacenados están afectados e invalida solo esos, preservando todos los resultados no afectados.\n\n### ¿Se puede usar Salsa fuera de tooling de compiladores?\n\nSí, Salsa es un framework de computación incremental de propósito general. Cualquier carga de trabajo donde se computan datos derivados repetidamente a partir de inputs que cambian con poca frecuencia se beneficia, build systems, data pipelines, interfaces reactivas y gestión de configuración son todos case es de uso viables.\n\n## Tips\n\n1. **Tip:** Al modelar computaciones incrementales, es importante que la granularidad de input coincida con la de invalidación. Si se rastrean cambios por archivo pero se computa por función, se recomputarán archivos enteros cuando cambie una sola función, anulando el propósito.\n2. **Tip:** Separar siempre las ` lecturas ` de las ` escrituras ` en las definiciones de queries. Un query que lee y muta estado rompe el DAG incremental y fuerza recomputación completa downstream.\n3. **Tip:** Utilizar ` salsa::accumulator ` para recolección de diagnósticos en lugar de producir side-effects durante queries. Los accumulators se integran de forma limpia con el grafo de dependencias y se invalidan correctamente.\n4. **Tip:** Perfilar las tasas de hit de queries con ` salsa::DebugQueryTable ` antes de optimizar. Se ha observado equipos dedicar semanas a optimizar un query que ya tenía 99.8% de cache hit, el cuello de botella estaba en otro lugar.\n\n- incremental-computation\n- salsa\n- adapton\n- rust-analyzer\n- compilers\n- ide\n- demand-driven\n- memoization\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Computación Incremental con los Frameworks Salsa y Adapton","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#title"},{"level":2,"text":"El problema: la recomputación está en todos lados","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#el-problema-la-recomputacin-est-en-todos-lados"},{"level":2,"text":"Salsa: computación incremental demand-driven","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#salsa-computacin-incremental-demand-driven"},{"level":3,"text":"El contador de revisiones","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#el-contador-de-revisiones"},{"level":3,"text":"Manejo de ciclos","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#manejo-de-ciclos"},{"level":2,"text":"Adapton: la base académica","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#adapton-la-base-acadmica"},{"level":2,"text":"Aplicando esto fuera de compiladores","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#aplicando-esto-fuera-de-compiladores"},{"level":2,"text":"Conclusiones prácticas","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#conclusiones-prcticas"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#questions"},{"level":3,"text":"¿Qué es la computación incremental?","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#question-1"},{"level":3,"text":"¿En qué se diferencia Salsa de la memoización regular?","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#question-2"},{"level":3,"text":"¿Se puede usar Salsa fuera de tooling de compiladores?","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#article-tips"}],"sections":[{"heading":"Computación Incremental con los Frameworks Salsa y Adapton","level":1,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks","content":"Compilers"},{"heading":"Computación Incremental con los Frameworks Salsa y Adapton","level":1,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#title","content":"2025-10-069 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks>)"},{"heading":"El problema: la recomputación está en todos lados","level":2,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#el-problema-la-recomputacin-est-en-todos-lados","content":"Considerar qué sucede al escribir un carácter en un IDE. El language server necesita:\n\n1. Re-lexear el archivo modificado (o al menos la región modificada)\n2. Re-parsear los nodos del syntax tree afectados\n3. Re-resolver nombres en el scope modificado\n4. Re-typecheckear expresiones que dependen de tipos que cambiaron\n5. Regenerar diagnósticos, completions, hover info\n\nPara una implementación ingenua, cada pulsación de tecla dispara una ejecución completa del pipeline. En un codebase de 100K líneas con type inference complejo, eso implica fácilmente 2-5 segundos. Los usuarios notan cualquier demora por encima de 100ms. Algo tiene que ceder.\n\nLa respuesta tradicional es \"almacenar todo en caché e invalidar con cuidado.\" Pero la invalidación manual de caché en un pipeline de compilador es una pesadilla. Las dependencias son profundas, circulares, y cambian de forma dependiendo del input. Si se omite un edge de invalidación, quedan resultados obsoletos. Si se sobre-invalida, se vuelve a recomputación completa.\n\nLos frameworks de computación incremental automatizan este proceso. Se describe la computación como un grafo de queries, el framework rastrea dependencias en runtime, y cuando un input cambia, recomputa exactamente lo necesario, nada más."},{"heading":"Salsa: computación incremental demand-driven","level":2,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#salsa-computacin-incremental-demand-driven","content":"[Salsa](<https://github.com/salsa-rs/salsa>) es un framework de Rust desarrollado por el equipo de rust-analyzer (originalmente por Niko Matsakis). Implementa computación incremental demand-driven con rastreo automático de dependencias.\n\nEl modelo central tiene tres conceptos:\n\n1. **Inputs**, valores mutables que se establecen desde fuera (contenido de archivos, configuración)\n2. **Funciones tracked**, computaciones puras que leen inputs y otras funciones tracked\n3. **La database**, mantiene todos los inputs, resultados almacenados en caché y metadatos de dependencias\n\nA continuación se muestra un ejemplo mínimo, un sistema que analiza archivos y computa un índice de símbolos:\n\n```rust\n#[salsa::input]\npub struct SourceFile {\n #[return_ref]\n pub name: String,\n #[return_ref]\n pub contents: String,\n}\n\n#[salsa::tracked]\npub struct ParsedFile<'db> {\n #[return_ref]\n pub ast: Ast,\n #[return_ref]\n pub errors: Vec<ParseError>,\n}\n\n#[salsa::tracked]\nfn parse_file<'db>(db: &'db dyn Db, file: SourceFile) -> ParsedFile<'db> {\n let contents = file.contents(db);\n let (ast, errors) = parser::parse(&contents);\n ParsedFile::new(db, ast, errors)\n}\n\n#[salsa::tracked]\nfn file_symbols<'db>(db: &'db dyn Db, file: SourceFile) -> Vec<Symbol> {\n let parsed = parse_file(db, file);\n let ast = parsed.ast(db);\n extract_symbols(ast)\n}\n\n#[salsa::tracked]\nfn all_symbols<'db>(db: &'db dyn Db, files: &[SourceFile]) -> SymbolIndex {\n let mut index = SymbolIndex::new();\n for &file in files {\n for symbol in file_symbols(db, file) {\n index.insert(symbol);\n }\n }\n index\n}\n```\n\nAl invocar ` all_symbols `, Salsa ejecuta la computación completa y registra que ` all_symbols ` depende de ` file_symbols ` para cada archivo, que depende de ` parse_file `, que depende del input ` contents ` del archivo.\n\nAl cambiar el contenido de un archivo y realizar la siguiente llamada a ` all_symbols `, Salsa:\n\n1. Nota que el input ` contents ` cambió para un archivo\n2. Verifica si el resultado de ` parse_file ` realmente cambió (quizá solo se modificó un comentario)\n3. Si el resultado del parse cambió, verifica si ` file_symbols ` cambió\n4. Solo recomputa ` all_symbols ` si la lista real de símbolos de ese archivo cambió\n\nEste es el insight clave: **early cutoff**. Incluso si un input cambia, si el resultado intermedio es igual al anterior, las computaciones downstream no necesitan re-correrse. En la práctica, esto significa que cambiar el cuerpo de una función no invalida el índice de símbolos, los símbolos no cambiaron."},{"heading":"El contador de revisiones","level":3,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#el-contador-de-revisiones","content":"Internamente, Salsa usa un contador global de revisiones. Cada vez que un input cambia, la revisión se incrementa. Cada valor almacenado en caché guarda dos cosas:\n\n- **verified\\_at**: la última revisión donde este valor fue confirmado válido\n- **changed\\_at**: la última revisión donde este valor realmente cambió\n\nAl acceder a un valor almacenado, Salsa verifica si ` verified_at == current_revision `. Si no es así, recorre el grafo de dependencias hacia atrás para determinar si algún input del que este valor depende (transitivamente) tiene ` changed_at > verified_at `. Si ninguna dependencia cambió, el valor sigue siendo válido, se actualiza ` verified_at ` y se devuelve el resultado almacenado sin recomputar.\n\n```\nRevisión 1: Computación inicial\n file.contents changed_at=1\n parse_file changed_at=1 verified_at=1\n file_symbols changed_at=1 verified_at=1\n all_symbols changed_at=1 verified_at=1\n\nRevisión 2: Editar archivo A (solo comentario)\n file_A.contents changed_at=2\n parse_file(A) → recomputar → AST idéntico → changed_at=1 verified_at=2\n file_symbols(A) → dependencia sin cambios → changed_at=1 verified_at=2\n all_symbols → dependencia sin cambios → changed_at=1 verified_at=2\n // Trabajo total: re-parseó un archivo. Todo lo demás fue gratis.\n\nRevisión 3: Editar archivo B (agregar función)\n file_B.contents changed_at=3\n parse_file(B) → recomputar → AST diferente → changed_at=3 verified_at=3\n file_symbols(B) → recomputar → nuevo símbolo → changed_at=3 verified_at=3\n all_symbols → recomputar (una dep cambió) → changed_at=3 verified_at=3\n // parse_file(A), file_symbols(A) intactos, siguen verified_at=2, válidos.\n```"},{"heading":"Manejo de ciclos","level":3,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#manejo-de-ciclos","content":"Los compiladores frecuentemente tienen dependencias circulares, una implementación de trait puede referenciar un tipo que referencia al trait. Salsa maneja ciclos explícitamente:\n\n```rust\n#[salsa::tracked(cycle_fn=recover_type_cycle)]\nfn infer_type<'db>(db: &'db dyn Db, expr: ExprId) -> Type {\n // ... puede llamar a infer_type en otras expresiones\n}\n\nfn recover_type_cycle<'db>(\n db: &'db dyn Db,\n _cycle: &salsa::Cycle,\n expr: ExprId,\n) -> Type {\n Type::Error // Romper el ciclo con un tipo error\n}\n```\n\nCuando Salsa detecta un ciclo (el mismo query se invoca recursivamente con la misma key), ejecuta la función de recovery. La función de recuperación determina el comportamiento, tipos de error, iteración de punto fijo, o cualquier estrategia apropiada para el dominio."},{"heading":"Adapton: la base académica","level":2,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#adapton-la-base-acadmica","content":"[Adapton](<http://adapton.org/>) precede a Salsa y provee el framework teórico. El artículo fundamental (Hammer et al., 2014) introduce \"demanded computation graphs\", un modelo donde las computaciones son nodos, las dependencias de datos son edges, y el sistema propaga cambios lazily a través del grafo.\n\nLa contribución clave es la distinción entre dos estrategias:\n\n**From-scratch consistency**: verificar que el grafo entero es consistente antes de devolver cualquier resultado. Caro upfront pero garantiza corrección.\n\n**Demanded consistency**: solo verificar las porciones del grafo que el query actual toca. Lazy, eficiente, pero requiere ordenamiento cuidadoso.\n\nSalsa usa demanded consistency. Al consultar ` all_symbols `, no verifica cada valor almacenado en el sistema, solo los que están en la cadena de dependencias de ` all_symbols `.\n\nA continuación se presenta una implementación simplificada estilo Adapton en TypeScript para concretizar los conceptos:\n\n```typescript\ntype CellId = number;\ntype ThunkId = number;\n\nlet currentRevision = 0;\nlet activeThunk: ThunkId | null = null;\n\ninterface Cell<T> {\n value: T;\n changedAt: number;\n dependents: Set<ThunkId>;\n}\n\ninterface Thunk<T> {\n fn: () => T;\n value: T | undefined;\n verifiedAt: number;\n changedAt: number;\n dependencies: Set<CellId | ThunkId>;\n dependents: Set<ThunkId>;\n}\n\nconst cells = new Map<CellId, Cell<any>>();\nconst thunks = new Map<ThunkId, Thunk<any>>();\n\nfunction createCell<T>(value: T): CellId {\n const id = cells.size;\n cells.set(id, { value, changedAt: currentRevision, dependents: new Set() });\n return id;\n}\n\nfunction setCell<T>(id: CellId, value: T): void {\n const cell = cells.get(id)!;\n if (Object.is(cell.value, value)) return;\n currentRevision++;\n cell.value = value;\n cell.changedAt = currentRevision;\n}\n\nfunction readCell<T>(id: CellId): T {\n const cell = cells.get(id)!;\n if (activeThunk !== null) {\n const thunk = thunks.get(activeThunk)!;\n thunk.dependencies.add(id);\n cell.dependents.add(activeThunk);\n }\n return cell.value;\n}\n\nfunction createThunk<T>(fn: () => T): ThunkId {\n const id = thunks.size + 10000;\n thunks.set(id, {\n fn,\n value: undefined,\n verifiedAt: -1,\n changedAt: -1,\n dependencies: new Set(),\n dependents: new Set(),\n });\n return id;\n}\n\nfunction queryThunk<T>(id: ThunkId): T {\n const thunk = thunks.get(id)!;\n\n if (thunk.verifiedAt === currentRevision) {\n return thunk.value!;\n }\n\n let needsRecompute = thunk.value === undefined;\n if (!needsRecompute) {\n for (const dep of thunk.dependencies) {\n if (dep < 10000) {\n const cell = cells.get(dep)!;\n if (cell.changedAt > thunk.verifiedAt) {\n needsRecompute = true;\n break;\n }\n } else {\n queryThunk(dep);\n const depThunk = thunks.get(dep)!;\n if (depThunk.changedAt > thunk.verifiedAt) {\n needsRecompute = true;\n break;\n }\n }\n }\n }\n\n if (needsRecompute) {\n thunk.dependencies.clear();\n const prevActive = activeThunk;\n activeThunk = id;\n const newValue = thunk.fn();\n activeThunk = prevActive;\n\n if (!Object.is(newValue, thunk.value)) {\n thunk.changedAt = currentRevision;\n thunk.value = newValue;\n }\n }\n\n thunk.verifiedAt = currentRevision;\n return thunk.value!;\n}\n```\n\nSon ~80 líneas que capturan el algoritmo esencial. Los frameworks reales agregan garbage collection de entradas de cache sin usar, ejecución paralela, durabilidad y detección de ciclos apropiada, pero la idea core es tracking de dependencias más early cutoff."},{"heading":"Aplicando esto fuera de compiladores","level":2,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#aplicando-esto-fuera-de-compiladores","content":"Los patrones de computación incremental se aplican efectivamente en contextos no-compilador. Consideremos un dashboard que agrega datos de 15 microservicios. Cada servicio se actualiza con diferente frecuencia, algunos cada segundo, otros cada hora. El dashboard computa métricas derivadas de combinaciones de estas fuentes.\n\nAntes: cada render fetchea las 15 fuentes y recomputa todas las métricas. Latencia: 800ms.\n\nDespués: al modelar cada fuente de datos como un input cell, cada métrica como una computación tracked, y usar un framework liviano estilo Adapton, en la mayoría de los renders solo 1-2 fuentes cambian, así que solo las métricas afectadas se recomputan. La latencia baja a 30-80ms dependiendo de qué cambió.\n\nEl patrón funciona en cualquier lugar donde tengas: inputs que cambian lento, computaciones derivadas caras, y consultas repetidas. Build systems (el grafo de acciones de Bazel), frameworks de UI reactivos (señales de Solid.js), motores de hojas de cálculo, todos usan variaciones de la misma idea."},{"heading":"Conclusiones prácticas","level":2,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#conclusiones-prcticas","content":"Para cualquier sistema que derive datos repetidamente de inputs que cambian, vale la pena considerar si la computación incremental encaja. La inversión upfront en modelar la computación como un grafo de queries paga dividendos cuando el dataset crece. Salsa es un punto de partida natural para proyectos Rust, y los conceptos se portan a otros lenguajes (el sketch en TypeScript de arriba sirve como punto de partida). El modelo mental, inputs, funciones tracked, grafos de dependencias, early cutoff, es la parte valiosa, no ningún framework específico.\n\nComo ejemplo concreto, reestructurar un language server alrededor de límites apropiados de queries de Salsa llevó el autocompletado de 400ms a 8ms. La computación era la misma. La única diferencia fue definir dónde estaban los límites de queries."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#questions","content":""},{"heading":"¿Qué es la computación incremental?","level":3,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#question-1","content":"La computación incremental es un paradigma de programación donde las computaciones reutilizan automáticamente resultados previos cuando los inputs cambian parcialmente. En lugar de recomputar todo desde cero, el sistema identifica qué cambió y solo recomputa las porciones afectadas del resultado."},{"heading":"¿En qué se diferencia Salsa de la memoización regular?","level":3,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#question-2","content":"La memoización regular almacena resultados por input pero no tiene mecanismo de invalidación, las entradas obsoletas persisten indefinidamente. Salsa rastrea dependencias entre computaciones, de modo que cuando un input cambia, determina exactamente qué resultados almacenados están afectados e invalida solo esos, preservando todos los resultados no afectados."},{"heading":"¿Se puede usar Salsa fuera de tooling de compiladores?","level":3,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#question-3","content":"Sí, Salsa es un framework de computación incremental de propósito general. Cualquier carga de trabajo donde se computan datos derivados repetidamente a partir de inputs que cambian con poca frecuencia se beneficia, build systems, data pipelines, interfaces reactivas y gestión de configuración son todos case es de uso viables."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#article-tips","content":"1. **Tip:** Al modelar computaciones incrementales, es importante que la granularidad de input coincida con la de invalidación. Si se rastrean cambios por archivo pero se computa por función, se recomputarán archivos enteros cuando cambie una sola función, anulando el propósito.\n2. **Tip:** Separar siempre las ` lecturas ` de las ` escrituras ` en las definiciones de queries. Un query que lee y muta estado rompe el DAG incremental y fuerza recomputación completa downstream.\n3. **Tip:** Utilizar ` salsa::accumulator ` para recolección de diagnósticos en lugar de producir side-effects durante queries. Los accumulators se integran de forma limpia con el grafo de dependencias y se invalidan correctamente.\n4. **Tip:** Perfilar las tasas de hit de queries con ` salsa::DebugQueryTable ` antes de optimizar. Se ha observado equipos dedicar semanas a optimizar un query que ya tenía 99.8% de cache hit, el cuello de botella estaba en otro lugar.\n\n- incremental-computation\n- salsa\n- adapton\n- rust-analyzer\n- compilers\n- ide\n- demand-driven\n- memoization\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks"},{"text":"Salsa","url":"https://github.com/salsa-rs/salsa"},{"text":"Adapton","url":"http://adapton.org/"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks"},"headline":"Computación Incremental con los Frameworks Salsa y Adapton","description":"Análisis de la computación incremental demand-driven tal como se implementa en rust-analyzer, memoización con invalidación automática, seguimiento de dependencias y los patrones arquitectónicos que permiten respuestas sub-milisegundo en IDEs.","datePublished":"2025-10-06","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Compilers","keywords":"incremental-computation, salsa, adapton, rust-analyzer, compilers, ide, demand-driven, memoization","wordCount":1575,"timeRequired":"PT9M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Computación Incremental con los Frameworks Salsa y Adapton","item":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Qué es la computación incremental?","acceptedAnswer":{"@type":"Answer","text":"La computación incremental es un paradigma de programación donde las computaciones reutilizan automáticamente resultados previos cuando los inputs cambian parcialmente. En lugar de recomputar todo desde cero, el sistema identifica qué cambió y solo recomputa las porciones afectadas del resultado."}},{"@type":"Question","name":"¿En qué se diferencia Salsa de la memoización regular?","acceptedAnswer":{"@type":"Answer","text":"La memoización regular almacena resultados por input pero no tiene mecanismo de invalidación, las entradas obsoletas persisten indefinidamente. Salsa rastrea dependencias entre computaciones, de modo que cuando un input cambia, determina exactamente qué resultados almacenados están afectados e invalida solo esos, preservando todos los resultados no afectados."}},{"@type":"Question","name":"¿Se puede usar Salsa fuera de tooling de compiladores?","acceptedAnswer":{"@type":"Answer","text":"Sí, Salsa es un framework de computación incremental de propósito general. Cualquier carga de trabajo donde se computan datos derivados repetidamente a partir de inputs que cambian con poca frecuencia se beneficia, build systems, data pipelines, interfaces reactivas y gestión de configuración son todos case es de uso viables."}}]}]}],"contentHash":"a2acf3ed90ceaf30fded6182261b2aca0874a0ba42ca7ff50f5c7a74c13a42c0","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":66773,"contentBytes":14409}}
