Estou trabalhando em um recurso de conclusão (intellisense) para C # no emacs.
A ideia é que, se um usuário digitar um fragmento e, em seguida, solicitar a conclusão por meio de uma combinação de teclas específica, o recurso de conclusão usará o reflexo do .NET para determinar as conclusões possíveis.
Fazer isso requer que o tipo de coisa que está sendo completada seja conhecido. Se for uma string, há um conjunto conhecido de métodos e propriedades possíveis; se for um Int32, tem um conjunto separado e assim por diante.
Usando o semantic, um pacote de lexer / parser de código disponível no emacs, posso localizar as declarações de variáveis e seus tipos. Dado isso, é simples usar reflexão para obter os métodos e propriedades no tipo e, em seguida, apresentar a lista de opções ao usuário. (Ok, não é muito simples de fazer no emacs, mas usando a capacidade de executar um processo do PowerShell dentro do emacs , torna-se muito mais fácil. Eu escrevo um assembly .NET personalizado para fazer reflexão, carrego no PowerShell e, em seguida, elisp rodando dentro O emacs pode enviar comandos para o PowerShell e ler as respostas, via comint. Como resultado, o emacs pode obter os resultados da reflexão rapidamente.)
O problema chega quando o código usa var
na declaração do que está sendo concluído. Isso significa que o tipo não está especificado explicitamente e a conclusão não funcionará.
Como posso determinar com segurança o tipo real usado, quando a variável é declarada com a var
palavra - chave? Só para ficar claro, não preciso determinar isso em tempo de execução. Quero determiná-lo em "Tempo de design".
Até agora, tenho estas ideias:
- compilar e invocar:
- extraia a declaração de declaração, por exemplo, `var foo =" um valor de string ";`
- concatenar uma instrução `foo.GetType ();`
- compilar dinamicamente o fragmento C # resultante em um novo assembly
- carregue o assembly em um novo AppDomain, execute o framgment e obtenha o tipo de retorno.
- descarregar e descartar o conjunto
Eu sei fazer tudo isso. Mas soa terrivelmente pesado para cada solicitação de conclusão no editor.
Acho que não preciso de um novo AppDomain sempre. Eu poderia reutilizar um único AppDomain para várias montagens temporárias e amortizar o custo de configurá-lo e desmontá-lo em várias solicitações de conclusão. Isso é mais um ajuste da ideia básica.
- compilar e inspecionar IL
Basta compilar a declaração em um módulo e inspecionar o IL para determinar o tipo real inferido pelo compilador. Como isso seria possível? O que eu usaria para examinar o IL?
Alguma ideia melhor por aí? Comentários? sugestões?
EDITAR - pensando mais sobre isso, compilar e invocar não é aceitável, porque a invocação pode ter efeitos colaterais. Portanto, a primeira opção deve ser descartada.
Além disso, acho que não posso assumir a presença do .NET 4.0.
ATUALIZAÇÃO - A resposta correta, não mencionada acima, mas gentilmente apontada por Eric Lippert, é implementar um sistema de inferência de tipo de fidelidade total. É a única maneira de determinar com segurança o tipo de uma var em tempo de design. Mas também não é fácil de fazer. Como não tenho ilusões de que quero tentar construir tal coisa, peguei o atalho da opção 2 - extrair o código de declaração relevante, compilá-lo e, em seguida, inspecionar o IL resultante.
Isso realmente funciona, para um subconjunto razoável dos cenários de conclusão.
Por exemplo, suponha que nos fragmentos de código a seguir, o? é a posição em que o usuário pede a conclusão. Isso funciona:
var x = "hello there";
x.?
A conclusão percebe que x é uma String e fornece as opções apropriadas. Ele faz isso gerando e compilando o seguinte código-fonte:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... e então inspecionando o IL com reflexão simples.
Isso também funciona:
var x = new XmlDocument();
x.?
O mecanismo adiciona as cláusulas using apropriadas ao código-fonte gerado, para que ele seja compilado corretamente e, então, a inspeção IL é a mesma.
Isso também funciona:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Significa apenas que a inspeção IL precisa encontrar o tipo da terceira variável local, em vez da primeira.
E isto:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
... que é apenas um nível mais profundo que o exemplo anterior.
Mas, o que não funciona é a conclusão de qualquer variável local cuja inicialização dependa em qualquer ponto de um membro da instância ou do argumento do método local. Gostar:
var foo = this.InstanceMethod();
foo.?
Nem sintaxe LINQ.
Terei que pensar sobre o quão valioso essas coisas são antes de considerar abordá-las através do que é definitivamente um "design limitado" (palavra educada para hack) para conclusão.
Uma abordagem para abordar o problema com dependências em argumentos de método ou métodos de instância seria substituir, no fragmento de código que é gerado, compilado e então analisado por IL, as referências a essas coisas por vars locais "sintéticos" do mesmo tipo.
Outra atualização - conclusão em vars que dependem de membros da instância, agora funciona.
O que fiz foi interrogar o tipo (via semântica) e, em seguida, gerar membros substitutos sintéticos para todos os membros existentes. Para um buffer C # como este:
public class CsharpCompletion
{
private static int PrivateStaticField1 = 17;
string InstanceMethod1(int index)
{
...lots of code here...
return result;
}
public void Run(int count)
{
var foo = "this is a string";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var mmm = count + z + CsharpCompletion.PrivateStaticField1;
var nnn = this.InstanceMethod1(mmm);
var fff = nnn.?
...more code here...
... o código gerado que é compilado, para que eu possa aprender com a saída IL o tipo do var nnn local, fica assim:
namespace Nsbwhi0rdami {
class CsharpCompletion {
private static int PrivateStaticField1 = default(int);
string InstanceMethod1(int index) { return default(string); }
void M0zpstti30f4 (int count) {
var foo = "this is a string";
var fred = new System.Collections.Generic.List<String> { foo, foo.Length.ToString() };
var z = fred.Count;
var mmm = count + z + CsharpCompletion.PrivateStaticField1;
var nnn = this.InstanceMethod1(mmm);
}
}
}
Todos os membros da instância e do tipo estático estão disponíveis no código do esqueleto. Compila com sucesso. Nesse ponto, determinar o tipo de var local é simples por meio do Reflection.
O que torna isso possível é:
- a capacidade de executar o PowerShell no emacs
- o compilador C # é muito rápido. Na minha máquina, leva cerca de 0,5s para compilar um assembly na memória. Não é rápido o suficiente para a análise entre pressionamentos de tecla, mas rápido o suficiente para suportar a geração sob demanda de listas de conclusão.
Ainda não examinei o LINQ.
Isso será um problema muito maior porque o lexer / analisador semântico que o emacs tem para C #, não "faz" o LINQ.
fonte
Respostas:
Posso descrever para você como fazemos isso com eficiência no IDE C # "real".
A primeira coisa que fazemos é executar um passe que analisa apenas as coisas de "nível superior" no código-fonte. Ignoramos todos os corpos de método. Isso nos permite construir rapidamente um banco de dados de informações sobre quais namespaces, tipos e métodos (e construtores, etc) estão no código-fonte do programa. Analisar cada linha de código em cada corpo de método levaria muito tempo se você estivesse tentando fazer isso entre as teclas.
Quando o IDE precisa descobrir o tipo de uma expressão específica dentro de um corpo de método - digamos que você digitou "foo". e precisamos descobrir quais são os membros de foo - fazemos a mesma coisa; pulamos tanto trabalho quanto podemos razoavelmente.
Começamos com uma passagem que analisa apenas as declarações de variáveis locais dentro desse método. Quando executamos essa passagem, fazemos um mapeamento de um par de "escopo" e "nome" para um "determinador de tipo". O "determinador de tipo" é um objeto que representa a noção de "Posso descobrir o tipo deste local se precisar". Determinar o tipo de local pode ser caro, por isso queremos adiar esse trabalho se for necessário.
Agora temos um banco de dados construído lentamente que pode nos dizer o tipo de cada local. Então, voltando ao "foo". - descobrimos em qual declaração a expressão relevante está e, em seguida, executamos o analisador semântico apenas nessa declaração. Por exemplo, suponha que você tenha o corpo do método:
e agora precisamos descobrir que foo é do tipo char. Construímos um banco de dados que contém todos os metadados, métodos de extensão, tipos de código-fonte e assim por diante. Construímos um banco de dados que possui determinantes de tipo para x, y e z. Analisamos a declaração que contém a expressão interessante. Começamos transformando-o sintaticamente para
Para descobrir o tipo de foo, devemos primeiro saber o tipo de y. Então, neste ponto, perguntamos ao determinador de tipo "qual é o tipo de y"? Em seguida, ele inicia um avaliador de expressão que analisa x.ToCharArray () e pergunta "qual é o tipo de x"? Temos um determinador de tipo para o que diz "Preciso procurar" String "no contexto atual". Não há nenhum tipo String no tipo atual, então examinamos o namespace. Também não está lá, então examinamos as diretivas using e descobrimos que existe um "using System" e que System tem um tipo String. OK, então esse é o tipo de x.
Em seguida, consultamos os metadados de System.String para o tipo de ToCharArray e ele diz que é um System.Char []. Super. Portanto, temos um tipo para y.
Agora perguntamos "System.Char [] tem um método Where?" Não. Portanto, examinamos as diretivas de uso; já pré-computamos um banco de dados contendo todos os metadados para métodos de extensão que poderiam ser usados.
Agora dizemos "OK, há dezoito dúzias de métodos de extensão chamados Onde no escopo, algum deles tem um primeiro parâmetro formal cujo tipo é compatível com System.Char []?" Então, começamos uma rodada de testes de conversibilidade. No entanto, os métodos de extensão Where são genéricos , o que significa que temos que fazer inferência de tipo.
Eu escrevi um mecanismo de inferência de tipo especial que pode lidar com fazer inferências incompletas do primeiro argumento para um método de extensão. Rodamos o tipo inferrer e descobrimos que existe um método Where que leva um
IEnumerable<T>
, e que podemos fazer uma inferência de System.Char [] paraIEnumerable<System.Char>
, então T é System.Char.A assinatura deste método é
Where<T>(this IEnumerable<T> items, Func<T, bool> predicate)
, e sabemos que T é System.Char. Também sabemos que o primeiro argumento entre parênteses para o método de extensão é um lambda. Portanto, iniciamos um inferrer de tipo de expressão lambda que diz "o parâmetro formal foo é assumido como System.Char", use esse fato ao analisar o resto do lambda.Agora temos todas as informações de que precisamos para analisar o corpo do lambda, que é "foo.". Procuramos o tipo de foo, descobrimos que, de acordo com o fichário lambda, é System.Char e pronto; exibimos informações de tipo para System.Char.
E fazemos tudo, exceto a análise de "nível superior" entre as teclas . Essa é a parte realmente complicada. Na verdade, escrever todas as análises não é difícil; está tornando-o rápido o suficiente para que você possa fazer isso na velocidade de digitação que é a parte realmente complicada.
Boa sorte!
fonte
Posso dizer aproximadamente como o Delphi IDE funciona com o compilador Delphi para fazer o intellisense (código de insight é como o Delphi o chama). Não é 100% aplicável ao C #, mas é uma abordagem interessante que merece consideração.
A maior parte da análise semântica em Delphi é feita no próprio analisador. As expressões são digitadas à medida que são analisadas, exceto em situações em que isso não é fácil - nesse caso, a análise antecipada é usada para descobrir o que é pretendido e, então, essa decisão é usada na análise.
A análise é em grande parte descendente recursiva LL (2), exceto para expressões, que são analisadas usando precedência de operador. Uma das coisas distintas sobre o Delphi é que ele é uma linguagem de passagem única, portanto, as construções precisam ser declaradas antes de serem usadas, portanto, nenhuma passagem de nível superior é necessária para trazer essas informações.
Essa combinação de recursos significa que o analisador tem praticamente todas as informações necessárias para a compreensão do código para qualquer ponto onde for necessário. A maneira como funciona é a seguinte: o IDE informa o lexer do compilador da posição do cursor (o ponto onde o insight do código é desejado) e o lexer transforma isso em um token especial (é chamado de token kibitz). Sempre que o analisador encontrar esse token (que pode estar em qualquer lugar), ele saberá que este é o sinal para enviar de volta todas as informações que possui ao editor. Ele faz isso usando um longjmp porque é escrito em C; o que ele faz é notificar o chamador final sobre o tipo de construção sintática (isto é, contexto gramatical) em que o ponto de kibitz foi encontrado, bem como todas as tabelas simbólicas necessárias para aquele ponto. Então, por exemplo, se o contexto está em uma expressão que é um argumento para um método, podemos verificar as sobrecargas do método, olhar para os tipos de argumento e filtrar os símbolos válidos apenas para aqueles que podem resolver para esse tipo de argumento (isso reduz em um muito material irrelevante no menu suspenso). Se estiver em um contexto de escopo aninhado (por exemplo, após um "."), O analisador terá devolvido uma referência ao escopo e o IDE pode enumerar todos os símbolos encontrados nesse escopo.
Outras coisas também são feitas; por exemplo, corpos de método são ignorados se o token kibitz não estiver em seu intervalo - isso é feito de forma otimista e revertido se ele ignorou o token. O equivalente aos métodos de extensão - ajudantes de classe em Delphi - tem um tipo de cache versionado, então sua pesquisa é razoavelmente rápida. Mas a inferência de tipo genérico do Delphi é muito mais fraca do que a do C #.
Agora, à questão específica: inferir os tipos de variáveis declaradas com
var
é equivalente à maneira como Pascal infere o tipo de constantes. Vem do tipo da expressão de inicialização. Esses tipos são construídos de baixo para cima. Sex
for do tipoInteger
ey
for do tipoDouble
, entãox + y
será do tipoDouble
, porque essas são as regras da linguagem; etc. Você segue essas regras até que tenha um tipo para a expressão completa no lado direito, e esse é o tipo que você usa para o símbolo à esquerda.fonte
Se você não quer ter que escrever seu próprio analisador para construir a árvore de sintaxe abstrata, você pode usar os analisadores de SharpDevelop ou MonoDevelop , ambos de código aberto.
fonte
Os sistemas Intellisense normalmente representam o código usando uma árvore de sintaxe abstrata, que permite resolver o tipo de retorno da função atribuída à variável 'var' mais ou menos da mesma maneira que o compilador. Se você usar o VS Intellisense, você notará que ele não fornecerá o tipo de var até que você termine de inserir uma expressão de atribuição válida (resolvível). Se a expressão ainda for ambígua (por exemplo, ela não pode inferir totalmente os argumentos genéricos da expressão), o tipo var não será resolvido. Este pode ser um processo bastante complexo, pois talvez seja necessário entrar bem fundo em uma árvore para resolver o tipo. Por exemplo:
O tipo de retorno é
IEnumerable<Bar>
, mas para resolver isso é necessário saber:IEnumerable
.OfType<T>
que se aplica a IEnumerable.IEnumerable<Foo>
e há um método de extensãoSelect
que se aplica a isso.foo => foo.Bar
possui o parâmetro foo do tipo Foo. Isso é inferido pelo uso de Select, que leva aFunc<TIn,TOut>
e, como TIn é conhecido (Foo), o tipo de foo pode ser inferido.IEnumerable<TOut>
e TOut pode ser inferido do resultado da expressão lambda, portanto, o tipo de item resultante deve serIEnumerable<Bar>
.fonte
Já que você tem como alvo o Emacs, pode ser melhor começar com o pacote CEDET. Todos os detalhes que Eric Lippert já cobre no analisador de código na ferramenta CEDET / Semantic for C ++. Há também um analisador C # (que provavelmente precisa de um pouco de TLC), portanto, as únicas partes ausentes estão relacionadas ao ajuste das partes necessárias para C #.
Os comportamentos básicos são definidos em algoritmos centrais que dependem de funções sobrecarregáveis que são definidas por idioma. O sucesso do mecanismo de conclusão depende de quanto ajuste foi feito. Com c ++ como guia, obter suporte semelhante ao C ++ não deve ser tão ruim.
A resposta de Daniel sugere o uso do MonoDevelop para fazer parsing e análise. Este poderia ser um mecanismo alternativo em vez do analisador C # existente ou poderia ser usado para aumentar o analisador existente.
fonte
var
. A Semantic o identifica corretamente como var, mas não fornece inferência de tipo. Minha pergunta era especificamente sobre como resolver isso . Também investiguei a possibilidade de conectar à conclusão CEDET existente, mas não consegui descobrir como. A documentação do CEDET não está ... ah ... completa.É um problema difícil de fazer bem. Basicamente, você precisa modelar a especificação / compilador da linguagem através da maior parte da lexing / parsing / typechecking e construir um modelo interno do código-fonte que você pode então consultar. Eric o descreve em detalhes para C #. Você sempre pode baixar o código-fonte do compilador F # (parte do F # CTP) e dar uma olhada
service.fsi
para ver a interface exposta do compilador F # que o serviço de linguagem F # consome para fornecer intellisense, dicas para tipos inferidos, etc. uma sensação de uma possível 'interface' se você já tiver o compilador disponível como uma API para chamar.A outra rota é reutilizar os compiladores como estão, conforme você está descrevendo, e então usar reflexão ou examinar o código gerado. Isso é problemático do ponto de vista de que você precisa de 'programas completos' para obter uma saída de compilação de um compilador, enquanto ao editar o código-fonte no editor, muitas vezes você só tem 'programas parciais' que ainda não analisam, não todos os métodos já foram implementados, etc.
Resumindo, acho que a versão de 'baixo orçamento' é muito difícil de fazer bem, e a versão 'real' é muito, muito difícil de fazer bem. (Onde 'difícil' aqui mede 'esforço' e 'dificuldade técnica'.)
fonte
NRefactory fará isso por você.
fonte
Para a solução "1", você tem um novo recurso no .NET 4 para fazer isso de forma rápida e fácil. Portanto, se você puder converter seu programa para .NET 4, será sua melhor escolha.
fonte