Design: call_indirect versus abstração

Criado em 7 mai. 2020  ·  36Comentários  ·  Fonte: WebAssembly/design

call_indirect tem sido um recurso muito útil para WebAssembly. No entanto, a eficiência e o bom comportamento da instrução dependem implicitamente da simplicidade do sistema de tipos do wasm. Em particular, cada valor wasm tem exatamente um tipo (estático) ao qual pertence. Esta propriedade evita convenientemente uma série de problemas conhecidos com chamadas de função não digitadas em linguagens digitadas. Mas agora que o wasm está se estendendo além dos tipos numéricos, ele chegou ao ponto em que precisamos entender esses problemas e mantê-los em mente.

call_indirect basicamente funciona comparando a assinatura esperada do chamador com a assinatura definida do receptor. Com apenas tipos numéricos, WebAssembly tinha a propriedade de que essas assinaturas eram iguais se e somente se uma chamada direta para a função referenciada pelo funcref teria o tipo verificado. Mas há duas razões que em breve não serão verdade:

  1. Com a subtipagem, uma chamada direta funcionaria, desde que a assinatura definida da função real seja uma "sub-assinatura" da assinatura esperada, o que significa que todos os tipos de entrada são subtipos dos tipos de parâmetro da função e todos os tipos de saída são supertipos da função tipos de resultado. Isso significa que uma verificação de igualdade entre a assinatura esperada de uma chamada indireta e a assinatura definida da função capturaria uma série de situações perfeitamente seguras, o que pode ser problemático para idiomas de suporte com uso pesado de subtipagem e chamadas indiretas (como foi levantado durante a discussão em diferindo subtipagem). Isso também significa que, se um módulo exportar intencionalmente uma função com uma assinatura mais fraca do que a função foi definida, então call_indirect pode ser usado para acessar a função com sua assinatura privada definida em vez de apenas sua assinatura pública mais fraca ( um problema que acabou de ser descoberto e, portanto, ainda não foi discutido).
  2. Com importações de tipo, um módulo pode exportar um tipo sem exportar a definição desse tipo, fornecendo abstração que sistemas como WASI planejam confiar fortemente. Essa abstração evita que outros módulos dependam de sua definição particular no momento da compilação . Mas, em tempo de execução, o tipo abstrato exportado é simplesmente substituído por sua definição. Isso é importante, por exemplo, para permitir que call_indirect funcione corretamente em funções exportadas cujas assinaturas exportadas fazem referência a esse tipo exportado. No entanto, se um módulo malicioso sabe qual é a definição desse tipo exportado, ele pode usar call_indirect para converter de um lado para outro entre o tipo exportado e sua definição pretendida para ser secreta porque call_indirect apenas compara assinaturas em tempo de execução, quando os dois tipos são realmente os mesmos . Assim, um módulo malicioso pode usar call_indirect para acessar segredos destinados a serem abstraídos pelo tipo exportado e pode usar call_indirect para forjar valores do tipo exportado que podem violar invariantes críticos de segurança não capturados em a definição do próprio tipo.

