Partilhar via


Diretivas #line melhoradas

Observação

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações propostas sejam finalizadas e incorporadas na especificação ECMA atual.

Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da Language Design Meeting (LDM).

Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações .

Questão campeã: https://github.com/dotnet/csharplang/issues/4747

Resumo

O compilador aplica o mapeamento definido pelas diretivas #line aos locais de diagnóstico e pontos de sequência emitidos para o PDB.

Atualmente, apenas o número da linha e o caminho do arquivo podem ser mapeados enquanto o caractere inicial é inferido a partir do código-fonte. A proposta é permitir especificar o mapeamento de extensão completa.

Motivação

DSLs que geram código-fonte C# (como ASP.NET Razor) atualmente não podem produzir mapeamento de origem preciso usando diretivas #line. Isso resulta em experiência de depuração deteriorada em alguns casos, já que os pontos de sequência emitidos para o PDB não conseguem ser mapeados para o local exato no código-fonte original.

Por exemplo, o seguinte código Razor

@page "/"
Time: @DateTime.Now

gera código assim (simplificado):

#line hidden
void Render()
{
   _builder.Add("Time:");
#line 2 "page.razor"
   _builder.Add(DateTime.Now);
#line hidden
}

A diretiva acima mapearia o ponto de sequência emitido pelo compilador para a instrução _builder.Add(DateTime.Now); para a linha 2, mas a coluna estaria incorreta (16 em vez de 7).

Na verdade, o gerador de código-fonte Razor gera incorretamente o seguinte código:

#line hidden
void Render()
{
   _builder.Add("Time:");
   _builder.Add(
#line 2 "page.razor"
      DateTime.Now
#line hidden
);
}

A intenção era preservar o caractere inicial e funciona para mapear a localização do diagnóstico. No entanto, isto não funciona para os pontos de sequência, uma vez que a diretiva #line só se aplica aos pontos de sequência que a seguem. Não há nenhum ponto de sequência no meio da instrução _builder.Add(DateTime.Now); (pontos de sequência só podem ser emitidos em instruções IL com pilha de avaliação vazia). A diretiva #line 2 no código acima, portanto, não tem efeito sobre o PDB gerado e o depurador não colocará um ponto de interrupção ou parada no trecho de @DateTime.Now na página do Razor.

Questões abordadas pela presente proposta: https://github.com/dotnet/roslyn/issues/43432https://github.com/dotnet/roslyn/issues/46526

Projeto detalhado

Alteramos a sintaxe dos line_indicator utilizados na diretiva pp_line do seguinte modo:

Atual:

line_indicator
    : decimal_digit+ whitespace file_name
    | decimal_digit+
    | 'default'
    | 'hidden'
    ;

Propôs:

line_indicator
    : '(' decimal_digit+ ',' decimal_digit+ ')' '-' '(' decimal_digit+ ',' decimal_digit+ ')' whitespace decimal_digit+ whitespace file_name
    | '(' decimal_digit+ ',' decimal_digit+ ')' '-' '(' decimal_digit+ ',' decimal_digit+ ')' whitespace file_name
    | decimal_digit+ whitespace file_name
    | decimal_digit+
    | 'default'
    | 'hidden'
    ;

Ou seja, a diretiva #line aceitaria 5 números decimais (de linha inicial, de caracteres iniciais, de linha final, de caracteres finais, de deslocamento de caracteres), 4 números decimais (linha inicial, de caracteres iniciais , de linha final, caractere final), ou um único (linha).

Se de deslocamento de caracteres não for especificado, seu valor padrão será 0, caso contrário, ele especifica o número de caracteres UTF-16. O número deve ser não negativo e menor do que o comprimento da linha seguindo a diretiva #line no arquivo não mapeado.

(de linha inicial , caractere inicial)-(de linha de fim , caractere final) especifica um intervalo no arquivo mapeado. de linha inicial e de linha final são inteiros positivos que especificam números de linha. caráter inicial, caráter final são inteiros positivos que especificam números de caracteres UTF-16. linha inicial, caractere inicial, linha final, caracteres finais são indexados a partir de 1, o que significa que a primeira linha do arquivo e o primeiro caractere UTF-16 em cada linha recebem o número 1.

