Compartir a través de


Directivas #line mejoradas

Nota

Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos e se incorporan en la especificación ECMA actual.

Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de lenguaje (LDM) correspondientes.

Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones de .

Problema planteado por el experto: https://github.com/dotnet/csharplang/issues/4747

Resumen

El compilador aplica la asignación definida por directivas #line a las ubicaciones de diagnóstico y los puntos de secuencia emitidos hacia la PDB.

Actualmente, solo se pueden mapear el número de línea y la ruta de acceso del archivo, mientras que el carácter inicial se infiere del código fuente. La propuesta es permitir la especificación de la asignación completa del intervalo.

Motivación

Las DSL que generan código fuente de C# (por ejemplo, ASP.NET Razor) no pueden generar actualmente una asignación de origen precisa mediante directivas #line. Esto da como resultado una experiencia de depuración degradada en algunos casos, ya que los puntos de secuencia emitidos a la PDB no se pueden asignar a la ubicación precisa en el código fuente original.

Por ejemplo, el siguiente código de Razor

@page "/"
Time: @DateTime.Now

genera código similar al siguiente (simplificado):

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

La directiva anterior asignaría el punto de secuencia que emite el compilador para la instrucción _builder.Add(DateTime.Now); a la línea 2, pero la columna estaría incorrecta (16 en lugar de 7).

El generador de código fuente de Razor genera realmente incorrectamente el código siguiente:

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

La intención era conservar el carácter inicial y esto es eficaz para la asignación de ubicaciones de diagnóstico. Sin embargo, esto no funciona para los puntos de secuencia, ya que la directiva #line solo se aplica a los puntos de secuencia que la siguen. No hay ningún punto de secuencia en medio de la instrucción _builder.Add(DateTime.Now); (los puntos de secuencia solo se pueden emitir en instrucciones de IL con pila de evaluación vacía). Por lo tanto, la directiva #line 2 en el código anterior no tiene ningún efecto en la PDB generada, y el depurador no colocará un punto de interrupción ni se detendrá en el fragmento de código @DateTime.Now en la página de Razor.

Cuestiones abordadas por esta propuesta: https://github.com/dotnet/roslyn/issues/43432https://github.com/dotnet/roslyn/issues/46526

Diseño detallado

Modificamos la sintaxis de line_indicator usada en la directiva pp_line de la siguiente manera:

Actuales:

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

Propuesto:

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

Es decir, la directiva #line aceptaría 5 números decimales (línea de inicio, carácter inicial, línea final, carácter final, desplazamiento de caracteres), 4 números decimales (línea de inicio, carácter de inicio, línea final, carácter final) o uno único (línea).

Si el desplazamiento de caracteres no se especifica, su valor predeterminado es 0; de lo contrario, especifica el número de caracteres UTF-16. El número debe ser no negativo y menor que la longitud de la línea después de la directiva #line del archivo no asignado.

(línea inicial, carácter inicial)-(línea final, carácter final) especifica un intervalo en el archivo asignado. de línea de inicio y de línea final son enteros positivos que especifican números de línea. carácter inicial, carácter final son enteros positivos que especifican números de caracteres UTF-16. línea de inicio, carácter de inicio, línea final, carácter final se basan en 1, lo que significa que la primera línea del archivo y el primer carácter UTF-16 en cada línea se asigna el número 1.

La aplicación restringiría estos números para que especifiquen un intervalo de origen de punto de secuencia válido:

  • línea inicial - 1 está dentro del rango [0, 0x20000000) y no es igual a 0xfeefee.
  • línea final - 1 está dentro del intervalo [0, 0x20000000) y no es igual a 0xfeefee.
  • El carácter inicial - 1 está dentro del rango [0, 0x10000)
  • carácter final - 1 está dentro del rango [0, 0x10000)
  • La línea final de es mayor o igual que la línea de inicio de.
  • línea inicial es igual a línea final entonces carácter final es mayor que carácter inicial.

Tenga en cuenta que los números especificados en la sintaxis de directiva son números basados en 1, pero los intervalos reales de la PDB se basan en cero. De ahí los ajustes de -1 anteriores.

Los intervalos asignados de los puntos de secuencia y las ubicaciones de diagnóstico a los que se aplica la directiva #line se calculan de la siguiente manera.

Sea d el número de base cero de la línea no asignada que contiene la directiva #line. Deje que span L = (start: (línea de inicio - 1, carácter de inicio - 1), end: (línea de finalización - 1, carácter final - 1)) sea un intervalo de base cero especificado por la directiva.

