Udostępnij za pośrednictwem


Ulepszone dyrektywy #line

Notatka

Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.

Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są przechwytywane w odpowiednich spotkania projektowego języka (LDM).

Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .

Kwestia mistrza: https://github.com/dotnet/csharplang/issues/4747

Streszczenie

Kompilator stosuje mapowanie zdefiniowane przez dyrektywy #line do lokalizacji diagnostycznych i punktów sekwencji emitowanych do pliku PDB.

Obecnie można mapować tylko numer wiersza i ścieżkę pliku, gdy znak początkowy jest wywnioskowany z kodu źródłowego. Propozycja polega na umożliwieniu określenia mapowania całego zakresu.

Motywacja

Listy DSL generujące kod źródłowy języka C# (na przykład ASP.NET Razor) nie mogą obecnie tworzyć dokładnego mapowania źródła przy użyciu dyrektyw #line. Powoduje to pogorszenie doświadczenia debugowania w niektórych przypadkach, ponieważ punkty odniesienia tworzone w pliku PDB nie mogą odzwierciedlać dokładnej lokalizacji w oryginalnym kodzie źródłowym.

Na przykład następujący kod Razor

@page "/"
Time: @DateTime.Now

generuje kod podobny do tego (uproszczony):

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

Powyższa dyrektywa mapuje punkt sekwencji emitowany przez kompilator instrukcji _builder.Add(DateTime.Now); na wiersz 2, ale kolumna będzie wyłączona (16 zamiast 7).

Generator źródła Razor faktycznie niepoprawnie generuje następujący kod:

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

Intencją było zachowanie znaku początkowego i działa ono na potrzeby mapowania lokalizacji diagnostycznej. Nie działa to jednak w przypadku punktów sekwencji, ponieważ dyrektywa #line dotyczy tylko następujących punktów sekwencji. Nie ma punktu sekwencyjnego w środku instrukcji _builder.Add(DateTime.Now); (punkty sekwencyjne można emitować tylko w instrukcjach IL, gdy stos oceny jest pusty). Dyrektywa #line 2 w powyższym kodzie nie ma zatem wpływu na wygenerowany plik PDB, a debuger nie umieści punktu przerwania ani nie zatrzyma się na fragmencie kodu @DateTime.Now na stronie Razor.

Problemy rozwiązane przez tę propozycję: https://github.com/dotnet/roslyn/issues/43432https://github.com/dotnet/roslyn/issues/46526

Szczegółowy projekt

Zmieniamy składnię line_indicator używaną w dyrektywie pp_line w następujący sposób:

Aktualny:

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

Proponowany:

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'
    ;

Oznacza to, że dyrektywa #line zaakceptowałaby 5 liczb dziesiętnych (wiersza początkowego, znaku początkowego , wiersza końcowego , znaku końcowego , przesunięcie znaku ), 4 liczby dziesiętne (wiersza początkowego, znaku początkowego , wiersza końcowego , znaku końcowego ) lub jeden (wiersza).

Jeśli przesunięcie znaku nie zostanie określone, jego wartość domyślna to 0, w przeciwnym razie określa liczbę znaków UTF-16. Liczba musi być nieujemna, a następnie mniejsza niż długość wiersza zgodnie z dyrektywą #line w pliku niezamapowanym.

(początek wiersza, początek znaku )-(koniec wiersza, koniec znaku ) określa zakres w zamapowanym pliku. wiersz początkowy i wiersz końcowy są dodatnimi liczbami całkowitymi określającymi liczby wierszy. znak początkowy, znak końcowy są dodatnimi liczbami całkowitymi określającymi liczby znaków UTF-16. wiersz początkowy, znak początkowy, wiersz końcowy, znak końcowy są numerowane od 1, co oznacza, że pierwszy wiersz pliku i pierwszy znak UTF-16 w każdym wierszu ma przypisaną liczbę 1.

Implementacja ograniczałaby te liczby tak, aby mogły określać prawidłowe źródło przedziału punktu sekwencji:

  • początek linii - 1 mieści się w zakresie [0, 0x20000000) i nie jest równy 0xfeefee.
  • koniec wiersza - 1 mieści się w zakresie [0, 0x20000000) i nie jest równy 0xfeefee.
  • znak początkowy - 1 znajduje się w przedziale [0, 0x10000)
  • znak końcowy — 1 mieści się w zakresie [0, 0x10000)
  • wiersz końcowy jest większy lub równy wierszowi początkowemu.
  • linia początkowa jest równa linia końcowa, jeśli znak końcowy jest większy niż znak początkowy.

Należy pamiętać, że liczby określone w składni dyrektywy są liczbami opartymi na 1, ale rzeczywiste zakresy w pliku PDB są oparte na zera. Dlatego właśnie powyższe zmiany -1.

Zakresy zmapowanych punktów sekwencyjnych i lokalizacje diagnoz, do których odnosi się dyrektywa #line, są obliczane w następujący sposób.

Niech d będzie liczbą zerobazową linii nienaznaczonej zawierającej dyrektywę #line. Niech zakres L = (początek: (wiersz początkowy - 1, znak początkowy - 1), koniec: (wiersz końcowy - 1, znak końcowy - 1)) będzie zerowym zakresem określonym przez instrukcję.

