Design: call_indirect versus abstracción

Creado en 7 may. 2020  ·  36Comentarios  ·  Fuente: WebAssembly/design

call_indirect ha sido una característica muy útil para WebAssembly. Sin embargo, la eficiencia y el buen comportamiento de la instrucción se ha basado implícitamente en la simplicidad del sistema de tipos de wasm. En particular, cada valor de wasm tiene exactamente un tipo (estático) al que pertenece. Esta propiedad evita convenientemente una serie de problemas conocidos con llamadas a funciones sin tipo en lenguajes con tipo. Pero ahora que wasm se está extendiendo más allá de los tipos numéricos, ha llegado al punto en el que necesitamos comprender estos problemas y tenerlos en cuenta.

call_indirect funciona fundamentalmente comparando la firma esperada de la persona que llama con la firma definida de la persona que llama. Con solo tipos numéricos, WebAssembly tenía la propiedad de que estas firmas eran iguales si y solo si se hubiera verificado el tipo de una llamada directa a la función a la que hace referencia el funcref correspondiente. Pero hay dos razones que pronto no serán ciertas:

  1. Con el subtipo, una llamada directa funcionaría siempre que la firma definida de la función real sea una "sub-firma" de la firma esperada, lo que significa que todos los tipos de entrada son subtipos de los tipos de parámetros de la función y todos los tipos de salida son supertipos de la función. tipos de resultados. Esto significa que una verificación de igualdad entre la firma esperada de una llamada indirecta y la firma definida de la función quedaría atrapada en una serie de situaciones perfectamente seguras, lo que podría ser problemático para soportar lenguajes con un uso intensivo de subtipos y llamadas indirectas (como se planteó durante la discusión sobre diferir el subtipo). También significa que, si un módulo exporta intencionalmente una función con una firma más débil de la que se definió con la función, entonces call_indirect se puede usar para acceder a la función con su firma privada definida en lugar de solo su firma pública más débil ( un problema que se acaba de descubrir y, por lo tanto, aún no se ha discutido).
  2. Con las importaciones de tipos, un módulo puede exportar un tipo sin exportar la definición de ese tipo, lo que proporciona una abstracción en la que los sistemas como WASI planean confiar en gran medida. Esa abstracción evita que otros módulos dependan de su definición particular en el momento de la compilación . Pero en el tiempo de ejecución, el tipo exportado abstracto simplemente se reemplaza con su definición. Esto es importante, por ejemplo, para permitir que call_indirect funcione correctamente en funciones exportadas cuyas firmas exportadas hacen referencia a ese tipo exportado. Sin embargo, si un módulo malintencionado sabe cuál es la definición de ese tipo exportado, puede usar call_indirect para realizar conversiones entre el tipo exportado y su definición destinada a ser secreta porque call_indirect solo compara firmas en tiempo de ejecución, cuando los dos tipos son realmente iguales . Por lo tanto, un módulo malicioso puede usar call_indirect para acceder a secretos destinados a ser abstraídos por el tipo exportado, y puede usar call_indirect para falsificar valores del tipo exportado que pueden violar invariantes críticos de seguridad no capturados en la definición del tipo en sí.

