1 puntos por GN⁺ 4 시간 전 | 1 comentarios | Compartir por WhatsApp
  • Ante es un diseño de lenguaje de sistemas que busca combinar la flexibilidad del conteo de referencias con la seguridad de la verificación de préstamos, evitando tanto los pánicos en tiempo de ejecución al estilo Rust como la sobrecarga de las comprobaciones de acceso exclusivo al estilo Swift
  • Su mecanismo central es shape-stability y la temporary uniq conversion: permite crear préstamos mutables de forma segura sobre campos de valores con conteo de referencias, y trata los valores internos de una unión como uniq solo dentro de un alcance limitado
  • Rc<RefCell<T>> de Rust puede provocar pánicos en tiempo de ejecución si se usa mal, y el borrowing system de Swift incluye comprobaciones de acceso exclusivo en tiempo de ejecución, pero Ante intenta manejar algunos casos con reglas en tiempo de compilación
  • Es un diseño work-in-progress, aún implementado solo en parte; como debe analizar tipos recursivamente para determinar si se puede llegar a cierto objeto, agregar campos puede convertirse en un breaking API change
  • Este enfoque debilita la premisa de que shared mutable borrowing siempre es imposible, y junto con técnicas como Vale, group borrowing y Rust GhostCell está ampliando las zonas de excepción en el diseño de seguridad de memoria

La combinación que busca Ante

  • Ante es un lenguaje de programación de sistemas que apunta a ser un Rust más simple, con seguridad de memoria y seguridad de hilos
  • Su modelo básico es la propiedad única y la verificación de préstamos, y los valores se colocan inline en la pila o dentro de estructuras y arreglos que los contienen
  • Cuando se quiere priorizar la simplicidad, se puede optar por el conteo de referencias agregando la palabra clave shared a un tipo
  • La función balance de un red-black tree que usa shared type Color y shared type RbTree t es tan corta como el ejemplo en Python y más pequeña que los ejemplos en C++ y Rust
  • La cuestión central es cómo manejar el préstamo mutable de datos con conteo de referencias sin el riesgo de pánico de borrow_mut() en Rust ni las comprobaciones de acceso exclusivo en tiempo de ejecución de Swift
  • Ante todavía está en estado work-in-progress: algunas partes están implementadas, otras son teóricas y el diseño sigue cambiando

shape-stability y múltiples referencias mutables

  • Shape-stability en Ante es la idea de que “una referencia a un objetivo con stable shape siempre es válida, sin importar qué cambios ocurran en otro lugar”
  • Gracias a este concepto, se pueden tener varias referencias de préstamo mutable simultáneamente a la misma estructura
  • En el ejemplo heal (healer: mut Entity) (target: mut Entity), es posible llamar a self_heal pasando la misma Entity como ambos argumentos para que se cure a sí misma
    • Aunque healer y target apunten a la misma Entity, en este código no se puede destruir la Entity, así que ambas referencias siguen siendo válidas
  • También pueden permitirse simultáneamente referencias mutables a la estructura misma, a sus campos y a los campos de sus campos
    • Aunque se usen al mismo tiempo ship: mut Spaceship y engine_alias: mut Engine = ship.engine, se determina que durante la ejecución de la función no se destruirán ni ship ni su engine
  • Rust y Swift no permiten una forma en la que varias referencias &mut apunten simultáneamente a los mismos datos

Préstamos mutables de campos de valores con conteo de referencias

  • En Ante, si se antepone shared a una definición de tipo, ese tipo pasa automáticamente a tener conteo de referencias
  • En el ejemplo shared mut type Spaceship, launch mantiene un Spaceship equivalente a Rc mientras pasa mut ship.engine a set_fuel
  • Como launch conserva el objeto contenedor Spaceship, se puede determinar que su campo engine también sigue vivo
  • La regla general es que siempre se puede crear una referencia de préstamo mut para un campo de un tipo shared mut
    • Sin embargo, no siempre se puede crear un préstamo mutable para todos los objetivos contenidos dentro de ese campo; se necesitan reglas separadas
  • Los ejemplos posteriores usan la notación más explícita Rc Spaceship en lugar del azúcar sintáctico shared mut type Spaceship
    • shared mut type Spaceship pasa a ser type Spaceship, y var ship: Spaceship pasa a ser var ship: Rc Spaceship

