tawasuyu

tawasuyu/brahman

Rust 467TOML 184Markdown 49JSON 176411 Sutichaykuna

feat(minga): multi-lenguaje en parser — Python, TypeScript, JavaScript, Go

Minga deja de ser Rust-only. Cualquiera de los cinco dialectos
(Rust + 4 nuevos) se ingresa al CAS por su AST normalizado, hashea
estructuralmente, sincroniza por DHT como cualquier nodo. La
auto-deteccion por extension hace que minga ingest archivo.{py,ts,js,go}
"simplemente funcione".

API nueva en minga_core::parse:
- Funciones por dialecto: python, typescript, javascript, go (~6 LOC
  c/u sobre el parse_with comun). Mas la rust existente.
- Enum Dialect con parse(source) y name() para logging.
- detect_by_extension(ext) -> Option<Dialect>: rs/py/pyi/ts/js/mjs/
  cjs/go (case-insensitive). None para extensiones desconocidas.

Wire en minga-cli:
- cmd_ingest deja de hardcodear parse::rust — usa
  detect_dialect(file)?.parse(...).
- initial_scan + cmd_watch cambian is_rs_file -> is_supported_source.
- CliError::UnsupportedLanguage { path, extension } nuevo, lista las
  extensiones reconocidas en el mensaje.

Notas sobre hashing:
- Hashing estructural (cas::hash_node) funciona para todos. NO es
  alpha-equivalente.
- Hashing alpha-equivalente (alpha::hash_node_alpha) sigue siendo
  Rust-only — cada lenguaje tiene reglas distintas para binder vs
  constructor; implementacion per-language queda como work futuro
  (requiere conocimiento profundo de cada gramatica).
- Sanity test structural_hash_distinguishes_languages verifica que
  "x = 1" parseado como Python != JS — las gramaticas no comparten
  kinds, hashes salen distintos. Importante para evitar colisiones.

Deps nuevas (workspace + minga-core):
- tree-sitter-python 0.23, tree-sitter-typescript 0.23 (modo
  LANGUAGE_TYPESCRIPT, no TSX), tree-sitter-javascript 0.23,
  tree-sitter-go 0.23.

Tests: 9 nuevos en parse::tests (parse basico para 5 dialectos +
detect_by_extension canonical/case-insensitive + name() +
structural_hash_distinguishes_languages). 108 verdes en minga-core,
10 en minga-cli, sin regresion.

Pendientes: alpha-hashing per-language; alpha-Rust documentados en
alpha.rs (if let, while let, let-else, let-chains, or_pattern con
bindings).

4db168253c1d77398f0dd8fd32f59d04e432bc13 · ruwaq Sergio · (5 killa ñawpaq) · tayta f9a3c3358 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 29

crates/modules/semantic_dht/minga-cli/src/commands.rs

crates/modules/semantic_dht/minga-cli/src/error.rs

  • silq'uyninpi t'ikrakurqan enum CliError pub

crates/modules/semantic_dht/minga-core/src/parse.rs

7 qillqakuna · +357 −12

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f02f39d..d181a05 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,76 @@ ratio/diff ver `git show <sha>`.
 
 ## 2026-05-09
 