En las dos situaciones anteriores, call_indirect se puede utilizar para omitir la abstracción de la firma exportada de un módulo. Como mencioné, hasta ahora esto no ha sido una preocupación porque wasm solo tenía tipos numéricos. Y originalmente pensé que, al diferir el subtipo, todas las preocupaciones con respecto a call_indirect también se habían diferido de manera efectiva. Pero lo que me di cuenta recientemente es que, al eliminar el subtipo, el tipo "nuevo" (llamado externref en https://github.com/WebAssembly/reference-types/pull/87) es efectivamente un sustituto para una importación de tipo abstracto. Si eso es lo que a la gente le gustaría que fuera, entonces, lamentablemente, debemos tener en cuenta la interacción anterior entre call_indirect y las importaciones de tipos.

Ahora hay muchas formas posibles de abordar los problemas anteriores con call_indirect , pero cada uno tiene sus compensaciones, y es simplemente un espacio de diseño demasiado grande para poder tomar una decisión rápidamente. Así que no estoy sugiriendo que podemos resolver este problema, aquí y ahora. Más bien, la decisión que se debe tomar en este momento es si se debe ganar tiempo para resolver el problema adecuadamente con respecto a externref . En particular, si por ahora restringimos call_indirect y func.ref solo para verificar el tipo cuando la firma asociada es completamente numérica, entonces servimos todos los casos de uso de core-wasm de llamadas indirectas y en el Al mismo tiempo, deje espacio para todas las posibles soluciones a los problemas anteriores. Sin embargo, no sé si esta restricción es práctica, tanto en términos de esfuerzo de implementación como en términos de si obstruye las aplicaciones de externref que la gente está esperando. La alternativa es dejar call_indirect y func.ref como están. Es posible que esto signifique que, dependiendo de la solución a la que lleguemos, externref podría no ser instanciable como lo sería una importación de tipo verdadero, y / o que externref podría (irónicamente) no poder tener cualquier supertipo (por ejemplo, podría no ser un subtipo de anyref si finalmente decidimos agregar anyref ).

Yo, hablando solo por mí, considero que ambas opciones son manejables. Si bien tengo una preferencia, no estoy presionando fuertemente la decisión de ir de una manera u otra, y creo que todos tienen mejor acceso a la información necesaria para tomar una decisión bien informada. Solo quería que todos supieran que hay que tomar una decisión y, al mismo tiempo, quería crear conciencia sobre el problema general con call_indirect . Si desea una explicación más completa de ese problema que la que proporciona el resumen anterior, lea lo siguiente.

call_indirect versus abstracción, en detalle

Usaré la notación call_indirect[ti*->to*](func, args) , donde [ti*] -> [to*] es la firma esperada de la función, func es simplemente un funcref (en lugar de una tabla funcref y un índice), y args son los to* valores para pasar a la función. De manera similar, usaré call($foo, args) para una llamada directa de la función con el índice $foo pasando argumentos args .

Ahora suponga que $foo es el índice de una función con tipos de entrada declarados ti* y tipos de salida to* . Es de esperar que call_indirect[ti*->to*](ref.func($foo), args) sea ​​equivalente a call($foo, args) . De hecho, ese es el caso en este momento. Pero no está claro que podamos mantener ese comportamiento.

call_indirect y subtipificación

Un ejemplo de problema potencial surgió en la discusión de la subtipificación. Suponga lo siguiente:

  • tsub es un subtipo de tsuper
  • La instancia de módulo IA exporta una función $fsub que se definió con el tipo [] -> [tsub]
  • el módulo MB importa una función $fsuper con el tipo [] -> [tsuper]
  • La instancia de módulo IB es el módulo MB instanciado con $fsub IA como $fsuper (lo cual es lógico, incluso si no es posible ahora, este problema se trata de posibles problemas futuros )

Ahora considere lo que debería suceder si IB ejecuta call_indirect[ -> tsuper](ref.func($fsuper)) . Estos son los dos resultados que parecen más plausibles:

  1. La llamada se realiza correctamente porque la firma esperada y la firma definida son compatibles.
  2. La llamada se interrumpe porque las dos firmas son distintas.

Si tuviéramos que elegir el resultado 1, tenga en cuenta que probablemente necesitaríamos emplear una de dos técnicas para que esto sea posible:

  1. Para funciones importadas, haga que call_indirect compare con la firma de importación en lugar de la firma de definición.
  2. Realice una comprobación en tiempo de ejecución de al menos tiempo lineal para comprobar la compatibilidad de subtipos de la firma esperada y la firma de definición.

Si prefiere la técnica 1, tenga en cuenta que no funcionará una vez que agreguemos Referencias de función escrita (con subtipos de variantes). Es decir, func.ref($fsub) será un ref ([] -> [tsub]) y también un ref ([] -> [tsuper]) y, sin embargo, la técnica 1 no será suficiente para evitar que call_indirect[ -> super](ref.func($fsub)) atrapen. Esto significa que el resultado 1 probablemente requiera la técnica 2, que tiene implicaciones de rendimiento preocupantes.

Así que consideremos el resultado 2 un poco más. La técnica de implementación aquí es verificar si la firma esperada de call_indirect en IB es igual a la firma de la definición de $fsub en IA. Al principio, la principal desventaja de esta técnica puede parecer que atrapa una serie de llamadas que son seguras de ejecutar. Sin embargo, otro inconveniente es que potencialmente introduce una fuga de seguridad para IA.

Para ver cómo, cambiemos un poco nuestro ejemplo y supongamos que, aunque la instancia IA define internamente $fsub para tener el tipo [] -> [tsub] , la instancia IA solo lo exporta con el tipo [] -> [tsuper] . Usando la técnica para el resultado 2, la instancia IB puede ejecutar (maliciosamente) call_indirect[ -> tsub]($fsuper) y la llamada tendrá éxito. Es decir, IB puede usar call_indirect para eludir el estrechamiento que IA hizo a la firma de su función. En el mejor de los casos, eso significa que IB depende de un aspecto de IA que no está garantizado por la firma de IA. En el peor de los casos, IB puede usar esto para acceder al estado interno que IA podría haber estado ocultando intencionalmente.

call_indirect y tipo Importaciones

Ahora dejemos de lado el subtipo y consideremos las importaciones de tipos . Por conveniencia, voy a hablar de importaciones de tipos, en lugar de solo importaciones de tipos de referencia, pero ese detalle es intrascendente. Para el ejemplo de ejecución aquí, suponga lo siguiente:

  • IC de instancia de módulo define un tipo capability y exporta el tipo pero no su definición como $handle
  • IC de instancia de módulo exporta una función $do_stuff que fue definida con el tipo [capability] -> [] pero exportada con el tipo [$handle] -> []
  • módulo MD importa un tipo $extern y una función $run con el tipo [$extern] -> []
  • ID de instancia de módulo es módulo MD instanciado con IA exportado $handle como $extern y con IA exportado $do_stuff como $run

Lo que este ejemplo configura son dos módulos en los que un módulo hace cosas con los valores del otro módulo sin saber o sin que se le permita saber cuáles son esos valores. Por ejemplo, este patrón es la base planificada para interactuar con WASI.

Ahora supongamos que el ID de instancia ha logrado obtener un valor e de tipo $extern y ejecuta call_indirect[$extern -> ](ref.func($run), e) . Estos son los dos resultados que parecen más plausibles:

  1. La llamada se realiza correctamente porque la firma esperada y la firma definida son compatibles.
  2. La llamada se interrumpe porque las dos firmas son distintas.

El resultado 2 hace que call_indirect prácticamente inútil con tipos importados. Entonces, para el resultado 1, tenga en cuenta que el tipo de entrada $extern no es el tipo de entrada definido de $do_stuff (que en su lugar es capability ), por lo que probablemente necesitemos usar uno de dos técnicas para cerrar esta brecha:

  1. Para funciones importadas, compare call_indirect con la firma de importación en lugar de la firma de definición.
  2. Reconozca que en tiempo de ejecución el tipo $extern en la ID de instancia representa capability .

Si prefiere la técnica 1, tenga en cuenta que una vez más no funcionará una vez que agreguemos las referencias de funciones escritas. (La razón fundamental es la misma que con el subtipo, pero se necesitaría aún más texto para ilustrar el análogo aquí).

Eso nos deja con la técnica 2. Desafortunadamente, una vez más, esto presenta un problema de seguridad potencial. Para ver por qué, supongamos que ID es malicioso y quiere acceder al contenido de $handle que IC había mantenido en secreto. Supongamos además que ID tiene una buena suposición de lo que realmente representa $handle , es decir, capability . ID puede definir la función de identidad $id_capability de tipo [capability] -> [capability] . Dado un valor e de tipo $extern , ID puede ejecutar call_indirect[$extern -> capability](ref.func($id_capability), e) . Usando la técnica 2, esta llamada indirecta tendrá éxito porque $extern representa capability en tiempo de ejecución, e ID obtendrá el capability procesar que e representa. De manera similar, dado un valor c de tipo capability , ID puede ejecutar call_indirect[capability -> $extern](ref.func($id_capability), c) para forjar c en un $extern .

Conclusión

Espero haber dejado claro que call_indirect tiene varios problemas de rendimiento, semánticos y / o de seguridad / abstracción importantes, problemas que WebAssembly ha tenido la suerte de haber evitado hasta ahora. Desafortunadamente, debido a que call_indirect es parte del WebAssembly principal, estos problemas abarcan varias propuestas en curso. Por el momento, creo que sería mejor centrarse en la propuesta más urgente, los tipos de referencia, donde debemos decidir si restringir o no call_indirect y func.ref solo a tipos numéricos para ahora, una restricción que podríamos relajarnos dependiendo de cómo terminemos resolviendo los problemas generales con call_indirect .

(Perdón por la publicación tan larga. Hice todo lo posible para explicar las interacciones complejas de las funciones de escritura en tiempo de compilación y escritura en tiempo de ejecución entre módulos y demostrar la importancia de esas interacciones de la manera más concisa posible).

Comentario más útil

Por otro lado, ha demostrado que la conversión anyref se puede utilizar para eludir los mecanismos de abstracción estática.

La abstracción de tipos estáticos es insuficiente en un lenguaje con conversiones de tipos dinámicos. Porque la abstracción estática se basa en la parametricidad y los moldes la rompen. No hay nada nuevo en eso, se han escrito artículos al respecto. Se necesitan otros mecanismos de abstracción en tal contexto.

Tratar de evitar eso restringiendo el uso de tipos abstractos frustra su propósito. Considere el caso de uso de WASI. No debería importar si un módulo WASI y cualquier tipo que exporte es implementado por el host o en Wasm. Si restringe arbitrariamente los tipos abstractos definidos por el usuario, entonces una implementación de Wasm ya no sería intercambiable con una implementación de host en general.

  1. No ayuda a que call_indirect respete el subtipo (que creo que ya dijiste explícitamente)

¿Eh? Es parte de las reglas de subtipificación, y también lo hace por definición.

  1. No impide que call_indirect se utilice para utilizar una función exportada con su firma definida en lugar de su firma exportada.

No dije que lo hiciera. Dije que este no es un problema con call_indirect en sí, sino una cuestión de elegir un mecanismo de abstracción de tipo adecuado para un lenguaje con casts.

Aparte, no hay ninguna razón convincente por la que la compilación de OCaml (o cualquier lenguaje similar) deba requerir la introducción de tipos de variantes. Incluso si eso podría ser un poco más rápido en teoría (lo que dudo que sea el caso en los motores de la generación actual, más probablemente lo contrario), los tipos de variantes son una complicación significativa que no debería ser necesaria para el MVP. No comparto su apetito por la complejidad prematura. ;)

Re igualdad en las funciones: hay lenguajes, como Haskell o SML, que no lo admiten, por lo que podrían beneficiarse directamente de las referencias de funciones. OCaml lanza para la igualdad estructural y explícitamente tiene un comportamiento definido por la implementación para la física. Se deja abierto si eso permite siempre devolver falso o lanzar para funciones, pero cualquiera de las dos podría ser suficiente en la práctica y vale la pena explorar antes de comprometerse a un costoso ajuste adicional.

[Como metacomentario, realmente agradecería que atenuara el tono de su conferencia y tal vez considerara la idea de que este es un mundo en el que, tal vez, el conjunto de personas competentes no es único y en el que ocasionalmente se han aplicado rastros de cerebros].

Todos 36 comentarios

¡Gracias por este artículo detallado, Ross! Tengo una pequeña pregunta: en la sección " call_indirect and Type Imports" escribes,

Si prefiere la técnica 1, tenga en cuenta que una vez más no funcionará una vez que agreguemos las referencias de funciones escritas.

¿Esto también está sujeto a la advertencia de la sección anterior de que el problema solo está presente una vez que agregamos el subtipo de variantes a las referencias de funciones escritas?