Dónde las uniones generan problemas de seguridad

  • Las uniones pueden guardar su contenido inline, lo que reduce el seguimiento de punteros y los fallos de caché, por lo que favorecen la velocidad
    • Si la union Engine de C está dentro de struct Spaceship, StringTheoryEngine e ImpulseEngine quedan ubicados dentro de la memoria de Spaceship
    • Esto contrasta con un enfoque como el de Java, que usa interfaces y punteros
  • El problema es que en lenguajes con seguridad de memoria es difícil soportar uniones de forma segura
  • En un ejemplo donde Engine es StringTheoryEngine(str: String) o ImpulseEngine(fuel: I32), puede ocurrir un segfault cuando ship y other_ship apuntan al mismo Spaceship
    • Primero se toma una referencia interna al string con match uniq ship.engine
    • Luego se cambia el mismo motor a otra variante con other_ship.engine := ImpulseEngine 0x42
    • Y después, si se modifica el str anterior, se termina usando el interior de un contenedor después de que fue destruido
  • Por eso Ante debe impedir que, cuando una referencia de préstamo mutable apunta a una unión, se cree una referencia de préstamo mutable a una de sus variantes
  • Esto es lo opuesto a la regla de las estructuras
    • Si se tiene una referencia mut a una estructura, se puede crear una referencia mut a un campo
    • Si se tiene una referencia mut a una unión, no se puede crear una referencia mut al interior de una variante

uniq y temporary uniq conversion

  • uniq significa exclusive mutable reference, es decir, referencia mutable exclusiva
  • Si una variable contiene un uniq Spaceship, es la única referencia utilizable a ese Spaceship
    • Es un concepto similar a &mut Spaceship en Rust
  • Para manejar de forma segura el interior de una unión, Ante usa temporary uniq conversion
  • La regla central es que, si dentro de un alcance específico no se usa ninguna otra referencia que pueda tener alias, se puede obtener temporalmente una referencia uniq
    • En la sección match uniq ship.engine, se accede a ship.engine como si fuera uniq
    • Durante esa sección, el compilador impide usar otras variables existentes que puedan contener indirectamente un Spaceship
  • Rust bloquea la existencia misma de uniq porque “podría haber otras referencias en alguna parte”, mientras que Ante permite uniq con la condición de no usar esas referencias en ese alcance
  • En este caso, uniq Spaceship no es realmente una referencia globalmente única, sino la única referencia utilizable dentro de ese alcance
    • Tiene un matiz similar a los punteros restrict de C

Accesos permitidos y rechazados

  • Dentro del alcance de match uniq ship.engine, acceder a other_ship: Rc Spaceship debería producir un error de compilación
    • Porque other_ship.engine podría ser un alias de ship.engine
    • Y porque modificar other_ship.engine mientras se usa ship.engine podría provocar un drop
  • Otra estructura que tenga un Rc Spaceship como campo, como HasAShip, también se rechaza por la misma razón
    • other.ship.engine también puede llegar indirectamente al mismo Spaceship
  • En cambio, sí se puede usar un entero como new_fuel: I32
    • Porque I32 no puede contener una referencia a Spaceship
  • Si Spaceship contiene un campo como follow_ship: Rc Spaceship, se rechaza
    • En ese caso, un uniq Spaceship también podría volver a ser alcanzable mediante una ruta interna propia, así que en general no se puede hacer la conversión mut -> uniq en tipos recursivos

Restricciones en llamadas y retornos de funciones

  • La conversión mut -> uniq también puede ocurrir en llamadas de funciones
  • Cuando foo (var ship: Rc Spaceship) (new_res: Resonator) llama a maybe_use_resonator ship new_res, en el punto de llamada ship se convierte en uniq Spaceship
    • El compilador solo debe verificar si otros argumentos pueden contener referencias a Spaceship
    • En el ejemplo, Resonator no contiene esas referencias, por lo que se permite
  • En el retorno, una referencia uniq convertida no puede devolverse como un uniq normal
    • Porque después del retorno ya no se aplica la comprobación del compilador de “no usar variables que puedan tener alias dentro del alcance”
  • En su lugar, el tipo de retorno puede especificarse como local uniq Foo
    • Internamente, cuando se convierte de mut ref a uniq ref, en realidad siempre se crea un local uniq
    • En la mayoría de los casos puede usarse como un uniq normal, pero al retornarlo se necesita hacerlo explícito

Costos de diseño y alternativas

  • Ante puede convertir una referencia con conteo de referencias como Rc Spaceship en un uniq Spaceship temporal sin errores en tiempo de ejecución
  • La desventaja es que el compilador debe inspeccionar recursivamente los tipos para responder preguntas como “¿se puede llegar a Spaceship desde Engine?”
  • Este análisis puede ser frágil
    • Agregar un campo a una estructura puede convertirse en un breaking API change
  • Jake, el creador de Ante, busca una mejor forma de mantener esta garantía
    • Un enfoque como group borrowing y Flix references, que asigna a cada tipo mutable compartido un tipo de marca único anónimo
    • Un enfoque que agrega un efecto como Mutates 'a al modificar tipos compartidos, eliminando el análisis de tipos
    • Un enfoque en el que el usuario comprueba en tiempo de ejecución si dos referencias apuntan a objetos distintos, o se ofrece una comprobación unsafe envuelta en una API safe
    • Un enfoque en el que el compilador rastrea valores que no están almacenados indirectamente dentro de un Rc y por lo tanto no pueden tener alias
  • También quedan como posibilidades ideas similares al iso permission de Pony, o permisos temporales que permitan mirar dentro de una estructura pero no usar referencias que apunten hacia afuera
  • Lo difícil es conservar esta flexibilidad mientras se mantienen los objetivos de Ante: usabilidad, legibilidad y simplicidad

