Metadata-Version: 2.4
Name: compickle
Version: 1.2.6b0
Summary: Español: biblioteca rapida con multiple soporte al estilo pinckle English: Quick library with multiple support in the Pinckle style
Author: Luis Fernando Montaño Hernandez
License: MIT
Keywords: serializer,pickle,binary,performance,python,serialization
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: C
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: System :: Archiving
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

<p align="center">
  <img src="https://raw.githubusercontent.com/lmontanohernandez8-png/Micronnx/main/logo2.png" width="600"/>
</p>

# 🥒 compickle

**Serialización binaria para Python con motor en C — rápido, compacto y sin dependencias.**

[![Python](https://img.shields.io/badge/Python-3.9%2B-blue?style=flat-square&logo=python&logoColor=white)](https://www.python.org/)
[![Motor C](https://img.shields.io/badge/Motor-C%20nativo-orange?style=flat-square&logo=c&logoColor=white)]()
[![Versión](https://img.shields.io/badge/versión-1.2.4-blue?style=flat-square)]()
[![Licencia](https://img.shields.io/badge/Licencia-MIT-green?style=flat-square)]()

---

## ¿Qué es compickle?

`compickle` es un serializador binario, distribuido como paquete real
(`compickle/__init__.py` como punto de entrada perezoso), con motor en **C**
(`compickle/compickle.c`, expuesto como extensión nativa `compickle._compickle`) y un
**fallback puro en Python** (`compickle/compickle.py`) que implementa exactamente el
mismo protocolo binario, para cuando la extensión C no está compilada o no se puede
importar en el entorno de destino. `__init__.py` decide cuál de los dos backends usar
y expone la API pública (`dumps`, `loads`, `dump`, `load`, `verify_stream`,
`dedup_reset`, `backend`) — el resto de este documento describe esa API, y cuando
haga falta detallar el mecanismo interno, indica explícitamente si algo vive en
`__init__.py` o en el submódulo `compickle.compickle`.

> **Versión actual: `1.2.4`**

---

## 📊 Estado medido (honesto)

Todo lo de esta sección viene de benchmarks reales corridos contra el código en esta
ronda de trabajo — no son estimaciones.

### compickle-C vs `pickle` (protocolo 5, acelerador `_pickle`)

Medido en 6 cargas de trabajo representativas (listas de enteros, strings repetidos,
diccionarios anidados, tuplas mixtas, estructuras grandes anidadas):

| | serialize | deserialize |
|---|---|---|
| compickle-C vs pickle-C | **~1.8x más rápido** | **~1.75x más rápido** |

compickle-C ganó en los 6 casos probados, sin excepción, con bytes de salida más
compactos en todos ellos (en el caso más grande, casi la mitad del tamaño de `pickle`).

### compickle-Python puro vs `pickle`-Python puro

Forzando la implementación pura Python de `pickle` (`pickle._Pickler`/`_Unpickler`,
evitando el acelerador `_pickle` que `pickle.dumps` usa por defecto de forma
transparente):

| | serialize | deserialize |
|---|---|---|
| compickle-Py vs pickle-PurePy | **~3.2x más rápido** | **~2.5x más rápido** |

**Alcance:** estas cifras salen de 6 workloads elegidos como representativos, no de
una prueba exhaustiva. No se probaron referencias circulares, arrays de NumPy,
payloads de cientos de MB, ni patrones de acceso parcial al stream.

### Mejora del backend Python puro sobre su propia versión anterior

Tras una ronda de optimización dirigida por profiling: **entre +13.7% y +22.6%**
combinado (serialize + deserialize). El mayor aporte vino de resolver los 4 tags más
frecuentes (entero pequeño, referencia, string corta, entero de 1 byte) directamente
en el bucle de listas/dicts en `_deserialize`, evitando entrar a la función completa
de despacho cuando no hace falta.

---

## 🐛 Bugs corregidos en esta ronda

Se encontraron y corrigieron **cinco bugs de corrección** preexistentes (no
introducidos en esta ronda; confirmado comparando byte a byte contra el código
anterior). Todos compartían la misma raíz: el opcode `0xFB`/`0x0E` que envuelve datos
deduplicados no siempre preservaba el `tag` original al leerlo de vuelta.

**En `compickle.py`:**
1. Pérdida del tag de compresión en clases con código fuente corto (`_emit_new` usaba
   el opcode genérico `0x0E` para cualquier bloque ≤63 bytes, incluso el comprimido
   con zlib) — causaba `UnicodeDecodeError` al deserializar.
2. `bytes`/`bytearray` no coincidían byte a byte con el backend C al deduplicar: el
   tag se escribía antes de saber si el dato sería una referencia o uno nuevo,
   duplicándolo en cada repetición.

**En `compickle.c`:**
3. `read_dedup_bytes` nunca descomprimía la fuente de clase: la rama `0xFB` descartaba
   el tag almacenado sin comprobar si era el de "comprimido".
4. `write_dedup` tenía el mismo bug que el punto 1, del lado C: bloques comprimidos
   ≤63 bytes perdían el tag al escribirse.
5. El más serio: `write_class_header` registraba una entrada de deduplicación
   redundante que nunca se emitía al stream, desincronizando permanentemente el
   contador de índices de referencia entre escritor y lector en cuanto se
   serializaba **más de una clase o más de una instancia** de la misma clase.

Tras los cinco fixes, la suite de regresión (55 casos: todos los tipos soportados,
valores límite, anidamiento profundo, deduplicación por identidad vs. por contenido,
y clases mezcladas y repetidas) pasa **55/55 en roundtrip** y **55/55 en
cross-validation byte-idéntica entre ambos backends**.

**Encontrados en la ronda de `show_source`/reestructuración a paquete real:**

6. `compickle` no era un paquete de verdad — faltaba `compickle/__init__.py` en el
   código fuente que se estaba versionando (existía en el repo real pero no se había
   incluido). Sin él, `compickle.py` se importaba directamente como módulo de nivel
   superior, y `_compickle.c` compilaba como extensión suelta en vez de vivir dentro
   del paquete. Corregido: estructura real `compickle/{__init__.py, compickle.py,
   compickle.c}`, `setup.py` compilando `compickle._compickle` (no `_compickle`
   suelto), `pyproject.toml` declarando `packages = ["compickle"]`. Verificado con una
   instalación real vía `pip install .` en un entorno aislado, confirmando que el
   `.so` cae dentro de `site-packages/compickle/` junto a `__init__.py`.
7. La extracción de fuente comprimida con zlib para `show_source=True` no se
   descomprimía del lado C — el bloque `0xFB` capturaba los bytes crudos aún
   comprimidos y los pasaba tal cual, produciendo binario ilegible en vez de código
   fuente. Corregido reusando en la captura el mismo patrón de reintento de buffer
   que `OP_SRC_Z` ya usa en `read_dedup_bytes`, para que `dedup_table` en C guarde
   siempre bytes ya descomprimidos, igual contrato que el backend Python puro.

**Detectado pero NO corregido en esta ronda (señalado, pendiente de decisión):**
en `__init__.py`, `_USE_C` existe como variable a nivel de módulo (`_USE_C = None`)
Y también tiene una rama en `__getattr__` que intenta resolverla perezosamente — pero
la rama de `__getattr__` nunca se ejecuta en la práctica, porque Python resuelve
`compickle._USE_C` contra la variable de módulo real antes de considerar
`__getattr__` (que solo se invoca para atributos que *no* existen ya en el
namespace). Esto significa que `compickle._USE_C` accedido directamente, antes de
llamar a cualquier función pública, siempre da `None` en vez de `True`/`False` —
aunque `dumps()`/`loads()`/`backend()`/etc. siguen funcionando correctamente porque
todas llaman a `_ensure_loaded()` explícitamente. `compickle.backend()` es la forma
correcta de consultar esto desde fuera; `_USE_C` es un detalle interno con guion
bajo. Confirmado con una reproducción aislada mínima del mismo patrón.

**Encontrado en la ronda de `classmethod`/`staticmethod`/`property`:**

8. **Crítico — corrupción de memoria del proceso completo.** `deserialize()` en
   `compickle.c`, al reconstruir cualquier función (`tag == 0x20`, la ruta que usan
   *todas* las funciones sin `__reduce__` propio), obtenía el `__dict__` de
   `builtins` vía `PyModule_GetDict()` — que devuelve una **referencia prestada**
   (no nueva) según la C API de CPython — y luego hacía `Py_XDECREF()` sobre ese
   resultado como si fuera una referencia propia. Cada `loads()` de una función
   decrementaba el refcount de `builtins.__dict__` (un objeto compartido
   globalmente por todo el proceso) en 1 de más, sin haberlo incrementado nunca.
   El refcount inicial es alto (cientos, por todas las referencias legítimas del
   intérprete), así que el bug sobrevivía sin síntomas visibles durante cientos de
   llamadas — hasta que el contador llegaba a 0 y CPython intentaba liberar memoria
   en uso activo por todo el proceso, con **segfault no determinista** en algún
   punto posterior, no en la llamada que de hecho causó el problema. Confirmado con
   una reproducción aislada mínima (una función C de 6 líneas replicando
   exactamente el patrón) que mostró el refcount real decreciendo de forma
   persistente entre llamadas, y con la propia librería: 600–650 llamadas
   repetidas a `loads()` sobre la misma función bastaban para crashear el proceso
   de forma reproducible. Corregido diferenciando explícitamente el caso
   `PyModule_GetDict()` (prestada, nunca se decrementa) del caso `PyDict_New()` de
   fallback si `import builtins` fallara (nueva, sí se decrementa) — antes ambos
   casos compartían un único `Py_XDECREF` incondicional. Verificado con 3000
   llamadas repetidas post-fix sin crash y con el refcount de `builtins.__dict__`
   perfectamente estable llamada a llamada (medido directamente con
   `sys.getrefcount()` en cada iteración, no solo "no crasheó").

   Este bug es **anterior a esta ronda** — afecta a cualquier `loads()` de una
   función en toda la serie `1.2.x` con el backend C activo, independientemente de
   `classmethod`/`staticmethod`/`property`. Se encontró porque una prueba de estrés
   para el código nuevo de esta ronda hizo miles de `loads()` repetidos, frecuencia
   que ninguna suite de pruebas anterior había ejercitado.

---

## 🔒 `verify_stream()` — inspección sin ejecución

`loads()` no es un lector de datos puro: cinco de sus opcodes (`0x0D`, `0x1C`, `0x1E`,
`0x1F`, `0x20`, `0x21` — función vía fuente, instancia con `__dict__`/`__slots__`,
objeto `__reduce__`, función y code-object vía `marshal`) ejecutan código como parte
de reconstruir el objeto. Esto es necesario para lo que esas rutas hacen, pero
significa que un archivo `.cpkl` no confiable puede ejecutar código arbitrario al
cargarlo con `loads()`.

`verify_stream(data)` camina la estructura completa de un stream **sin construir
ningún objeto real**: nunca llama `exec()`, nunca llama `marshal.loads()` sobre nada
que vaya a ejecutarse, nunca invoca un callable. Confirma que los tags son válidos,
que las longitudes no se salen del buffer, y que toda referencia de deduplicación
apunta a un índice que realmente existe — y, cuando encuentra uno de los seis
opcodes que requieren ejecución, no intenta "validar" su contenido (no hay forma de
validar código arbitrario sin ejecutarlo): calcula dónde termina ese bloque usando
solo aritmética de longitudes, lo registra como no verificado, y sigue caminando
el resto.

```python
import compickle

reporte = compickle.verify_stream(datos_no_confiables)

if not reporte.ok:
    print("Stream corrupto o mal formado:", reporte.error)
elif reporte.fully_verified:
    print("Todo el árbol es dato puro — cero ejecución necesaria en loads()")
else:
    print(f"{len(reporte.unexecuted_blocks)} bloque(s) requieren ejecución para verse:")
    for pos, tag, motivo, _ in reporte.unexecuted_blocks:
        print(f"  offset {pos}: tag 0x{tag:02X} — {motivo}")
```

**Lo que `verify_stream()` garantiza y lo que no garantiza — sin rodeos:**

- `reporte.ok == False` → el stream está corrupto o mal formado (tag desconocido,
  longitud que se sale del buffer, referencia a un índice que no existe, bytes
  sobrantes tras el objeto raíz). Un stream así **también falla en `loads()`**,
  nunca al revés — se probó explícitamente que ambos backends coinciden en esto.
- `reporte.ok == True and reporte.fully_verified == True` → el árbol completo es
  dato puro (números, strings, listas, dicts, bytes, sets...). No hay ningún
  opcode que vaya a ejecutar código si se llama `loads()` sobre este mismo stream.
- `reporte.ok == True and reporte.fully_verified == False` → el stream está bien
  formado, pero contiene al menos un bloque que solo se puede confirmar
  ejecutándolo. **`verify_stream()` no dice si ese código es seguro** — solo dice
  que existe y en qué posición (`reporte.unexecuted_blocks`). Decidir si ejecutarlo
  vía `loads()` sigue siendo responsabilidad de quien llama.

Es decir: `verify_stream()` responde "¿este archivo va a intentar ejecutar algo, y
si no, puedo confiar en su estructura?" — no responde "¿es seguro este archivo?" en
términos absolutos, porque esa pregunta no tiene respuesta posible para un formato
que soporta código ejecutable por diseño.

**Verificado:**
- 300 estructuras anidadas aleatorias (números, floats, strings, bytes, listas,
  dicts, tuplas, sets, profundidad variable) → `fully_verified=True` en el 100%,
  roundtrip con `loads()` exacto en el 100%, en **ambos backends**.
- 151+ streams (aleatorios + con clases/funciones/`__reduce__` mezclados) comparados
  campo por campo entre backend C y Python → **0 discrepancias**.
- Confirmado con un espía en `__new__` que `verify_stream()` no ejecuta ningún
  código real, ni siquiera para el `__reduce__` más simple.
- Índices de referencia fuera de rango se detectan como corrupción real
  (`reporte.ok=False`), consistente con que `loads()` también falla ahí.
- Sin fugas de referencias detectadas en el motor C tras miles de iteraciones en
  el camino de éxito y en el camino de error (conteo de objetos vivos estable).
- Límite de anidamiento (200 niveles) se activa correctamente sin desbordar la
  pila del proceso, en ambos backends.

**Disponible en ambos motores**, con la misma API y el mismo tipo de resultado
(`compickle.StreamReport`) sin importar cuál esté activo:

```python
compickle.backend()          # → 'c' o 'python'
compickle.verify_stream(datos)  # devuelve StreamReport en ambos casos, idéntico
```

### `show_source=True` — leer el código sin ejecutarlo

Por defecto, `reporte.unexecuted_blocks` te dice *dónde* está el código y *por qué*
requiere ejecución, pero no *qué dice* ese código. `verify_stream(data,
show_source=True)` agrega ese texto — para que la persona que llama pueda leerlo y
decidir, en vez de que `verify_stream()` decida por ella.

```python
reporte = compickle.verify_stream(datos_no_confiables, show_source=True)

for pos, tag, motivo, codigo in reporte.unexecuted_blocks:
    print(f"--- offset {pos}: {motivo} ---")
    print(codigo)
```

Qué contiene `codigo` según el tipo de bloque:

- **Clases e instancias (`0x1C`/`0x1E`), funciones vía fuente (`0x0D`),
  `__reduce__` con fuente embebida (`0x1F`)**: el código fuente Python real,
  capturado desde el propio stream (descomprimiendo zlib si aplica) y decodificado
  como UTF-8 — texto legible, no una aproximación.
- **Funciones y code objects vía `marshal` (`0x20`/`0x21`)**: estos NO tienen fuente
  Python guardada — solo bytecode compilado. `codigo` trae el **desensamblado**
  (`dis.dis()`) de ese bytecode: instrucciones legibles, no el texto fuente original
  (que no existe en el stream).

**Por qué esto es seguro — verificado explícitamente, no solo argumentado:**

Extraer y mostrar este texto es lectura/decodificación pura. Ninguno de los pasos
involucrados (descomprimir zlib, decodificar UTF-8, `marshal.loads()` sobre el
blob, `dis.dis()` sobre el code object resultante) ejecuta la función o clase que
describen — `marshal.loads()` sobre bytes solo reconstruye una estructura de datos
(constantes, nombres, instrucciones), no invoca nada, y `dis.dis()` solo lee esa
estructura y la formatea como texto. Esto se confirmó con una función cuyo cuerpo
llama `os._exit(1)` (terminaría el proceso entero si se ejecutara): pasarla por
`verify_stream(..., show_source=True)` no lo dispara, el desensamblado se genera
igual, y el proceso sigue vivo.

**Lo que `show_source=True` NO te da — para que no se lea de más:**

Leer el código y decidir que "se ve bien" es juicio humano, no una garantía
mecánica. `verify_stream()` no analiza si el código es dañino — un fragmento que se
ve trivial puede seguir haciendo algo dañino si después se ejecuta vía `loads()`.
`show_source=True` te da la información para decidir; no toma la decisión por vos.

**Nota de robustez (distinta de "ejecuta código Python"):** la documentación de
CPython advierte que el formato `marshal` en sí no está diseñado para ser resistente
a datos adversariales a nivel del propio parser de C. Esto es un riesgo de bajo
nivel del parser, no de "el código Python se ejecuta" (eso no ocurre, confirmado
arriba) — pero tampoco es una garantía absoluta de robustez ante bytes construidos
específicamente para explotar el parser de `marshal` de una versión dada de CPython.

**Cambio de formato (rompe compatibilidad con `1.2.1`):** cada entrada de
`unexecuted_blocks` pasó de ser `(pos, tag, motivo)` a `(pos, tag, motivo, codigo)`.
`codigo` es `None` cuando se llama sin `show_source=True` — el resto de los campos
del reporte (`ok`, `fully_verified`, `bytes_total`, `bytes_consumed`, `tag_counts`)
es idéntico con o sin esta opción. Código que hacía
`for pos, tag, motivo in reporte.unexecuted_blocks` necesita el cuarto valor ahora.

---

## ⚙️ Instalación

`compickle` es un paquete real (`compickle/__init__.py` + `compickle/compickle.py` +
`compickle/compickle.c`), no un módulo suelto. La extensión C (`compickle._compickle`)
se compila e instala **dentro** de la carpeta del paquete al hacer `pip install`.

```bash
# Desde el directorio raíz del proyecto (donde está pyproject.toml)
pip install .

# O en modo editable, para desarrollo
pip install -e .
```

Esto compila `compickle/compickle.c` y coloca el `.so` resultante en
`compickle/_compickle.cpython-*.so`, junto a `__init__.py` — necesario para que el
`from ._compickle import ...` (import relativo) de `__init__.py` funcione.

**Si la compilación falla o `_compickle.so` no está presente por cualquier motivo**,
`compickle` usa el fallback puro Python de forma automática y silenciosa — no hace
falta configurar nada distinto en el código que lo consume. `compickle.backend()`
dice cuál de los dos está activo en cualquier momento.

Verificado explícitamente: instalación limpia vía `pip install .` en un entorno
aislado, confirmando que el `.so` compilado cae dentro de `site-packages/compickle/`
y que `dumps`/`loads`/`verify_stream`/`backend`/`dedup_reset` funcionan correctamente
contra esa instalación real (no solo contra el árbol de código fuente).

---

## 🚀 Uso rápido

```python
import compickle

datos = {
    "nombre": "Rex",
    "edad": 5,
    "activo": True,
    "coordenadas": (4.0, 2.0),
    "etiquetas": {"perro", "mascota"},
}

# Serializar a archivo
compickle.dump(datos, "datos.cpkl")

# Deserializar desde archivo
copia = compickle.load("datos.cpkl")

# Serializar/deserializar en memoria (bytes)
raw = compickle.dumps(datos)
copia2 = compickle.loads(raw)

# Ver qué motor está activo
print(compickle.backend())  # → 'c' o 'python'
```

---

## 📖 API completa

### `compickle.dump(obj, path)`

Serializa `obj` y escribe el resultado binario en `path`.

```python
compickle.dump(mi_objeto, "salida.cpkl")
```

### `compickle.dumps(obj) → bytes`

Serializa `obj` y devuelve los bytes resultantes directamente. Con el motor C, cada
llamada crea su propio `DedupState` en el stack — no hace falta llamar
`dedup_reset()` antes de cada `dumps()`.

```python
raw: bytes = compickle.dumps(mi_objeto)
```

### `compickle.load(path) → object`

Lee el archivo binario en `path` y reconstruye el objeto original.

```python
obj = compickle.load("salida.cpkl")
```

### `compickle.loads(data: bytes) → object`

Deserializa directamente desde un objeto `bytes`.

```python
obj = compickle.loads(raw_bytes)
```

### `compickle.dedup_reset()`

Limpia los cachés de `source`/`exec`/clases (tanto del motor C como del fallback
Python). El `DedupState` de serialización es local por llamada y no requiere reseteo
manual — esta función es para liberar memoria en procesos de larga duración que hayan
serializado muchas clases o funciones distintas.

```python
compickle.dedup_reset()
```

### `compickle.verify_stream(data: bytes, show_source: bool = False) → StreamReport`

*Nuevo en `1.2.1`, extendido en `1.2.2` con `show_source`.* Camina la estructura de
`data` sin deserializar de verdad — nunca ejecuta código, nunca construye objetos.
Ver la sección [🔒 `verify_stream()`](#-verify_stream--inspección-sin-ejecución)
arriba para el comportamiento completo, incluida la nota de seguridad de
`show_source=True`.

```python
reporte = compickle.verify_stream(datos_no_confiables, show_source=True)
reporte.ok               # bool: ¿estructura válida?
reporte.fully_verified   # bool: ¿válida Y sin ningún bloque que requiera ejecución?
reporte.error             # str | None: motivo si ok es False
reporte.unexecuted_blocks # list[(pos, tag, motivo, codigo)]: bloques que requieren ejecución
                           # codigo es None salvo que show_source=True
reporte.tag_counts        # dict[int, int]: conteo de cada opcode visto
reporte.bytes_total        # int
reporte.bytes_consumed     # int
```

### `compickle.backend() → str`

Devuelve el motor activo.

```python
compickle.backend()  # → 'c' o 'python'
```

---

## 🧩 Tipos soportados

| Tipo Python | Tag | Deduplicado | Notas |
|---|---|:---:|---|
| `None` | `0x00` | — | 1 byte |
| `False` / `True` | `0x80` / `0x81` | — | 1 byte |
| `int` (0–58) | `0xC0–0xFA` | — | 1 byte: opcode directo `0xC0 + valor` |
| `int` (59–255) | `0x11` | — | 2 bytes: tag + `uint8` |
| `int` (-1..-30) | `0x10` | — | 2 bytes |
| `int` (-31..-256) | `0x12` | — | 2 bytes |
| `int` (256–65535) | `0x0F` | — | 3 bytes: tag + `uint16` big-endian |
| `int` (arbitrario) | `0x02` | — | signo(1) + longitud + bytes big-endian |
| `float` (0.0 / -0.0 / 1.0 / -1.0 / NaN / ±inf) | `0x82`–`0x88` | — | 1 byte cada uno |
| `float` (general) | `0x03` | — | 9 bytes: tag + IEEE 754 doble precisión |
| `complex` | `0x04` | — | 17 bytes: tag + 2× `float64` |
| `str` | `0x05` / `0x15` | ✅ | `0x15` para strings nuevas ≤63 bytes UTF-8 |
| `bytes` | `0x06` | ✅ | Por contenido |
| `bytearray` | `0x07` | ✅ | Por contenido |
| `list` | `0x08` | — | Recursivo |
| `tuple` | `0x09` | — | Recursivo |
| `set` | `0x0A` | — | Ordenado por `repr()` para determinismo |
| `frozenset` | `0x0B` | — | Ordenado por `repr()` para determinismo |
| `dict` | `0x0C` | — | Recursivo en claves y valores |
| `function` / `lambda` | `0x20` | ✅ (bytecode) | Vía `marshal`: code object + defaults + freevars |
| Generador / corutina | `0x08` | — | Se consume y materializa como lista |
| `types.CodeType` | `0x21` | ✅ | Vía `marshal` directo |
| `type` (clase) | `0x12` | ✅ (fuente) | Nombre + módulo + `inspect.getsource` |
| Instancia con `__dict__` | `0x1C` | ✅ (fuente) | Encabezado de clase + `__dict__` |
| Instancia con `__slots__` | `0x1E` | ✅ (fuente) | Recorre el MRO completo |
| Objeto con `__reduce__` | `0x1F` | ✅ | Tuplas de 2 a 5 elementos |
| `classmethod` | `0x22` | ✅ (bytecode) | Envuelve el `__func__` interno (tag `0x20`) |
| `staticmethod` | `0x23` | ✅ (bytecode) | Envuelve el `__func__` interno (tag `0x20`) |
| `property` | `0x24` | ✅ (bytecode) | `fget`/`fset`/`fdel`, cualquiera puede ser `None` |

**Bound method (`types.MethodType`) no tiene tag propio.** Tiene `__reduce__`
heredado de la clase builtin `method`, así que ya cae en `0x1F` (`__reduce__`,
detectado antes de que se evalúe ningún tag dedicado) y reconstruye correctamente
`__func__` + `__self__` — no requiere un opcode dedicado.

**Prioridad de serialización de instancias:** `__reduce__` personalizado en el MRO →
`0x1F`; si no, `classmethod`/`staticmethod`/`property` por tipo exacto → `0x22`/
`0x23`/`0x24`; si no, `__dict__` disponible → `0x1C`; si no, `__slots__` sin
`__dict__` → `0x1E`. El chequeo de `classmethod`/`staticmethod`/`property` va
**antes** de la rama genérica `__dict__` a propósito: los tres SÍ tienen `__dict__`
propio (heredado de `object`, con metadata trivial como `__name__`/`__doc__`), así
que si cayeran en la rama genérica se intentaría `inspect.getsource()` sobre la
clase builtin en sí — que falla con `"<class 'X'> is a built-in class"` — en vez de
sobre la función envuelta, que es lo que de verdad hace falta preservar.

**Nota sobre clases/funciones reconstruidas:** al deserializar, `compickle` no
recupera el objeto `type`/`function` original — ejecuta el código fuente capturado en
un espacio de nombres nuevo y devuelve una clase/función equivalente por
comportamiento, pero distinta por identidad (`is`). Verificado comparando por
contenido/comportamiento, no por identidad de objeto.

---

## 🔬 Cómo funciona internamente

### Motor C (`compickle/compickle.c`)

**Buffer de salida (`Buf`).** Arranca en 256 bytes y se duplica vía `realloc` cuando
hace falta.

**Arena allocator.** Bloque contiguo para los datos de cada entrada de dedup —
`reset` es mover un puntero, sin `malloc`/`free` por entrada.

**`DedupState` local por llamada.**

```c
typedef struct {
    Arena    arena;
    DEntry  *table;
    int      count, cap;
    int     *buckets;
    int      nbuckets;
    IdEntry *id_tab;
    int      id_nb, id_count;
} DedupState;
```

Se declara como variable local dentro de `py_serialize_fast()` (`dedup_init(&ds)` al
entrar, `dedup_destroy(&ds)` al salir) — no hay estado global de dedup entre llamadas
distintas. Arranca en `DEDUP_CAP_INIT = 256` entradas y rehashea automáticamente
cuando el factor de carga supera `HASH_LOAD_MAX = 0.65`.

**`id_tab` (shortcut por identidad de puntero).** Antes de calcular el hash FNV-1a
del contenido, se busca `id(obj)` en `id_tab` — si hay hit, se emite la referencia
directamente sin volver a hashear ni comparar bytes.

**Tabla hash FNV-1a.** Hash de 32 bits sobre el `tag` de tipo más los bytes del dato;
colisiones resueltas con listas enlazadas.

**Codificación de longitud variable:**

```
n ≤ 0x3F     → 1 byte
n ≤ 0x3FFF   → 2 bytes  (0x40 | n>>8, n & 0xFF)
n > 0x3FFF   → 5 bytes  (0xFF + uint32 big-endian)
```

**Referencias dedup:**

```
idx ≤ 0xFE     → [0xFE][idx_1byte]      (2 bytes)
idx ≤ 0xFFFF   → [0xFD][idx_2bytes_be]  (3 bytes)
idx > 0xFFFF   → [0xFC][idx_4bytes_be]  (5 bytes)
```

Si hay hit de dedup, **no se reescribe el type tag** — solo la referencia. Este es
justamente el mecanismo donde vivía el bug #5 de la sección anterior: registrar un
índice sin emitir nada al stream para él desincronizaba las referencias siguientes.

**`read_table` dinámico.** Arranca en 256 entradas y crece con `realloc`.

**Cachés globales (`source_cache`, `exec_cache`).** Únicos estados que persisten
entre llamadas — mapean objeto → fuente capturada, y fuente → namespace ya
ejecutado, para no repetir I/O ni volver a `exec()` el mismo código.

### Fallback Python (`compickle/compickle.py`)

Implementa el mismo protocolo en Python puro:

- Deduplicación con `_dedup_id: dict[int, int]` (shortcut por identidad) y
  `_dedup_cnt: dict[tuple[int, bytes], int]` (por contenido, fusionados con
  `setdefault` para reducir accesos al dict).
- `_REF1`: tabla precalculada de referencias de 1 byte de índice.
- Serialización acumulada en `bytearray`; deserialización sobre `memoryview` sin
  copia.
- `_DISPATCH: dict[type, Callable]` para despacho por tipo exacto, evitando la
  cascada de `isinstance` en el caso común.
- `_deserialize` resuelve inline, en el propio bucle de listas/dicts, los 4 tags más
  frecuentes (entero pequeño, referencia, string corta, entero de 1 byte) antes de
  entrar a la recursión completa — la optimización de mayor impacto de esta ronda.

---

## 🧪 Ejemplos avanzados

Todos los ejemplos de esta sección se ejecutaron contra el código real de esta
sesión antes de documentarlos.

### Clase con `__dict__`

```python
import compickle

class Punto:
    def __init__(self, x, y, etiqueta='sin etiqueta'):
        self.x = x
        self.y = y
        self.etiqueta = etiqueta

    def distancia_origen(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

p = Punto(3, 4, 'origen')
raw = compickle.dumps(p)
p2 = compickle.loads(raw)
print(p2.x, p2.y, p2.etiqueta)      # → 3 4 origen
print(p2.distancia_origen())        # → 5.0
```

### Clase con `__slots__`

```python
class Vector:
    __slots__ = ("x", "y", "z")
    def __init__(self, x, y, z):
        self.x, self.y, self.z = x, y, z

v = Vector(1, 2, 3)
raw = compickle.dumps(v)
v2 = compickle.loads(raw)
print(v2.x, v2.y, v2.z)             # → 1 2 3
```

### Múltiples instancias de varias clases mezcladas

```python
class Alpha:
    def __init__(self, v):
        self.v = v

class Beta:
    def __init__(self, w, z):
        self.w = w
        self.z = z

payload = {
    "alphas": [Alpha(1), Alpha(2), Alpha(3)],
    "mixed": [Alpha(10), Beta(5, 6), Alpha(11)],
}
raw = compickle.dumps(payload)
back = compickle.loads(raw)
print([a.v for a in back["alphas"]])           # → [1, 2, 3]
print(back["mixed"][1].w, back["mixed"][1].z)  # → 5 6
```

### Deduplicación en acción

```python
repetidos = ["usuario_activo"] * 1000
raw = compickle.dumps(repetidos)
print(len(raw))   # ≈2000 bytes, no 1000× el tamaño del string
```

### Objeto con `__reduce__`

```python
class Color:
    def __init__(self, r, g, b):
        self.r, self.g, self.b = r, g, b

    def __reduce__(self):
        return (Color, (self.r, self.g, self.b))

c = Color(255, 128, 0)
raw = compickle.dumps(c)
c2 = compickle.loads(raw)
print(c2.r, c2.g, c2.b)   # → 255 128 0
```

### Lambda y funciones vía `marshal`

```python
fn = lambda x: x * 2
raw = compickle.dumps(fn)
fn2 = compickle.loads(raw)
print(fn2(21))   # → 42
```

### `classmethod`, `staticmethod` y `property`

```python
class Configuracion:
    valor_por_defecto = 10

    @classmethod
    def desde_entorno(cls, nombre):
        return cls(nombre, cls.valor_por_defecto)

    @staticmethod
    def validar(nombre):
        return len(nombre) > 0

    def __init__(self, nombre, valor):
        self.nombre = nombre
        self._valor = valor

    @property
    def valor(self):
        return self._valor

    @valor.setter
    def valor(self, nuevo):
        if nuevo < 0:
            raise ValueError("no puede ser negativo")
        self._valor = nuevo

# Cada uno se serializa por separado, tomado de Configuracion.__dict__
cm = Configuracion.__dict__["desde_entorno"]
raw_cm = compickle.dumps(cm)
cm2 = compickle.loads(raw_cm)
cfg = cm2.__get__(None, Configuracion)("produccion")
print(cfg.nombre, cfg.valor)   # → produccion 10

sm = Configuracion.__dict__["validar"]
raw_sm = compickle.dumps(sm)
sm2 = compickle.loads(raw_sm)
print(sm2.__get__(None, Configuracion)("produccion"))   # → True

prop = Configuracion.__dict__["valor"]
raw_prop = compickle.dumps(prop)
prop2 = compickle.loads(raw_prop)

class Reconstruida:
    valor = prop2
    def __init__(self, v):
        self._valor = v

r = Reconstruida(5)
print(r.valor)      # → 5
r.valor = 20
print(r.valor)      # → 20
```

### Inspeccionar un archivo no confiable antes de cargarlo

> Nota: como con cualquier clase serializada por `compickle` (ver
> [Limitaciones conocidas](#️-limitaciones-conocidas)), `Config` necesita estar
> definida en un archivo real para que `inspect.getsource()` pueda leerla — no
> funciona pegando este bloque directo en el REPL o en `python -c`.

```python
import compickle

# Caso 1: datos puros -- llega de una API externa, sin clases ni funciones
datos_api = {"usuario": "ana", "puntos": [10, 25, 40], "activo": True}
raw = compickle.dumps(datos_api)

reporte = compickle.verify_stream(raw)
print(reporte.fully_verified)   # → True: nada que ejecutar, seguro llamar loads()

# Caso 2: un archivo que sí trae una clase serializada
class Config:
    def __init__(self, modo):
        self.modo = modo

raw_con_clase = compickle.dumps({"config": Config("produccion"), "version": 3})

reporte2 = compickle.verify_stream(raw_con_clase)
print(reporte2.ok)               # → True: la estructura es válida
print(reporte2.fully_verified)   # → False: hay un bloque que requiere ejecución
for pos, tag, motivo, _ in reporte2.unexecuted_blocks:
    print(f"offset {pos}: 0x{tag:02X} — {motivo}")
# → offset 10: 0x1C — instancia: clase materializada vía exec() de su fuente

# Caso 3: lo mismo, pero leyendo el código real antes de decidir
reporte3 = compickle.verify_stream(raw_con_clase, show_source=True)
for pos, tag, motivo, codigo in reporte3.unexecuted_blocks:
    print(f"offset {pos}: {motivo}")
    print(codigo)
# → offset 10: instancia: clase materializada vía exec() de su fuente
#   # clase: Config
#   class Config:
#       def __init__(self, modo):
#           self.modo = modo

# verify_stream() no decide por vos si ese bloque es seguro -- ni siquiera
# con show_source=True. Solo te deja ver qué hay ahí para que decidas vos
# si llamar loads() sobre él.
```

---

## 📦 Formato binario — tabla completa de opcodes

```
Opcode        Tipo / Significado
──────────────────────────────────────────────────────────────────────────
0x00          None
0x02          int arbitrario: signo(1) + longitud + bytes big-endian
0x03          float general: 8 bytes IEEE 754 big-endian
0x04          complex: 2 × float64 big-endian (16 bytes)
0x05          str: tag + dedup(UTF-8) [strings largas o referencias]
0x06          bytes: tag + dedup(contenido)
0x07          bytearray: tag + dedup(contenido)
0x08          list / generador materializado: len(n) + n × serialize(item)
0x09          tuple: len(n) + n × serialize(item)
0x0A          set: len(n) + n × serialize(item ordenado por repr())
0x0B          frozenset: len(n) + n × serialize(item ordenado por repr())
0x0C          dict: len(n) + n × (serialize(k) + serialize(v))
0x0E          dedup corto (nuevo): longitud(1 byte, ≤63) + datos
0x0F          int 256–65535: 2 bytes uint16 big-endian
0x10          int negativo pequeño -1..-30: [0x10][magnitud-1]
0x11          int 59–255: [0x11][valor]
0x12          class: nombre + módulo + dedup(source)
0x15          str corta nueva (≤63 bytes UTF-8): [0x15][len][datos]
0x1B          class source comprimida con zlib (dentro de dedup)
0x1C          instancia __dict__: class_header + serialize(__dict__)
0x1D          class source sin comprimir (dentro de dedup)
0x1E          instancia __slots__: class_header + serialize(dict de slots)
0x1F          instancia __reduce__: callable_ref + args + flags + [state] + [list_items] + [dict_items]
0x20          función/lambda vía marshal: code + defaults + freevars
0x21          types.CodeType vía marshal directo
0x80 / 0x81   False / True
0x82–0x88     float: +0.0 / -0.0 / 1.0 / -1.0 / NaN / +inf / -inf
0xC0–0xFA     int pequeño positivo (valor = opcode − 0xC0, rango 0–58)
0xFB          dedup largo (nuevo): tag(1) + longitud + datos (>63 bytes)
0xFC          referencia dedup: 4 bytes big-endian de índice
0xFD          referencia dedup: 2 bytes big-endian de índice
0xFE          referencia dedup: 1 byte de índice
```

---

## ⚠️ Limitaciones conocidas

- **Clases y callables de `__reduce__` requieren fuente accesible:**
  `inspect.getsource()` debe poder leer el código original. No funciona con clases
  definidas en el REPL o vía `exec()`/`python -c` sin archivo de respaldo. Funciones
  normales y lambdas sí funcionan en el REPL vía `marshal`.
- **Funciones no portables entre versiones de Python:** el bytecode serializado con
  `marshal` está ligado al magic number de la versión de CPython que lo generó.
- **Generadores se consumen:** serializar un objeto generador ya instanciado lo
  materializa en lista y lo agota. Para preservar la capacidad de generar, serializar
  la función generadora en sí, no su resultado.
- **No compatible con `pickle`:** formato binario propio, no intercambiable con
  `pickle`, `marshal` u otros serializadores estándar.
- **Referencias circulares no auditadas** en esta ronda de trabajo.
- **`verify_stream()` no es un sandbox ni un antivirus.** Confirma estructura
  (tags válidos, longitudes consistentes, referencias en rango) y te dice
  exactamente qué bloques requerirían ejecutar código si llamaras `loads()` —
  pero no analiza ni juzga si ese código sería dañino. Un stream con
  `fully_verified=False` puede contener una clase totalmente inofensiva o
  una maliciosa; `verify_stream()` no distingue entre ambas, solo señala
  dónde está el código que tendrías que confiar en ejecutar.
- **`reason` de `property` difiere ligeramente entre backends.** Con
  `show_source=True`, `code_text` es idéntico byte a byte entre C y Python (ambos
  usan la misma función de render). Pero el campo corto `reason` (visible incluso
  sin `show_source`) dice `"envuelve fget, fset"` en Python (específico) y
  `"envuelve accesor(es)"` en C (genérico) — decisión deliberada para evitar un
  buffer `static` reusado en la recursión de C que habría corrompido `reason` al
  serializar múltiples `property` en el mismo stream. Si el texto corto exacto de
  `reason` importa para tu caso de uso, usa `code_text` (con `show_source=True`),
  que sí es consistente entre ambos backends.

---

## 📄 Licencia

MIT — úsalo como quieras.