No lo es. Todos los problemas de la sección de subtipos son independientes de las importaciones de tipos y todos los problemas de la sección de importaciones de tipos son independientes del subtipo. Con respecto al problema particular sobre el que está preguntando, considere que una función exportada puede devolver un valor de tipo ref ([] -> [capability]) como un valor de tipo ref ([] -> [$handle]) , que luego se puede convertir en un funcref e indirectamente llamado a. A diferencia de la función exportada, este cambio en perspectiva del valor ocurre en el tiempo de ejecución en lugar del tiempo del enlace, por lo que no podemos resolverlo comparándolo con la firma de importación, ya que la referencia de la función nunca se importó.

module instance IC defines a type capability and exports the type but not its definition as $handle
¿Cómo funcionará esto? ¿Tiene que haber algo que conecte capability y $handle para que IC sepa cómo lidiar con eso?
También basado en https://github.com/WebAssembly/proposal-type-imports/blob/master/proposals/type-imports/Overview.md#exports , los tipos importados son completamente abstractos. Entonces, incluso si se exporta $capability , es abstracto. Quizás estoy malinterpretando algo.

Pregunta similar para la exportación por module instance IC exports a function $do_stuff that was defined with type [capability] -> [] but exported with type [$handle] -> [] .

Puedo imaginar algún tipo de relación de subtipo usada para esto, por ejemplo, si $capability <: $handle , entonces podemos export $capability as $handle . Pero al principio de esta sección se mencionó dejar de lado la subtipificación, así que lo dejo de lado ... Pero también pensé un poco más en ello:
Si: $capability <: $handle , podemos export $capability as $handle , pero export ([$capability] -> []) as ([$handle] -> []) debería "fallar" porque las funciones son contravariantes en el argumento.

Con las exportaciones de tipos, un módulo especifica una firma, como type $handle; func $do_stuff_export : [$handle] -> [] , y luego crea una instancia de la firma, como type $handle := capability; func $do_stuff_export := $do_stuff . (Ignore la sintaxis específica por completo.) El verificador de tipo luego verifica "dado que $handle representa capability en este módulo, ¿la exportación func $do_stuff_export := $do_stuff válida en este módulo?". Dado que el tipo de $do_stuff es [capability] -> [] , su firma se alinea exactamente con la de $do_stuff_export después de instanciar $handle con capability , por lo que el cheque tiene éxito. (No hay subtipos involucrados aquí, solo sustitución de variables).

Sin embargo, tenga en cuenta que la firma en sí no dice nada sobre $handle . Esto significa que se supone que todos los demás deben tratar $handle como un tipo abstracto. Es decir, la firma abstrae intencionalmente los detalles de la implementación del módulo, y se supone que todos los demás deben respetar esa abstracción. El propósito de este número es ilustrar que call_indirect se puede usar para eludir esa abstracción.

¡Ojalá eso aclare un poco el problema!

Gracias, eso aclara las cosas. Tendré una pregunta sobre la sección de subtipo (lamento saltar):

Estoy siguiendo el escenario en el que queremos que IB ejecute call_indirect[ -> tsuper](ref.func($fsuper)) para tener éxito, haciendo que call_indirect "compare con la firma de importación en lugar de la firma de definición".

Y agregó que (debido a las referencias de funciones escritas) también necesitamos

  1. Realice una comprobación en tiempo de ejecución de al menos tiempo lineal para comprobar la compatibilidad de subtipos de la firma esperada y la firma de definición.

¿Debería ser esto una compatibilidad entre la "firma esperada y la firma de importación "? Dado que asumimos que hemos hecho que call_indirect compare la firma de importación con la firma esperada.

Si se verifica la compatibilidad entre esperado e importado, más tarde, call_indirect[ -> tsub]($fsuper) debería fallar.

Las técnicas 1 y 2 se dan como dos formas ortogonales de hacer que funcione esa llamada indirecta. Desafortunadamente, la técnica 1 es incompatible con las referencias de funciones escritas y la técnica 2 probablemente sea demasiado cara. Por lo tanto, parece probable que ninguno de estos funcione. Por lo tanto, el resto de la sección considera lo que sucede si no usamos ninguno de estos y nos quedamos con una simple comparación de igualdad entre la firma esperada y definida. Perdón por la confusion; no tener una semántica planificada significa que tengo que discutir tres semánticas potenciales.

Tenga cuidado de no sacar demasiadas conclusiones. ;)

Mi suposición es que call_indirect debería permanecer tan rápido como hoy y, por lo tanto, solo requerir una prueba de equivalencia de tipo, sin importar cuántos subtipos agreguemos al idioma. Al mismo tiempo, la verificación en tiempo de ejecución debe ser coherente con el sistema de tipos estáticos, es decir, debe respetar la relación de subtipificación.

Ahora bien, estos requisitos aparentemente contradictorios se pueden conciliar con bastante facilidad, siempre que nos aseguremos de que los tipos utilizables con call_indirect estén siempre en las hojas de la jerarquía de subtipos.

Una forma establecida de hacer cumplir esto es introducir la noción de tipos _exactos_ en el sistema de tipos. Un tipo exacto no tiene subtipos, solo supertipos, y tendríamos (exact T) <: T .

Con eso, podemos requerir que el tipo de destino en call_indirect sea ​​un tipo exacto. Además, el tipo de funciones en sí mismas, naturalmente, ya es el tipo exacto de esa función.

Un módulo también podría requerir tipos exactos en las importaciones de funciones, si quisiera asegurarse de que solo se pueda instanciar con funciones que tengan éxito en una verificación de tiempo de ejecución prevista.

Eso es todo lo que se necesita para garantizar que la técnica de implementación actual de una comparación de puntero simple en tipos de funciones canonicalizadas siga siendo válida. Es independiente de los otros subtipos que haya, o de qué tan sofisticados hagamos el subtipo de funciones. (FWIW, hablé de esto con Luke hace un tiempo y planeé crear un PR, pero se bloqueó por cambios pendientes en la historia de subtipo y a qué propuesta se traslada ahora).

(Una desventaja es que refinar la definición de una función a un subtipo ya no es un cambio compatible con versiones anteriores en general, al menos no si su tipo exacto se ha usado en alguna parte. Pero ese inconveniente es inevitable bajo nuestras restricciones, independientemente de cómo apliquemos exactamente ellos.)

Un par de apartes:

La alternativa es dejar call_indirect y func.ref como están.

AFAICS, no es factible rechazar ref.func en funciones que involucran tipos de referencia. Eso paralizaría severamente muchos casos de uso, es decir, todo lo que involucre funciones de primera clase que operan en externref (devoluciones de llamada, ganchos, etc.).

Es posible que esto signifique que, dependiendo de la solución a la que lleguemos, externref podría no ser instanciable como lo sería una importación de tipo verdadero, y / o que externref podría (irónicamente) no tener ningún supertipo (por ejemplo, podría no poder ser un subtipo de anyref si finalmente decidimos agregar anyref).

¿Puedes elaborar? No veo la conexión.

Tenga cuidado de no sacar demasiadas conclusiones. ;)

No estoy seguro de a qué conclusión te refieres. Mi conclusión declarada es que hay una serie de problemas con call_indirect que debemos tener en cuenta y debemos comenzar a planificar. Parece que está sugiriendo que estos problemas no tienen importancia porque tiene una solución en mente. Pero esa solución no ha sido revisada ni aceptada por el GC, y no deberíamos planificarla hasta que lo haya hecho. Solicité específicamente no discutir las soluciones porque tomará un tiempo evaluarlas y compararlas y hay decisiones que debemos tomar antes de que tengamos tiempo para hacer esas evaluaciones y comparaciones correctamente. Pero, para evitar que las personas se formen la percepción de que este problema está resuelto y, en consecuencia, evitar la decisión urgente, me tomaré un segundo para discutir rápidamente su solución.

Una forma establecida de hacer cumplir esto es introducir la noción de tipos exactos en el sistema de tipos.