Em ambas as situações acima, call_indirect pode ser usado para contornar a abstração da assinatura exportada de um módulo. Como mencionei, até agora isso não foi uma preocupação porque o wasm só tinha tipos numéricos. E, originalmente, pensei que, ao adiar a subtipagem, todas as preocupações relacionadas a call_indirect também haviam sido efetivamente adiadas. Mas o que percebi recentemente é que, ao remover a subtipagem, o "novo" tipo (denominado externref em https://github.com/WebAssembly/reference-types/pull/87) é efetivamente um substituto para uma importação de tipo abstrato. Se isso é o que as pessoas gostariam que fosse, infelizmente precisamos levar em consideração a interação acima entre call_indirect e importações de tipo.

Agora, há muitas maneiras possíveis de resolver os problemas acima com call_indirect , mas cada uma tem suas desvantagens e é simplesmente um espaço de design muito grande para ser capaz de tomar uma decisão rapidamente. Então, eu não estou sugerindo que vamos resolver este problema aqui e agora. Em vez disso, a decisão a ser tomada no momento é se ganhamos tempo para resolver o problema adequadamente com relação a externref . Em particular, se por enquanto restringirmos call_indirect e func.ref para apenas verificação de tipo quando a assinatura associada for inteiramente numérica, então serviremos a todos os casos de uso de chamadas indiretas e no ao mesmo tempo, deixe espaço para todas as soluções potenciais para os problemas acima. No entanto, não sei se essa restrição é prática, tanto em termos de esforço de implementação quanto em termos de se ela obstrui as aplicações de externref que as pessoas estão esperando. A alternativa é deixar call_indirect e func.ref como estão. É possível que isso signifique que, dependendo da solução que chegarmos, externref pode não ser instanciado como uma importação de tipo verdadeiro seria, e / ou que externref pode (ironicamente) não ser capaz de ter quaisquer supertipos (por exemplo, pode não ser capaz de ser um subtipo de anyref se decidirmos eventualmente adicionar anyref ).

Eu, falando apenas por mim, considero as duas opções administráveis. Embora eu tenha uma preferência, não estou pressionando fortemente a decisão de seguir um caminho ou outro, e acredito que todos vocês têm melhor acesso às informações necessárias para chegar a uma decisão bem informada. Eu só queria que vocês soubessem que há uma decisão a ser tomada e, ao mesmo tempo, queria estabelecer a consciência do problema geral com call_indirect . Se desejar uma explicação mais completa desse problema do que a fornecida pelo resumo acima, leia o seguinte.

call_indirect versus abstração, em detalhes

Usarei a notação call_indirect[ti*->to*](func, args) , onde [ti*] -> [to*] é a assinatura esperada da função, func é simplesmente um funcref (ao invés de uma tabela funcref e um índice), e args são os to* valores a serem passados ​​para a função. Da mesma forma, usarei call($foo, args) para uma chamada direta da função com o índice $foo passando argumentos args .

Agora, suponha que $foo seja o índice de uma função com tipos de entrada declarados ti* e tipos de saída to* . Você pode esperar que call_indirect[ti*->to*](ref.func($foo), args) seja equivalente a call($foo, args) . Na verdade, esse é o caso agora. Mas não está claro se podemos manter esse comportamento.

call_indirect e subtipagem

Um exemplo de problema potencial surgiu na discussão de subtipagem. Suponha o seguinte:

  • tsub é um subtipo de tsuper
  • a instância do módulo IA exporta uma função $fsub que foi definida com o tipo [] -> [tsub]
  • o módulo MB importa uma função $fsuper com o tipo [] -> [tsuper]
  • a instância do módulo IB é o módulo MB instanciado com $fsub IA como $fsuper (o que é bom - mesmo que não seja possível agora, este problema é sobre problemas futuros em potencial)

Agora considere o que deve acontecer se o IB executar call_indirect[ -> tsuper](ref.func($fsuper)) . Aqui estão os dois resultados que parecem mais plausíveis:

  1. A chamada é bem-sucedida porque a assinatura esperada e a assinatura definida são compatíveis.
  2. A chamada é bloqueada porque as duas assinaturas são distintas.

Se tivéssemos que escolher o resultado 1, percebemos que provavelmente precisaríamos empregar uma das duas técnicas para tornar isso possível:

  1. Para funções importadas, faça com que call_indirect compare com a assinatura de importação em vez da assinatura de definição.
  2. Faça uma verificação pelo menos em tempo de execução linear para compatibilidade de subtipo da assinatura esperada e a assinatura de definição.

Se você preferir a técnica 1, perceba que ela não funcionará depois que adicionarmos as referências de função digitada (com subtipagem variante). Ou seja, func.ref($fsub) será um ref ([] -> [tsub]) e também um ref ([] -> [tsuper]) , e ainda assim a técnica 1 não será suficiente para impedir call_indirect[ -> super](ref.func($fsub)) de aprisionamento. Isso significa que o resultado 1 provavelmente requer a técnica 2, que tem implicações de desempenho preocupantes.

Portanto, vamos considerar o resultado 2 um pouco mais. A técnica de implementação aqui é verificar se a assinatura esperada de call_indirect em IB é igual à assinatura da definição de $fsub em IA. A princípio, a principal desvantagem dessa técnica pode parecer ser que ela intercepta uma série de chamadas que são seguras de executar. No entanto, outra desvantagem é que potencialmente introduz um vazamento de segurança para IA.

Para ver como, vamos mudar nosso exemplo um pouco e supor que, embora a instância IA defina internamente $fsub como o tipo [] -> [tsub] , a instância IA só o exporta com o tipo [] -> [tsuper] . Usando a técnica do resultado 2, a instância IB pode (maliciosamente) executar call_indirect[ -> tsub]($fsuper) e a chamada será bem-sucedida. Ou seja, o IB pode usar call_indirect para contornar o estreitamento que IA fez à assinatura de sua função. Na melhor das hipóteses, isso significa que IB depende de um aspecto de IA que não é garantido pela assinatura de IA. Na pior das hipóteses, o IB pode usar isso para acessar o estado interno que o IA pode ter ocultado intencionalmente.

call_indirect e importações de tipo

Agora vamos deixar os subtipos de lado e considerar as importações de tipos . Por conveniência, vou falar sobre importações de tipo, em vez de apenas importações de tipo de referência, mas esse detalhe é irrelevante. Para o exemplo de execução aqui, suponha o seguinte:

  • módulo instância IC define um tipo capability e exporta o tipo, mas não sua definição como $handle
  • a instância do módulo IC exporta uma função $do_stuff que foi definida com o tipo [capability] -> [] mas exportada com o tipo [$handle] -> []
  • o módulo MD importa um tipo $extern e uma função $run com o tipo [$extern] -> []
  • o ID da instância do módulo é o módulo MD instanciado com $handle exportados de IAs como $extern e com IAs exportados $do_stuff como $run

O que este exemplo configura são dois módulos onde um módulo faz coisas com os valores do outro módulo sem saber ou ter permissão para saber quais são esses valores. Por exemplo, este padrão é a base planejada para interagir com o WASI.

Agora vamos supor que o ID da instância conseguiu obter um valor e do tipo $extern e executa call_indirect[$extern -> ](ref.func($run), e) . Aqui estão os dois resultados que parecem mais plausíveis:

  1. A chamada é bem-sucedida porque a assinatura esperada e a assinatura definida são compatíveis.
  2. A chamada é bloqueada porque as duas assinaturas são distintas.

O resultado 2 torna call_indirect praticamente inútil com tipos importados. Portanto, para o resultado 1, perceba que o tipo de entrada $extern não é o tipo de entrada definido de $do_stuff (que em vez disso é capability ), então provavelmente precisaríamos usar um dos duas técnicas para preencher essa lacuna:

  1. Para funções importadas, faça com que call_indirect compare com a assinatura de importação em vez da assinatura de definição.
  2. Reconheça que, em tempo de execução, o tipo $extern no ID da instância representa capability .

Se você preferir a técnica 1, perceba que mais uma vez ela não funcionará depois que adicionarmos as referências de função digitada. (O motivo fundamental é o mesmo da subtipagem, mas seria necessário ainda mais texto para ilustrar o análogo aqui.)

Isso nos deixa com a técnica 2. Infelizmente, mais uma vez, isso representa um possível problema de segurança. Para ver o motivo, suponha que o ID seja malicioso e deseje obter o conteúdo de $handle que o IC manteve em segredo. Suponha ainda que o ID tenha um bom palpite sobre o que $handle realmente representa, ou seja, capability . O ID pode definir a função de identidade $id_capability do tipo [capability] -> [capability] . Dado um valor e do tipo $extern , ID pode então executar call_indirect[$extern -> capability](ref.func($id_capability), e) . Usando a técnica 2, esta chamada indireta terá sucesso porque $extern representa capability em tempo de execução, e o ID obterá o capability bruto que e representa de volta. Da mesma forma, dado um valor c do tipo capability , ID pode executar call_indirect[capability -> $extern](ref.func($id_capability), c) para forjar c em um $extern .

Conclusão

Espero ter deixado claro que call_indirect tem uma série de problemas significativos de desempenho, semântica e / ou segurança / abstração futuros - problemas que o WebAssembly teve a sorte de ter evitado até agora. Infelizmente, como call_indirect parte do WebAssembly principal, esses problemas afetam várias propostas em andamento. No momento, acho que seria melhor nos concentrarmos na proposta mais urgente, Tipos de referência, onde precisamos decidir se devemos ou não restringir call_indirect e func.ref apenas a tipos numéricos para agora - uma restrição que podemos relaxar dependendo de como acabaremos resolvendo os problemas gerais com call_indirect .

(Desculpe pela longa postagem. Eu tentei o meu melhor para explicar as interações complexas dos recursos de digitação em tempo de compilação em módulo cruzado e em tempo de execução e demonstrar a importância dessas interações da forma mais concisa possível.)

Comentários muito úteis

Por outro lado, você demonstrou que anyref castable pode ser usado para contornar os mecanismos de abstração estática.

A abstração de tipo estático é insuficiente em uma linguagem com conversão de tipo dinâmica. Porque a abstração estática depende da parametricidade, e as projeções quebram isso. Não há nada de novo nisso, artigos já foram escritos sobre isso. Outros mecanismos de abstração são necessários em tal contexto.

Tentar contornar isso restringindo o uso de tipos abstratos anula seu propósito. Considere o caso de uso WASI. Não deve importar se um módulo WASI e qualquer tipo que ele exporta é implementado pelo host ou no Wasm. Se você restringir arbitrariamente tipos abstratos definidos pelo usuário, uma implementação Wasm não será mais intercambiável com uma implementação de host em geral.

  1. Isso não ajuda a fazer call_indirect respeitar os subtipos (o que eu acho que você já disse explicitamente)

Huh? Faz parte das regras de subtipagem, o mesmo acontece por definição.

  1. Isso não impede que call_indirect seja usado para usar uma função exportada com sua assinatura definida em vez de sua assinatura exportada.

Eu não disse que sim. Eu disse que este não é um problema com call_indirect em si, mas uma questão de escolher um mecanismo de abstração de tipo adequado para uma linguagem com casts.

Como um aparte, não há nenhuma razão convincente para que a compilação do OCaml (ou qualquer linguagem semelhante) exija a introdução de tipos variantes. Mesmo que isso pudesse ser um pouco mais rápido em teoria (o que eu duvido que seja o caso nos motores da geração atual, mais provavelmente o contrário), os tipos variantes são uma complicação significativa que não deveria ser necessária para o MVP. Não concordo com seu apetite por complexidade prematura. ;)

Re igualdade nas funções: há linguagens, como Haskell ou SML, que não suportam isso, portanto, podem se beneficiar diretamente de func refs. OCaml lança para igualdade estrutural e explicitamente tem um comportamento definido pela implementação para um físico. É deixado em aberto se isso permite sempre retornar falso ou lançar para funções, mas qualquer um pode ser suficiente na prática e vale a pena explorar antes de comprometer-se com um invólucro extra caro.

[Como um meta-comentário, eu realmente apreciaria se você abrandasse sua palestra e talvez considerasse a ideia de que este é um mundo onde, talvez, o conjunto de pessoas competentes não seja único e que traços de cérebros tenham sido ocasionalmente aplicados antes.]

Todos 36 comentários

Obrigado por este artigo detalhado, Ross! Eu tenho uma pequena pergunta: na seção " call_indirect e digite importações" você escreve,

Se você preferir a técnica 1, perceba que mais uma vez ela não funcionará depois que adicionarmos as referências de função digitada.

Isso também está sujeito à advertência da seção anterior de que o problema só está presente depois de adicionarmos a subtipagem de variantes às referências de função digitadas?