+### feat(minga): multi-lenguaje en parser — Python, TypeScript, JavaScript, Go
+Minga deja de ser Rust-only. Cualquiera de los cinco dialectos
+(Rust + 4 nuevos) se ingresa al CAS por su AST normalizado, hashea
+estructuralmente, sincroniza por DHT como cualquier nodo. La
+auto-detección por extensión hace que `minga ingest archivo.py` o
+`.ts` o `.go` "simplemente funcione".
+
+API nueva en `minga_core::parse`:
+- Funciones por dialecto (~6 LOC c/u sobre el `parse_with` común):
+  `python`, `typescript`, `javascript`, `go`. Más la `rust` existente.
+- Enum `Dialect` con `parse(source) -> Result<SemanticNode>` y
+  `name() -> &'static str` para logging.
+- `detect_by_extension(ext) -> Option<Dialect>`: mapea `rs`/`py`/
+  `pyi`/`ts`/`js`/`mjs`/`cjs`/`go` (case-insensitive). `None` para
+  extensiones desconocidas — el caller decide si es error o se
+  ignora silente.
+
+Wire en `minga-cli`:
+- `cmd_ingest` deja de hardcodear `parse::rust` — usa
+  `detect_dialect(file)?.parse(...)`. Acepta `.py`, `.ts`, `.js`,
+  `.go` además de `.rs`.
+- `initial_scan` y `cmd_watch` cambian `is_rs_file` → `is_supported_source`
+  para incluir todas las extensiones soportadas en el filtro.
+- `CliError::UnsupportedLanguage { path, extension }` nuevo, con
+  mensaje que lista las extensiones reconocidas.
+
+Notas sobre hashing:
+- El AST normalizado (`SemanticNode`) descarta whitespace y
+  comentarios — propiedad universal de tree-sitter (extras). Misma
+  lógica para los 5 dialectos.
+- Hashing **estructural** (`cas::hash_node`) funciona para todos:
+  dos textos semánticamente equivalentes-por-estructura producen el
+  mismo hash. NO α-equivalente (las variables ligadas distinguen).
+- Hashing **α-equivalente** (`alpha::hash_node_alpha`) sigue siendo
+  Rust-only: cada lenguaje tiene reglas distintas para qué es
+  binder vs. constructor (def/lambda en Python, arrow functions en
+  TS/JS, func + closures en Go). Implementación per-language queda
+  como work futuro — requiere conocimiento profundo de cada
+  gramática y no se plantilla genéricamente.
+- Sanity test `structural_hash_distinguishes_languages` verifica
+  que `x = 1` parseado como Python ≠ parseado como JavaScript: las
+  gramáticas no comparten kinds y los hashes salen distintos.
+  Importante para evitar colisiones cuando el mismo source se
+  ingresa bajo dialectos distintos.
+
+Deps nuevas (workspace + minga-core):
+- `tree-sitter-python = "0.23"`
+- `tree-sitter-typescript = "0.23"` (sólo el modo `LANGUAGE_TYPESCRIPT`,
+  no TSX — bumpear a TSX es agregar otro dialecto cuando se necesite).
+- `tree-sitter-javascript = "0.23"`
+- `tree-sitter-go = "0.23"`
+
+Tests:
+- 9 nuevos en `parse::tests`: parse básico para los 5 dialectos
+  (Python con type hints, TS con tipos, JS sin tipos, Go con
+  package declaration), `detect_by_extension` canonical +
+  case-insensitive, `dialect_name`, `structural_hash_distinguishes_languages`.
+- 108 tests verdes en minga-core (39 → 48 unit + integration tests
+  pre-existentes intactos).
+- 10 tests verdes en minga-cli (sin regresión en el path Rust;
+  el refactor a `detect_dialect`/`is_supported_source` no rompe
+  nada).
+
+Pendientes futuros del changelog:
+- α-hashing per-language (Python: def/lambda/comprehensions;
+  TS/JS: function/arrow/destructuring; Go: func/closure). Trabajo
+  profundo, scope independiente.
+- α-Rust pendientes documentados en `alpha.rs`: `if let`,
+  `while let`, `let-else`, let-chains, `or_pattern` con bindings.
+
 ### feat(brahman-handshake): multi-key identity — rotación de session sin perder peer_id lógico
 Cierra el último pendiente del plan de red P2P. Hasta ahora, rotar
 la keypair libp2p de un nodo cambiaba su `peer_id`, lo que
diff --git a/Cargo.lock b/Cargo.lock
index 956bbcd..941c298 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5874,7 +5874,11 @@ dependencies = [
  "serde-big-array",
  "thiserror 2.0.18",
  "tree-sitter",
+ "tree-sitter-go",
+ "tree-sitter-javascript",
+ "tree-sitter-python",
  "tree-sitter-rust",
+ "tree-sitter-typescript",
 ]
 
 [[package]]