Los tipos exactos no son una solución establecida. En todo caso, los tipos exactos han establecido problemas que sus proponentes todavía están trabajando para abordar. Curiosamente, aquí hay un hilo en el que el equipo de TypeScript vio originalmente cómo los tipos exactos del formulario que está proponiendo podrían resolver algunos problemas, pero luego se dieron cuenta) (https://github.com/microsoft/TypeScript/issues/12936 # issuecomment-284590083) que los tipos exactos introdujeron más problemas de los que resolvieron. (Nota para el contexto: esa discusión fue impulsada por los tipos de objetos exactos de Flow, que en realidad no son una forma de tipo exacto (en el sentido teórico) sino que simplemente no permiten el análogo de objeto de la subtipificación de prefijo). Podría imaginarnos repitiendo ese hilo aquí.

Como ejemplo de cómo este tipo de problemas podrían desarrollarse para WebAssembly, suponga que no hayamos diferido el subtipo. El tipo de ref.null sería exact nullref usando tipos exactos. Pero exact nullref no sería un subtipo de exact anyref . De hecho, de acuerdo con la semántica habitual de tipos exactos, es probable que ningún valor pertenezca a exact anyref porque probablemente el tipo de tiempo de ejecución de ningún valor sea exactamente anyref . Esto haría call_indirect completamente inutilizable por anyref s.

Ahora, tal vez tenga en mente alguna versión diferente de tipos exactos, pero tomaría un tiempo comprobar que esta versión diferente resuelve de alguna manera los muchos problemas abiertos con tipos exactos. Entonces, mi punto aquí no es descartar esta solución, sino reconocer que no es obvio que esta sea la solución y no tomar decisiones con esa expectativa.

¿Puedes elaborar? No veo la conexión.

Estás haciendo referencia a una oración larga. ¿En qué parte te gustaría que me explicara? Una suposición es que es posible que se esté perdiendo el problema general con call_indirect y las importaciones de tipos. Su sugerencia de tipos exactos solo aborda los problemas con el subtipo, pero establecimos anteriormente que call_indirect tiene problemas incluso sin ningún subtipo.

Eso paralizaría severamente muchos casos de uso, es decir, todo lo que involucre funciones de primera clase que operan en externref (devoluciones de llamada, ganchos, etc.).

Sí, esto es algo sobre lo que esperaba obtener más información. Tengo entendido que el caso de uso principal de call_indirect es admitir punteros de función C / C ++ y métodos virtuales C ++. Tengo entendido también que este caso de uso está actualmente restringido a firmas numéricas. Conozco más usos potenciales de call_indirect , pero como mencioné, estaba sugiriendo una restricción temporal , así que lo que importa es cuáles son los usos actuales de call_indirect . Dado que call_indirect todavía requiere una tabla e índice en lugar de simplemente un funcref , no parece particularmente bien diseñado para admitir devoluciones de llamada. No sabía si eso era porque actualmente no se está utilizando para este propósito.

Todos conocen las bases de código dirigidas a esta función mucho mejor que yo, por lo que si conocen algunos programas reales que necesitan esta funcionalidad ahora, sería muy útil proporcionar aquí algunos ejemplos de los patrones de uso necesarios. Además de ser útil para averiguar si necesitamos admitir esta funcionalidad en este momento, si la funcionalidad es necesaria ahora, estos ejemplos serían útiles para informar la mejor manera de proporcionarla rápidamente al abordar los problemas anteriores.

@RossTate :

En todo caso, los tipos exactos han establecido problemas que sus proponentes todavía están trabajando para abordar. Curiosamente, aquí hay un hilo en el que el equipo de TypeScript vio originalmente cómo los tipos exactos del formulario que propones podrían resolver algunos problemas, pero luego se dieron cuenta de que los tipos exactos introducían más problemas de los que resolvían. (Nota para el contexto: esa discusión fue impulsada por los tipos de objetos exactos de Flow, que en realidad no son una forma de tipo exacto (en el sentido teórico) sino que simplemente no permiten el análogo de objeto de la subtipificación de prefijo). Podría imaginarnos repitiendo ese hilo aquí.

Los paréntesis son clave aquí. No estoy seguro de qué es exactamente lo que tienen en mente en ese hilo, pero no parece ser lo mismo. De lo contrario, declaraciones como "se supone que un tipo T & U siempre se puede asignar a T , pero esto falla si T es un tipo exacto" no tendrían sentido (esto no ' t falla, porque T & U no sería válido o sería inferior). Las otras preguntas son principalmente sobre pragmática, es decir, dónde querría un programador usarlas (para objetos), que no se aplican en nuestro caso.

Para los sistemas de tipos de bajo nivel, ¿no eran los tipos exactos un ingrediente crucial incluso en algunos de sus propios artículos?

Como ejemplo de cómo este tipo de problemas podrían desarrollarse para WebAssembly, suponga que no hayamos diferido el subtipo. El tipo de ref.null sería nullref exacto usando tipos exactos. Pero exact nullref no sería un subtipo de exact anyref.

No hay desacuerdo aquí. No tener subtipos es el propósito de tipos exactos.

De hecho, de acuerdo con la semántica habitual de tipos exactos, es probable que ningún valor pertenezca a anyref exacto porque probablemente el tipo de tiempo de ejecución de ningún valor sea exactamente anyref.

Correcto, la combinación (exact anyref) no es un tipo útil, dado que el único propósito de anyref es ser un supertipo. Pero, ¿por qué es eso un problema?

Esto haría que call_indirect sea completamente inutilizable para anyrefs.

¿Estás seguro de que ahora no estás confundiendo los niveles? Un tipo de función (exact (func ... -> anyref)) es perfectamente útil. Simplemente no es compatible con un tipo, digamos, (func ... -> (ref $T)) . Es decir, exact evita el subtipo no trivial en tipos de funciones. ¡Pero ese es el punto!

¿Quizás estás mezclando (exact (func ... -> anyref)) con (func ... -> exact anyref) ? Estos son tipos no relacionados.

Su sugerencia de tipos exactos solo aborda los problemas con el subtipo, pero establecimos anteriormente que call_indirect tiene problemas incluso sin ningún subtipo.

De alguna manera, está asumiendo que podrá exportar un tipo sin su definición como un medio para definir un tipo de datos abstracto. Claramente, ese enfoque no funciona en presencia de conversiones de tipo dinámico (call_indirect o de otro tipo). Es por eso que sigo diciendo que necesitaremos una abstracción de tipo de estilo newtype, no una abstracción de tipo de estilo ML.

Tengo entendido que el caso de uso principal de call_indirect es admitir punteros de función C / C ++

Sí, pero ese no es el único caso de uso de ref.func , al que me refería, porque lo incluyó en su restricción sugerida (¿tal vez innecesariamente?). En particular, habrá call_ref , que no implica controles de tipo.

De alguna manera, está asumiendo que podrá exportar un tipo sin su definición como un medio para definir un tipo de datos abstracto. Claramente, ese enfoque no funciona en presencia de conversiones de tipo dinámico (call_indirect o de otro tipo). Es por eso que sigo diciendo que necesitaremos una abstracción de tipo de estilo newtype, no una abstracción de tipo de estilo ML.

Bien, parece que está de acuerdo en que los tipos exactos no hacen nada para solucionar el problema con call_indirect y el tipo de importación. Pero también está diciendo que no tiene sentido abordar ese problema porque de todos modos será un problema debido a las conversiones en tiempo de ejecución. Hay una manera fácil de prevenir ese problema: no permita que las personas realicen conversiones en tiempo de ejecución en tipos abstractos (a menos que el tipo abstracto diga explícitamente que se puede convertir). Después de todo, es un tipo opaco, por lo que no deberíamos poder asumir que exhibe la estructura necesaria para realizar un yeso. Entonces, incluso si existe la posibilidad de que los tipos exactos aborden el problema de subtipificación, es prematuro ignorar la otra mitad del problema.

Como dije, cada solución tiene ventajas y desventajas. Parece que está asumiendo que su solución tiene solo las compensaciones que usted mismo ha identificado, y parece que está asumiendo que el CG preferiría su solución a otras. Yo también tengo una posible solución a este problema. Garantiza comprobaciones en tiempo constante, se basa en una tecnología ya utilizada en máquinas virtuales, resuelve todos los problemas aquí (creo), no requiere agregar ningún tipo nuevo y, de hecho, agrega funcionalidad adicional a WebAssembly con aplicaciones conocidas. Sin embargo, no presumo que funciona como esperaba y que no he pasado por alto algunas deficiencias porque usted y otros no han tenido la oportunidad de revisarlo. Tampoco presumo que el GC prefiera sus compensaciones sobre las opciones alternativas. En cambio, estoy tratando de averiguar qué podemos hacer para darnos tiempo para analizar las opciones, de modo que el GC, en lugar de solo yo, pueda ser quien tome una decisión informada sobre este tema transversal.

En particular, habrá call_ref , que no implica controles de tipo.

La palabra clave en tu oración es voluntad . Soy plenamente consciente de que hay aplicaciones de call_indirect con tipos no numéricos que la gente quiere de haber apoyado. Y espero que vamos a llegar a un diseño que apoyos que la funcionalidad y las direcciones de los temas anteriormente. Pero, como dije, idealmente podemos tener algo de tiempo para desarrollar ese diseño de modo que no estemos lanzando rápidamente una característica con implicaciones transversales antes de haber tenido la oportunidad de investigar esas implicaciones. Entonces, mi pregunta es si existen programas importantes que necesitan esa funcionalidad ahora . Si es así, no es necesario formular hipótesis; solo señale algunos e ilustre cómo se basan actualmente en esta funcionalidad.

De alguna manera, está asumiendo que podrá exportar un tipo sin su definición como un medio para definir un tipo de datos abstracto. Claramente, ese enfoque no funciona en presencia de conversiones de tipo dinámico (call_indirect o de otro tipo). Es por eso que sigo diciendo que necesitaremos una abstracción de tipo de estilo newtype, no una abstracción de tipo de estilo ML.

Esto me parece un tema fundamental. ¿Permitir la confidencialidad de las definiciones de tipos exportados es un objetivo de la propuesta de importación de tipos? De este hilo deduzco que @rossberg cree que actualmente no es un objetivo. Discutamos y acordamos esta pregunta antes de discutir las soluciones para que todos podamos trabajar desde el mismo conjunto de supuestos.

@RossTate :

Bien, parece que está de acuerdo en que los tipos exactos no hacen nada para solucionar el problema con call_indirect y type import.

Sí, si con eso se refiere a la cuestión de cómo agregar una función para definir tipos de datos abstractos. Hay varias formas en que la abstracción de tipos puede funcionar de manera coherente, pero esta característica está más adelante.

La palabra clave en tu oración es voluntad. Soy plenamente consciente de que hay aplicaciones de call_indirect con tipos no numéricos que la gente querrá que se admitan.

La instrucción call_ref está en la propuesta de referencia de la función, por lo que es bastante cercana, en cualquier caso, antes de cualquier mecanismo de tipo de datos abstracto potencial. ¿Está sugiriendo que lo dejemos en espera hasta entonces?

@tlively :

¿Permitir la confidencialidad de las definiciones de tipos exportados es un objetivo de la propuesta de importación de tipos? De este hilo deduzco que @rossberg cree que actualmente no es un objetivo.

Es un objetivo, pero un mecanismo de tipo de datos abstracto es una característica separada. Y tal mecanismo debe diseñarse de manera que no afecte el diseño de las importaciones. Si lo hiciera, lo estaríamos haciendo muy mal: la abstracción debe garantizarse en el sitio de definición, no en el sitio de uso. Sin embargo, afortunadamente, esto no es ciencia espacial y el espacio de diseño está bastante bien explicado.

Gracias, @rossberg , eso tiene sentido. Agregar primitivas de abstracción en una propuesta de seguimiento después de las importaciones y exportaciones de tipos me suena bien, pero sería genial si pudiéramos escribir los detalles de cómo planeamos hacerlo pronto. El diseño de importaciones y exportaciones de tipos restringe e informa el diseño de importaciones y exportaciones de tipos abstractos, por lo que es importante que tengamos una buena idea de cómo funcionará la abstracción en el futuro antes de finalizar el diseño inicial.

Además de detallar ese plan, dado que este problema con call_indirect está demostrando que afecta las decisiones urgentes, ¿puede explicar por qué parece estar rechazando mi sugerencia de que los tipos abstractos no deben ser convertibles (a menos que estén explícitamente restringidos a ser convertibles )? Son opacos, por lo que la sugerencia parece estar en línea con las prácticas comunes de tipos abstractos.

@tlively , sí, estuvo de acuerdo. Además de varias otras cosas que tenía la intención de escribir durante un tiempo. Lo haré una vez que haya superado todas las consecuencias del n . ° 69 . ;)

@RossTate , porque eso haría que los tipos de datos abstractos sean incompatibles con las conversiones. Solo porque quiero evitar que otros vean _a través_ de un tipo abstracto, no necesariamente quiero evitar que ellos (o yo mismo) proyecten _a_ un tipo abstracto. Crear una dicotomía tan falsa rompería los casos de uso centrales de los moldes. Por ejemplo, por supuesto, quiero poder pasar un valor de tipo abstracto a una función polimórfica.

@rossberg ¿Puede aclarar cuál es este caso de uso central que tiene en mente? Mi mejor suposición para interpretar tu ejemplo tiene una solución trivial, pero tal vez te refieres a otra cosa.

@RossTate , considere las funciones polimórficas. Aparte de los Wasm-genéricos, al compilarlos usando conversiones arriba / abajo de anyref, entonces debería ser posible usarlos con valores de tipo abstracto como cualquier otro, sin envolvimiento adicional en otros objetos. Por lo general, desea poder tratar los valores de tipo abstracto como cualquier otro.

Bien, consideremos funciones polimórficas y supongamos que el tipo importado es Handle :

  1. Java tiene funciones polimórficas. Sus funciones polimórficas esperan que todos los valores (referencia de Java) sean objetos. En particular, deben tener una v-table. Un módulo Java que usa Handle probablemente especificará una clase Java CHandle posiblemente implementando interfaces. Las instancias de esta clase tendrán un miembro (de nivel wasm) de tipo Handle y una tabla v que proporciona punteros de función a las implementaciones de varios métodos de clase e interfaz. Cuando se le da a una función polimórfica de nivel de superficie, que en el nivel de wasm es solo una función en objetos, el módulo puede usar el mismo mecanismo que usa para convertir a otras clases para convertir a CHandle .
  2. OCaml tiene funciones polimórficas. Sus funciones polimórficas esperan que todos los valores OCaml respalden la igualdad física. Debido a que wasm no puede razonar sobre la seguridad de tipos de OCaml, es probable que sus funciones polimórficas también necesiten hacer un uso intensivo de moldes. Una estructura de fundición especializada probablemente lo haría más eficiente. Por cualquiera de estas razones, es probable que un módulo OCaml especifique un tipo de datos algebraicos o un tipo de registro THandle que se ajuste a estas normas y tenga un miembro (nivel wasm) de tipo Handle . Sus funciones polimórficas luego emitirían valores OCaml a THandle tal como lo harían con cualquier otro tipo de datos algebraicos o tipo de registro.

En otras palabras, debido a que los módulos se basan en normas sobre cómo se representan los valores de nivel de superficie para implementar cosas como funciones polimórficas, y los tipos importados abstractos como Handle no satisfacen estas normas, el ajuste de valores es inevitable. Esta es la misma razón por la que una de las aplicaciones originales de anyref fue reemplazada por Tipos de interfaz. Y desarrollamos estudios de casos que demuestran que anyref no es necesario, ni siquiera adecuado, para admitir funciones polimórficas.

Por otro lado, ha demostrado que el anyref convertible se puede utilizar para eludir los mecanismos de abstracción estática. El plan del mecanismo de abstracción al que aludió es un intento de solucionar este problema mediante mecanismos de abstracción dinámica. Pero existen varios problemas con los mecanismos de abstracción dinámica. Por ejemplo, uno no puede exportar su tipo i31ref como su tipo abstracto Handle sin el riesgo de que otros módulos usen anyref y forjen identificadores (por ejemplo, a capacidades). En su lugar, uno tiene que saltar a través de aros y gastos generales adicionales que serían innecesarios si, en cambio, solo aseguráramos la abstracción estática estándar.

Además, ahora que (creo) entiendo mejor cómo pretendes usar tipos exactos, me doy cuenta de que tu intención no aborda ninguno de los dos problemas principales a los que llamé la atención con call_indirect y subtipos:

  1. No ayuda a que call_indirect respeten el subtipo (que creo que ya dijiste explícitamente)
  2. No evita que se utilice call_indirect para utilizar una función exportada con su firma definida en lugar de su firma exportada.

Así que este no es un problema trivial de resolver. Por eso, dadas las limitaciones de tiempo, preferiría centrarme en evaluar cómo darnos tiempo para resolverlo correctamente. No creo que sea necesario tener primero una discusión sobre si vale la pena descartar la abstracción estática por anyref . Ese es el tipo de gran discusión que esperaba evitar para no retrasar más las cosas.

Por otro lado, ha demostrado que la conversión anyref se puede utilizar para eludir los mecanismos de abstracción estática.

La abstracción de tipos estáticos es insuficiente en un lenguaje con conversiones de tipos dinámicos. Porque la abstracción estática se basa en la parametricidad y los moldes la rompen. No hay nada nuevo en eso, se han escrito artículos al respecto. Se necesitan otros mecanismos de abstracción en tal contexto.

Tratar de evitar eso restringiendo el uso de tipos abstractos frustra su propósito. Considere el caso de uso de WASI. No debería importar si un módulo WASI y cualquier tipo que exporte es implementado por el host o en Wasm. Si restringe arbitrariamente los tipos abstractos definidos por el usuario, entonces una implementación de Wasm ya no sería intercambiable con una implementación de host en general.

  1. No ayuda a que call_indirect respete el subtipo (que creo que ya dijiste explícitamente)

¿Eh? Es parte de las reglas de subtipificación, y también lo hace por definición.

  1. No impide que call_indirect se utilice para utilizar una función exportada con su firma definida en lugar de su firma exportada.

No dije que lo hiciera. Dije que este no es un problema con call_indirect en sí, sino una cuestión de elegir un mecanismo de abstracción de tipo adecuado para un lenguaje con casts.

Aparte, no hay ninguna razón convincente por la que la compilación de OCaml (o cualquier lenguaje similar) deba requerir la introducción de tipos de variantes. Incluso si eso podría ser un poco más rápido en teoría (lo que dudo que sea el caso en los motores de la generación actual, más probablemente lo contrario), los tipos de variantes son una complicación significativa que no debería ser necesaria para el MVP. No comparto su apetito por la complejidad prematura. ;)