Não é. Todos os problemas na seção de subtipagem são independentes de importações de tipo e todos os problemas na seção de importações de tipo são independentes de subtipagem. Com relação ao problema específico que você está perguntando, considere que um valor do tipo ref ([] -> [capability]) pode ser retornado por uma função exportada como um valor do tipo ref ([] -> [$handle]) , que então pode ser transformado em um funcref e indiretamente chamado para. Ao contrário da função exportada, essa mudança na perspectiva do valor ocorre no tempo de execução, e não no tempo de link, portanto, não podemos resolvê-lo comparando com a assinatura de importação, pois a referência da função nunca foi importada.

module instance IC defines a type capability and exports the type but not its definition as $handle
Como isso vai funcionar? Precisa haver algo que conecte capability e $handle para que o IC saiba como lidar com isso?
Também com base em https://github.com/WebAssembly/proposal-type-imports/blob/master/proposals/type-imports/Overview.md#exports , os tipos importados são completamente abstratos. Portanto, mesmo que $capability seja exportado, é abstrato. Talvez eu esteja entendendo mal alguma coisa.

Pergunta semelhante para a exportação de module instance IC exports a function $do_stuff that was defined with type [capability] -> [] but exported with type [$handle] -> [] .

Posso imaginar algum tipo de relação de subtipagem usada para isso, por exemplo, se $capability <: $handle , então podemos export $capability as $handle . Mas no início desta seção foi mencionado para deixar a subtipagem de lado, então estou deixando isso de lado ... Mas também pensei um pouco mais sobre isso:
Se: $capability <: $handle , podemos export $capability as $handle , mas export ([$capability] -> []) as ([$handle] -> []) deve "falhar" porque as funções são contravariantes no argumento.

Com exportações de tipo, um módulo especifica uma assinatura, como type $handle; func $do_stuff_export : [$handle] -> [] , e instancia a assinatura, como type $handle := capability; func $do_stuff_export := $do_stuff . (Ignore a sintaxe específica completamente.) O verificador de tipo então verifica "dado que $handle representa capability neste módulo, a exportação func $do_stuff_export := $do_stuff válida neste módulo?". Uma vez que o tipo de $do_stuff é [capability] -> [] , sua assinatura se alinha exatamente com a de $do_stuff_export após instanciar $handle com capability , então o verificação bem-sucedida. (Não há subtipos envolvidos aqui, apenas substituição de variável.)

Observe, porém, que a própria assinatura não diz nada sobre $handle . Isso significa que todos os outros devem tratar $handle como um tipo abstrato. Ou seja, a assinatura abstrai intencionalmente os detalhes da implementação do módulo e todos os outros devem respeitar essa abstração. O objetivo desta edição é ilustrar que call_indirect pode ser usado para contornar essa abstração.

Espero que isso esclareça um pouco a questão!

Obrigado, isso esclarece as coisas. Terei uma pergunta sobre a seção de subtipagem (desculpe pular):

Estou seguindo o cenário em que queremos que o IB executando call_indirect[ -> tsuper](ref.func($fsuper)) seja bem-sucedido, fazendo com que call_indirect "compare com a assinatura de importação ao invés da assinatura de definição."

E você adicionou que (devido às referências de função digitadas) também precisamos

  1. Faça uma verificação pelo menos em tempo de execução linear para compatibilidade de subtipo da assinatura esperada e a assinatura de definição.

Deve haver compatibilidade entre a "assinatura esperada e a assinatura de importação "? Uma vez que estamos assumindo que fizemos call_indirect comparar a assinatura de importação com a assinatura esperada.

Se a compatibilidade for verificada entre o esperado e a importação, mais tarde call_indirect[ -> tsub]($fsuper) deve falhar.

As técnicas 1 e 2 são apresentadas como duas maneiras ortogonais de fazer a chamada indireta funcionar. Infelizmente, a técnica 1 é incompatível com referências de função digitadas e a técnica 2 é provavelmente muito cara. Portanto, nenhuma dessas opções parece funcionar. Portanto, o restante da seção considera o que acontece se não usarmos nenhum desses dois e ficarmos apenas com a comparação de igualdade simples entre a assinatura esperada e a definida. Desculpe pela confusão; não ter uma semântica planejada significa que tenho que discutir três semânticas potenciais.

Tenha cuidado para não tirar conclusões precipitadas. ;)

Minha suposição é que call_indirect deve permanecer tão rápido quanto hoje e, portanto, apenas requer um teste de equivalência de tipo, não importa quantos subtipos adicionemos à linguagem. Ao mesmo tempo, a verificação do tempo de execução precisa ser coerente com o sistema de tipo estático, ou seja, deve respeitar a relação de subtipagem.

Agora, esses requisitos aparentemente contraditórios podem, na verdade, ser reconciliados com bastante facilidade, contanto que tenhamos certeza de que os tipos utilizáveis ​​com call_indirect estão sempre nas folhas da hierarquia de subtipos.

Uma maneira estabelecida de garantir isso é introduzir a noção de tipos _exatos_ no sistema de tipos. Um tipo exato não tem subtipos, apenas supertipos, e teríamos (exact T) <: T .

Com isso, podemos exigir que o tipo de destino em call_indirect seja um tipo exato. Além disso, o tipo de funções em si já é o tipo exato dessa função.

Um módulo também pode exigir tipos exatos nas importações de função, se quiser ter certeza de que só pode ser instanciado com funções que têm êxito em uma verificação de tempo de execução pretendida.

Isso é tudo o que é necessário para garantir que a técnica de implementação atual de uma comparação de ponteiro simples em tipos de função canonicalizados permaneça válida. É independente de quais outros subtipos existem, ou de quão sofisticados fazemos os subtipos de funções. (FWIW, eu discuti isso com Luke um tempo atrás, e planejei criar um PR, mas foi bloqueado nas alterações pendentes na história de subtipagem, e para qual proposta agora se move.)

(Uma desvantagem é que refinar uma definição de função para um subtipo não é mais uma mudança compatível com versões anteriores em geral, pelo menos não se seu tipo exato foi usado em algum lugar. Mas essa desvantagem é inevitável sob nossas restrições, independentemente de como exatamente aplicamos eles.)

Alguns apartes:

A alternativa é deixar call_indirect e func.ref como estão.

AFAICS, não é possível proibir ref.func em funções que envolvem tipos de referência. Isso prejudicaria gravemente muitos casos de uso, ou seja, tudo que envolva funções de primeira classe operando em externref (retornos de chamada, ganchos, etc.).

É possível que isso signifique que, dependendo da solução que chegarmos, externref pode não ser instanciado como um verdadeiro tipo import seria, e / ou externref pode (ironicamente) não ser capaz de ter quaisquer supertipos (por exemplo, pode não pode ser um subtipo de anyref se decidirmos eventualmente adicionar anyref).

Você pode elaborar? Não vejo a conexão.

Tenha cuidado para não tirar conclusões precipitadas. ;)

Não sei a que conclusão você está se referindo. Minha conclusão declarada é que há vários problemas com call_indirect quais precisamos estar cientes e para os quais devemos começar a planejar. Você parece estar sugerindo que essas questões são irrelevantes porque você tem uma solução em mente. Mas essa solução não foi revisada ou aceita pelo CG, e não devemos planejá-la até que seja. Eu pedi especificamente para não discutir soluções porque vai demorar um pouco para avaliá-las e compará-las e há decisões que precisamos tomar antes de termos tempo para fazer essas avaliações e comparações de maneira adequada. Mas, para evitar que as pessoas tenham a percepção de que esse problema está resolvido e, consequentemente, evitar a decisão urgente, vou demorar um segundo para discutir rapidamente a sua solução.

Uma maneira estabelecida de fazer cumprir isso é introduzir a noção de tipos exatos no sistema de tipos.