Funkcja M, która mapuje pozycję (wiersz, znak) w zakresie dyrektywy #line w pliku źródłowym zawierającym dyrektywę #line do odpowiadającej pozycji (odpowiadająca linia, odpowiadający znak), jest zdefiniowana w następujący sposób:

M(l, c) =

l == d + 1 => (L.start.line + ld – 1, L.start.character + max(c — przesunięcie znaku, 0))

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

Konstrukcje składni skojarzone z punktami sekwencji są określane przez implementację kompilatora i nie są objęte tą specyfikacją. Kompilator decyduje również o nieprzypisanej rozpiętości każdego punktu sekwencji. Ten zakres może częściowo lub w pełni obejmować skojarzoną konstrukcję składni.

Po określeniu niezmapowanych zakresów przez kompilator, funkcja M zdefiniowana powyżej jest stosowana do ich pozycji początkowych i końcowych, z wyłączeniem pozycji końcowej wszystkich punktów sekwencji w zakresie dyrektywy #line, których niezmapowana lokalizacja znajduje się w wierszu d + 1 i z znakami mniejszymi niż przesunięcie znaków. Położenie końcowe wszystkich tych punktów sekwencji to L.end.

Przykład [5.i] pokazuje, dlaczego konieczne jest zapewnienie możliwości określenia położenia końcowego pierwszego punktu sekwencji.

Powyższa definicja umożliwia generatorowi niezamapowanego kodu źródłowego uniknięcie posiadania szczegółowej wiedzy o tym, które dokładne konstrukcje źródłowe języka C# generują punkty sekwencyjne. Mapowane zakresy punktów sekwencji w zakresie dyrektywy #line pochodzą ze względnej pozycji odpowiednich niezmapowanych zakresów względem pierwszego niezmapowanego zakresu.

Określenie przesunięcia znaku umożliwia generatorowi wstawianie dowolnego prefiksu jednowierszowego w pierwszym wierszu. Ten prefiks to generowany kod, który nie znajduje się w zmapowanym pliku. Taki wstawiony prefiks wpływa na wartość pierwszego niezamapowanego zakresu punktu sekwencji. W związku z tym początkowy znak kolejnych punktów sekwencji musi być przesunięty o długość prefiksu (przesunięcie znaku). Zobacz przykład [2].

obraz

Przykłady

W celu jasności przykładów użyto pseudoskładni spanof('...') i lineof('...'), aby wyrazić odwzorowane położenie początkowe oraz numer wiersza określonego fragmentu kodu.

1. Pierwsze i kolejne segmenty

Rozważ następujący kod z niemapowanym numerem wiersza opartym na zera wymienionymi po prawej stronie:

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

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

Dyrektywa obejmuje 4 zakresy punktów sekwencji z następującymi niemapowanymi i mapowanymi zakresami: (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. Przesunięcie znaków

Razor generuje prefiks _builder.Add( o długości 15 znaków (w tym dwie poprzedzające spacje).

Brzytwa:

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

Wygenerowany język C#:

#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) przesunięcie znaku = 15

Obejmuje:

  • _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. Razor: Jednowierszowa rozpiętość

Brzytwa:

@page "/"
Time: @DateTime.Now

Wygenerowany język C#:

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

4. Razor: Rozpiętość wielowierszowa

Brzytwa:

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

Wygenerowany język C#:

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

5. Razor: konstrukcje blokowe

ja. blok zawierający wyrażenia

W tym przykładzie zamapowany zakres pierwszego punktu sekwencji skojarzonego z instrukcją IL emitowaną dla instrukcji _builder.Add(Html.Helper(() => musi obejmować całe wyrażenie Html.Helper(...) w wygenerowanych plikach a.razor. Jest to osiągane przez zastosowanie reguły [1] do pozycji końcowej punktu sekwencji.

@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. blok zawierający instrukcje

Używa istniejącego formularza #line line file od

a) Razor nie dodaje żadnego prefiksu, b) { nie jest obecny w wygenerowanym pliku i nie można umieścić na nim punktu sekwencji, dlatego zakres pierwszego niezamapowanego punktu sekwencji jest nieznany Razor.

Początkowy znak Console w pliku wygenerowanym musi być wyrównany z plikiem Razor.

@{Console.WriteLine(1);Console.WriteLine(2);}
#line lineof('@{') "a.razor"
  Console.WriteLine(1);Console.WriteLine(2);
#line hidden
iii. blok zawierający kod najwyższego poziomu (@code, @functions)

Używa istniejącego formularza #line line file od

a) Razor nie dodaje żadnego prefiksu, b) { nie jest obecny w wygenerowanym pliku i nie można umieścić na nim punktu sekwencji, dlatego zakres pierwszego niezamapowanego punktu sekwencji jest nieznany Razor.

Początkowy znak [Parameter] w wygenerowanym pliku musi być zgodny z plikiem Razor.

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

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

Używa istniejącego formularza #line line file, ponieważ a) Razor nie dodaje żadnego prefiksu. b) zakres pierwszego nieprzypisanego punktu sekwencji może nie być znany Razorowi (lub nie musi być konieczny do znania).

Początkowy znak słowa kluczowego w wygenerowanym pliku musi być zgodny z plikiem 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