Re igualdad en las funciones: hay lenguajes, como Haskell o SML, que no lo admiten, por lo que podrían beneficiarse directamente de las referencias de funciones. OCaml lanza para la igualdad estructural y explícitamente tiene un comportamiento definido por la implementación para la física. Se deja abierto si eso permite siempre devolver falso o lanzar para funciones, pero cualquiera de las dos podría ser suficiente en la práctica y vale la pena explorar antes de comprometerse a un costoso ajuste adicional.

[Como metacomentario, realmente agradecería que atenuara el tono de su conferencia y tal vez considerara la idea de que este es un mundo en el que, tal vez, el conjunto de personas competentes no es único y en el que ocasionalmente se han aplicado rastros de cerebros].

Como meta comentario, te agradecería mucho que bajaras el tono de tus conferencias.

Escuchó.

y quizás consideró la idea de que este es un mundo donde, quizás, el conjunto de personas competentes no es único y que en ocasiones se han aplicado trazas de cerebros.

Mi consejo aquí se basa en la consulta con varios expertos.

La abstracción de tipos estáticos es insuficiente en un lenguaje con conversiones de tipos dinámicos. Porque la abstracción estática se basa en la parametricidad y los moldes la rompen. No hay nada nuevo en eso, se han escrito artículos al respecto. Se necesitan otros mecanismos de abstracción en tal contexto.