A implementação restringiria esses números para que eles especifiquem uma extensão de fonte de ponto de sequência válida:

  • linha de partida - 1 está dentro do intervalo [0, 0x20000000) e não é igual a 0xfeefee.
  • linha final - 1 está dentro do intervalo [0, 0x20000000) e não é igual a 0xfeefee.
  • caractere inicial - 1 está dentro do intervalo [0, 0x10000)
  • o caractere final - 1 está dentro do intervalo [0, 0x10000)
  • A linha final de é maior ou igual à linha inicial de.
  • da linha inicial for igual a linha final então de caracteres finais for maior do que de caracteres iniciais.

Observe que os números especificados na sintaxe da diretiva são números baseados em 1, mas os intervalos reais no PDB são baseados em zero. Daí os ajustes -1 acima mencionados.

Os intervalos mapeados de pontos de sequência e os locais de diagnóstico à qual a diretiva #line se aplica são calculados como segue.

Deixemos d ser o número baseado em zero da linha não mapeada que contém a diretiva #line. Deixe o intervalo L = (start: (start line - 1, start character - 1), end: (end line - 1, end character - 1)) ser o intervalo baseado em zero especificado pela instrução.

A função M que mapeia uma posição (linha, caractere) dentro do escopo da diretiva #line no arquivo de origem que contém a diretiva #line para uma posição mapeada (linha mapeada, caractere mapeado) é definida da seguinte forma:

M(l, c) =

l == d + 1 => (L.start.line + ld – 1, L.start.character + max(ccharacter offset, 0))

l>d + 1 => (L.start.line + ld – 1, c)

As construções de sintaxe às quais os pontos de sequência estão associados são determinadas pela implementação do compilador e não são cobertas por esta especificação. O compilador também decide para cada ponto de sequência sua extensão não mapeada. Essa extensão pode cobrir parcial ou totalmente a construção de sintaxe associada.

Uma vez que os trechos não mapeados são determinados pelo compilador, a função M definida acima é aplicada às suas posições inicial e final, com exceção da posição final de todos os pontos de sequência dentro do escopo da diretiva #line cuja localização não mapeada está na linha d + 1 e caractere menor que o offset de caracteres. A posição final de todos esses pontos de sequência é L.end.

O exemplo [5.i] demonstra por que razão é necessário fornecer a capacidade de especificar a posição final da primeira extensão de pontos de sequência.

A definição acima permite que o gerador do código-fonte não mapeado evite o conhecimento íntimo de quais construções de origem exatas da linguagem C# produzem pontos de sequência. Os vãos mapeados dos pontos de sequência no âmbito da diretiva #line são derivados da posição relativa dos vãos não mapeados correspondentes ao primeiro vão não mapeado.

Especificar o de deslocamento de caracteres permite que o gerador insira qualquer prefixo de linha única na primeira linha. Esse prefixo é um código gerado que não está presente no arquivo mapeado. Esse prefixo inserido afeta o valor da primeira extensão de ponto de sequência não mapeada. Portanto, o primeiro caractere dos intervalos de pontos de sequência subsequentes precisa ser deslocado pelo comprimento do prefixo (desvio de caracteres). Ver exemplo [2].

imagem

Exemplos

Para maior clareza, os exemplos usam spanof('...') e lineof('...') pseudosintaxe para expressar a posição inicial da extensão mapeada e o número da linha, respectivamente, do trecho de código especificado.

Primeiro e seguintes vãos

Considere o seguinte código com números de linha não mapeados, baseados em zero, listados à direita.

#line (1,10)-(1,15) "a" // 3
  A();B(                // 4
);C();                  // 5
    D();                // 6

d = 3 L = (0, 9).. (0, 14)

Existem 4 extensões de pontos sequenciais a que a diretiva se aplica, com as seguintes extensões não mapeadas e mapeadas: (4, 2).. (4, 5) => (0, 9).. (0, 14) (4, 6).. (5, 1) => (0, 15).. (1, 1) (5, 2).. (5, 5) => (1, 2).. (1, 5) (6, 4).. (6, 7) => (2, 4).. (2, 7)

2. Deslocamento de caracteres