La función M que asigna una posición (línea, carácter) dentro del ámbito de la directiva #line del archivo de origen que contiene la directiva #line a una posición asignada (línea asignada, carácter asignado) se define de la siguiente manera:

M(l, c) =

l == d + 1 => (L.start.line + ld – 1, L.start.character + max(ccharacter offset, 0))

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

Las construcciones de sintaxis a las que están asociados los puntos de secuencia están determinadas por la implementación del compilador y no cubiertas por esta especificación. El compilador también decide el intervalo no mapeado para cada punto de secuencia. Este intervalo puede cubrir parcialmente o completamente la construcción de sintaxis asociada.

Una vez que el compilador determina los intervalos no asignados, la función M definida anteriormente se aplica a sus posiciones inicial y final, con la excepción de la posición final de todos los puntos de secuencia dentro del ámbito de la directiva #line, cuya ubicación no asignada está en línea d + 1 y carácter menor que el desplazamiento de caracteres. La posición final de todos estos puntos de secuencia es L.end.

En el ejemplo [5.i] se muestra por qué es necesario proporcionar la capacidad de especificar la posición final del primer intervalo de puntos de secuencia.

La definición anterior permite que el generador del código fuente sin mapa evite tener conocimiento detallado de cuáles construcciones exactas del lenguaje C# generan puntos de secuencia. Los intervalos asignados de los puntos de secuencia dentro del ámbito de la directiva #line se obtienen de la posición relativa de los correspondientes intervalos no asignados respecto al primer intervalo no asignado.

Especificar el desplazamiento de caracteres permite al generador insertar cualquier prefijo de una sola línea en la primera línea. Este prefijo es un código generado que no está presente en el archivo mapeado. Este prefijo insertado afecta al valor del primer intervalo de puntos de secuencia sin asignar. Por lo tanto, el carácter inicial de los intervalos de puntos de secuencia posteriores debe desplazarse en función de la longitud del prefijo (desplazamiento de carácter). Vea el ejemplo [2].

imagen

Ejemplos

Para mayor claridad, en los ejemplos se utilizan el pseudocódigo spanof('...') y lineof('...') para expresar la posición inicial del intervalo asignado y el número de línea, respectivamente, del fragmento de código especificado.

1. Intervalo primero y siguientes

Observe el código siguiente con números de línea sin asignar basados en cero enumerados a la derecha:

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

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

Hay 4 intervalos de puntos de secuencia a los que se aplica la directiva con los siguientes intervalos no asignados y asignados: (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. Desplazamiento de caracteres

Razor genera el prefijo _builder.Add( con una longitud de 15 (incluidos dos espacios iniciales).

Razor:

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

C# generado:

#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) desplazamiento de caracteres = 15

Intervalos:

  • _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: rango de una sola línea

Razor:

@page "/"
Time: @DateTime.Now

C# generado:

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

4. Razor: intervalo de varias líneas

Cuchilla

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

C# generado:

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

5. Razor: construcciones de bloque

i. bloque que contiene expresiones

En este ejemplo, el intervalo asignado del primer punto de secuencia asociado a la instrucción IL que se emite para la instrucción _builder.Add(Html.Helper(() => debe cubrir toda la expresión de Html.Helper(...) en el archivo generado a.razor. Esto se logra mediante la aplicación de la regla [1] a la posición final del punto de secuencia.

@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. bloque que contiene sentencias

Usa el formato de #line line file existente desde

a) Razor no agrega ningún prefijo, b) { no está presente en el archivo generado y no puede haber un punto de secuencia colocado en él, por lo que el intervalo del primer punto de secuencia sin asignar es desconocido para Razor.

El carácter inicial de Console en el archivo generado debe estar alineado con el archivo de Razor.

@{Console.WriteLine(1);Console.WriteLine(2);}
#line lineof('@{') "a.razor"
  Console.WriteLine(1);Console.WriteLine(2);
#line hidden
iii. bloque que contiene código de nivel superior (@code, @functions)

Usa el formato de #line line file existente desde

a) Razor no agrega ningún prefijo, b) { no está presente en el archivo generado y no puede haber un punto de secuencia colocado en él, por lo que el intervalo del primer punto de secuencia sin asignar es desconocido para Razor.

El carácter inicial de [Parameter] en el archivo generado debe estar alineado con el archivo de 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

Usa el formulario #line line file existente, ya que a) Razor no agrega ningún prefijo. b) es posible que Razor no conozca el intervalo del primer punto de secuencia sin asignar (o no debería necesitar conocerlo).

El carácter inicial de la palabra clave en el archivo generado debe estar alineado con el archivo de 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