다음을 통해 공유


향상된 #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

상세 디자인

다음과 같이 지시문에 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의 실제 범위는 0부터 시작하는 것입니다. 따라서 위의 -1 조정.

시퀀스 지점의 매핑된 범위와 지시문이 #line 적용되는 진단 위치는 다음과 같이 계산됩니다.

지시문을 포함하는 매핑되지 않은 줄의 0부터 시작하는 번호로 설정합니다#line. 범위 L = (start: (start line - 1, start character - 1), end: (end line - 1, end character - 1)) 지시문에 지정된 0부터 시작하는 범위가 됩니다.

매핑된 위치(매핑된 선, 매핑된 문자)에 #line 지시문을 포함하는 소스 파일의 #line 지시문 범위 내에 있는 위치(선, 문자)를 매핑하는 함수 M은 다음과 같이 정의됩니다.

M(l, c) =

l(2) == d + 1 => (L.start.line + ld – 1, L.start.character + max(c문자 오프셋, 0))

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

시퀀스 지점이 연결된 구문 구문은 컴파일러 구현에 의해 결정되며 이 사양에서는 다루지 않습니다. 컴파일러는 매핑되지 않은 범위의 각 시퀀스 지점도 결정합니다. 이 범위는 연결된 구문 구문을 부분적으로 또는 완전히 포함할 수 있습니다.

매핑되지 않은 범위가 컴파일러에 의해 결정되면 위에 정의된 함수 M이 시작 및 끝 위치에 적용됩니다. 매핑되지 않은 위치가 줄 d + 1이고 문자 오프셋보다 작은 문자가 있는 #line 지시문 범위 내의 모든 시퀀스 지점의 끝 위치를 제외하고. 이러한 모든 시퀀스 지점의 끝 위치는 L.end입니다.

예제 [5.i]는 첫 번째 시퀀스 지점 범위의 끝 위치를 지정하는 기능을 제공해야 하는 이유를 보여 줍니다.

위의 정의를 사용하면 매핑되지 않은 소스 코드를 생성하여 C# 언어의 정확한 소스 구문이 시퀀스 지점을 생성하는지 잘 알지 못하게 할 수 있습니다. 지시문 범위에 있는 시퀀스 지점의 #line 매핑된 범위는 매핑되지 않은 해당 범위의 상대 위치에서 매핑되지 않은 첫 번째 범위로 파생됩니다.

문자 오프셋지정하면 생성기에서 첫 번째 줄에 한 줄 접두사를 삽입할 수 있습니다. 이 접두사는 매핑된 파일에 없는 생성된 코드입니다. 이러한 삽입된 접두사는 매핑되지 않은 첫 번째 시퀀스 지점 범위의 값에 영향을 줍니다. 따라서 후속 시퀀스 지점 범위의 시작 문자는 접두사(문자 오프셋)의 길이로 오프셋되어야 합니다. 예제 [2]를 참조하세요.

이미지

예시

명확하게 하기 위해 예제에서는 지정된 코드 조각의 매핑된 범위 시작 위치와 줄 번호를 각각 표현하기 위해 사용 및 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( (두 개의 선행 공백 포함).

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 = 4L = (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) 식을 포함하는 블록

이 예제에서는 문에 대해 내보내는 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

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