Design: call_indirect versus abstraction

Créé le 7 mai 2020  ·  36Commentaires  ·  Source: WebAssembly/design

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 :

  1. Avec le sous-typage, un appel direct fonctionnerait tant que la signature définie de la fonction réelle est une "sous-signature" de la signature attendue, ce qui signifie que tous les types d'entrée sont des sous-types des types de paramètres de la fonction et tous les types de sortie sont des supertypes de la fonction. types de résultats. Cela signifie qu'une vérification d'égalité entre la signature attendue d'un appel indirect et la signature définie de la fonction serait piégée dans un certain nombre de situations parfaitement sûres, ce qui pourrait être problématique pour la prise en charge des langages avec une utilisation intensive du sous-typage et des appels indirects (comme cela a été soulevé lors de la discussion sur différer le sous-typage). Cela signifie également que, si un module exporte intentionnellement une fonction avec une signature plus faible que celle avec laquelle la fonction a été définie, alors 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é).
  2. Avec les importations de type, un module peut exporter un type sans exporter la définition de ce type, fournissant une abstraction sur laquelle des systèmes comme WASI prévoient de s'appuyer fortement. Cette abstraction empêche d'autres modules de dépendre de sa définition particulière au moment de la compilation . Mais au moment de l'exécution, le type exporté abstrait est simplement remplacé par sa définition. Ceci est important, par exemple, pour permettre à 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étail

J'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-typage

Un 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
  • l'instance de module IA exporte une fonction $fsub qui a été définie avec le type [] -> [tsub]
  • le module MB importe une fonction $fsuper de type [] -> [tsuper]
  • l'instance de module IB est le module MB instancié avec les $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 :

  1. L'appel réussit car la signature attendue et la signature définie sont compatibles.
  2. L'appel est intercepté car les deux signatures sont distinctes.

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 :

  1. Pour les fonctions importées, comparez call_indirect avec la signature d'importation plutôt qu'avec la signature de définition.
  2. 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.

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 Importations

Mettons maintenant de côté le sous-typage et considérons les importations de

  • l'instance de module IC définit un type capability et exporte le type mais pas sa définition comme $handle
  • l'instance de module IC exporte une fonction $do_stuff qui a été définie avec le type [capability] -> [] mais exportée avec le type [$handle] -> []
  • le module MD importe un type $extern et une fonction $run de type [$extern] -> []
  • L'ID d'instance du module est le MD du module instancié avec les $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 :

  1. L'appel réussit car la signature attendue et la signature définie sont compatibles.
  2. L'appel est intercepté car les deux signatures sont distinctes.

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 :

  1. Pour les fonctions importées, comparez call_indirect avec la signature d'importation plutôt qu'avec la signature de définition.
  2. Sachez qu'au moment de l'exécution, le type $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 .

Conclusion

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.)

Commentaire le plus utile

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.

  1. 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.

  1. 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.]

Tous les 36 commentaires

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

  1. 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 :

  1. Java a des fonctions polymorphes. Ses fonctions polymorphes s'attendent à ce que toutes les valeurs (référence Java) soient des objets. En particulier, ils doivent avoir une v-table. Un module Java utilisant Handle spécifiera probablement une classe Java 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 .
  2. OCaml a des fonctions polymorphes. Ses fonctions polymorphes s'attendent à ce que toutes les valeurs OCaml prennent en charge l'égalité physique. Étant donné que wasm ne peut pas raisonner sur la sécurité des types d'OCaml, ses fonctions polymorphes devront également probablement faire un usage intensif des transtypages. Une structure de coulée spécialisée rendrait probablement cela plus efficace. Pour l'une ou l'autre de ces raisons, un module OCaml spécifierait probablement un type de données algébriques ou un type d'enregistrement 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 :

  1. 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)
  2. 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.

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.

  1. 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.

  1. 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!

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

frehberg picture frehberg  ·  6Commentaires

thysultan picture thysultan  ·  4Commentaires

jfbastien picture jfbastien  ·  6Commentaires

ghost picture ghost  ·  7Commentaires

void4 picture void4  ·  5Commentaires