Tipos exatos dificilmente são uma solução estabelecida. No mínimo, os tipos exatos estabeleceram problemas que seus proponentes ainda estão trabalhando para resolver. Curiosamente, aqui está um tópico em que a equipe do TypeScript viu originalmente como os tipos exatos da forma que você está propondo podem resolver alguns problemas, mas então eles [perceberam] (https://github.com/microsoft/TypeScript/issues/12936 # issuecomment-284590083) que tipos exatos introduziram mais problemas do que resolveram. (Observação para o contexto: essa discussão foi provocada pelos tipos de objeto exatos de Flow, que não são realmente uma forma do tipo exato (no sentido teórico), mas simplesmente desautorizam o análogo ao objeto de subtipagem de prefixo.) Eu poderia nos imaginar repetindo essa discussão aqui.

Como um exemplo de como esses tipos de problemas podem funcionar para WebAssembly, suponha que não tenhamos adiado a subtipagem. O tipo de ref.null seria exact nullref usando tipos exatos. Mas exact nullref não seria um subtipo de exact anyref . Na verdade, de acordo com a semântica usual de tipos exatos, provavelmente nenhum valor pertenceria a exact anyref porque provavelmente nenhum tipo de tempo de execução do valor é exatamente anyref . Isso tornaria call_indirect completamente inutilizável para anyref s.

Agora, talvez você tenha alguma versão diferente de tipos exatos em mente, mas demoraria um pouco para verificar se essa versão diferente de alguma forma resolve os muitos problemas em aberto com tipos exatos. Portanto, meu objetivo aqui não é descartar essa solução, mas reconhecer que não é óbvio que essa é a solução e não tomar decisões com essa expectativa.

Você pode elaborar? Não vejo a conexão.

Você está se referindo a uma longa frase. Qual parte você gostaria que eu elaborasse? Uma suposição é que você pode não ter entendido o problema geral com call_indirect e digitar importações. Sua sugestão de tipos exatos aborda apenas problemas com subtipagem, mas estabelecemos acima que call_indirect tem problemas mesmo sem qualquer subtipagem.

Isso prejudicaria gravemente muitos casos de uso, ou seja, tudo que envolva funções de primeira classe operando em externref (retornos de chamada, ganchos, etc.).

Sim, então isso é algo sobre o qual eu esperava obter mais informações. Meu entendimento é que o principal caso de uso de call_indirect é oferecer suporte a ponteiros de função C / C ++ e métodos virtuais C ++. Meu entendimento também é que este caso de uso está atualmente restrito a assinaturas numéricas. Eu sei de mais usos potenciais de call_indirect , mas como mencionei, estava sugerindo uma restrição temporária , então o que importa é quais são os usos atuais de call_indirect . Dado que call_indirect ainda requer uma tabela e índice em vez de simplesmente um funcref , não parece particularmente bem projetado para suportar callbacks. Não sabia se era porque atualmente não está sendo usado para essa finalidade.

Vocês conhecem as bases de código direcionadas a esse recurso muito melhor do que eu, então se vocês souberem de alguns programas reais que precisam dessa funcionalidade agora, seria muito útil fornecer alguns exemplos dos padrões de uso necessários aqui. Além de ser útil para descobrir se precisamos oferecer suporte a essa funcionalidade agora, se a funcionalidade for necessária agora, esses exemplos seriam úteis para informar a melhor forma de fornecê-la rapidamente enquanto abordamos os problemas acima.

@RossTate :

No mínimo, os tipos exatos estabeleceram problemas que seus proponentes ainda estão trabalhando para resolver. Curiosamente, aqui está um tópico em que a equipe do TypeScript viu originalmente como os tipos exatos da forma que você está propondo podem resolver alguns problemas, mas então eles finalmente perceberam que os tipos exatos introduziam mais problemas do que resolviam. (Observação para o contexto: essa discussão foi provocada pelos tipos de objeto exatos de Flow, que não são realmente uma forma do tipo exato (no sentido teórico), mas simplesmente desautorizam o análogo ao objeto de subtipagem de prefixo.) Eu poderia nos imaginar repetindo essa discussão aqui.

Os parênteses são fundamentais aqui. Não tenho certeza do que exatamente eles têm em mente nesse tópico, mas não parece ser a mesma coisa. Caso contrário, declarações como "assume-se que um tipo T & U é sempre atribuível a T , mas isso falha se T é um tipo exato" não faria sentido (isso não t falhar, porque T & U seria inválido ou inferior). As outras questões são principalmente sobre pragmática, ou seja, onde um programador iria querer usá-las (para objetos), o que não se aplica ao nosso caso.

Para sistemas de tipo de baixo nível, os tipos exatos não eram um ingrediente crucial, mesmo em alguns de seus próprios artigos?

Como um exemplo de como esses tipos de problemas podem funcionar para WebAssembly, suponha que não tenhamos adiado a subtipagem. O tipo de ref.null seria nullref exato usando tipos exatos. Mas nullref exato não seria um subtipo de anyref exato.

Nenhuma discordância aqui. Não ter subtipos é o propósito dos tipos exatos.

Na verdade, de acordo com a semântica usual de tipos exatos, provavelmente nenhum valor pertenceria a anyref exata porque provavelmente nenhum tipo de tempo de execução do valor é exatamente anyref.

Certo, a combinação (exact anyref) não é um tipo útil, visto que o único propósito de anyref é ser um supertipo. Mas por que isso é um problema?

Isso tornaria call_indirect completamente inutilizável para anyrefs.

Tem certeza de que não está confundindo os níveis agora? Um tipo de função (exact (func ... -> anyref)) é perfeitamente útil. Simplesmente não é compatível com um tipo, digamos, (func ... -> (ref $T)) . Ou seja, exact evita subtipos não triviais em tipos de função. Mas esse é o ponto principal!

Talvez você esteja misturando (exact (func ... -> anyref)) com (func ... -> exact anyref) ? Esses são tipos não relacionados.

Sua sugestão de tipos exatos aborda apenas problemas com subtipagem, mas estabelecemos acima que call_indirect tem problemas mesmo sem qualquer subtipagem.

De alguma forma, você está assumindo que será capaz de exportar um tipo sem sua definição como um meio de definir um tipo de dado abstrato. Claramente, essa abordagem não funciona na presença de conversões de tipo dinâmico (call_indirect ou outro). É por isso que continuo dizendo que precisaremos de uma abstração de tipo no estilo newtype, não de uma abstração de tipo no estilo ML.

Meu entendimento é que o principal caso de uso de call_indirect é oferecer suporte a ponteiros de função C / C ++

Sim, mas esse não é o único caso de uso de ref.func , ao qual eu estava me referindo, porque você o incluiu em sua restrição sugerida (talvez desnecessariamente?). Em particular, haverá call_ref , que não envolve verificações de tipo.

De alguma forma, você está assumindo que será capaz de exportar um tipo sem sua definição como um meio de definir um tipo de dado abstrato. Claramente, essa abordagem não funciona na presença de conversões de tipo dinâmico (call_indirect ou outro). É por isso que continuo dizendo que precisaremos de uma abstração de tipo no estilo newtype, não de uma abstração de tipo no estilo ML.

Ok, então você parece concordar que tipos exatos não fazem nada para resolver o problema com call_indirect e importação de tipo. Mas você também está dizendo que não há por que abordar esse problema, porque será um problema de qualquer maneira devido a conversões em tempo de execução. Existe uma maneira fácil de evitar esse problema: não permitir que as pessoas executem conversões em tempo de execução em tipos abstratos (a menos que o tipo abstrato diga explicitamente que pode ser convertido). Afinal, é um tipo opaco, portanto não devemos supor que ele exiba a estrutura necessária para realizar uma moldagem. Portanto, mesmo que haja a possibilidade de que tipos exatos tratem do problema de subtipagem, é prematuro desconsiderar a outra metade do problema.

Como eu disse, toda solução tem vantagens e desvantagens. Você parece estar presumindo que sua solução tem apenas as compensações que você mesmo identificou e parece estar presumindo que o CG preferiria sua solução a outras. Eu também tenho uma solução potencial para esse problema. Ele garante verificações constantes, é baseado em uma tecnologia já usada em máquinas virtuais, resolve todos os problemas aqui (acredito), não exige a adição de nenhum tipo novo e, na verdade, adiciona funcionalidade adicional ao WebAssembly com aplicativos conhecidos. No entanto, não estou presumindo que funcione como espero e que não tenha esquecido algumas lacunas porque você e outras pessoas não tiveram a oportunidade de examiná-lo. Também não estou presumindo que o CG preferiria suas compensações às opções alternativas. Em vez disso, estou tentando descobrir o que podemos fazer para nos dar tempo de analisar as opções para que o CG, e não apenas eu, possa ser aquele que toma uma decisão informada sobre este tópico transversal.

Em particular, haverá call_ref , que não envolve verificações de tipo.

A palavra-chave da sua frase é vontade . Estou plenamente consciente de que existem aplicações de call_indirect com tipos não numéricos que as pessoas vão querer ter apoiado. E eu espero que vamos chegar a um design que suporta essa funcionalidade e aborda as questões acima. Mas, como eu disse, idealmente podemos ter algum tempo para desenvolver esse design, de modo que não estejamos enviando rapidamente um recurso com implicações transversais antes de termos a chance de investigar essas implicações. Minha pergunta é: existem programas importantes que precisam dessa funcionalidade agora . Se houver, não há necessidade de criar hipóteses; apenas aponte para alguns e ilustre como eles atualmente contam com essa funcionalidade.

De alguma forma, você está assumindo que será capaz de exportar um tipo sem sua definição como um meio de definir um tipo de dado abstrato. Claramente, essa abordagem não funciona na presença de conversões de tipo dinâmico (call_indirect ou outro). É por isso que continuo dizendo que precisaremos de uma abstração de tipo no estilo newtype, não de uma abstração de tipo no estilo ML.

Isso me parece uma questão fundamental. Habilitar a confidencialidade das definições dos tipos exportados é uma meta da proposta de importação de tipos? Percebi neste tópico que @RossTate acha que deveria ser um objetivo e @rossberg acha que não é atualmente um objetivo. Vamos discutir e concordar com essa questão antes de discutir as soluções para que todos possamos trabalhar com o mesmo conjunto de suposições.

@RossTate :

Ok, então você parece concordar que os tipos exatos não fazem nada para resolver o problema com call_indirect e type import.

Sim, se com isso você quer dizer a questão de como adicionar um recurso para definir tipos de dados abstratos. Existem várias maneiras de como a abstração de tipo pode funcionar de forma consistente, mas esse recurso está mais adiante.

A palavra-chave da sua frase é vontade. Estou totalmente ciente de que existem aplicativos de call_indirect com tipos não numéricos que as pessoas gostariam de ter suporte.

A instrução call_ref está na função ref proposta, portanto, bastante próxima, em qualquer caso, antes de qualquer mecanismo de tipo abstrato de dados potencial. Você está sugerindo que o deixemos em espera até então?

@tlively :

Habilitar a confidencialidade das definições dos tipos exportados é uma meta da proposta de importação de tipos? Percebi neste tópico que @RossTate acha que deveria ser um objetivo e @rossberg acha que não é atualmente um objetivo.

É um objetivo, mas um mecanismo de tipo de dados abstrato é um recurso separado. E tal mecanismo deve ser projetado de tal forma que não afete o projeto das importações. Se isso acontecesse, estaríamos fazendo muito errado - a abstração deve ser garantida no local de definição, não no local de uso. Felizmente, porém, isso não é ciência de foguetes, e o espaço de design é bastante bem exportado.

Obrigado, @rossberg , isso faz sentido. Adicionar primitivas de abstração em uma proposta de acompanhamento após importações e exportações de tipos parece bom para mim, mas seria ótimo se pudéssemos escrever os detalhes de como planejamos fazer isso em breve. O projeto de importações e exportações de tipo restringe e informa o projeto de importações e exportações de tipo abstrato, por isso é importante que tenhamos uma boa ideia de como a abstração funcionará no futuro antes de finalizarmos o projeto inicial.

Além de detalhar esse plano, uma vez que este problema com call_indirect está demonstrando que afeta decisões urgentes, você pode explicar por que parece estar rejeitando minha sugestão de que os tipos abstratos não devem ser castable (a menos que explicitamente restrito a castable )? Eles são opacos, de modo que a sugestão parece estar de acordo com as práticas comuns de tipos abstratos.

@tlively , sim, concordou. Além de várias outras coisas que pretendia escrever por um tempo. Servirá assim que tiver trabalhado em todas as consequências do # 69 . ;)

@RossTate , porque isso tornaria os tipos de dados abstratos incompatíveis com os casts. Só porque quero evitar que outros vejam _através_ de um tipo abstrato, não quero necessariamente impedir que eles (ou a mim mesmo) convertam _para_ um tipo abstrato. Criar essa falsa dicotomia quebraria os casos de uso centrais dos elencos. Por exemplo, é claro que quero passar um valor do tipo abstrato para uma função polimórfica.

@rossberg Você pode esclarecer qual é esse caso de uso central que você tem em mente? Meu melhor palpite ao interpretar seu exemplo é trivialmente solucionável, mas talvez você queira dizer outra coisa.

@RossTate , considere funções polimórficas. Com exceção dos genéricos do Wasm, ao compilá-los usando up / down casts de anyref, deve ser possível usá-los com valores do tipo abstrato como qualquer outro, sem empacotamento extra em outros objetos. Em geral, você deseja tratar valores de tipo abstrato como qualquer outro.

Ok, vamos considerar funções polimórficas e vamos supor que o tipo importado seja Handle :

  1. Java possui funções polimórficas. Suas funções polimórficas esperam que todos os valores (referência Java) sejam objetos. Em particular, eles devem ter uma tabela v. Um módulo Java usando Handle provavelmente especificará uma classe Java CHandle possivelmente implementando interfaces. As instâncias desta classe terão um membro (nível wasm) do tipo Handle e uma v-table que fornece ponteiros de função para as implementações de vários métodos de classe e interface. Quando fornecido a uma função polimórfica de nível de superfície, que no nível de wasm é apenas uma função em objetos, o módulo pode usar o mesmo mecanismo que usa para lançar em outras classes para CHandle .
  2. OCaml tem funções polimórficas. Suas funções polimórficas esperam que todos os valores OCaml suportem igualdade física. Como o wasm não pode raciocinar sobre a segurança de tipo do OCaml, suas funções polimórficas provavelmente também precisarão fazer uso intenso de moldes. Uma estrutura de fundição especializada provavelmente tornaria isso mais eficiente. Por qualquer um desses motivos, um módulo OCaml provavelmente especificaria um tipo de dados algébrico ou tipo de registro THandle que se encaixa nessas normas e tem um membro (nível wasm) do tipo Handle . Suas funções polimórficas então lançariam os valores OCaml para THandle da mesma forma que fariam para qualquer outro tipo de dados algébrico ou tipo de registro.

Em outras palavras, como os módulos dependem de normas sobre como os valores de nível de superfície são representados para implementar coisas como funções polimórficas, e tipos abstratos importados como Handle não satisfazem essas normas, o agrupamento de valores é inevitável. Este é o mesmo motivo pelo qual um dos aplicativos originais de anyref foi substituído por Tipos de interface. E desenvolvemos estudos de caso demonstrando que anyref não é necessário, nem mesmo adequado, para suportar funções polimórficas.

Por outro lado, você demonstrou que o castable anyref pode ser usado para contornar os mecanismos de abstração estática. O plano do mecanismo de abstração ao qual você aludiu é uma tentativa de corrigir esse problema por meio de mecanismos de abstração dinâmicos. Mas existem vários problemas com os mecanismos de abstração dinâmica. Por exemplo, não se pode exportar seu tipo i31ref como um tipo Handle abstrato sem o risco de outros módulos usando anyref e lançando para forjar alças (por exemplo, para capacidades). Em vez disso, é preciso pular por cima de obstáculos e overheads adicionais que seriam desnecessários se, em vez disso, apenas garantíssemos a abstração estática padrão.

Além disso, agora que (acho) entendi melhor como você pretende usar tipos exatos, percebo que sua intenção não aborda nenhum dos dois principais problemas para os quais chamei a atenção com call_indirect e subtipagem:

  1. Não ajuda call_indirect respeitar os subtipos (o que eu acho que você já disse explicitamente)
  2. Isso não impede que call_indirect seja usado para usar uma função exportada com sua assinatura definida em vez de sua assinatura exportada.

Portanto, este não é um problema trivial de resolver. É por isso que, dadas as limitações de tempo, prefiro me concentrar em avaliar como nos dar tempo para resolvê-lo adequadamente. Não acho que seja necessário primeiro ter uma discussão sobre se anyref vale a pena jogar fora a abstração estática. Esse é o tipo de grande discussão que eu esperava evitar para não atrasar mais as coisas.

Por outro lado, você demonstrou que anyref castable pode ser usado para contornar os mecanismos de abstração estática.

A abstração de tipo estático é insuficiente em uma linguagem com conversão de tipo dinâmica. Porque a abstração estática depende da parametricidade, e as projeções quebram isso. Não há nada de novo nisso, artigos já foram escritos sobre isso. Outros mecanismos de abstração são necessários em tal contexto.

Tentar contornar isso restringindo o uso de tipos abstratos anula seu propósito. Considere o caso de uso WASI. Não deve importar se um módulo WASI e qualquer tipo que ele exporta é implementado pelo host ou no Wasm. Se você restringir arbitrariamente tipos abstratos definidos pelo usuário, uma implementação Wasm não será mais intercambiável com uma implementação de host em geral.

  1. Isso não ajuda a fazer call_indirect respeitar os subtipos (o que eu acho que você já disse explicitamente)

Huh? Faz parte das regras de subtipagem, o mesmo acontece por definição.

  1. Isso não impede que call_indirect seja usado para usar uma função exportada com sua assinatura definida em vez de sua assinatura exportada.

Eu não disse que sim. Eu disse que este não é um problema com call_indirect em si, mas uma questão de escolher um mecanismo de abstração de tipo adequado para uma linguagem com casts.

Como um aparte, não há nenhuma razão convincente para que a compilação do OCaml (ou qualquer linguagem semelhante) exija a introdução de tipos variantes. Mesmo que isso pudesse ser um pouco mais rápido em teoria (o que eu duvido que seja o caso nos motores da geração atual, mais provavelmente o contrário), os tipos variantes são uma complicação significativa que não deveria ser necessária para o MVP. Não concordo com seu apetite por complexidade prematura. ;)

