El problema fundamental que Goose aborda es la complejidad y el costo de rendimiento inherentes a la gestión de memoria en lenguajes de sistemas modernos. Históricamente, los lenguajes de bajo nivel como C/C++ ofrecen control granular sobre la memoria a expensas de la seguridad (errores de use-after-free, double-free, buffer overflows), mientras que lenguajes como Rust introducen sistemas de propiedad y 'borrowing' para garantizar la seguridad en tiempo de compilación, pero con una curva de aprendizaje pronunciada y la necesidad de anotaciones explícitas. Los lenguajes con recolectores de basura (Java, Go) simplifican la gestión, pero introducen pausas de latencia y un mayor consumo de memoria.

Goose propone una alternativa radical: un modelo de memoria basado en 'data stacks' donde todos los valores dinámicos residen en stacks gestionados por el compilador. Este enfoque elimina la necesidad de asignadores de heap, recolectores de basura, conteo de referencias o destructores. La memoria se libera automáticamente al finalizar el alcance de un valor, y el compilador garantiza la seguridad de las referencias en tiempo de compilación sin anotaciones explícitas. Esto permite que Goose logre un rendimiento superior y un menor consumo de memoria, resolviendo el dilema entre seguridad y eficiencia que ha plagado el desarrollo de sistemas por décadas.

Arquitectura del Sistema

La arquitectura de Goose se centra en un modelo de memoria unificado y determinista. Un programa Goose utiliza la pila de llamadas nativa, datos estáticos y un número 'N' de 'data stacks', cuyo número es determinado estáticamente por el compilador. Cada 'data stack' es una reserva de espacio de direcciones grande con un 'bump pointer'. No existe otro tipo de memoria. En cada 'data stack', a lo sumo un valor redimensionable está activo y siempre se encuentra en la parte superior, lo que garantiza que el crecimiento nunca mueva datos existentes y no requiera comprobaciones de capacidad. Este modelo es probado en tiempo de compilación, y en tiempo de ejecución solo se gestionan los 'bump pointers'.

Las estructuras de datos anidadas permanecen 'inline' y contiguas, sin punteros internos. Por ejemplo, un array de strings o un registro con campos de tamaño variable se almacenan como un único bloque de memoria. Esto optimiza el uso de la caché y reduce la sobrecarga de memoria. Las referencias son 'relative references' (offsets de 1, 2 o 4 bytes) en lugar de direcciones de máquina absolutas, lo que hace que las estructuras de datos sean independientes de la posición y facilita su serialización/deserialización directa a/desde archivos. Los 'Algebraic Data Types' (ADTs) pueden almacenarse en modo fijo (con espacio para la variante más grande) o en modo variable (cada valor ocupa solo el espacio de su variante), permitiendo optimizaciones de memoria y acceso a referencias internas en el modo variable.

El compilador de Goose genera un único archivo C, lo que permite la interoperabilidad directa con código C existente a través de extern fn. El backend TinyCC integrado puede compilar y ejecutar programas en proceso sin un paso de construcción explícito. Para la concurrencia, Goose adopta un modelo de 'shared-nothing' donde cada 'worker thread' se compila como un programa separado con su propia memoria. La comunicación entre hilos se realiza a través de 'typed queues' que transmiten valores 'flat' (sin referencias internas) mediante memcpy, eliminando las condiciones de carrera, bloqueos y problemas de ordenación de memoria.

Flujo de Gestión de Memoria en Goose

  1. 1 Declaración de Variable El compilador asigna la variable a un 'data stack' o a la pila de llamadas na...
  2. 2 Crecimiento de Estructura Un 'bump pointer' avanza en el 'data stack'; las referencias existentes perma...
  3. 3 Acceso a Referencia El compilador verifica estáticamente que la referencia no sobreviva a su prop...
  4. 4 Fin de Alcance La memoria se libera con una única operación de 'store' al 'bump pointer' del...

Flujo de Compilación y Ejecución

  1. 1 Código Fuente Goose El compilador de Goose procesa el archivo .goose.
  2. 2 Generación de C El compilador emite un único archivo .c que incluye el programa y partes del ...
  3. 3 Compilación C Un compilador C (MSVC, Clang, GCC) compila el archivo .c y el runtime de Goose.
  4. 4 Ejecución El ejecutable nativo resultante se ejecuta directamente.
