Поделиться через


Расширенные директивы #line

Заметка

Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.

Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия отражены в соответствующих записях собрания по проектированию языка (LDM).

Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .

Проблема чемпиона: https://github.com/dotnet/csharplang/issues/4747

Сводка

Компилятор применяет отображение, заданное директивами #line, на диагностические локации и точки выполнения, создаваемые в PDB.

В настоящее время можно сопоставить только номер строки и путь к файлу, а начальный символ выводится из исходного кода. Предложение заключается в том, чтобы разрешить указание полного отображения диапазона.

Мотивация

DsLs, создающие исходный код C# (например, ASP.NET Razor), в настоящее время не могут создавать точное сопоставление источников с помощью директив #line. Это приводит к снижению производительности отладки в некоторых случаях, так как точки последовательности, создаваемые PDB, не могут сопоставляться с точным расположением исходного исходного кода.

Например, следующий код Razor

@page "/"
Time: @DateTime.Now

создает код, такой как это (упрощено):

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

Указанная выше директива сопоставляет точку последовательности, выдаваемую компилятором для оператора _builder.Add(DateTime.Now);, с строкой 2, но столбец будет отключен (16 вместо 7).

Генератор источника Razor фактически неправильно создает следующий код:

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

Намерение было сохранить начальную характеристику, и это работает для картирования позиции диагностики. Однако это не работает для точек последовательности, так как директива #line применяется только к точкам последовательности, следующим за ним. В середине инструкции _builder.Add(DateTime.Now); отсутствует точка последовательности (такие точки могут создаваться только в инструкциях IL с пустым стеком вычисления). Директива #line 2 в приведенном выше коде не оказывает эффекта на созданный PDB, и отладчик не помещает точку останова или не остановится на фрагменте @DateTime.Now на странице Razor.

Вопросы, рассмотренные этим предложением: https://github.com/dotnet/roslyn/issues/43432https://github.com/dotnet/roslyn/issues/46526

Подробный дизайн

Мы изменим синтаксис line_indicator, используемый в директиве pp_line, как показано ниже.

Текущий:

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

Предложено:

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

То есть, команда #line принимает либо 5 десятичных чисел (начальная строка, начальная позиция символа, конечная строка, конечная позиция символа, смещение символов), 4 десятичных числа (начальная строка, начальная позиция символа, конечная строка, конечная позиция символа), или одно число (строка).

Если смещение символов не указано значение по умолчанию равно 0, в противном случае указывает число символов UTF-16. Число должно быть неотрицательно и меньше длины строки, следующей за директивой #line в несопоставленном файле.

(начальной строки, начального символа)-(конечной строки, конечного символа) задает диапазон в сопоставленном файле. начальная строка и конечная строка являются положительными целыми числами, указывающими номера строк. начальный символ, конечный символ являются положительными целыми числами, указывающими номера символов UTF-16. Каждое из значений: начальная строка, начальный символ, конечная строка, конечный символ — начинаются с 1, что означает: первая строка файла и первый символ UTF-16 в каждой строке имеют номер 1.

Внедрение будет ограничивать эти числа, чтобы они указывали на допустимый диапазон источника точки последовательности :

  • начальная строка - 1 находится в диапазоне [0, 0x20000000) и не равна 0xfeefee.
  • конец строки - 1 находится в диапазоне [0, 0x20000000) и не равен 0xfeefee.
  • начальный символ - 1 находится в диапазоне [0, 0x10000)
  • концевого символа – 1 находится в диапазоне [0, 0x10000)
  • конечная строка больше или равна начальной строке.
  • начальная строка равна конечной строке, затем конечный символ больше, чем начальный символ.

Обратите внимание, что числа, указанные в синтаксисе директивы, являются 1 числами, но фактические диапазоны в PDB основаны на нулях. Следовательно, вышеупомянутые корректировки -1.

Сопоставленные диапазоны точек последовательности и расположения диагностики, к которым применяется директива #line, вычисляются следующим образом.

Пусть d будет строкой с нулевой нумерацией, содержащей директиву #line. Пусть диапазон L = (start: (вначальной строке - 1, в начальном символе - 1), end: (вконцевой строке - 1, в конечном символе - 1)) является диапазоном, начатым с нуля, заданным директивой.