Re igualdade nas funções: há linguagens, como Haskell ou SML, que não suportam isso, portanto, podem se beneficiar diretamente de func refs. OCaml lança para igualdade estrutural e explicitamente tem um comportamento definido pela implementação para um físico. É deixado em aberto se isso permite sempre retornar falso ou lançar para funções, mas qualquer um pode ser suficiente na prática e vale a pena explorar antes de comprometer-se com um invólucro extra caro.

[Como um meta-comentário, eu realmente apreciaria se você abrandasse sua palestra e talvez considerasse a ideia de que este é um mundo onde, talvez, o conjunto de pessoas competentes não seja único e que traços de cérebros tenham sido ocasionalmente aplicados antes.]

Como um meta comentário, eu realmente apreciaria se você abrandasse sua palestra

Ouvi.

e talvez considerada a ideia de que este é um mundo onde, talvez, o conjunto de pessoas competentes não seja um único e que traços de cérebros tenham sido ocasionalmente aplicados antes.

Meu conselho aqui se baseia na consulta a vários especialistas.

A abstração de tipo estático é insuficiente em uma linguagem com conversão de tipo dinâmica. Porque a abstração estática depende da parametricidade, e as projeções quebram isso. Não há nada de novo nisso, artigos já foram escritos sobre isso. Outros mecanismos de abstração são necessários em tal contexto.