Estos expertos con los que he consultado incluyen autores de algunos de dichos artículos.

Ahora, como un intento de verificar que he sintetizado correctamente sus consejos, envié un correo electrónico a otro autor de algunos de dichos artículos, uno de quien no he discutido este tema antes. Esto es lo que pregunté:

Supongamos que tengo una función polimórfica f(...). Mi lenguaje escrito tiene subtipos (subsuntivos) y casting explícito. Sin embargo, una conversión de t1 a t2 solo comprueba si t2 es un subtipo de t1. Suponga que las variables de tipo como X por defecto no tienen subtipos o supertipos (además de ellos mismos, por supuesto). ¿Esperaría que f fuera relacionalmente paramétrico con respecto a X?

Esta fue su respuesta:

Sí, creo que esto sería paramétrico, ya que la única capacidad que le brinda es escribir conversiones en X que sean equivalentes a una función de identidad, que ya es relacionalmente paramétrica.

Esto está en línea con mi consejo. Ahora, por supuesto, esto es una simplificación del problema en cuestión, pero hemos hecho un esfuerzo por investigar el problema más específicamente en WebAssembly, y hasta ahora nuestra exploración ha sugerido que esta expectativa se mantiene incluso en la escala de WebAssembly. excepto call_indirect , de ahí este problema.

Tenga en cuenta que los teoremas a los que se refiere se aplican a lenguajes en los que todos los valores se pueden convertir. Esta observación es de donde se nos ocurrió la idea de restringir la moldeabilidad.

Considere el caso de uso de WASI.

No entiendo las afirmaciones que hace. Hemos considerado el caso de uso de WASI. Por nosotros, incluyo a varios expertos en seguridad e incluso específicamente en seguridad basada en capacidades.

Como meta comentario, realmente agradecería no tener que apelar a la autoridad o al CG para que se escuchen mis sugerencias. Sugerí que restringir los moldes permitiría garantizar la parametricidad estática incluso en presencia de moldes. Inmediatamente hizo caso omiso de esa sugerencia, apelando a documentos anteriores para justificar ese despido. Sin embargo, cuando le ofrecí esta misma sugerencia a un autor de esos artículos, inmediatamente llegaron a la misma conclusión que yo y que tú podrías haberlo hecho. Antes de eso, sugerí que evaluar las posibles soluciones sería un proceso largo. No hizo caso de esa sugerencia, insistiendo en que usted (todo por su cuenta) había resuelto el problema, atrayéndonos a ambos a esta larga conversación. Es extremadamente difícil progresar y evitar frustrarse cuando las sugerencias de uno se rechazan repetidamente de manera tan casual. (Debo aclarar que no estoy tratando de descartar su sugerencia como una posible solución aquí; estoy tratando de demostrar que no es la única solución y, por lo tanto, debe evaluarse junto con varias otras).

Creo que es importante y oportuno tener un diseño detallado definido y analizado que aborde las inquietudes planteadas en este tema: en realidad, no creo que los tipos abstractos deban considerarse una característica más lejana; WASI los necesita ahora.

También tengo la esperanza de que exact + newtype pueda abordar las preocupaciones, pero estoy de acuerdo en que no podemos simplemente apostar la granja a esta corazonada en este momento comprometiéndonos prematuramente con un diseño cuando enviamos (en breve) tipos de referencia. Necesitamos tiempo para tener una discusión adecuada al respecto.

Dicho esto, no veo el peligro de permitir externref en call_indirect firmas en la propuesta de tipos de referencia. Sí, si un módulo exporta un externref (como una constante global o devolviéndolo de una función ...), no hemos determinado si podemos abatir ese externref . Pero call_indirect no está rebajando un externref ; está rebajando un funcref , y externref no tiene un rol diferente al de i32 el control de igualdad de tipo de función. Por lo tanto, en ausencia de importaciones de tipos, exportaciones de tipos y subtipos en juego en call_indirect , no veo cómo nos comprometemos con una nueva elección de diseño con la que aún no nos hemos comprometido en el MVP. .

Si no hay un peligro, tal vez podríamos reducir esta discusión intensa a una discusión menos intensa en la propuesta de Importaciones de tipo (donde todavía creo que deberíamos incluir el apoyo de tipo abstracto adecuado).

Seguro. Creo que es una buena idea examinar si existe un peligro o no.

