次の方法で共有


強化された #line ディレクティブ

手記

この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。

機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。

機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。

チャンピオンの課題: https://github.com/dotnet/csharplang/issues/4747

概要

コンパイラは、#line ディレクティブによって定義されたマッピングを、PDB に出力される診断の場所とシーケンス ポイントに適用します。

現在、ソース コードから開始文字が推論されている間は、行番号とファイル パスのみをマップできます。 提案では、完全スパン マッピングの指定を許可します。

モチベーション

C# ソース コード (ASP.NET Razor など) を生成する DSL では、現在、#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 行目にマップしますが、列はオフになります (7 ではなく 16)。

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 には影響を与えません。デバッガーは Razor ページの @DateTime.Now スニペットにブレークポイントや停止を配置しません。

この提案で対処される問題: https://github.com/dotnet/roslyn/issues/43432https://github.com/dotnet/roslyn/issues/46526

詳細な設計

pp_line ディレクティブで使用される line_indicator の構文を次のように修正します。

現在:

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 つの 10 進数(開始行、開始文字 、終了行 、終了文字 、文字オフセット )、4 つの 10 進数(開始行、開始文字 、終了行 、終了文字 )、または単一のもの(行)を受け入れます。

文字オフセット 指定されていない場合、既定値は 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 内の実際のスパンは 0 から始まる点に注意してください。 したがって、上記の -1 調整が行われました。

シーケンスポイントのマッピングされたスパンと、#line ディレクティブが適用される診断の位置は、次のように計算されます。

d#line ディレクティブを含むマップされていない行の 0 から始まる番号にします。 スパン L = (開始: (開始行 - 1, 開始文字 - 1), 終了: (終了行 - 1, 終了文字 - 1)) をディレクティブで指定されたゼロベースのスパンと定義します。

#line ディレクティブを含むソース ファイル内の #line ディレクティブのスコープ内の位置 (行、文字) をマップされた位置 (マップされた行、マップされた文字) にマップする関数 M は、次のように定義されます。

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 が、その開始位置と終了位置に適用されます。ただし、マップされていない場所が行 d + 1 であり、文字位置が文字オフセットより小さい #line ディレクティブの範囲内にあるすべてのシーケンス ポイントの終了位置は除きます。 これらすべてのシーケンス ポイントの終了位置は L.endです。

例 [5.i] は、最初のシーケンス ポイント スパンの終了位置を指定する機能を提供する必要がある理由を示しています。

上記の定義により、マップされていないソース コードのジェネレーターは、どの C# 言語の正確なソース コンストラクトがシーケンス ポイントを生成するかについての詳しい知識を回避できます。 #line ディレクティブのスコープ内のシーケンス ポイントのマップされたスパンは、対応するマップされていないスパンの最初のマップされていないスパンの相対位置から派生します。

文字オフセット を指定すると、ジェネレーターは先頭行に 1 行のプレフィックスを挿入できます。 このプレフィックスは、マップされたファイルに存在しないコードが生成されます。 このような挿入されたプレフィックスは、マップされていない最初のシーケンス ポイント スパンの値に影響します。 したがって、後続のシーケンス ポイント スパンの開始文字は、プレフィックスの長さ (文字オフセット) でオフセットする必要があります。 例 [2] を参照してください。

image

使用例

わかりやすくするために、spanof('...')lineof('...') 擬似構文を使用して、指定されたコード スニペットのマップされたスパンの開始位置と行番号をそれぞれ表します。

1. 最初およびそれ以降のスパン

マップされていない 0 から始まる行番号が右側に一覧表示されている次のコードを考えてみましょう。

#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 では、長さ 15 の _builder.Add( プレフィックス (先頭に 2 つのスペースを含む) が生成されます。

Razor:

@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: 単一行スパン

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: 複数行スパン

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. 式を含むブロック

この例では、_builder.Add(Html.Helper(() => ステートメントに対して出力される IL 命令に関連付けられている最初のシーケンス ポイントのマップされたスパンは、生成されたファイル a.razor内の Html.Helper(...) の式全体をカバーする必要があります。 これは、ルール[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

a) Razor ではプレフィックスが追加されないため、既存の #line line file フォームを使用します。 b) マップされていない最初のシーケンス ポイントのスパンが 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