Esses especialistas com quem consultei incluem os autores de alguns desses artigos.

Agora, na tentativa de verificar se sintetizei corretamente seus conselhos, acabei de enviar um e-mail a outro autor de alguns desses artigos, de quem não discuti este tópico antes. Aqui está o que eu perguntei:

Suponha que eu tenha uma função polimórfica f(...). Minha linguagem digitada tem subtipagem (subsuntiva) e conversão explícita. No entanto, uma conversão de t1 para t2 apenas verifica se t2 é um subtipo de t1. Suponha que variáveis ​​de tipo como X por padrão não tenham subtipos ou supertipos (além de si mesmas, é claro). Você esperaria que f fosse relacionalmente paramétrico em relação a X?

Aqui estava a resposta deles:

Sim, eu acho que isso seria paramétrico, uma vez que a única habilidade que isso dá é escrever casts em X que são equivalentes a uma função de identidade, que já é paramétrica relacionalmente.

Isso está de acordo com meu conselho. Agora, é claro, esta é uma simplificação do problema em questão, mas fizemos um esforço para investigar o problema mais especificamente para WebAssembly e, até agora, nossa exploração sugeriu que essa expectativa continua a se manter mesmo na escala de WebAssembly exceto call_indirect , daí este problema.

Observe que os teoremas aos quais você está se referindo se aplicam a linguagens em que todos os valores podem ser convertidos. Esta observação é de onde surgiu a ideia de restringir a moldabilidade.

Considere o caso de uso WASI.

Eu não entendo as afirmações que você está fazendo. Nós consideramos o caso de uso WASI. Por nós, estou incluindo vários especialistas em segurança e até mesmo em segurança especificamente baseada em capacidade.

Como um meta-comentário, eu realmente apreciaria não precisar apelar à autoridade ou ao CG para que minhas sugestões sejam ouvidas. Sugeri que a restrição de moldes permitiria garantir a parametricidade estática mesmo na presença de moldes. Você imediatamente desconsiderou essa sugestão, apelando para documentos anteriores para justificar essa demissão. No entanto, quando ofereci essa mesma sugestão a um autor desses artigos, eles imediatamente chegaram à mesma conclusão que eu e você poderia ter chegado. Antes disso, sugeri que avaliar as soluções potenciais seria um processo longo. Você desconsiderou essa sugestão, insistindo que você (sozinho) havia resolvido o problema, puxando nós dois para esta longa conversa. É extremamente difícil progredir e não ficar frustrado quando as sugestões de alguém são repetidamente rejeitadas de maneira tão casual. (Devo esclarecer que não estou tentando descartar sua sugestão como uma possível solução aqui; estou tentando demonstrar que não é a única solução e, portanto, deve ser avaliada juntamente com várias outras.)

Acho que ter um design detalhado definido e examinado que trate das questões levantadas nesta questão é importante e oportuno: Na verdade, não acho que os tipos abstratos devam ser considerados um recurso mais distante; WASI precisa deles agora.

Também tenho esperança de que exact + newtype possa resolver as preocupações, mas concordo que não podemos simplesmente apostar a fazenda neste palpite neste momento, comprometendo-nos prematuramente com um design quando nós (em breve) enviamos tipos de referência. Precisamos de tempo para ter uma discussão adequada sobre isso.

Dito isso, não vejo perigo em permitir externref em call_indirect assinaturas na proposta de tipos de referência. Sim, se um módulo exportar um externref (como um const global ou retornando-o de uma função ...), não determinamos se podemos reduzir esse externref . Mas call_indirect não está fazendo downcast de externref ; está fazendo downcast de funcref , e externref não tem uma função diferente de i32 wrt a verificação de igualdade de tipo de funcref. Assim, na ausência de importações de tipo, exportações de tipo e subtipagem em jogo em call_indirect , não vejo como estamos nos comprometendo com uma nova escolha de design com a qual ainda não nos comprometemos no MVP .