La corriente más amplia en seguridad de memoria

  • Antes se consideraba que shared mutable borrowing era imposible, y se parte de la perspectiva de que Rust también fue diseñado sobre esa creencia
  • Se están acumulando varias excepciones
    • Ante puede obtener referencias de préstamo uniq desde datos shared-mutable mediante reglas de local uniqueness
    • Vale puede obtener referencias de préstamo inmutables desde datos shared-mutable mediante pure function
    • group borrowing puede crear referencias de préstamo shared-mutable incluso cuando no hay shape-stability
    • GhostCell de Rust permite que los grafos de objetos se apunten libremente entre sí, pero en un momento dado solo puede haber una referencia mutable a uno de ellos
  • Esta corriente sugiere que podría haber principios más generales para tratar shared mutable borrowing en el diseño de seguridad de memoria

Comparación con Cell de Rust

  • Los usuarios de Rust pueden preguntarse cuál es la diferencia entre poner un Cell en un campo de una estructura y el enfoque de Ante
  • En el ejemplo de Ante, se puede obtener una referencia mut String a status: String desde Rc Spaceship y agregar directamente " (refueling)"
  • Con el enfoque Cell<String> de Rust, no se puede obtener un &mut String desde Rc<Spaceship>
    • En su lugar, se pone un valor predeterminado temporal con status_ref.replace(String::new())
    • Luego se modifica el String extraído
    • Y al final se vuelve a colocar con replace(status)
  • Este enfoque tiene varias desventajas
    • Hay que crear una instancia predeterminada como ""
    • Existe el riesgo de olvidar la última llamada a replace
    • Existe el riesgo de que alguien lea status mientras el valor está reemplazado
  • Ante permite obtener temporalmente una referencia al string status, y mientras tanto el compilador impide que otro código acceda a él

1 comentarios

 
GN⁺ 4 시간 전
Comentarios en Lobste.rs
  • El hecho de que las “referencias prestadas mutables compartidas” se consideraran imposibles no fue simplemente un sacrificio que Rust aceptó para alcanzar su objetivo, sino algo mucho más cercano al objetivo central de Rust
    porque el estado mutable compartido dificulta el razonamiento local sobre el código
    "References are like jumps" de withoutboats trata muy bien este punto. La idea es que impedir cambios accidentales sobre estado con alias es clave para construir con facilidad sistemas que funcionen correctamente, y que las reglas de lifetimes de Rust no son solo un mecanismo para evitar el garbage collection, sino una estructura más profunda para asegurar la capacidad de razonamiento en un lenguaje que permite al mismo tiempo estado mutable y estado con alias

    • Pensé lo mismo antes cuando el autor habló del borrow checker de Mojo. El borrow checker de Rust preserva la semántica de valor incluso en programas de un solo hilo
  • Se ve bastante bien
    Si entendí bien, la magia de pasar de una referencia compartida a una mutable solo es posible porque está limitada a tipos que no se comparten entre hilos, y la unicidad de Rc parece garantizarse tratando todos los objetos del mismo tipo como si estuvieran prestados con el mismo lifetime
    Puede que la preferencia entre una sintaxis explícita y una más natural sea cuestión de gustos, pero muestra que, si el compilador sabe más sobre Cell, puede permitir referencias mutables hacia este de manera más flexible
    Y además evita la terminología confusa de Rust, donde mut se usa como si significara no mutabilidad sino exclusividad/unicidad

    • Yo también me preguntaba cómo funcionaría entre hilos. Tenía dudas del tipo “¿la promoción de uniq implica adquirir un lock?”, pero entiendo que la comparación no es con Arc, sino con Rc
    • ¿Podrías explicar un poco más esa parte de que mut significa exclusividad/unicidad?
  • Me pregunto si alguien tiene idea de cuál podría ser el principio unificador que se insinúa al final

  • También vale la pena revisar discusiones anteriores sobre publicaciones del blog de antelang.org

  • No entiendo bien cómo funciona esto. Parece querer decir algo como “si tienes un puntero mutable a un objeto, puedes obtener una referencia mutable a un slice de ese objeto”
    Pero si es así, entonces parecería posible hacer algo como mutref someobjext = …, mutref subfield = someobjext.a.b, someobjext.a = somethingelse, y entonces subfield podría invalidarse o romperse al cambiar de valor
    El artículo tenía muchas explicaciones, comparaciones con otros lenguajes y ejemplos de código, pero me costó encontrar una explicación básica de la semántica paso a paso de cómo se supone que funciona esto