call_indirect
a été une fonctionnalité très utile pour WebAssembly. Cependant, l'efficacité et le bon comportement de l'instruction reposaient implicitement sur la simplicité du système de type wasm. En particulier, chaque valeur wasm a exactement un type (statique) auquel elle appartient. Cette propriété évite commodément un certain nombre de problèmes connus avec les appels de fonction non typés dans les langages typés. Mais maintenant que wasm s'étend au-delà des types numériques, il est arrivé au point où nous devons comprendre ces problèmes et les garder à l'esprit.
call_indirect
fonctionne fondamentalement en comparant la signature attendue de l'appelant à la signature définie de l'appelé. Avec uniquement des types numériques, WebAssembly avait la propriété que ces signatures étaient égales si et seulement si un appel direct à la fonction référencée par le funcref
aurait été vérifié de type. Mais il y a deux raisons qui ne seront bientôt plus vraies :
call_indirect
peut être utilisé pour accéder à la fonction avec sa signature privée définie plutôt que simplement sa signature publique plus faible ( un problème qui vient d'être découvert et n'a donc pas encore été discuté).call_indirect
de fonctionner correctement sur les fonctions exportées dont les signatures exportées font référence à ce type exporté. Cependant, si un module malveillant connaît la définition de ce type exporté, il peut utiliser call_indirect
pour effectuer une conversion entre le type exporté et sa définition destinée à être secrète car call_indirect
ne compare les signatures qu'au moment de l'exécution, lorsque les deux types sont effectivement les mêmes . Ainsi, un module malveillant peut utiliser call_indirect
pour accéder aux secrets destinés à être abstraits par le type exporté, et peut utiliser call_indirect
pour forger des valeurs du type exporté qui peuvent violer les invariants critiques de sécurité non capturés dans la définition du type lui-même.Dans les deux situations ci-dessus, call_indirect
peut être utilisé pour contourner l'abstraction de la signature exportée d'un module. Comme je l'ai mentionné, jusqu'à présent, cela n'a pas été un problème car wasm n'avait que des types numériques. Et à l'origine, je pensais qu'en différant le sous-typage, toutes les préoccupations concernant call_indirect
avaient également été effectivement différées. Mais ce que j'ai réalisé récemment, c'est qu'en supprimant le sous-typage, le "nouveau" type (nommé externref
dans https://github.com/WebAssembly/reference-types/pull/87) est effectivement un remplaçant pour une importation de type abstrait. Si c'est ce que les gens aimeraient que ce soit réellement, alors nous devons malheureusement prendre en considération l'interaction ci-dessus entre call_indirect
et les importations de type.
Il existe maintenant de nombreuses façons potentielles de résoudre les problèmes ci-dessus avec call_indirect
, mais chacune a ses inconvénients, et c'est tout simplement un espace de conception beaucoup trop grand pour pouvoir prendre une décision rapidement. Je ne suggère donc externref
. En particulier, si nous restreignons pour l'instant call_indirect
et func.ref
à la vérification de type uniquement lorsque la signature associée est entièrement numérique, alors nous servons tous les cas d'utilisation de core-wasm d'appels indirects et au en même temps laisser place à toutes les solutions potentielles aux problèmes ci-dessus. Cependant, je ne sais pas si cette restriction est pratique, à la fois en termes d'effort de mise en œuvre et en termes de blocage des applications de externref
que les gens attendent. L'alternative est de laisser call_indirect
et func.ref
tels quels. Il est simplement possible que cela signifie que, selon la solution à laquelle nous arrivons, externref
pourrait ne pas être instanciable comme le serait une importation de type vrai, et/ou que externref
pourrait (ironiquement) ne pas être capable d'avoir n'importe quel supertype (par exemple, il pourrait ne pas être un sous-type de anyref
si nous décidons finalement d'ajouter anyref
).
Pour ma part, je considère que les deux options sont gérables. Bien que j'aie une préférence, je ne pousse pas fortement la décision d'aller dans un sens ou dans l'autre, et je pense que vous avez tous un meilleur accès aux informations nécessaires pour prendre une décision éclairée. Je voulais juste que vous sachiez qu'il y a une décision à prendre, et en même temps je voulais prendre conscience du problème primordial avec call_indirect
. Si vous souhaitez une explication plus détaillée de ce problème que ce que le résumé ci-dessus fournit, veuillez lire ce qui suit.
call_indirect
versus Abstraction, en détailJ'utiliserai la notation call_indirect[ti*->to*](func, args)
, où [ti*] -> [to*]
est la signature attendue de la fonction, func
est simplement une fonction funcref (plutôt qu'une table funcref et un index), et args
sont les valeurs to*
à transmettre à la fonction. De même, j'utiliserai call($foo, args)
pour un appel direct de la fonction avec l'index $foo
passant des arguments args
.
Supposons maintenant que $foo
soit l'index d'une fonction avec les types d'entrée déclarés ti*
et les types de sortie to*
. Vous pourriez vous attendre à ce que call_indirect[ti*->to*](ref.func($foo), args)
soit équivalent à call($foo, args)
. En effet, c'est le cas en ce moment. Mais il n'est pas clair que nous puissions maintenir ce comportement.
call_indirect
et sous-typageUn exemple de problème potentiel est apparu dans la discussion sur le sous-typage. Supposons ce qui suit :
tsub
est un sous-type de tsuper
$fsub
qui a été définie avec le type [] -> [tsub]
$fsuper
de type [] -> [tsuper]
$fsub
d'IA en tant que $fsuper
(ce qui est judicieux, même si ce n'est pas possible maintenant, ce problème concerne les problèmes potentiels à Considérez maintenant ce qui devrait arriver si IB exécute call_indirect[ -> tsuper](ref.func($fsuper))
. Voici les deux résultats qui semblent les plus plausibles :
Si nous devions choisir le résultat 1, sachez que nous aurions probablement besoin d'utiliser l'une des deux techniques suivantes pour rendre cela possible :
call_indirect
avec la signature d'importation plutôt qu'avec la signature de définition.Si vous préférez la technique 1, sachez que cela ne fonctionnera pas une fois que nous aurons ajouté des références de fonction typées (avec sous-typage de variante). C'est-à-dire que func.ref($fsub)
sera un ref ([] -> [tsub])
et aussi un ref ([] -> [tsuper])
, et pourtant la technique 1 ne sera pas suffisante pour empêcher call_indirect[ -> super](ref.func($fsub))
de piéger. Cela signifie que le résultat 1 nécessite probablement la technique 2, qui a des implications concernant les performances.
Considérons donc un peu plus le résultat 2. La technique d'implémentation ici consiste à vérifier si la signature attendue du call_indirect
dans IB est égale à la signature de la définition de $fsub
dans IA. Au début, le principal inconvénient de cette technique peut sembler être qu'elle piège un certain nombre d'appels dont l'exécution est sûre. Cependant, un autre inconvénient est qu'il introduit potentiellement une fuite de sécurité pour IA.
Pour voir comment, changeons un peu notre exemple et supposons que, bien que l'instance IA définisse en interne $fsub
pour avoir le type [] -> [tsub]
, l'instance IA ne l' exporte qu'avec le type [] -> [tsuper]
. En utilisant la technique pour le résultat 2, l'instance IB peut (malveusement) exécuter call_indirect[ -> tsub]($fsuper)
et l'appel réussira. C'est-à-dire que IB peut utiliser call_indirect
pour contourner le rétrécissement IA fait à la signature de sa fonction. Au mieux, cela signifie qu'IB dépend d'un aspect d'IA qui n'est pas garanti par la signature d'IA. Au pire, IB peut l'utiliser pour accéder à l'état interne qu'IA aurait pu cacher intentionnellement.
call_indirect
et Type ImportationsMettons maintenant de côté le sous-typage et considérons les importations de
capability
et exporte le type mais pas sa définition comme $handle
$do_stuff
qui a été définie avec le type [capability] -> []
mais exportée avec le type [$handle] -> []
$extern
et une fonction $run
de type [$extern] -> []
$handle
exportés par IA en tant que $extern
et avec les IA exportés $do_stuff
sous la forme $run
Ce que cet exemple met en place, ce sont deux modules où un module fait des choses avec les valeurs de l'autre module sans savoir ou être autorisé à savoir quelles sont ces valeurs. Par exemple, ce modèle est la base planifiée pour interagir avec WASI.
Supposons maintenant que l'ID d'instance ait réussi à obtenir une valeur e
de type $extern
et exécute call_indirect[$extern -> ](ref.func($run), e)
. Voici les deux résultats qui semblent les plus plausibles :
Le résultat 2 rend call_indirect
pratiquement inutile avec les types importés. Donc, pour le résultat 1, réalisez que le type d'entrée $extern
n'est pas le type d'entrée défini de $do_stuff
(qui est plutôt capability
), nous aurions donc probablement besoin d'utiliser l'un des deux techniques pour combler cette lacune :
call_indirect
avec la signature d'importation plutôt qu'avec la signature de définition.$extern
dans l'ID d'instance représente capability
.Si vous préférez la technique 1, sachez qu'elle ne fonctionnera plus une fois que nous aurons ajouté des références de fonction typées. (La raison fondamentale est la même que pour le sous-typage, mais il faudrait encore plus de texte pour illustrer l'analogue ici.)
Cela nous laisse avec la technique 2. Malheureusement, cela présente encore une fois un problème de sécurité potentiel. Pour voir pourquoi, supposons que ID soit malveillant et veuille accéder au contenu de $handle
qu'IC avait gardé secret. Supposons en outre que ID ait une bonne idée de ce que représente réellement $handle
, à savoir capability
. ID peut définir la fonction d'identité $id_capability
de type [capability] -> [capability]
. Étant donné une valeur e
de type $extern
, ID peut alors exécuter call_indirect[$extern -> capability](ref.func($id_capability), e)
. En utilisant la technique 2, cet appel indirect réussira car $extern
représente capability
au moment de l'exécution, et ID obtiendra le capability
brut que e
représente en retour. De même, étant donné une valeur c
de type capability
, ID peut exécuter call_indirect[capability -> $extern](ref.func($id_capability), c)
pour forger c
en un $extern
.
J'espère avoir clairement indiqué que call_indirect
présente un certain nombre de problèmes importants de performances, de sémantique et/ou de sécurité/d'abstraction à venir, des problèmes que WebAssembly a eu la chance d'éviter jusqu'à présent. Malheureusement, en raison du fait que call_indirect
fait partie du noyau WebAssembly, ces problèmes recoupent un certain nombre de propositions en cours. Pour le moment, je pense qu'il serait préférable de se concentrer sur la proposition la plus urgente, les types de référence, où nous devons décider de restreindre ou non call_indirect
et func.ref
aux seuls types numériques pour maintenant — une restriction que nous pourrions peut-être assouplir en fonction de la façon dont nous finirons par résoudre les problèmes généraux avec call_indirect
.
(Désolé pour le long message. J'ai fait de mon mieux pour expliquer les interactions complexes des fonctionnalités de typage au moment de la compilation entre modules et de la saisie au moment de l'exécution et de démontrer l'importance de ces interactions de la manière la plus concise possible.)
Merci pour cet article détaillé, Ross ! J'ai une petite question : dans la section " call_indirect
and Type Imports" vous écrivez,
Si vous préférez la technique 1, sachez qu'elle ne fonctionnera plus une fois que nous aurons ajouté des références de fonction typées.
Est-ce également soumis à la mise en garde de la section précédente selon laquelle le problème n'est présent qu'une fois que nous avons ajouté un sous-typage de variante aux références de fonction typées ?
Ce n'est pas. Tous les problèmes de la section de sous-typage sont indépendants des importations de type et tous les problèmes de la section d'importation de type sont indépendants du sous-typage. En ce qui concerne le problème particulier que vous posez, considérez qu'une valeur de type ref ([] -> [capability])
peut être renvoyée par une fonction exportée en tant que valeur de type ref ([] -> [$handle])
, qui peut ensuite être transformée en un funcref
et indirectement appelé. Contrairement à la fonction exportée, ce changement de perspective de la valeur se produit au moment de l'exécution plutôt qu'au moment de la liaison, nous ne pouvons donc pas le résoudre en comparant avec la signature d'importation puisque la référence de fonction n'a jamais été elle-même importée.
module instance IC defines a type capability and exports the type but not its definition as $handle
Comment cela fonctionnera-t-il ? Il doit y avoir quelque chose qui relie capability
et $handle
pour qu'IC sache comment y faire face ?
Également basé sur https://github.com/WebAssembly/proposal-type-imports/blob/master/proposals/type-imports/Overview.md#exports , les types importés sont complètement abstraits. Donc même si $capability
est exporté, il est abstrait. Peut-être que j'ai mal compris quelque chose.
Question similaire pour l'exportation de module instance IC exports a function $do_stuff that was defined with type [capability] -> [] but exported with type [$handle] -> []
.
Je peux imaginer une sorte de relation de sous-typage utilisée pour cela, par exemple si $capability <: $handle
, alors nous pouvons export $capability as $handle
. Mais au début de cette section, il était mentionné de mettre le sous-typage de côté, donc je mets cela de côté... Mais j'y ai aussi réfléchi un peu plus :
Si : $capability <: $handle
, nous pouvons export $capability as $handle
, mais export ([$capability] -> []) as ([$handle] -> [])
devrait "échouer" car les fonctions sont contravariantes dans l'argument.
Avec les exportations de type, un module spécifie une signature, comme type $handle; func $do_stuff_export : [$handle] -> []
, puis instancie la signature, comme type $handle := capability; func $do_stuff_export := $do_stuff
. (Ignorez entièrement la syntaxe spécifique.) Le vérificateur de type vérifie alors "étant donné que $handle
représente capability
dans ce module, l'export func $do_stuff_export := $do_stuff
valide dans ce module ?". Puisque le type de $do_stuff
est [capability] -> []
, sa signature s'aligne exactement avec celle de $do_stuff_export
après avoir instancié $handle
avec capability
, donc le vérification réussit. (Il n'y a pas de sous-typage impliqué ici, juste une substitution de variable.)
Notez, cependant, que la signature elle-même ne dit rien sur $handle
. Cela signifie que tout le monde est censé traiter $handle
comme un type abstrait. C'est-à-dire que la signature abstrait intentionnellement les détails de l'implémentation du module, et tout le monde est censé respecter cette abstraction. Le but de ce numéro est d'illustrer que call_indirect
peut être utilisé pour contourner cette abstraction.
Espérons que cela clarifie un peu le problème !
Merci, ça clarifie les choses. J'aurai une question sur la section de sous-typage (désolé de sauter par-dessus):
Je suis le scénario dans lequel nous voulons qu'IB exécute call_indirect[ -> tsuper](ref.func($fsuper))
pour réussir, en ayant call_indirect
"comparer avec la signature d'importation plutôt qu'avec la signature de définition".
Et vous avez ajouté que (en raison des références de fonction tapées), nous avons également besoin
- Effectuez une vérification à l'exécution au moins linéaire de la compatibilité des sous-types de la signature attendue et de la signature de définition.
S'agit-il d'une compatibilité entre « la signature attendue et la signature d'importation » ? Puisque nous supposons que nous avons fait call_indirect comparer la signature d'importation avec la signature attendue.
Si la compatibilité est vérifiée entre l'attente et l'importation, alors plus tard, call_indirect[ -> tsub]($fsuper)
devrait échouer.
Les techniques 1 et 2 sont présentées comme deux manières orthogonales de faire fonctionner cet appel indirect. Malheureusement, la technique 1 est incompatible avec les références de fonctions typées, et la technique 2 est probablement trop chère. Donc, aucun de ceux-ci ne semble susceptible de fonctionner. Ainsi, le reste de la section considère ce qui se passe si nous n'utilisons aucun de ces éléments et nous en tenons simplement à une simple comparaison d'égalité entre la signature attendue et définie. Désolé pour la confusion; ne pas avoir de sémantique planifiée signifie que je dois discuter de trois sémantiques potentielles.
Attention à ne pas sauter à trop de conclusions. ;)
Mon hypothèse est que call_indirect
devrait rester aussi rapide qu'aujourd'hui et, par conséquent, ne nécessiter qu'un test d'équivalence de type, quel que soit le sous-typage que nous ajoutons au langage. En même temps, le contrôle d'exécution doit être cohérent avec le système de type statique, c'est-à-dire qu'il doit respecter la relation de sous-typage.
Maintenant, ces exigences apparemment contradictoires peuvent en fait être conciliées assez facilement, tant que nous nous assurons que les types utilisables avec call_indirect
sont toujours à la fin de la hiérarchie des sous-types.
Une manière établie de faire respecter cela est d'introduire la notion de types _exacts_ dans le système de types. Un type exact n'a pas de sous-types, seulement des supertypes, et nous aurions (exact T) <: T
.
Avec cela, nous pouvons exiger que le type cible à call_indirect
soit un type exact. De plus, le type des fonctions elles-mêmes est naturellement déjà le type exact de cette fonction.
Un module pourrait également exiger des types exacts sur les importations de fonctions, s'il voulait s'assurer qu'il ne peut être instancié qu'avec des fonctions qui réussissent une vérification d'exécution prévue.
C'est tout ce qui est nécessaire pour s'assurer que la technique d'implémentation actuelle d'une simple comparaison de pointeurs sur des types de fonctions canoniques reste valide. C'est indépendant de ce qu'il y a d'autre sous-typage, ou de la fantaisie avec laquelle nous créons le sous-typage des fonctions. (FWIW, j'en ai discuté avec Luke il y a quelque temps et j'avais prévu de créer un PR, mais il a été bloqué sur les modifications en attente de l'histoire de sous-typage, et sur la proposition vers laquelle se déplace maintenant.)
(Un inconvénient est que raffiner une définition de fonction en un sous-type n'est plus un changement rétrocompatible en général, du moins pas si son type exact a été utilisé n'importe où. Mais cet inconvénient est inévitable sous nos contraintes, quelle que soit la façon dont nous appliquons exactement eux.)
Quelques apartés :
L'alternative est de laisser call_indirect et func.ref tels quels.
AFAICS, il n'est pas possible d'interdire ref.func sur les fonctions qui impliquent des types de référence. Cela paralyserait gravement de nombreux cas d'utilisation, c'est-à-dire tout ce qui implique des fonctions de première classe fonctionnant sur externref
(callbacks, hooks, etc.).
Il est simplement possible que cela signifie que, selon la solution à laquelle nous arrivons, externref pourrait ne pas être instanciable comme le serait une importation de type vrai, et/ou que externref pourrait (ironiquement) ne pas pouvoir avoir de supertypes (par exemple, pourrait ne pas pouvoir être un sous-type de anyref si nous décidons finalement d'ajouter anyref).
Peux-tu élaborer? Je ne vois pas le lien.
Attention à ne pas sauter à trop de conclusions. ;)
Je ne sais pas à quelle conclusion vous faites référence. Ma conclusion déclarée est qu'il y a un certain nombre de problèmes avec call_indirect
dont nous devons être conscients et que nous devrions commencer à planifier. Vous semblez suggérer que ces problèmes sont sans importance parce que vous avez une solution en tête. Mais cette solution n'a pas été examinée ou acceptée par le CG, et nous ne devrions pas la planifier tant qu'elle ne l'a pas été. J'ai spécifiquement demandé de ne pas discuter de solutions car cela prendra un certain temps pour les évaluer et les comparer et il y a des décisions que nous devons prendre avant d'avoir le temps de faire ces évaluations et comparaisons correctement. Mais, afin d'éviter que les gens n'aient l'impression que ce problème est résolu et par conséquent d'éviter la décision urgente, je vais prendre une seconde pour discuter rapidement de votre solution.
Une manière établie de faire respecter cela est d'introduire la notion de types exacts dans le système de types.
Les types exacts ne sont guère une solution établie. Si quoi que ce soit, les types exacts ont établi des problèmes que ses partisans s'efforcent toujours de résoudre. Fait intéressant, voici un fil où l'équipe TypeScript a initialement vu comment les types exacts du formulaire que vous proposez pourraient résoudre certains problèmes, mais ils ont finalement [réalisé) (https://github.com/microsoft/TypeScript/issues/12936 #issuecomment-284590083) que les types exacts introduisaient plus de problèmes qu'ils n'en résolvaient. (Remarque pour le contexte : cette discussion a été suscitée par les types d' objets exacts de Flow, qui ne sont pas en fait une forme de type exact (au sens théorique) mais qui interdisent simplement l'analogue d'objet du sous-typage de préfixe.) Je pourrais nous imaginer en train de rejouer ce fil ici.
À titre d'exemple de la façon dont ces types de problèmes peuvent se produire pour WebAssembly, supposons que nous n'ayons pas différé le sous-typage. Le type de ref.null
serait exact nullref
utilisant des types exacts. Mais exact nullref
ne serait pas un sous-type de exact anyref
. En fait, selon la sémantique habituelle des types exacts, aucune valeur n'appartiendrait probablement à exact anyref
car le type d'exécution d'aucune valeur n'est probablement exactement anyref
. Cela rendrait call_indirect
complètement inutilisable pour anyref
s.
Maintenant, vous avez peut-être en tête une version différente des types exacts, mais il faudrait un certain temps pour vérifier que cette version différente résout d'une manière ou d'une autre les nombreux problèmes ouverts avec les types exacts. Mon propos ici n'est donc pas de rejeter cette solution, mais de reconnaître qu'il n'est pas évident que ce soit la solution et de ne pas prendre de décisions avec cette attente.
Peux-tu élaborer? Je ne vois pas le lien.
Vous faites référence à une longue phrase. Sur quelle partie voudriez-vous que je développe ? On suppose que vous manquez peut-être le problème global avec call_indirect
et tapez les importations. Votre suggestion de types exacts ne traite que des problèmes de sous-typage, mais nous avons établi ci-dessus que call_indirect
a des problèmes même sans aucun sous-typage.
Cela paralyserait gravement de nombreux cas d'utilisation, c'est-à-dire tout ce qui implique des fonctions de première classe fonctionnant sur
externref
(callbacks, hooks, etc.).
Ouais, donc c'est quelque chose sur lequel j'espérais obtenir plus d'informations. Je crois comprendre que le principal cas d'utilisation de call_indirect
est de prendre en charge les pointeurs de fonction C/C++ et les méthodes virtuelles C++. D'après ce que je comprends, ce cas d'utilisation est actuellement limité aux signatures numériques. Je connais d'autres utilisations potentielles de call_indirect
, mais comme je l'ai mentionné, je suggérais une restriction temporaire , donc ce qui compte, c'est quelles sont les utilisations actuelles de call_indirect
. Étant donné que call_indirect
nécessite toujours une table et un index plutôt qu'un simple funcref
, il ne semble pas particulièrement bien conçu pour prendre en charge les rappels. Je ne savais pas si c'était parce qu'actuellement, il n'est pas utilisé à cette fin.
Vous connaissez tous les bases de code ciblant cette fonctionnalité bien mieux que moi, donc si vous connaissez tous de vrais programmes nécessitant cette fonctionnalité maintenant, il serait très utile de fournir quelques exemples des modèles d'utilisation nécessaires ici. En plus d'être utiles pour déterminer si nous devons prendre en charge cette fonctionnalité dès maintenant, si la fonctionnalité est nécessaire maintenant, ces exemples seraient utiles pour indiquer la meilleure façon de la fournir rapidement tout en résolvant les problèmes ci-dessus.
@RossTate :
Si quoi que ce soit, les types exacts ont établi des problèmes que ses partisans s'efforcent toujours de résoudre. Fait intéressant, voici un fil où l'équipe TypeScript a initialement vu comment les types exacts du formulaire que vous proposez pouvaient résoudre certains problèmes, mais ils ont finalement réalisé que les types exacts introduisaient plus de problèmes qu'ils n'en résolvaient. (Remarque pour le contexte : cette discussion a été suscitée par les types d'objets exacts de Flow, qui ne sont pas en fait une forme de type exact (au sens théorique) mais simplement interdisent l'analogue d'objet du sous-typage de préfixe.) Je pourrais nous imaginer rejouer ce fil ici.
Les parenthèses sont la clé ici. Je ne sais pas exactement ce qu'ils ont en tête dans ce fil, mais cela ne semble pas être la même chose. Sinon, des déclarations comme "on suppose qu'un type T & U
est toujours assignable à T
, mais cela échoue si T
est un type exact" n'aurait aucun sens (cela n'a pas de sens t échouer, car T & U
serait invalide ou inférieur). Les autres questions concernent principalement la pragmatique, c'est-à-dire, où un programmeur voudrait-il les utiliser (pour les objets), ce qui ne s'applique pas dans notre cas.
Pour les systèmes de types de bas niveau, les types exacts n'étaient-ils pas un ingrédient crucial, même dans certains de vos propres articles ?
À titre d'exemple de la façon dont ces types de problèmes peuvent se produire pour WebAssembly, supposons que nous n'ayons pas différé le sous-typage. Le type de ref.null serait exact nullref en utilisant des types exacts. Mais exact nullref ne serait pas un sous-type de exact anyref.
Pas de désaccord ici. Ne pas avoir de sous-types est le but des types exacts.
En fait, selon la sémantique habituelle des types exacts, il est probable qu'aucune valeur n'appartiendrait à exact anyref, car il est probable que le type d'exécution d'aucune valeur n'est exactement anyref.
Exact, la combinaison (exact anyref)
n'est pas un type utile, étant donné que le seul but de anyref est d'être un supertype. Mais pourquoi est-ce un problème ?
Cela rendrait call_indirect complètement inutilisable pour anyrefs.
Êtes-vous sûr de ne pas confondre les niveaux maintenant ? Une fonction de type (exact (func ... -> anyref))
est parfaitement utile. Ce n'est tout simplement pas compatible avec un type, disons, (func ... -> (ref $T))
. C'est-à-dire que exact
empêche le sous-typage non trivial sur les types de fonction. Mais c'est tout l'intérêt !
Peut-être que vous confondez (exact (func ... -> anyref))
avec (func ... -> exact anyref)
? Ce sont des types sans rapport.
Votre suggestion de types exacts ne traite que des problèmes de sous-typage, mais nous avons établi ci-dessus que call_indirect a des problèmes même sans aucun sous-typage.
Vous supposez en quelque sorte que vous pourrez exporter un type sans sa définition comme moyen de définir un type de données abstrait. De toute évidence, cette approche ne fonctionne pas en présence de transtypages dynamiques (call_indirect ou autre). C'est pourquoi je n'arrête pas de dire que nous aurons besoin d'une abstraction de type de style newtype, pas d'une abstraction de type de style ML.
Je crois comprendre que le principal cas d'utilisation de call_indirect est de prendre en charge les pointeurs de fonction C/C++
Oui, mais ce n'est pas le seul cas d'utilisation de ref.func
, auquel je faisais référence, car vous l'avez inclus dans votre restriction suggérée (peut-être inutilement ?). En particulier, il y aura call_ref
, qui n'implique pas de vérification de type.
Vous supposez en quelque sorte que vous pourrez exporter un type sans sa définition comme moyen de définir un type de données abstrait. De toute évidence, cette approche ne fonctionne pas en présence de transtypages dynamiques (call_indirect ou autre). C'est pourquoi je n'arrête pas de dire que nous aurons besoin d'une abstraction de type de style newtype, pas d'une abstraction de type de style ML.
D'accord, vous semblez donc convenir que les types exacts ne font rien pour résoudre le problème avec call_indirect
et type import. Mais vous dites également qu'il ne sert à rien de résoudre ce problème car ce sera de toute façon un problème en raison des transtypages à l'exécution. Il existe un moyen simple d'éviter ce problème : n'autorisez pas les utilisateurs à effectuer des transtypages à l'exécution sur des types abstraits (à moins que le type abstrait ne dise explicitement qu'il est castable). Après tout, c'est un type opaque, nous ne devrions donc pas pouvoir supposer qu'il présente la structure nécessaire pour effectuer un moulage. Ainsi, même s'il existe une possibilité que les types exacts résolvent le problème de sous-typage, il est prématuré d'ignorer l'autre moitié du problème.
Comme je l'ai dit, chaque solution comporte des compromis. Vous semblez présumer que votre solution n'a que les compromis que vous avez vous-même identifiés, et vous semblez présumer que le CG préférerait votre solution aux autres. Moi aussi, j'ai une solution potentielle à ce problème. Il garantit des contrôles à temps constant, est basé sur une technologie déjà utilisée dans les machines virtuelles, résout tous les problèmes ici (je crois), ne nécessite pas l'ajout de nouveaux types et ajoute en fait des fonctionnalités supplémentaires à WebAssembly avec des applications connues. Cependant, je ne présume pas que cela fonctionne comme je le souhaite et que je n'ai pas négligé certaines lacunes parce que vous et d'autres n'avez pas eu l'occasion de l'examiner. Je ne présume pas non plus que le CG préférerait ses compromis à ceux des options alternatives. Au lieu de cela, j'essaie de comprendre ce que nous pouvons faire pour nous donner le temps d'analyser les options afin que le CG, plutôt que moi seul, puisse être celui qui prend une décision éclairée sur ce sujet transversal.
En particulier, il y aura
call_ref
, qui n'implique pas de vérification de type.
Le mot clé dans votre phrase est volonté . Je suis parfaitement conscient qu'il existe des applications de call_indirect
avec des types non numériques que les gens veulent avoir pris en charge. Et je pense que nous allons arriver à une conception qui soutient cette fonctionnalité et traite des questions ci - dessus. Mais, comme je l'ai dit, idéalement, nous pouvons avoir un peu de temps pour développer cette conception afin de ne pas livrer rapidement une fonctionnalité avec des implications transversales avant d'avoir eu la chance d'étudier ces implications. Ma question est donc de savoir s'il y a des programmes majeurs qui ont besoin de cette fonctionnalité maintenant . S'il y en a, il n'est pas nécessaire d'émettre des hypothèses ; pointez-en simplement quelques-uns et illustrez comment ils s'appuient actuellement sur cette fonctionnalité.
Vous supposez en quelque sorte que vous pourrez exporter un type sans sa définition comme moyen de définir un type de données abstrait. De toute évidence, cette approche ne fonctionne pas en présence de transtypages dynamiques (call_indirect ou autre). C'est pourquoi je n'arrête pas de dire que nous aurons besoin d'une abstraction de type de style newtype, pas d'une abstraction de type de style ML.
Cela me semble être une question fondamentale. Permettre la confidentialité des définitions des types exportés est-il un objectif de la proposition d'importation de types ? Je déduis de ce fil que @RossTate pense que cela devrait être un objectif et @rossberg pense que ce n'est pas un objectif actuellement. Discutons et mettons-nous d'accord sur cette question avant de discuter des solutions afin que nous puissions tous travailler à partir du même ensemble d'hypothèses.
@RossTate :
D'accord, vous semblez donc convenir que les types exacts ne font rien pour résoudre le problème avec call_indirect et type import.
Oui, si vous entendez par là la question de savoir comment ajouter une fonctionnalité pour définir des types de données abstraits. Il existe un certain nombre de façons dont l'abstraction de type peut fonctionner de manière cohérente, mais une telle fonctionnalité est plus avancée.
Le mot clé de votre phrase est volonté. Je suis pleinement conscient qu'il existe des applications de call_indirect avec des types non numériques que les gens voudront avoir pris en charge.
L'instruction call_ref
est dans la proposition de référence de fonction, donc assez proche, en tout cas avant tout mécanisme de type de données abstrait potentiel. Proposez-vous que nous le mettions en attente jusque-là?
@tlively :
Permettre la confidentialité des définitions des types exportés est-il un objectif de la proposition d'importation de types ? Je déduis de ce fil que @RossTate pense que cela devrait être un objectif et @rossberg pense que ce n'est pas un objectif actuellement.
C'est un objectif, mais un mécanisme de type de données abstrait est une fonctionnalité distincte. Et un tel mécanisme doit être conçu de telle sorte qu'il n'affecte pas la conception des importations. Si c'était le cas, nous le ferions très mal -- l'abstraction doit être assurée sur le site de définition, pas sur le site d'utilisation. Heureusement, cependant, ce n'est pas sorcier et l'espace de conception est assez bien exposé.
Merci, @rossberg , c'est logique. L'ajout de primitives d'abstraction dans une proposition de suivi après les importations et les exportations de type me semble bien, mais ce serait formidable si nous pouvions écrire les détails de la façon dont nous prévoyons de le faire quelque part bientôt. La conception des importations et exportations de types contraint et informe la conception des importations et exportations de types abstraits, il est donc important que nous ayons une bonne idée de la façon dont l'abstraction fonctionnera avant de finaliser la conception initiale.
En plus de détailler ce plan, puisque ce problème avec call_indirect
démontre qu'il affecte les décisions urgentes, pouvez-vous expliquer pourquoi vous semblez rejeter ma suggestion selon laquelle les types abstraits ne devraient pas être castables (à moins qu'ils ne soient explicitement contraints de l'être ) ? Ils sont opaques, de sorte que la suggestion semble être conforme aux pratiques courantes des types abstraits.
@tlively , oui, d'accord. Plus diverses autres choses que j'avais l'intention d'écrire pendant un moment. Je le ferai une fois que j'aurai travaillé sur toutes les retombées du #69 . ;)
@RossTate , car cela rendrait les types de données abstraits incompatibles avec les casts. Juste parce que je veux empêcher les autres de voir _à_ travers_ un type abstrait, je ne veux pas nécessairement les empêcher (ou moi-même) de convertir _en_ un type abstrait. Créer une telle fausse dichotomie briserait les cas d'utilisation centraux des moulages. Par exemple, bien sûr, je veux pouvoir passer une valeur de type abstrait à une fonction polymorphe.
@rossberg Pouvez-vous préciser quel est ce cas d'utilisation central que vous avez en tête ? Ma meilleure estimation pour interpréter votre exemple est trivialement soluble, mais peut-être que vous voulez dire autre chose.
@RossTate , considérez les fonctions polymorphes. À court de Wasm-generics, lors de leur compilation en utilisant des transtypages haut/bas à partir d'anyref, il devrait être possible de les utiliser avec des valeurs de type abstrait comme n'importe quel autre, sans encapsulation supplémentaire dans d'autres objets. Vous voulez généralement pouvoir traiter les valeurs de type abstrait comme n'importe quelle autre.
Bon, considérons les fonctions polymorphes, et supposons que le type importé soit Handle
:
CHandle
implémentant éventuellement des interfaces. Les instances de cette classe auront un membre (au niveau wasm) de type Handle
et une v-table qui fournit des pointeurs de fonction vers les implémentations de diverses méthodes de classe et d'interface. Lorsqu'il est donné à une fonction polymorphe au niveau de la surface, qui au niveau wasm n'est qu'une fonction sur les objets, le module peut utiliser le même mécanisme qu'il utilise pour transtyper vers d'autres classes pour transposer en CHandle
.THandle
qui correspond à ces normes et a un membre (au niveau Wasm) de type Handle
. Ses fonctions polymorphes convertiraient ensuite les valeurs OCaml en THandle
comme elles le feraient pour tout autre type de données algébriques ou type d'enregistrement.En d'autres termes, parce que les modules s'appuient sur des normes sur la façon dont les valeurs au niveau de la surface sont représentées afin d'implémenter des choses comme les fonctions polymorphes, et que les types importés abstraits comme Handle ne satisfont pas à ces normes, l'encapsulation des valeurs est inévitable. C'est la même raison pour laquelle l'une des applications originales pour anyref
été remplacée par Interface Types. Et nous avons développé des études de cas démontrant que anyref
n'est pas nécessaire, ni même bien adapté, pour prendre en charge les fonctions polymorphes.
D'un autre côté, vous avez démontré que le castable anyref
peut être utilisé pour contourner les mécanismes d'abstraction statique. Le plan de mécanisme d'abstraction auquel vous avez fait allusion est une tentative de corriger ce problème grâce à des mécanismes d'abstraction dynamique. Mais il y a un certain nombre de problèmes avec les mécanismes d'abstraction dynamique. Par exemple, on ne peut pas exporter votre type i31ref
tant que type abstrait Handle
sans risquer que d'autres modules utilisent anyref
et transposent pour forger des descripteurs (par exemple en capacités). Au lieu de cela, il faut sauter à travers des cerceaux et des frais généraux supplémentaires qui seraient inutiles si nous assurons plutôt une abstraction statique standard.
De plus, maintenant que (je pense) je comprends mieux comment vous avez l'intention d'utiliser des types exacts, je me rends compte que votre intention ne résout aucun des deux problèmes majeurs sur lesquels j'ai attiré l'attention avec call_indirect
et le sous-typage :
call_indirect
respecte le sous-typage (ce que je pense que vous avez déjà dit explicitement)call_indirect
d'être utilisé pour utiliser une fonction exportée avec sa signature définie plutôt que sa signature exportée.Ce n'est donc pas un problème trivial à résoudre. C'est pourquoi, compte tenu des contraintes de temps, je préférerais me concentrer sur l'évaluation de la façon de nous donner du temps pour le résoudre correctement. Je ne pense pas qu'il devrait être nécessaire d'avoir d'abord une discussion pour savoir si anyref
vaut la peine de jeter l'abstraction statique pour. C'est le genre de grosse discussion que j'espérais éviter pour ne pas retarder davantage les choses.
D'un autre côté, vous avez démontré qu'anyref castable peut être utilisé pour contourner les mécanismes d'abstraction statique.
L'abstraction de type statique est insuffisante dans un langage avec des transtypages dynamiques. Parce que l'abstraction statique repose sur la paramétrisation, et les casts cassent cela. Il n'y a rien de nouveau à ce sujet, des articles ont été écrits à ce sujet. D'autres mécanismes d'abstraction sont nécessaires dans un tel contexte.
Essayer de contourner cela en limitant l'utilisation de types abstraits va à l'encontre de leur objectif. Considérez le cas d'utilisation WASI. Peu importe qu'un module WASI et tout type qu'il exporte soit implémenté par l'hôte ou dans Wasm. Si vous restreignez arbitrairement les types abstraits définis par l'utilisateur, une implémentation Wasm ne serait plus interchangeable avec une implémentation hôte en général.
- Cela n'aide pas à faire en sorte que call_indirect respecte le sous-typage (ce que je pense que vous avez déjà dit explicitement)
Hein? Cela fait partie des règles de sous-typage, comme par définition.
- Cela n'empêche pas call_indirect d'être utilisé pour utiliser une fonction exportée avec sa signature définie plutôt que sa signature exportée.
Je n'ai pas dit que oui. J'ai dit que celui-ci n'est pas un problème avec call_indirect lui-même, mais une question de choisir un mécanisme d'abstraction de type approprié pour un langage avec des transtypages.
Soit dit en passant, il n'y a aucune raison impérieuse pour laquelle la compilation d'OCaml (ou de tout autre langage similaire) devrait nécessiter l'introduction de types variants. Même si cela pourrait être légèrement plus rapide en théorie (ce qui, je doute que ce soit le cas dans les moteurs de génération actuelle, plus probablement le contraire), les types de variantes sont une complication importante qui ne devrait pas être nécessaire pour le MVP. Je ne partage pas tout à fait votre appétit pour la complexité prématurée. ;)
Re l'égalité sur les fonctions : il existe des langages, tels que Haskell ou SML, qui ne prennent pas en charge cela, et pourraient donc bénéficier directement des références de fonction. OCaml lance une égalité structurelle et a explicitement un comportement défini par l'implémentation pour une physique. Il reste ouvert si cela permet de toujours retourner false ou de lancer des fonctions, mais l'un ou l'autre pourrait bien être suffisant dans la pratique et mériter d'être exploré avant de s'engager dans un emballage supplémentaire coûteux.
[En tant que méta-commentaire, j'apprécierais vraiment que vous ayez atténué votre discours et que vous considériez peut-être l'idée qu'il s'agit d'un monde où, peut-être, l'ensemble des personnes compétentes n'est pas unique et que des traces de cerveaux ont parfois été appliquées auparavant.]
En tant que méta-commentaire, j'apprécierais vraiment que vous ayez atténué votre discours
Entendu.
et peut-être considéré l'idée que c'est un monde où, peut-être, l'ensemble des personnes compétentes n'est pas unique et que des traces de cerveaux ont parfois été appliquées auparavant.
Mon conseil ici est basé sur la consultation de plusieurs experts.
L'abstraction de type statique est insuffisante dans un langage avec des transtypages dynamiques. Parce que l'abstraction statique repose sur la paramétrisation, et les casts cassent cela. Il n'y a rien de nouveau à ce sujet, des articles ont été écrits à ce sujet. D'autres mécanismes d'abstraction sont nécessaires dans un tel contexte.
Ces experts que j'ai consultés comprennent les auteurs de certains de ces articles.
Maintenant, pour tenter de vérifier que j'ai correctement synthétisé leurs conseils, je viens d'envoyer un e-mail à un autre auteur de certains de ces articles, un dont je n'ai jamais discuté de ce sujet auparavant. Voici ce que j'ai demandé :
Supposons que j'ai une fonction polymorphe f
(...). Mon langage typé a un sous-typage (subsomptif) et un casting explicite. Cependant, un transtypage de t1 vers t2 vérifie uniquement si t2 est un sous-type de t1. Supposons que les variables de type comme X par défaut n'aient pas de sous-types ou de supertypes (à part eux-mêmes bien sûr). Vous attendriez-vous à ce que f soit relationnellement paramétrique par rapport à X ?
Voici leur réponse :
Oui, je pense que ce serait paramétrique puisque la seule capacité que cela vous donne est d'écrire des casts sur X qui sont équivalents à une fonction d'identité, qui est déjà paramétrique relationnelle.
C'est conforme à mes conseils. Maintenant, bien sûr, il s'agit d'une simplification du problème à portée de main, mais nous avons fait un effort pour étudier le problème plus spécifiquement pour WebAssembly, et jusqu'à présent, notre exploration a suggéré que cette attente continue même à l'échelle de WebAssembly sauf pour call_indirect
, d'où ce problème.
Notez que les théorèmes auxquels vous faites référence s'appliquent aux langues dans lesquelles toutes les valeurs sont castables. C'est de ce constat qu'est venue l'idée de restreindre la coulabilité.
Considérez le cas d'utilisation WASI.
Je ne comprends pas les affirmations que vous faites. Nous avons considéré le cas d'utilisation de WASI. Par nous, j'inclus plusieurs experts en sécurité et même spécifiquement en sécurité basée sur les capacités.
En tant que méta-commentaire, j'apprécierais vraiment de ne pas avoir besoin de faire appel à l'autorité ou au CG pour faire entendre mes suggestions. J'ai suggéré que restreindre les moulages permettrait d'assurer la paramétrisation statique même en présence de moulages. Vous avez immédiatement ignoré cette suggestion, faisant appel à des documents antérieurs pour justifier ce licenciement. Pourtant, lorsque j'ai proposé cette même suggestion à un auteur de ces articles, ils sont immédiatement arrivés à la même conclusion que moi et que vous auriez pu. Avant cela, j'ai suggéré que l'évaluation des solutions potentielles serait un long processus. Vous avez ignoré cette suggestion, insistant sur le fait que vous (tout seul) aviez résolu le problème, nous entraînant tous les deux dans cette longue conversation. Il est extrêmement difficile de progresser et de ne pas être frustré lorsque ses suggestions sont rejetées à plusieurs reprises avec tant de désinvolture. (Je dois préciser que je n'essaie pas de rejeter votre suggestion comme une solution possible ici ; j'essaie de démontrer que ce n'est pas la seule solution et qu'elle devrait donc être évaluée aux côtés de plusieurs autres.)
Je pense qu'il est important et opportun d'avoir une conception détaillée qui répond aux préoccupations soulevées dans ce numéro : je ne pense pas réellement que les types abstraits devraient être considérés comme une fonctionnalité plus éloignée ; WASI en a besoin maintenant.
J'ai également l'espoir que exact
+ newtype
puisse répondre aux préoccupations, mais je suis d'accord que nous ne pouvons pas simplement parier la ferme sur cette intuition à ce stade en nous engageant prématurément dans une conception lorsque nous expédions (bientôt) des types de référence. Nous avons besoin de temps pour en discuter correctement.
Cela étant dit, je ne vois pas le danger d'autoriser externref
dans les signatures call_indirect
dans la proposition de types de référence. Oui, si un module exporte une externref
(en tant que const global ou en la retournant depuis une fonction ...), nous n'avons pas déterminé si nous pouvons downcaster ce externref
. Mais call_indirect
n'est pas en train de downcaster un externref
; il abaisse un funcref
, et externref
n'a pas un rôle différent de celui de i32
rapport à la vérification d'égalité de type funcref. Ainsi, en l'absence d'importation de type, d'exportation de type et de sous-typage en jeu dans call_indirect
, je ne vois pas comment nous nous engageons dans un nouveau choix de conception auquel nous ne nous sommes pas déjà engagés dans le MVP .
S'il n'y a pas de danger, peut-être pourrions-nous réduire cette discussion intense à une discussion moins intense dans la proposition Type Imports (où je pense toujours que nous devrions inclure une prise en charge appropriée des types abstraits) ?
Sûr. Je pense que c'est une bonne idée d'examiner s'il y a un danger ou non.
En ce qui concerne WASI, la conception est encore très changeante, mais une option qui semble toujours viable consiste à utiliser quelque chose comme i31ref
pour ses "handles", disons parce qu'il ne nécessite pas d'allocation dynamique de mémoire. WASI peut décider d'autres options, mais le fait est que personne ne le sait à l'heure actuelle, et il serait bon que les décisions prises maintenant n'affectent pas de telles décisions sur toute la ligne.
Actuellement, externref
est le seul type abstrait disponible, et donc un hôte basé sur WASI instancierait externref
avec i31ref
(ou quels que soient les "handles" WASI). Mais je crois comprendre que WASI souhaite déplacer son implémentation dans WebAssembly autant que possible afin de réduire le code dépendant de l'hôte. Pour faciliter cela, à un moment donné, les systèmes WASI pourraient vouloir traiter externref
comme n'importe quel autre type d'importation et l'instancier avec le type abstrait exporté Handle
WASI. Mais si Handle
vaut i31ref
, alors l'implémentation ci-dessus de call_indirect
nécessaire pour lui permettre de fonctionner au-delà des limites du module peut également être utilisée pour permettre aux gens de forger des poignées via externref
.
Donc, l'une de mes questions, que je remarque maintenant n'a pas été clairement énoncée dans mon message d'origine, est-ce que les gens veulent que externref
soient instanciables comme le seront les autres importations de type abstrait?
Donc, l'une de mes questions, que je remarque maintenant n'a pas été clairement énoncée dans mon message d'origine, est-ce que les gens veulent que
externref
soit instanciable comme le seront les autres importations de type abstrait?
Merci d'avoir explicitement soulevé cette question. FWIW, je n'ai jamais compris que externref
était instanciable à partir d'un module WebAssembly. Cela implique la participation de l'hôte à la virtualisation si WASI veut utiliser externref
comme descripteurs, mais cela me semble correct, ou du moins semble une discussion séparable.
Hmm, laissez-moi voir si je peux clarifier. Je soupçonne que vous êtes déjà à bord avec un tas de ce qui suit, mais il est plus facile pour moi de repartir de zéro.
Du point de vue d'un module wasm, externref
ne signifie pas référence d'hôte. C'est juste un type opaque dont le module ne sait rien. Ce sont plutôt les conventions autour de externref
qui l'interprètent comme une référence d'hôte. Par exemple, les conventions d'un module utilisant externref
pour interagir avec le DOM seraient apparentes dans les fonctions impliquant externref
que le module importe, comme parentNode : [externref] -> [externref]
et childNode : [externref, i32] -> [externref]
. L'environnement du module, tel que l'hôte lui-même, est ce qui donne réellement l'interprétation de externref
tant que références d'hôte, et il fournit des implémentations des méthodes importées qui corroborent cette interprétation.
Cependant, l'environnement du module n'a pas besoin d'être l'hôte et externref
n'a pas besoin d'être des références d'hôte. L'environnement pourrait être un autre module qui fournit des fonctionnalités pour un type qui ressemble à des références d'hôte présentant les conventions attendues. Supposons que le module E soit l'environnement du module M, et que ce module M importe parentNode
et childNode
comme ci-dessus. Disons que E veut utiliser le module M mais veut restreindre l'accès de M au DOM, disons parce que E a une confiance limitée en M ou parce que E veut limiter tous les bogues que M pourrait avoir et sait que les besoins de M ne devraient pas dépasser ces restrictions. Ce que E pourrait faire, c'est instancier M avec "MonitoredRef" en tant que externref
Disons que, en particulier, E veut donner à M des nœuds DOM mais s'assurer que M ne monte pas plus haut dans l'arbre DOM. Ensuite, le MonitoredRef de E pourrait être spécifiquement ref (struct externref externref)
, où le deuxième externref
(du point de vue de E) est le nœud DOM sur lequel M opère, mais le premier externref
est un ancêtre de ce nœud que M n'est pas autorisé à franchir. E pourrait alors instancier les parentNode
M de telle sorte qu'il se trompe si ces deux références sont les mêmes. E lui-même importerait ses propres fonctions parentNode
et childNode
, faisant de E effectivement un moniteur d'exécution des interactions DOM.
Espérons que c'était suffisamment concret pour brosser le bon tableau, sans être trop concret pour se perdre dans les détails. Il y a évidemment un certain nombre de modèles comme celui-ci. Donc, je suppose qu'une autre façon de formuler la question est la suivante : voulons-nous que externref
ne représente qu'exactement les références d'hôte ?
La seule partie qui me semble discutable est "ce que E pourrait faire est d'instancier M avec "MonitoredRef" en tant que M externref
." Je n'ai pas l'impression qu'il existe des plans pour permettre aux choses abstraites d'apparaître comme externref
dans d'autres modules. Je crois comprendre que externref
n'est pas du tout un outil d'abstraction.
Je ne connais pas non plus de tels plans ; Je ne sais pas non plus si quelqu'un avait envisagé cette option. Autrement dit, externref
être un type "primitif", par exemple comme i32
, ou un type "instanciable", par exemple comme des types importés ?
Dans mon message d'origine, j'ai indiqué que l'une ou l'autre façon est gérable. Le compromis d'opter pour l'interprétation "primitive" est que externref
est sensiblement moins utile/composable que les types importés, puisque ces derniers prendront en charge les cas d'utilisation de externref
ainsi que les modèles ci-dessus. En tant que tel, le externref
« primitif » semble susceptible de devenir un vestige, n'existant que pour la compatibilité descendante. Mais cela semble peu susceptible d'être particulièrement problématique, juste une nuisance. Le plus gros problème que je peux voir est que, tout comme le bon comportement de call_indirect
sur les types numériques fonctionne parce qu'ils n'ont pas de supertypes, le bon comportement de call_indirect
peut finir par dépendre de externref
n'ayant pas non plus de supertypes.
Ah hah, oui, cela explique la différence de compréhension : je suis d'accord avec @tlively que externref
n'est pas du tout abstrait et qu'il n'y a pas de notion d'"instanciation de externref
avec un type", et je pense que nous pouvons nous sentir assez confiants à propos de cela à l'avenir. (Étant donné que externref
est un type primitif, par opposition à un paramètre de type explicitement déclaré, il n'est pas clair comment on pourrait même tenter de l'instancier module par module.)
En l'absence de downcasts, ce fait rend wasm quasi inutile pour la mise en œuvre / virtualiser API WASI qui est la raison pour laquelle le plan de WASI a été de transition de i32
poignées directement type Imports (et pourquoi je Filed de type-importations /#6 , car il nous en faut même un peu plus).
Étant donné que
externref
est un type primitif, par opposition à un paramètre de type explicitement déclaré, il n'est pas clair comment on pourrait même tenter de l'instancier module par module.
Lorsque nous ajoutons des importations de type, nous pouvons traiter les modules sans importations de type mais avec externref
comme ayant import type externref
en haut. Tout vérifierait de la même manière car, contrairement aux autres types primitifs, externref
n'a pas d'opérations primitives associées (au-delà d'une valeur par défaut). Mais avec cette importation implicite, nous pouvons désormais faire des choses comme la virtualisation, le sandboxing et la surveillance de l'exécution.
Mais avant de faire des allers-retours là-dessus, je pense qu'il serait utile de déterminer si nous sommes tous sur la même longueur d'onde à propos de quelque chose. Faites-moi savoir si vous êtes d'accord ou en désaccord avec l'énoncé suivant et pourquoi : « Une fois que les importations de type sont disponibles, les modules n'ont aucune raison d'utiliser externref
et sont plus réutilisables/composables s'ils utilisent une importation de type à la place. »
Faites-moi savoir si vous êtes d'accord ou non avec l'énoncé suivant et pourquoi : "Une fois que les importations de type sont disponibles, les modules n'ont aucune raison d'utiliser
externref
et sont plus réutilisables/composables s'ils utilisent plutôt une importation de type."
Je suis d'accord avec cette affirmation dans l'abstrait. En pratique, je pense que externref restera courant dans les contextes Web pour faire référence à des objets JS externes car il ne nécessite aucune configuration supplémentaire au moment de l'instanciation. Mais ce n'est qu'une prédiction et cela ne me dérangerait pas si je me trompais et que tout le monde passe à l'utilisation des importations de type après tout. La valeur de externref est que nous pouvons l'avoir plus tôt que nous pouvons avoir des mécanismes plus riches comme les importations de type. Je préférerais garder externref simple et le voir tomber hors d'usage plutôt que de le faire maladroitement en faire quelque chose de plus puissant plus tard, lorsqu'il y aura des alternatives plus élégantes.
@tlively ,
FWIW, je n'ai jamais compris que externref était instanciable depuis l'intérieur d'un module WebAssembly.
Exact, l'idée est que externref est le type "primitif" de pointeurs étrangers. Pour faire abstraction des détails d'implémentation d'un type référence, vous aurez besoin d'autre chose : quelque chose comme anyref ou un type import.
@lukewagner , je serais d'
@RossTate :
Ces experts que j'ai consultés comprennent les auteurs de certains de ces articles.
Excellent. Ensuite, je suppose que vous avez remarqué que votre serviteur est lui-même l'auteur de quelques-uns de ces articles, au cas où vous souhaiteriez plus d'autorité. :)
Voici ce que j'ai demandé :
Supposons que j'ai une fonction polymorphe f(...). Mon langage typé a un sous-typage (subsomptif) et un casting explicite. Cependant, un transtypage de t1 vers t2 vérifie uniquement si t2 est un sous-type de t1. Supposons que les variables de type comme X par défaut n'aient pas de sous-types ou de supertypes (à part eux-mêmes bien sûr). Vous attendriez-vous à ce que f soit relationnellement paramétrique par rapport à X ?
Soupir. Je donnerais la même réponse à cette question précise. Mais cette question comprend plusieurs hypothèses spécifiques, par exemple sur la nature des moulages, et sur une distinction assez inhabituelle entre quantification bornée et non bornée qui existe rarement dans un langage de programmation. Et je suppose que c'est pour une raison.
Quand j'ai dit "l'abstraction de type statique est insuffisante", je ne voulais pas dire que ce n'était pas _techniquement_ possible (bien sûr que c'était le cas), mais que ce n'était pas _pratiquement_ adéquat. En pratique, vous ne voulez pas de bifurcation entre l'abstraction de type et le sous-typage/castabilité (ou entre les types paramétriques et non paramétriques), car cela casserait artificiellement la composition basée sur les casts.
Je ne comprends pas les affirmations que vous faites.
Si vous recevez une valeur de type abstrait, vous voudrez peut-être toujours oublier son type exact, par exemple pour la mettre dans une sorte d'union, et la récupérer plus tard par un downcast. Vous voudrez peut-être le faire pour la même raison que pour tout autre type de référence. L'abstraction de type ne doit pas gêner certains modèles d'utilisation qui sont valides avec des types réguliers de la même sorte.
Votre réponse semble être : et alors, enveloppez tout dans des types auxiliaires sur les sites d'utilisation respectifs, par exemple dans des variantes. Mais cela pourrait impliquer une surcharge importante d'emballage/déballage, cela nécessite des fonctionnalités de système de type plus complexes et il est plus compliqué à utiliser.
Je pense que c'est à cela que se résument plusieurs de nos désaccords : si le MVP doit prendre en charge les unions de types de référence, ou s'il doit exiger l'introduction et l'encodage avec des types variants explicites. Pour le meilleur ou pour le pire, les unions sont un complément naturel à l'interface heap des moteurs typiques, et elles sont faciles et peu coûteuses à prendre en charge aujourd'hui. Des variantes pas tellement, il s'agit d'une approche beaucoup plus axée sur la recherche qui induirait probablement des frais généraux supplémentaires et des performances moins prévisibles, du moins dans les moteurs existants. Et je dis qu'en tant que personne des systèmes de types, je préfère de loin les variantes aux unions dans d'autres circonstances, comme les langages destinés aux utilisateurs. ;)
En tant que méta-commentaire, j'apprécierais vraiment de ne pas avoir besoin de faire appel à l'autorité ou au CG pour faire entendre mes suggestions.
Puis-je gentiment suggérer que les conversations sur diverses propositions pourraient mieux fonctionner si elles étaient lancées par _demandant_ aux champions respectifs des choses qui ne sont pas claires, par exemple des justifications spécifiques ou des plans futurs (qui ne sont pas toujours évidents ou écrits encore), avant de supposer l'absence de une réponse et faire des affirmations et des suggestions générales sur la base de ces hypothèses ?
Excellent. Ensuite, je suppose que vous avez remarqué que votre serviteur est lui-même l'auteur de quelques-uns de ces articles, au cas où vous souhaiteriez plus d'autorité. :)
Oui, ce qui rend extrêmement problématique le fait que vous suggérez qu'il existe des papiers affirmant que ma suggestion ne fonctionne pas, même si vous savez que ma suggestion concerne spécifiquement les conditions dans lesquelles ces affirmations ont été faites.
En pratique, vous ne voulez pas de bifurcation entre l'abstraction de type et le sous-typage/castabilité (ou entre les types paramétriques et non paramétriques), car cela casserait artificiellement la composition basée sur les casts.
Ceci est une opinion, pas un fait (ce qui en fait quelque chose de parfaitement raisonnable sur lequel nous ne sommes pas d'accord). Je dirais qu'il n'y a pas de langages d'assemblage typés par l'industrie et indépendants des langues pour les systèmes multilingues, et il est donc impossible de faire des déclarations sur la pratique. C'est quelque chose qui mérite une discussion approfondie (séparée). Pour cette discussion, il serait utile que vous fournissiez d'abord des études de cas détaillées afin que le GC puisse comparer les compromis.
Puis-je gentiment suggérer que les conversations sur diverses propositions pourraient mieux fonctionner si elles commençaient en interrogeant les champions respectifs sur des choses qui ne sont pas claires, par exemple des justifications spécifiques ou des plans futurs (qui ne sont pas toujours évidents ou écrits encore), avant de supposer l'absence de une réponse et faire des affirmations et des suggestions générales sur la base de ces hypothèses ?
WebAssembly/proposal-type-imports#4, WebAssembly/proposal-type-imports#6 et WebAssembly/proposal-type-imports#7 ont chacun essentiellement demandé plus de détails sur ce plan. Le dernier de ces points soulève le problème pour le GC, mais WebAssembly/gc#86 souligne que la proposition actuelle du GC ne prend pas en charge les mécanismes d'abstraction dynamique.
Au niveau méta, on nous a demandé de mettre cette discussion de côté et de nous concentrer sur le sujet à l'étude. J'ai trouvé la réponse de @tlively à ma question très utile. Je suis en fait très intéressé à obtenir précisément vos réflexions sur cette question.
@RossTate :
J'ai trouvé la réponse de @tlively à ma question très utile. Je suis en fait très intéressé à obtenir précisément vos réflexions sur cette question.
Hm, je pensais déjà l'avoir commenté plus haut . Ou voulez-vous dire autre chose?
Nan. Je pensais que ce commentaire impliquait peut-être un accord avec sa réponse, mais je voulais d'abord confirmer. Merci!
@lukewagner , qu'en
Je suis d'accord avec le fait que externref
sera pour toujours un type primitif et non rétroactivement réinterprété en tant que paramètre de type. Je pense que, étant donné cela, les types de référence sont bons à utiliser tels quels.
J'aimerais accepter l'offre de
Impressionnant. Ensuite, nous sommes tous sur la même longueur d'onde (et moi aussi, je pense que
Ainsi, externref
ne sera pas instanciable, et les modules recherchant cette flexibilité supplémentaire des importations de types devront se convertir en importations de types lorsque la fonctionnalité sera publiée. Il me vient à l'esprit que, pour rendre cette transition fluide, nous devrons probablement faire en sorte que (certains?) Les importations de type soient instanciées par externref
par défaut si aucune instanciation n'est fournie.
Et j'aimerais également saisir l'offre d'élargir la portée des importations de types. La plupart des principales applications des importations de types nécessitent une abstraction, il me semble donc naturel que l'abstraction fasse partie de cette proposition.
En attendant, bien que nous ayons répondu à la question pressante à propos de externref
, ce qu'il faut faire plus généralement à propos de call_indirect
n'est toujours pas résolu, bien qu'avec quelques discussions utiles sur la façon dont cela pourrait être résolu, alors je Je laisserai toujours le problème ouvert.
Merci!
Commentaire le plus utile
L'abstraction de type statique est insuffisante dans un langage avec des transtypages dynamiques. Parce que l'abstraction statique repose sur la paramétrisation, et les casts cassent cela. Il n'y a rien de nouveau à ce sujet, des articles ont été écrits à ce sujet. D'autres mécanismes d'abstraction sont nécessaires dans un tel contexte.
Essayer de contourner cela en limitant l'utilisation de types abstraits va à l'encontre de leur objectif. Considérez le cas d'utilisation WASI. Peu importe qu'un module WASI et tout type qu'il exporte soit implémenté par l'hôte ou dans Wasm. Si vous restreignez arbitrairement les types abstraits définis par l'utilisateur, une implémentation Wasm ne serait plus interchangeable avec une implémentation hôte en général.
Hein? Cela fait partie des règles de sous-typage, comme par définition.
Je n'ai pas dit que oui. J'ai dit que celui-ci n'est pas un problème avec call_indirect lui-même, mais une question de choisir un mécanisme d'abstraction de type approprié pour un langage avec des transtypages.
Soit dit en passant, il n'y a aucune raison impérieuse pour laquelle la compilation d'OCaml (ou de tout autre langage similaire) devrait nécessiter l'introduction de types variants. Même si cela pourrait être légèrement plus rapide en théorie (ce qui, je doute que ce soit le cas dans les moteurs de génération actuelle, plus probablement le contraire), les types de variantes sont une complication importante qui ne devrait pas être nécessaire pour le MVP. Je ne partage pas tout à fait votre appétit pour la complexité prématurée. ;)
Re l'égalité sur les fonctions : il existe des langages, tels que Haskell ou SML, qui ne prennent pas en charge cela, et pourraient donc bénéficier directement des références de fonction. OCaml lance une égalité structurelle et a explicitement un comportement défini par l'implémentation pour une physique. Il reste ouvert si cela permet de toujours retourner false ou de lancer des fonctions, mais l'un ou l'autre pourrait bien être suffisant dans la pratique et mériter d'être exploré avant de s'engager dans un emballage supplémentaire coûteux.
[En tant que méta-commentaire, j'apprécierais vraiment que vous ayez atténué votre discours et que vous considériez peut-être l'idée qu'il s'agit d'un monde où, peut-être, l'ensemble des personnes compétentes n'est pas unique et que des traces de cerveaux ont parfois été appliquées auparavant.]