Se não houver perigo, talvez pudéssemos reduzir essa discussão intensa a uma discussão menos intensa na proposta de importação de tipo (onde ainda acho que devemos incluir o suporte de tipo abstrato adequado).

Certo. Acho que é uma boa ideia examinar se existe um perigo ou não.

Em relação ao WASI, o design ainda está em constante mudança, mas uma opção que ainda parece viável é usar algo como i31ref para suas "alças", digamos porque não requer alocação dinâmica de memória. WASI pode decidir sobre outras opções, mas a questão é que ninguém sabe no momento, e seria bom que as decisões tomadas agora não afetassem tais decisões no futuro.

Atualmente, externref é o único tipo abstrato disponível e, portanto, um host baseado em WASI instanciaria externref com i31ref (ou quaisquer "alças" WASI). Mas meu entendimento é que WASI deseja mover sua implementação para WebAssembly, tanto quanto possível, a fim de reduzir o código dependente do host. Para facilitar isso, em algum ponto os sistemas WASI podem querer tratar externref como qualquer outro tipo de importação e instanciá-lo com o tipo abstrato exportado de WASI Handle . Mas se Handle for i31ref , então a implementação acima de call_indirect necessária para habilitá-lo a trabalhar além dos limites do módulo também pode ser usada para permitir que as pessoas forjem identificadores por meio de externref .

Portanto, uma das minhas perguntas, que agora estou percebendo não foi declarada claramente em meu post original, é as pessoas querem que externref seja instanciado assim como outras importações de tipo abstrato serão?

Portanto, uma das minhas perguntas, que agora estou percebendo não foi declarada claramente em meu post original, é as pessoas querem que externref seja instanciado assim como outras importações de tipo abstrato serão?

Obrigado por levantar esta questão explicitamente. FWIW, eu nunca entendi que externref pode ser instanciado de dentro de um módulo WebAssembly. Isso implica a participação do host na virtualização se o WASI quiser usar externref como identificadores, mas isso parece ok para mim, ou pelo menos parece uma discussão separável.

Hmm, deixe-me ver se posso esclarecer. Eu suspeito que você já esteja a bordo com um monte de coisas que se seguem, mas é mais fácil para mim começar do zero.

Da perspectiva de um módulo wasm, externref não significa referência de host. É apenas um tipo opaco sobre o qual o módulo não sabe nada. Em vez disso, são as convenções em torno de externref que o interpretam como uma referência de host. Por exemplo, as convenções de um módulo usando externref para interagir com o DOM seriam aparentes nas funções envolvendo externref que o módulo importa, como parentNode : [externref] -> [externref] e childNode : [externref, i32] -> [externref] . O ambiente do módulo, como o próprio host, é o que realmente dá a interpretação de externref como referências de host e fornece implementações dos métodos importados que corroboram essa interpretação.

No entanto, o ambiente do módulo não precisa ser o host e externref não precisa ser referências de host. O ambiente pode ser outro módulo que fornece funcionalidade para algum tipo que se pareça com referências de host exibindo as convenções esperadas. Vamos supor que o módulo E é o ambiente do módulo M, e esse módulo M importa parentNode e childNode como acima. Digamos que E deseja usar o módulo M, mas deseja restringir o acesso de M ao DOM, digamos porque E tem confiança limitada em M ou porque E deseja limitar quaisquer bugs que M possa ter e sabe que as necessidades de M não devem exceder essas restrições. O que E poderia fazer é instanciar M com "MonitoredRef" como M's externref . Digamos que, em particular, E deseja fornecer a M nós DOM, mas garantir que M não suba na árvore DOM. Então E's MonitoredRef poderia ser especificamente ref (struct externref externref) , onde o segundo externref (da perspectiva de E) é o nó DOM em que M está operando, mas o primeiro externref é um ancestral de aquele nó que M não tem permissão para passar. E poderia então instanciar o parentNode M de forma que erraria se essas duas referências fossem iguais. O próprio E importaria suas próprias funções parentNode e childNode , tornando E efetivamente um monitor de tempo de execução das interações DOM.

Esperançosamente, isso foi concreto o suficiente para pintar o quadro certo, embora não muito concreto para se perder nos detalhes. Obviamente, existem vários padrões como esse. Portanto, acho que outra maneira de formular a pergunta é: queremos que externref represente apenas referências de host exatamente?

A única parte que parece questionável para mim é "o que E poderia fazer é instanciar M com" MonitoredRef "como M's externref ." Não tenho a impressão de que existem planos para permitir que coisas abstratas apareçam como externref em outros módulos. Meu entendimento é que externref não é uma ferramenta de abstração de forma alguma.

Também não conheço nenhum desses planos; Também não sei se alguém havia considerado a opção. Ou seja, externref ser um tipo "primitivo", por exemplo, i32 , ou um tipo "instanciável", por exemplo, tipos importados?

Em minha postagem original, indiquei que qualquer uma das formas é administrável. A desvantagem de ir para a interpretação "primitiva" é que externref é substancialmente menos útil / combinável do que os tipos importados, já que o último suportará os casos de uso de externref , bem como os padrões acima. Como tal, o "primitivo" externref parece provável que se torne vestigial - existindo apenas para compatibilidade com versões anteriores. Mas isso parece improvável de ser particularmente problemático, apenas um incômodo. O maior problema que posso ver é que, assim como o bom comportamento de call_indirect em tipos numéricos funciona porque eles não têm supertipos, o bom comportamento de call_indirect pode acabar dependendo de externref não tendo supertipos.

Ah hah, sim, isso explica a diferença no entendimento: eu concordo com @tlively que externref não é abstrato e não há nenhuma noção de "instanciar externref com um tipo", e eu acho que podemos nos sentir muito confiantes sobre isso daqui para frente. (Como externref é um tipo primitivo, ao contrário de um parâmetro de tipo declarado explicitamente, não está claro como alguém poderia tentar instanciá-lo por módulo.)

Na ausência de downcasts, este fato torna o wasm quase inútil para implementar / virtualizar APIs WASI, e é por isso que o plano para WASI tem sido fazer a transição de i32 manipuladores diretamente para importações de tipo (e por que eu entrei as importações de tipo / # 6 , b / c precisamos ainda um pouco mais).

Como externref é um tipo primitivo, ao contrário de um parâmetro de tipo declarado explicitamente, não está claro como alguém poderia tentar instanciá-lo por módulo.

Quando adicionamos importações de tipo, podemos tratar os módulos sem importações de tipo, mas com externref como tendo import type externref no topo. Tudo faria a verificação de tipo da mesma forma porque, ao contrário de outros tipos primitivos, externref não tem operações primitivas associadas (além de ter um valor padrão). Mas com essa importação implícita, agora podemos fazer coisas como virtualização, sandbox e monitoramento de tempo de execução.

Mas antes de ir e voltar sobre isso, acho que ajudaria a avaliar se estamos todos na mesma página sobre algo. Deixe-me saber se você concorda ou discorda da seguinte declaração e por quê: "Uma vez que as importações de tipo estão disponíveis, os módulos não têm razão para usar externref e são mais reaproveitáveis ​​/ combináveis ​​se usarem uma importação de tipo."

Deixe-me saber se você concorda ou discorda da seguinte declaração e por que: "Uma vez que as importações de tipo estão disponíveis, os módulos não têm razão para usar externref e são mais reaproveitáveis ​​/ combináveis ​​se usarem uma importação de tipo."

Eu concordo com esta afirmação no resumo. Na prática, acho que externref permanecerá comum em contextos da web para se referir a objetos JS externos porque não requer configuração adicional no momento da instanciação. Mas isso é apenas uma previsão e eu não me importaria se eu descobrisse que estou errado e todos passassem a usar importações de tipo, afinal. O valor de externref é que podemos tê-lo mais cedo do que podemos ter mecanismos mais ricos, como importações de tipo. Eu prefiro manter externref simples e vê-lo cair em desuso do que calçá-lo desajeitadamente para ser algo mais poderoso mais tarde, quando houver alternativas mais elegantes.

@tlively ,

FWIW, nunca entendi que externref pode ser instanciado de dentro de um módulo WebAssembly.

Certo, a ideia é que externref é o tipo "primitivo" de ponteiros estrangeiros. Para abstrair os detalhes de implementação de um tipo de referência, você precisará de algo mais: algo como anyref ou uma importação de tipo.

@lukewagner , eu

@RossTate :

Esses especialistas com quem consultei incluem os autores de alguns desses artigos.

Excelente. Então, suponho que você tenha notado que você mesmo é o autor de alguns desses artigos, caso esteja procurando por mais autoridade. :)