Функция M, которая сопоставляет позицию (строку, символ) в области директивы #line в исходном файле, содержащую директиву #line с сопоставленной позицией (сопоставленной строкой, сопоставленной символом), определяется следующим образом:

M(l, c) =

l == d + 1 => (L.start.line + ld – 1, L.start.character + max(cсмещение символов, 0))

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

Синтаксические конструкции, с которыми связаны точки последовательности, определяются реализацией компилятора и не охватываются этой спецификацией. Компилятор также решает для каждой точки последовательности ее несопоставленный диапазон. Этот диапазон может частично или полностью охватывать связанную конструкцию синтаксиса.

После определения незамеченных диапазонов компилятором функция M, определенных выше, применяется к их начальным и конечным позициям, за исключением конечной позиции всех точек последовательности в области директивы #line, где не сопоставленное расположение находится в строке d + 1 и символ меньше смещения символов. Конечная позиция всех этих точек последовательности L.end.

В примере [5.i] показано, почему необходимо предоставить возможность указания конечной позиции первого диапазона точек последовательности.

Приведенное выше определение позволяет генератору непрослеживаемого исходного кода избежать необходимости глубокого понимания, какие конкретные синтаксические конструкции языка C# создают точки отсчета. Сопоставленные диапазоны точек последовательности, находящиеся в области действия директивы #line, выводятся из их относительного положения по отношению к первому несопоставленному диапазону.

Указание смещения символов позволяет генератору вставлять любой однострочный префикс на первую строку. Этот префикс кода создается как код, который отсутствует в сопоставленном файле. Такой вставленный префикс влияет на значение первого несопоставленного диапазона точек последовательности. Поэтому первый символ последующих диапазонов точек в последовательности необходимо смещать на длину префикса (смещение символа). См. пример [2].

изображения

Примеры

Для ясности примеры используют псевдосинтаксис spanof('...') и lineof('...') для выражения начала сопоставленного диапазона и номера строки указанного фрагмента кода соответственно.

1. Первые и последующие диапазоны

Рассмотрим следующий код с нумерацией строк, начинающейся с нуля, перечисленной справа:

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

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

Существует 4 диапазона точек последовательности, к которым применяется директива, со следующими несопоставленными и сопоставленными интервалами: (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. Смещение символов

Razor создает префикс _builder.Add( длиной 15 символов (включая два ведущих пробела).

Бритва:

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

Сгенерированный 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) смещение символов = 15

Диапазоны:

  • _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: однострочный диапазон

Бритва:

@page "/"
Time: @DateTime.Now

Сгенерированный C#:

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

4. Razor: многострочный диапазон

Бритва:

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

Сгенерированный 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: блоковые конструкции

i. блок, содержащий выражения

В этом примере сопоставленный диапазон первой точки последовательности, связанной с инструкцией IL, которая сгенерирована для оператора _builder.Add(Html.Helper(() => , должен охватывать полное выражение Html.Helper(...) в сгенерированном файле a.razor. Это достигается применением правила [1] к конечной позиции точки последовательности.

@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. блок, содержащий выражения

Использует существующую форму #line line file с тех пор

a) Razor не добавляет префикс, b) { отсутствует в созданном файле, и на него нельзя разместить точку последовательности, поэтому диапазон первой неопознанной точки последовательности неизвестен для Razor.

Начальный символ Console в созданном файле должен быть выровнен с файлом Razor.

@{Console.WriteLine(1);Console.WriteLine(2);}
#line lineof('@{') "a.razor"
  Console.WriteLine(1);Console.WriteLine(2);
#line hidden
iii. блок, содержащий код верхнего уровня (@code, @functions)

Использует существующую форму #line line file с тех пор

a) Razor не добавляет префикс, b) { отсутствует в созданном файле, и на ней не может быть размещена точка последовательности, поэтому диапазон первой неопознанной точки последовательности неизвестен для Razor.

Начальный символ [Parameter] в созданном файле должен быть выровнен с файлом 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

Используется существующая форма #line line file, поскольку а) Razor не добавляет префикс. b) диапазон первой непроецированной точки последовательности может быть неизвестен Razor (или Razor не обязательно должен знать это).

Начальный символ ключевого слова в созданном файле должен быть выровнен с файлом 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