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 + l – d – 1, L.start.character + max(c — przesunięcie znaku, 0))
l>d + 1 => (L.start.line + l – d – 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].
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
C# feature specifications