@@ -10335,12 +10339,42 @@ dependencies = [
  "tree-sitter-language",
 ]
 
+[[package]]
+name = "tree-sitter-go"
+version = "0.23.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b13d476345220dbe600147dd444165c5791bf85ef53e28acbedd46112ee18431"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
+[[package]]
+name = "tree-sitter-javascript"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf40bf599e0416c16c125c3cec10ee5ddc7d1bb8b0c60fa5c4de249ad34dc1b1"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
 [[package]]
 name = "tree-sitter-language"
 version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782"
 
+[[package]]
+name = "tree-sitter-python"
+version = "0.23.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
 [[package]]
 name = "tree-sitter-rust"
 version = "0.23.3"
@@ -10351,6 +10385,16 @@ dependencies = [
  "tree-sitter-language",
 ]
 
+[[package]]
+name = "tree-sitter-typescript"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
 [[package]]
 name = "trice"
 version = "0.4.0"
diff --git a/Cargo.toml b/Cargo.toml
index b7f40b9..3a4206b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -148,6 +148,10 @@ libp2p-allow-block-list = "0.6"
 # === Code parsing (minga) ===
 tree-sitter = "0.24"
 tree-sitter-rust = "0.23"
+tree-sitter-python = "0.23"
+tree-sitter-typescript = "0.23"
+tree-sitter-javascript = "0.23"
+tree-sitter-go = "0.23"
 
 # === FS notify ===
 notify = "6.1"
diff --git a/crates/modules/semantic_dht/minga-cli/src/commands.rs b/crates/modules/semantic_dht/minga-cli/src/commands.rs
index e052560..200b832 100644
--- a/crates/modules/semantic_dht/minga-cli/src/commands.rs
+++ b/crates/modules/semantic_dht/minga-cli/src/commands.rs
@@ -83,7 +83,8 @@ pub fn cmd_ingest(
     let repo = PersistentRepo::open(repo_path.join(REPO_DIRNAME))?;
 
     let source = fs::read_to_string(file)?;
-    let node = parse::rust(&source)?;
+    let dialect = detect_dialect(file)?;
+    let node = dialect.parse(&source)?;
     let hash = repo.nodes.put(&node)?;
     repo.mst.insert(hash)?;
     repo.attestations
@@ -96,6 +97,21 @@ pub fn cmd_ingest(
     })
 }
 