Con respecto a WASI, el diseño todavía está en proceso de cambio, pero una opción que todavía parece viable es usar algo como i31ref para sus "identificadores", digamos porque no requiere asignación de memoria dinámica. WASI puede decidir sobre otras opciones, pero el punto es que nadie sabe en este momento, y sería bueno que las decisiones tomadas ahora no afecten a tales decisiones en el futuro.

Actualmente, externref es el único tipo abstracto disponible, por lo que un host basado en WASI instanciaría externref con i31ref (o lo que sean los "manejadores" de WASI). Pero tengo entendido que WASI quiere trasladar su implementación a WebAssembly tanto como sea posible para reducir el código dependiente del host. Para facilitar esto, en algún momento los sistemas WASI pueden querer tratar externref como cualquier otro tipo de importación e instanciarlo con el resumen exportado de WASI tipo Handle . Pero si Handle es i31ref , entonces la implementación anterior de call_indirect necesaria para permitir que funcione a través de los límites del módulo también se puede usar para permitir que las personas forjen identificadores a través de externref .

Entonces, una de mis preguntas, que ahora estoy notando que no se expresó claramente en mi publicación original, es ¿la gente quiere que externref sea ​​instanciable al igual que otras importaciones de tipo abstracto?

Entonces, una de mis preguntas, que ahora estoy notando que no se expresó claramente en mi publicación original, es ¿la gente quiere que externref sea ​​instanciable al igual que otras importaciones de tipo abstracto?

Gracias por plantear explícitamente esta pregunta. FWIW, nunca he entendido que externref sea ​​instanciable desde dentro de un módulo WebAssembly. Eso implica la participación del anfitrión en la virtualización si WASI quiere usar externref como identificadores, pero eso me parece bien, o al menos parece una discusión separable.

Hmm, déjame ver si puedo aclarar. Sospecho que ya estás a bordo con un montón de lo que sigue, pero es más fácil para mí comenzar desde cero.

Desde la perspectiva de un módulo wasm, externref no significa referencia de host. Es solo un tipo opaco del que el módulo no sabe nada. Más bien, son las convenciones en torno a externref que lo interpretan como una referencia de host. Por ejemplo, las convenciones de un módulo que usa externref para interactuar con el DOM serían evidentes en las funciones que involucran externref que el módulo importa, como parentNode : [externref] -> [externref] y childNode : [externref, i32] -> [externref] . El entorno del módulo, como el propio host, es lo que realmente da la interpretación de externref como referencias de host, y proporciona implementaciones de los métodos importados que corroboran esa interpretación.

Sin embargo, el entorno del módulo no tiene que ser el host y externref no tienen que ser referencias de host. El entorno podría ser otro módulo que proporcione funcionalidad para algún tipo que parezca referencias de host que exhiben las convenciones esperadas. Supongamos que el módulo E es el entorno del módulo M, y que el módulo M importa parentNode y childNode como se indicó anteriormente. Digamos que E quiere usar el módulo M pero quiere restringir el acceso de M al DOM, digamos porque E tiene una confianza limitada en M o porque E quiere vincular cualquier error que M pueda tener y sabe que las necesidades de M no deben exceder estas restricciones. Lo que E podría hacer es crear una instancia de M con "MonitoredRef" como externref M. Digamos que, en particular, E quiere dar a M nodos DOM pero asegurarse de que M no suba más arriba en el árbol DOM. Entonces E's MonitoredRef podría ser específicamente ref (struct externref externref) , donde el segundo externref (desde la perspectiva de E) es el nodo DOM en el que M está operando, pero el primer externref es un antepasado de ese nodo que M no tiene permitido pasar. E podría entonces instanciar parentNode M de modo que se equivoque si estas dos referencias son iguales. E mismo importaría sus propias funciones parentNode y childNode , haciendo de E efectivamente un monitor en tiempo de ejecución de las interacciones DOM.

Con suerte, eso fue lo suficientemente concreto para pintar la imagen correcta, mientras que no demasiado concreto para perderse en los detalles. Obviamente, hay una serie de patrones como este. Así que supongo que otra forma de formular la pregunta es, ¿queremos que externref solo represente exactamente referencias de host?

La única parte que me suena cuestionable es "lo que E podría hacer es instanciar M con" MonitoredRef "como externref M". No tengo la impresión de que haya planes para permitir que las cosas abstractas aparezcan como externref en otros módulos. Tengo entendido que externref no es una herramienta de abstracción en absoluto.

Tampoco conozco ninguno de esos planes; Tampoco sé si alguien había considerado la opción. Es decir, ¿debería externref ser un tipo "primitivo", por ejemplo, como i32 , o un tipo "instanciable", por ejemplo, como tipos importados?

En mi publicación original, indiqué que cualquiera de las dos formas es manejable. La compensación de optar por la interpretación "primitiva" es que externref es sustancialmente menos útil / componible que los tipos importados, ya que este último admitirá los casos de uso de externref así como los patrones anteriores. Como tal, "primitivo" externref parece probable que se convierta en vestigial, sólo existe por compatibilidad con versiones anteriores. Pero parece poco probable que eso sea particularmente problemático, solo una molestia. El mayor problema que puedo ver es que, al igual que el buen comportamiento de call_indirect en tipos numéricos funciona porque no tienen supertipos, el buen comportamiento de call_indirect puede terminar dependiendo de externref tampoco tiene supertipos.

Ah, sí, esto explica la diferencia de comprensión: estoy de acuerdo con @tlively en que externref no es abstracto en absoluto y no existe la noción de "instanciar externref con un tipo", y yo Creo que podemos estar bastante seguros de que esto ocurra en el futuro. (Dado que externref es un tipo primitivo, a diferencia de un parámetro de tipo declarado explícitamente, no está claro cómo se podría intentar crear una instancia por módulo).

En ausencia de abatimientos, este hecho hace que wasm sea casi inútil para implementar / virtualizar las API de WASI, por lo que el plan para WASI ha sido la transición de i32 maneja directamente a Type Imports (y por qué presenté type-import / # 6 , b / c necesitamos incluso un poquito más).

Dado que externref es un tipo primitivo, a diferencia de un parámetro de tipo declarado explícitamente, no está claro cómo se podría intentar instanciarlo por módulo.

Cuando agregamos importaciones de tipos, podemos tratar los módulos sin importaciones de tipos pero con externref como si tuvieran import type externref en la parte superior. Todo se verificaría de la misma manera porque, a diferencia de otros tipos primitivos, externref no tiene operaciones primitivas asociadas (más allá de tener un valor predeterminado). Pero con esa importación implícita, ahora podemos hacer cosas como virtualización, sandboxing y monitoreo en tiempo de ejecución.

Pero antes de ir y venir sobre eso, creo que sería útil evaluar si todos estamos en la misma página sobre algo. Hágame saber si está de acuerdo o en desacuerdo con la siguiente declaración y por qué: "Una vez que las importaciones de tipos están disponibles, los módulos no tienen ninguna razón para usar externref y son más reutilizables / componibles si usan una importación de tipos en su lugar".

Avíseme si está de acuerdo o en desacuerdo con la siguiente declaración y por qué: "Una vez que las importaciones de tipos están disponibles, los módulos no tienen ninguna razón para usar externref y son más reutilizables / componibles si usan una importación de tipos en su lugar".

Estoy de acuerdo con esta afirmación en abstracto. En la práctica, creo que externref seguirá siendo común en contextos web para hacer referencia a objetos JS externos porque no requiere configuración adicional en el momento de la instanciación. Pero eso es solo una predicción y no me importaría si resultara estar equivocado y todos cambian a usar importaciones de tipos después de todo. El valor de externref es que podemos tenerlo antes de lo que podemos tener mecanismos más ricos como las importaciones de tipos. Prefiero mantener externref simple y verlo caer en desuso que torpemente calzarlo para convertirlo en algo más poderoso más adelante cuando haya alternativas más elegantes.

@tlively ,

FWIW, nunca he entendido que externref sea instanciable desde dentro de un módulo WebAssembly.

Correcto, la idea es que externref es el tipo "primitivo" de punteros extranjeros. Para abstraer los detalles de implementación de un tipo de referencia, necesitará algo más: algo como anyref o una importación de tipo.

@lukewagner , estaría de acuerdo con ampliar el alcance de la propuesta de importación de tipos, si eso es preferible. Pero la compensación es que la propuesta tomaría más tiempo. Tenía la impresión de que las importaciones de tipos podrían ser útiles sin tipos de datos abstractos en el lenguaje y, por lo tanto, deseables más temprano que tarde. Significaría que ya podría expresar y _usar_ interfaces WASI, pero aún no _ implementarlas_ en Wasm. Pero de cualquier manera está bien para mí.

@RossTate :

Estos expertos con los que he consultado incluyen autores de algunos de dichos artículos.

Excelente. Entonces supongo que ha notado que el suyo es el autor de un par de estos artículos, en caso de que esté buscando más autoridad. :)