Aqui está o que eu perguntei:

Suponha que eu tenha uma função polimórfica f (...). Minha linguagem digitada tem subtipagem (subsuntiva) e conversão explícita. No entanto, uma conversão de t1 para t2 apenas verifica se t2 é um subtipo de t1. Suponha que variáveis ​​de tipo como X por padrão não tenham subtipos ou supertipos (além de si mesmas, é claro). Você esperaria que f fosse relacionalmente paramétrico em relação a X?

Suspirar. Eu daria a mesma resposta a essa pergunta específica. Mas esta questão incorpora várias suposições específicas, por exemplo, sobre a natureza dos moldes e sobre uma distinção bastante incomum entre quantificação limitada e ilimitada que raramente existe em qualquer linguagem de programação. E suponho que seja por um motivo.

Quando eu disse "abstração de tipo estático é insuficiente", não quis dizer que não seja _tecnicamente_ possível (claro que é), mas que não é _praticamente_ adequado. Na prática, você não quer uma bifurcação entre abstração de tipo e subtipagem / moldabilidade (ou entre tipos paramétricos e não paramétricos), porque isso quebraria artificialmente a composição com base em moldes.

Eu não entendo as afirmações que você está fazendo.

Se você receber um valor do tipo abstrato, você ainda pode querer esquecer seu tipo exato, por exemplo, colocá-lo em algum tipo de união e, posteriormente, recuperá-lo por meio de um downcast. Você pode querer fazer isso pelo mesmo motivo que deseja para qualquer outro tipo de referência. A abstração de tipo não deve atrapalhar certos padrões de uso que são válidos com tipos regulares do mesmo tipo.

Sua resposta parece ser: então, embrulhe tudo em tipos auxiliares nos respectivos locais de uso, por exemplo, em variantes. Mas isso pode implicar em sobrecarga substancial de empacotamento / desdobramento, requer recursos de sistema de tipos mais complexos e é mais complicado de usar.

Acho que é a isso que várias de nossas divergências se resumem: se o MVP deve oferecer suporte a uniões de tipos de referência ou se deve exigir a introdução e a codificação com tipos de variantes explícitos. Para o bem ou para o mal, os sindicatos são uma combinação natural para a interface de heap dos motores típicos e são fáceis e baratos de suporte hoje. As variantes nem tanto, são uma abordagem muito mais pesquisável que provavelmente induziria sobrecarga extra e desempenho menos previsível, pelo menos em motores existentes. E estou dizendo que, como uma pessoa de sistemas de tipos, prefere variantes a sindicatos em outras circunstâncias, como linguagens voltadas para o usuário. ;)

Como um meta-comentário, eu realmente apreciaria não precisar apelar à autoridade ou ao CG para que minhas sugestões sejam ouvidas.

Gostaria de sugerir que conversas sobre várias propostas poderiam funcionar melhor se começadas por _perguntando_ aos respectivos defensores sobre coisas que não são claras, por exemplo, fundamentos específicos ou planos futuros (que nem sempre são óbvios ou escritos ainda), antes de _assumir_ a ausência de uma resposta e fazer amplas afirmações e sugestões com base nessas suposições?

Excelente. Então, suponho que você tenha notado que você mesmo é o autor de alguns desses artigos, caso esteja procurando por mais autoridade. :)

Sim, o que torna extremamente problemático quando você sugere que há documentos afirmando que minha sugestão não funciona, embora você saiba que minha sugestão aborda especificamente as condições sob as quais essas reivindicações foram feitas.

Na prática, você não quer uma bifurcação entre abstração de tipo e subtipagem / moldabilidade (ou entre tipos paramétricos e não paramétricos), porque isso quebraria artificialmente a composição com base em moldes.

Esta é uma opinião, não um fato (tornando-se algo perfeitamente razoável para nós discordarmos). Eu diria que não existem linguagens assembly com tipos agnósticos de linguagem para sistemas multilíngues e, portanto, é impossível fazer afirmações sobre a prática. Isso é algo que merece uma discussão completa (separada). Para essa discussão, seria útil se você primeiro fornecer alguns estudos de caso detalhados para que o CG possa comparar as compensações.

Gostaria de sugerir que conversas sobre várias propostas poderiam funcionar melhor se começadas perguntando aos respectivos defensores sobre coisas que não são claras, por exemplo, fundamentos específicos ou planos futuros (que nem sempre são óbvios ou escritos ainda), antes de presumir a ausência de uma resposta e fazer amplas afirmações e sugestões com base nessas suposições?

WebAssembly / proposal-type-import # 4, WebAssembly / proposal-type-import # 6 e WebAssembly / proposal-type-import # 7, cada um essencialmente pediu mais detalhes sobre este plano. O último desses direciona o problema para o GC, mas WebAssembly / gc # 86 aponta que a proposta atual do GC não suporta, de fato, mecanismos de abstração dinâmica.

No nível meta, fomos solicitados a deixar essa discussão de lado e focar no tópico em questão. Achei a resposta de @tlively à minha pergunta muito útil. Na verdade, estou bastante interessado em saber especificamente sua opinião sobre essa questão.

@RossTate :

Achei a resposta de @tlively à minha pergunta muito útil. Na verdade, estou bastante interessado em saber especificamente sua opinião sobre essa questão.

Hm, pensei já ter comentado sobre isso acima . Ou você quer dizer outra coisa?

Não. Achei que aquele comentário poderia implicar em concordância com a resposta dele, mas queria confirmar primeiro. Obrigado!

@lukewagner , quais são seus pensamentos?

Concordo com o acima que externref será para sempre um tipo primitivo e não será reinterpretado retroativamente como um parâmetro de tipo. Acho que, dado isso, os tipos de referência estão prontos para uso.

Eu gostaria de aceitar a oferta de @rossberg para expandir o escopo da proposta de importação de tipo para que ela cubra a capacidade do wasm de implementar tipos abstratos. Assim que pudermos definir isso, acho que isso desbloqueará uma discussão mais aprofundada sobre referências e subtipos de função.

Incrível. Então, estamos todos na mesma página (e eu, também, acho que @tlively fornece um bom resumo das compensações envolvidas e a justificativa para uma decisão).

Portanto, externref não será instanciado, e os módulos que procuram essa flexibilidade extra de importações de tipo deverão ser convertidos para importações de tipo quando o recurso for lançado. Ocorreu-me que, para tornar essa transição suave, provavelmente precisaremos fazê-la de forma que (alguns?) Tipos de importações sejam instanciados por externref por padrão se nenhuma instanciação for fornecida.

E eu também gostaria de aceitar a oferta para expandir o escopo das importações de tipo. Muitas das principais aplicações de importação de tipo precisam de abstração, então me parece natural que a abstração faça parte dessa proposta.

Nesse ínterim, embora tenhamos abordado a questão urgente sobre externref , o que fazer de forma mais geral sobre call_indirect ainda não foi resolvido, embora com alguma discussão útil sobre como poderia ser resolvido, então eu ainda deixarei o problema em aberto.

Obrigado!

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

JimmyVV picture JimmyVV  ·  4Comentários

thysultan picture thysultan  ·  4Comentários

jfbastien picture jfbastien  ·  6Comentários

beriberikix picture beriberikix  ·  7Comentários

frehberg picture frehberg  ·  6Comentários