+/// Detecta el dialecto desde la extensión del archivo. Error si la
+/// extensión no corresponde a un lenguaje soportado.
+fn detect_dialect(file: &Path) -> Result<parse::Dialect, CliError> {
+    let ext = file
+        .extension()
+        .and_then(|e| e.to_str())
+        .unwrap_or("");
+    parse::detect_by_extension(ext).ok_or_else(|| {
+        CliError::UnsupportedLanguage {
+            path: file.to_path_buf(),
+            extension: ext.to_string(),
+        }
+    })
+}
+
 /// `minga listen <addr>`: arranca el peer, escucha en `addr`, y
 /// acepta sincronizaciones entrantes hasta que el proceso se cierre.
 pub async fn cmd_listen(
@@ -191,7 +207,7 @@ pub async fn cmd_watch(
             continue;
         }
         for path in &event.paths {
-            if is_rs_file(path) {
+            if is_supported_source(path) {
                 match ingest_into_repo(&repo, &keypair, path) {
                     Ok(hash) => {
                         eprintln!("ingerido: {} → {}", path.display(), hash);
@@ -213,7 +229,7 @@ fn initial_scan(repo: &PersistentRepo, keypair: &Keypair, dir: &Path) {
     };
     for entry in entries.flatten() {
         let p = entry.path();
-        if is_rs_file(&p) {
+        if is_supported_source(&p) {
             let _ = ingest_into_repo(repo, keypair, &p);
         }
     }
@@ -225,7 +241,8 @@ fn ingest_into_repo(
     file: &Path,
 ) -> Result<ContentHash, CliError> {
     let source = fs::read_to_string(file)?;
-    let node = parse::rust(&source)?;
+    let dialect = detect_dialect(file)?;
+    let node = dialect.parse(&source)?;
     let hash = repo.nodes.put(&node)?;
     repo.mst.insert(hash)?;
     repo.attestations
@@ -234,8 +251,14 @@ fn ingest_into_repo(
     Ok(hash)
 }
 
-fn is_rs_file(path: &Path) -> bool {
-    path.extension().and_then(|e| e.to_str()) == Some("rs") && path.is_file()
+/// Detecta si un archivo debe ingerirse: existe, es regular, y su
+/// extensión corresponde a un dialecto soportado.
+fn is_supported_source(path: &Path) -> bool {
+    if !path.is_file() {
+        return false;
+    }
+    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
+    parse::detect_by_extension(ext).is_some()
 }
 
 fn is_relevant_event(event: &notify::Event) -> bool {
diff --git a/crates/modules/semantic_dht/minga-cli/src/error.rs b/crates/modules/semantic_dht/minga-cli/src/error.rs
index f509f41..5592be3 100644
--- a/crates/modules/semantic_dht/minga-cli/src/error.rs
+++ b/crates/modules/semantic_dht/minga-cli/src/error.rs
@@ -40,4 +40,10 @@ pub enum CliError {
 
     #[error("notify (file watcher): {0}")]
     Notify(#[from] notify::Error),
+
+    #[error(
+        "lenguaje no soportado para {path}: extensión '{extension}' no mapea \
+         a ningún dialecto conocido (rs, py, pyi, ts, js, mjs, cjs, go)"
+    )]
+    UnsupportedLanguage { path: PathBuf, extension: String },
 }
diff --git a/crates/modules/semantic_dht/minga-core/Cargo.toml b/crates/modules/semantic_dht/minga-core/Cargo.toml
index 6e385b6..c3b7427 100644
--- a/crates/modules/semantic_dht/minga-core/Cargo.toml
+++ b/crates/modules/semantic_dht/minga-core/Cargo.toml
@@ -9,6 +9,10 @@ description = "Minga core: semantic AST, content addressing, Merkle Search Tree.
 [dependencies]
 tree-sitter = { workspace = true }
 tree-sitter-rust = { workspace = true }
+tree-sitter-python = { workspace = true }
+tree-sitter-typescript = { workspace = true }
+tree-sitter-javascript = { workspace = true }
+tree-sitter-go = { workspace = true }
 blake3 = { workspace = true }
 thiserror = { workspace = true }
 ed25519-dalek = { workspace = true }
diff --git a/crates/modules/semantic_dht/minga-core/src/parse.rs b/crates/modules/semantic_dht/minga-core/src/parse.rs
index f0766b3..3a6e26f 100644
--- a/crates/modules/semantic_dht/minga-core/src/parse.rs
+++ b/crates/modules/semantic_dht/minga-core/src/parse.rs
@@ -1,8 +1,29 @@
-//! Adaptadores de parsing por dialecto. Hoy: Rust vía tree-sitter-rust.
+//! Adaptadores de parsing por dialecto.
 //!
-//! `parse::rust` produce un `SemanticNode` normalizado a partir de una
-//! cadena de código fuente. El error es opaco a propósito: el caller no
-//! necesita distinguir "gramática inválida" de "fallo del parser".
+//! Cada función devuelve un [`SemanticNode`] normalizado a partir del
+//! source code. La normalización vive en `ast::SemanticNode::from_tree_sitter`
+//! y es agnóstica al lenguaje — cualquier tree-sitter grammar produce
+//! el mismo shape de árbol semántico (sin whitespace, sin comentarios).
+//!
+//! Lenguajes soportados (cada uno son ~6 LOC + dep tree-sitter-X):
+//! - [`rust`] — Rust completo (con α-hashing en `alpha::hash_node_alpha`).
+//! - [`python`] — Python 3.x.
+//! - [`typescript`] — TypeScript (no TSX).
+//! - [`javascript`] — JavaScript / ECMAScript.
+//! - [`go`] — Go.
+//!
+//! Para hashing α-equivalente, sólo Rust tiene implementación dedicada
+//! hoy. Otros lenguajes caen al [`crate::cas::hash_node`] estructural,
+//! que es α-NO-equivalente: dos versiones del mismo término que
+//! difieren en nombres de variables ligadas tendrán hashes distintos.
+//! Suficiente para detección de cambios; no para detección de
+//! equivalencia semántica.
+//!
+//! ## Auto-detección por extensión
+//!
+//! [`detect_by_extension`] mapea `.rs` → Rust, `.py` → Python, etc.
+//! Útil para `minga ingest` cuando el caller no quiere especificar
+//! el dialecto a mano.
 
 use crate::ast::SemanticNode;
 use thiserror::Error;
@@ -16,10 +37,183 @@ pub enum ParseError {
     NoTree,
 }
 
-pub fn rust(source: &str) -> Result<SemanticNode, ParseError> {
-    let lang: Language = tree_sitter_rust::LANGUAGE.into();
+/// Identificadores estables de cada dialecto soportado.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Dialect {
+    Rust,
+    Python,
+    TypeScript,
+    JavaScript,
+    Go,
+}
+
+impl Dialect {
+    /// Nombre canónico para logging / display.
+    pub fn name(self) -> &'static str {
+        match self {
+            Dialect::Rust => "rust",
+            Dialect::Python => "python",
+            Dialect::TypeScript => "typescript",
+            Dialect::JavaScript => "javascript",
+            Dialect::Go => "go",
+        }
+    }
+
+    /// Parsea `source` con la gramática de este dialecto.
+    pub fn parse(self, source: &str) -> Result<SemanticNode, ParseError> {
+        match self {
+            Dialect::Rust => rust(source),
+            Dialect::Python => python(source),
+            Dialect::TypeScript => typescript(source),
+            Dialect::JavaScript => javascript(source),
+            Dialect::Go => go(source),
+        }
+    }
+}
+
+/// Mapea una extensión de archivo (sin el `.`) al dialecto correspondiente.
+/// `None` si la extensión no corresponde a un lenguaje soportado.
+///
+/// ```
+/// use minga_core::parse::{detect_by_extension, Dialect};
+/// assert_eq!(detect_by_extension("rs"), Some(Dialect::Rust));
+/// assert_eq!(detect_by_extension("py"), Some(Dialect::Python));
+/// assert_eq!(detect_by_extension("unknown"), None);
+/// ```
+pub fn detect_by_extension(ext: &str) -> Option<Dialect> {
+    match ext.to_ascii_lowercase().as_str() {
+        "rs" => Some(Dialect::Rust),
+        "py" | "pyi" => Some(Dialect::Python),
+        "ts" => Some(Dialect::TypeScript),
+        "js" | "mjs" | "cjs" => Some(Dialect::JavaScript),
+        "go" => Some(Dialect::Go),
+        _ => None,
+    }
+}
+
+fn parse_with(lang: Language, source: &str) -> Result<SemanticNode, ParseError> {
     let mut parser = Parser::new();
     parser.set_language(&lang).map_err(|_| ParseError::Language)?;
     let tree = parser.parse(source, None).ok_or(ParseError::NoTree)?;
     Ok(SemanticNode::from_tree_sitter(tree.root_node(), source.as_bytes()))
 }
+
+pub fn rust(source: &str) -> Result<SemanticNode, ParseError> {
+    parse_with(tree_sitter_rust::LANGUAGE.into(), source)
+}
+
+pub fn python(source: &str) -> Result<SemanticNode, ParseError> {
+    parse_with(tree_sitter_python::LANGUAGE.into(), source)
+}
+
+pub fn typescript(source: &str) -> Result<SemanticNode, ParseError> {
+    parse_with(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), source)
+}
+
+pub fn javascript(source: &str) -> Result<SemanticNode, ParseError> {
+    parse_with(tree_sitter_javascript::LANGUAGE.into(), source)
+}
+
+pub fn go(source: &str) -> Result<SemanticNode, ParseError> {
+    parse_with(tree_sitter_go::LANGUAGE.into(), source)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn assert_parses(d: Dialect, source: &str) -> SemanticNode {
+        let node = d.parse(source).expect("parse should succeed");
+        // Sanity: el root siempre tiene al menos un child para code real.
+        assert!(
+            !node.children.is_empty(),
+            "{}: root node sin children — parse posiblemente vacío",
+            d.name()
+        );
+        node
+    }
+
+    #[test]
+    fn rust_parses_basic() {
+        assert_parses(Dialect::Rust, "fn add(a: i32, b: i32) -> i32 { a + b }");
+    }
+
+    #[test]
+    fn python_parses_basic() {
+        assert_parses(
+            Dialect::Python,
+            "def add(a: int, b: int) -> int:\n    return a + b\n",
+        );
+    }
+
+    #[test]
+    fn typescript_parses_basic() {
+        assert_parses(
+            Dialect::TypeScript,
+            "function add(a: number, b: number): number { return a + b; }",
+        );
+    }
+
+    #[test]
+    fn javascript_parses_basic() {
+        assert_parses(
+            Dialect::JavaScript,
+            "function add(a, b) { return a + b; }",
+        );
+    }
+
+    #[test]
+    fn go_parses_basic() {
+        assert_parses(
+            Dialect::Go,
+            "package main\n\nfunc add(a, b int) int {\n    return a + b\n}\n",
+        );
+    }
+
+    #[test]
+    fn detect_extension_canonical() {
+        assert_eq!(detect_by_extension("rs"), Some(Dialect::Rust));
+        assert_eq!(detect_by_extension("py"), Some(Dialect::Python));
+        assert_eq!(detect_by_extension("pyi"), Some(Dialect::Python));
+        assert_eq!(detect_by_extension("ts"), Some(Dialect::TypeScript));
+        assert_eq!(detect_by_extension("js"), Some(Dialect::JavaScript));
+        assert_eq!(detect_by_extension("mjs"), Some(Dialect::JavaScript));
+        assert_eq!(detect_by_extension("cjs"), Some(Dialect::JavaScript));
+        assert_eq!(detect_by_extension("go"), Some(Dialect::Go));
+        assert_eq!(detect_by_extension("unknown"), None);
+        assert_eq!(detect_by_extension(""), None);
+    }
+
+    #[test]
+    fn detect_extension_case_insensitive() {
+        assert_eq!(detect_by_extension("RS"), Some(Dialect::Rust));
+        assert_eq!(detect_by_extension("Py"), Some(Dialect::Python));
+        assert_eq!(detect_by_extension("TS"), Some(Dialect::TypeScript));
+    }
+
+    #[test]
+    fn dialect_name_canonical() {
+        assert_eq!(Dialect::Rust.name(), "rust");
+        assert_eq!(Dialect::Python.name(), "python");
+        assert_eq!(Dialect::TypeScript.name(), "typescript");
+        assert_eq!(Dialect::JavaScript.name(), "javascript");
+        assert_eq!(Dialect::Go.name(), "go");
+    }
+
+    #[test]
+    fn structural_hash_distinguishes_languages() {
+        // Mismo "shape" textual pero distintos lenguajes producen
+        // árboles distintos (las gramáticas no coinciden) y por tanto
+        // hashes estructurales distintos. Importante para evitar
+        // colisiones en el CAS cuando el mismo source se ingiere
+        // bajo dialectos distintos.
+        use crate::cas::hash_node;
+        let py = Dialect::Python.parse("x = 1").unwrap();
+        let js = Dialect::JavaScript.parse("x = 1").unwrap();
+        assert_ne!(
+            hash_node(&py),
+            hash_node(&js),
+            "py y js deberían tener hashes distintos para el mismo source"
+        );
+    }
+}