Esto es lo que pregunté:

Supongamos que tengo una función polimórfica f (...). Mi lenguaje escrito tiene subtipos (subsuntivos) y casting explícito. Sin embargo, una conversión de t1 a t2 solo comprueba si t2 es un subtipo de t1. Suponga que las variables de tipo como X por defecto no tienen subtipos o supertipos (además de ellos mismos, por supuesto). ¿Esperaría que f fuera relacionalmente paramétrico con respecto a X?

Suspiro. Daría la misma respuesta a esa pregunta específica. Pero esta pregunta encarna varios supuestos específicos, por ejemplo, sobre la naturaleza de los moldes y sobre una distinción bastante inusual entre cuantificación limitada e ilimitada que rara vez existe en cualquier lenguaje de programación. Y supongo que es por una razón.

Cuando dije "la abstracción de tipo estático es insuficiente", entonces no quise decir que no es _técnicamente_ posible (por supuesto que lo es), pero que no es _prácticamente_ adecuado. En la práctica, no desea una bifurcación entre la abstracción de tipos y el subtipo / calificabilidad (o entre tipos paramétricos y no paramétricos), porque eso rompería artificialmente la composición basada en moldes.

No entiendo las afirmaciones que hace.

Si recibe un valor de tipo abstracto, es posible que aún desee olvidar su tipo exacto, por ejemplo, ponerlo en algún tipo de unión y luego recuperarlo a través de un abatimiento. Es posible que desee hacer eso por la misma razón que puede quererlo para cualquier otro tipo de referencia. La abstracción de tipos no debe interferir con ciertos patrones de uso que son válidos con tipos regulares del mismo tipo.

Su respuesta parece ser: y qué, envuelva todo en tipos auxiliares en los sitios de uso respectivos, por ejemplo, en variantes. Pero eso podría implicar una sobrecarga sustancial de envoltura / desenvolvimiento, requiere características de sistema de tipo más complejas y es más complicado de usar.

Creo que esto es a lo que se reducen varios de nuestros desacuerdos: si el MVP debería admitir uniones de tipos de referencia, o si debería requerir la introducción y codificación con tipos de variantes explícitos. Para bien o para mal, las uniones son una combinación natural para la interfaz de montón de motores típicos, y su soporte es fácil y económico en la actualidad. Las variantes no tanto, son un enfoque mucho más investigador que probablemente induciría una sobrecarga adicional y un rendimiento menos predecible, al menos en los motores existentes. Y estoy diciendo que, como persona de sistemas de tipos, prefiere las variantes a las uniones en otras circunstancias, como los lenguajes de cara al usuario. ;)

Como meta comentario, realmente agradecería no tener que apelar a la autoridad o al CG para que se escuchen mis sugerencias.

Me permito sugerir amablemente que las conversaciones sobre diversas propuestas podrían funcionar mejor si se inician _preguntando_ a los respectivos campeones sobre cosas que no están claras, por ejemplo, fundamentos específicos o planes futuros (que no siempre son obvios o no están escritos todavía), antes de _ suponer_ la ausencia de una respuesta y hacer afirmaciones y sugerencias amplias basadas en estos supuestos?

Excelente. Entonces supongo que ha notado que el suyo es el autor de un par de estos artículos, en caso de que esté buscando más autoridad. :)

Sí, lo que hace que sea extremadamente problemático cuando sugieres que hay documentos que afirman que mi sugerencia no funciona, aunque sabes que mi sugerencia aborda específicamente las condiciones bajo las cuales se hicieron esas afirmaciones.

En la práctica, no desea una bifurcación entre la abstracción de tipos y el subtipo / calificabilidad (o entre tipos paramétricos y no paramétricos), porque eso rompería artificialmente la composición basada en moldes.

Esta es una opinión, no un hecho (por lo que es algo perfectamente razonable en lo que no estemos de acuerdo). Yo diría que no hay lenguajes ensambladores mecanografiados en la industria que sean independientes del lenguaje para sistemas multilingües, por lo que es imposible hacer afirmaciones sobre la práctica. Esto es algo que merece una discusión exhaustiva (separada). Para esa discusión, sería útil que primero proporcionara algunos estudios de caso detallados para que el GC pueda comparar las compensaciones.

Me permito sugerir amablemente que las conversaciones sobre varias propuestas podrían funcionar mejor si se comienza preguntando a los respectivos campeones sobre cosas que no están claras, por ejemplo, fundamentos específicos o planes futuros (que no siempre son obvios o están escritos todavía), antes de asumir la ausencia de una respuesta y hacer afirmaciones y sugerencias amplias basadas en estos supuestos?

WebAssembly / propuesta-tipo-importaciones n. ° 4, WebAssembly / propuesta-tipo-importaciones n. ° 6 y WebAssembly / propuesta-tipo-importaciones n. ° 7 esencialmente solicitaron más detalles sobre este plan. El último de estos incide en GC, pero WebAssembly / gc # 86 señala que la propuesta actual de GC no admite mecanismos de abstracción dinámica.

En el nivel meta, se nos pidió que dejáramos de lado esta discusión y nos enfocáramos en el tema en cuestión. Encontré la respuesta de

@RossTate :

Encontré la respuesta de

Hm, pensé que ya lo había comentado arriba . ¿O te refieres a otra cosa?

No. Pensé que ese comentario podría haber implicado estar de acuerdo con su respuesta, pero quería confirmar primero. ¡Gracias!

@lukewagner , ¿qué piensas?

Estoy de acuerdo con lo anterior en que externref siempre será un tipo primitivo y no se reinterpretará retroactivamente como un parámetro de tipo. Creo que, dado eso, los tipos de referencia están listos para usarse tal como están.

Me gustaría aceptar la oferta de @rossberg para expandir el alcance de la propuesta de Importaciones de tipos para que cubra la capacidad de wasm para implementar tipos abstractos. Una vez que podamos concretar eso, creo que desbloqueará más discusiones sobre las referencias de funciones y el subtipo.

Impresionante. Entonces todos estamos en la misma página (y yo también creo que @tlively proporciona un buen resumen de las compensaciones involucradas y la justificación de una decisión).

Por lo tanto, externref no será instanciable, y se esperará que los módulos que busquen esa flexibilidad adicional de importación de tipos se conviertan en importaciones de tipos cuando se lance la función. Se me ocurre que, para que la transición sea fluida, es probable que necesitemos hacer que (¿algunas?) Importaciones de tipos sean instanciadas por externref de forma predeterminada si no se proporciona una instanciación.

Y también me gustaría aceptar la oferta para ampliar el alcance de las importaciones tipo. Muchas de las principales aplicaciones de las importaciones de tipos necesitan abstracción, por lo que me parece natural que la abstracción sea parte de esa propuesta.

Mientras tanto, aunque hemos abordado la pregunta urgente sobre externref , qué hacer de manera más general con call_indirect aún no está resuelto, aunque con una discusión útil sobre cómo podría resolverse, así que Todavía dejaré el problema abierto.

¡Gracias!

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

thysultan picture thysultan  ·  4Comentarios

konsoletyper picture konsoletyper  ·  6Comentarios

badumt55 picture badumt55  ·  8Comentarios

mfateev picture mfateev  ·  5Comentarios

void4 picture void4  ·  5Comentarios