CapaTecnologíaJustificación
compute Goose Compiler Responsable de la inferencia de tipos, análisis de vida de referencias, optimización de layout de datos y generación de código C. vs LLVM, GCC, Rustc
storage Data Stacks Mecanismo primario de gestión de memoria, eliminando la necesidad de heap y GC. Permite referencias estables y layouts de datos contiguos. vs Heap Allocation (malloc/free), Garbage Collection (JVM, Go), Reference Counting (ARC) --stack-reserve 2G (default)
networking Typed Queues (for threads) Facilita la comunicación entre 'worker threads' con un modelo 'shared-nothing', garantizando la seguridad de datos a través de `memcpy` de valores 'flat'. vs Shared Memory (locks, atomics), Message Passing (Actor Model), RPC frameworks
orchestration TinyCC (TCC) Backend de compilación C integrado para ejecución 'in-process', permitiendo un ciclo de desarrollo rápido sin pasos de construcción explícitos. vs GCC, Clang, MSVC

Trade-offs

Ganancias
  • ▲ Rendimiento de ejecución
  • ▲ Consumo de memoria
  • ▲▲ Seguridad de memoria
  • ▲ Simplicidad del modelo de memoria
  • ▲ Serialización/deserialización de datos
Costes
  • ▲ Flexibilidad en la gestión de memoria dinámica
  • △ Uso de funciones recursivas con datos crecibles
  • △ Indexación directa de arrays de elementos de tamaño variable
  • △ Polimorfismo dinámico (solo ADT tags)
  • △ Ausencia de closures que escapan o punteros a funciones
struct Item { id: i32, weight: f32 }
var items: Item[>..] = [];
let first .= items.push(Item { 1, 0.5 }); // a reference to element 0
for i: i32 in 2..1000001 { items.push(Item { i, 0.0 }); }
first.weight = 99.5; // still valid, a million pushes later
Demuestra cómo una referencia a un elemento de un array `Item[>..]` permanece válida incluso después de que se añaden un millón de elementos, gracias al modelo de memoria de Goose.
fn load(text: u8[:]) -> User[>..], u8[] {
  var users: User[>..] = [];
  each_split(text, '\n') { users.push(parse_user(it)); };
  return users, "";
}
fn parse_user(line: u8[:]) -> User {
  let comma = find(line, ",");
  guard comma >= 0 else { return [], str("expected name,age: ", line) from load; }
  let age, ok = parse_int(line[comma + 1..]);
  guard ok else { return [], str("bad age: ", line) from load; }
  let name = trim(line[..comma]);
  return User { name: name, age: age as i32 };
}
Ilustra cómo `return E from f` permite retornar un error desde una función anidada directamente a una función 'ancestral' (`load` en este caso), con verificación estática y sin tipos `Result` o `?`.

Fundamentos Teóricos

El concepto de gestión de memoria basada en 'stacks' y la eliminación de la recolección de basura se remonta a los primeros lenguajes de programación y sistemas operativos, donde los 'call stacks' eran la forma dominante de gestionar la memoria local de las funciones. Sin embargo, la aplicación de este principio a estructuras de datos dinámicas y complejas, garantizando la seguridad de las referencias en tiempo de compilación sin anotaciones explícitas, es una innovación que se alinea con la investigación en 'ownership types' y 'region-based memory management'.

Aunque Goose no cita directamente un paper fundacional, su enfoque recuerda a los principios explorados en trabajos sobre 'linear types' o 'affine types', donde los recursos (incluida la memoria) tienen un único propietario y se utilizan exactamente una vez. El sistema de 'borrowing' de Rust, influenciado por la investigación en 'region inference' y 'alias analysis', busca resolver problemas similares. La capacidad de Goose para inferir las raíces de las referencias y rechazar aquellas que puedan 'outlive' a sus propietarios es una forma de 'lifetime analysis' que se realiza implícitamente, simplificando la experiencia del desarrollador en comparación con los sistemas explícitos. La idea de 'stable references' en arrays que crecen sin reubicación es un patrón de diseño conocido en la optimización de estructuras de datos, pero Goose lo eleva a una garantía a nivel de lenguaje.