Razor produz o prefixo _builder.Add( com 15 caracteres (incluindo dois espaços à esquerda).

Navalha:

@page "/"                                  
@F(() => 1+1, 
   () => 2+2
) 

C# gerado:

#line hidden
void Render()            
{ 
#line spanof('F(...)') 15 "page.razor"  // 4
  _builder.Add(F(() => 1+1,            // 5
  () => 2+2                            // 6
));                                    // 7
#line hidden
}
);
}

d = 4 L = (1, 1).. (3,0) deslocamento de caracteres = 15

Intervalos:

  • _builder.Add(F(…)); =>F(…): (5, 2).. (7, 2) => (1, 1).. (3, 0)
  • 1+1 =>1+1: (5, 23).. (5, 25) => (1, 9).. (1, 11)
  • 2+2 =>2+2: (6, 7).. (6, 9) => (2, 7).. (2, 9)

3. Navalha: Vão de linha única

Navalha:

@page "/"
Time: @DateTime.Now

C# gerado:

#line hidden
void Render()
{
  _builder.Add("Time:");
#line spanof('DateTime.Now') 15 "page.razor"
  _builder.Add(DateTime.Now);
#line hidden
);
}

4. Navalha: Vão de várias linhas

Navalha:

@page "/"                                  
@JsonToHtml(@"
{
  ""key1"": "value1",
  ""key2"": "value2"
}") 

C# gerado:

#line hidden
void Render()
{
  _builder.Add("Time:");
#line spanof('JsonToHtml(@"...")') 15 "page.razor"
  _builder.Add(JsonToHtml(@"
{
  ""key1"": "value1",
  ""key2"": "value2"
}"));
#line hidden
}
);
}

5. Navalha: construções de bloco

i. bloco contendo expressões

Neste exemplo, a extensão mapeada do primeiro ponto de sequência associado à instrução IL emitida para a instrução _builder.Add(Html.Helper(() => precisa cobrir toda a expressão de Html.Helper(...) no arquivo gerado a.razor. Isto é conseguido através da aplicação da regra [1] à posição final do ponto sequencial.

@Html.Helper(() => 
{
    <p>Hello World</p>
    @DateTime.Now
})
#line spanof('Html.Helper(() => { ... })') 13 "a.razor"
_builder.Add(Html.Helper(() => 
#line lineof('{') "a.razor"
{
#line spanof('DateTime.Now') 13 "a.razor"
_builder.Add(DateTime.Now);
#line lineof('}') "a.razor"
}
#line hidden
)
II. bloco contendo declarações

Usa o formulário #line line file existente desde

a) Razor não adiciona nenhum prefixo, b) { não está presente no arquivo gerado e não pode haver um ponto de sequência colocado nele, portanto, a extensão do primeiro ponto de sequência não mapeado é desconhecida para Razor.

O caractere inicial de Console no arquivo gerado deve ser alinhado com o arquivo Razor.

@{Console.WriteLine(1);Console.WriteLine(2);}
#line lineof('@{') "a.razor"
  Console.WriteLine(1);Console.WriteLine(2);
#line hidden
iii. bloco contendo código de nível superior (@code, @functions)

Usa o formulário #line line file existente desde

a) Razor não adiciona nenhum prefixo, b) { não está presente no arquivo gerado e não pode haver um ponto de sequência colocado nele, portanto, a extensão do primeiro ponto de sequência não mapeado é desconhecida para Razor.

O caractere inicial de [Parameter] no arquivo gerado deve ser alinhado com o arquivo Razor.

@code {
    [Parameter]
    public int IncrementAmount { get; set; }
}
#line lineof('[') "a.razor"
    [Parameter]
    public int IncrementAmount { get; set; }
#line hidden

6. Navalha: @for, @foreach, @while, @do, @if, @switch, @using, @try, @lock

Usa o formulário #line line file existente, uma vez que a) o Razor não adiciona nenhum prefixo. b) a extensão do primeiro ponto de sequência não mapeado pode não ser conhecida pela Razor (ou não deveria precisar saber).

O caractere inicial da palavra-chave no arquivo gerado deve estar alinhado com o arquivo Razor.

@for (var i = 0; i < 10; i++)
{
}
@if (condition)
{
}
else
{
}
#line lineof('for') "a.razor"
 for (var i = 0; i < 10; i++)
{
}
#line lineof('if') "a.razor"
 if (condition)
{
}
else
{
}
#line hidden