Pipeline de la biblioteca IEEE — Guía de integración

Recorrido de extremo a extremo de cómo la línea q <= to_integer(unsigned(addr)) de un usuario viaja desde el texto VHDL en crudo hasta un grafo real de celdas de netlist. Escrito para que cualquiera que conecte un host (aplicación MAUI, panel web, ejecutor de CLI) pueda entender qué llamadas van a dónde y cuáles son los contratos.

Estado: Modelo de dos rutas del núcleo vigente al 2026-09-14 (verificado contra el código). La forma del pipeline (semilla de LibraryCompiler → árbol de DeferredFunction → síntesis vía IEEEPrimitives.TryLower o tiempo de ejecución vía DeferredFunction.Evaluate) no ha cambiado desde la redacción de la v1.19 (2 de mayo de 2026, #20–#29). Dos adiciones posteriores por incorporar: (1) La Fase 5 (2026-07-22) añadió un lote de funciones IEEE solo de tiempo de ejecución a StandardFunctions.EvaluateFunction — alias de std_logic_arith (shl, shr, ext, sxt, conv_unsigned, conv_signed), math_real (floor, ceil, round, sqrt), y meta-valores de std_logic_1164 (to_x01, is_x); estas no tienen primitivas en la ruta de síntesis (véase la nota en la Capa 4). (2) El valor por defecto para funciones no resueltas ya no devuelve null silenciosamente — emite una advertencia IEEE-STUB y un marcador de posición "0".


Resumen

flowchart LR
    UserVHDL["User VHDL<br/>q &lt;= to_integer(unsigned(addr))"]
    Parser[Parser tokenises]
    LibCompiler[LibraryCompiler<br/>seeds FunctionRegistry<br/>with IEEE names]
    OpParser["Operation.ParseFunctionCall<br/>(recurses on nested)"]
    DF[DeferredFunction tree<br/>Outer DF: to_integer<br/>  Args: [Inner DF: unsigned<br/>            Args: [addr_var]]]
    Synth[ConcurrentAssignSynthesizer]
    Lookup[IEEEPrimitives.TryLower]
    Cells["Cell graph:<br/>addr → BUF (cast=unsigned) → TO_INTEGER → q"]
    Sim[DeferredFunction.Evaluate<br/>(simulator path)]

    UserVHDL --> Parser
    LibCompiler -.seeds.-> Parser
    Parser --> OpParser
    OpParser --> DF
    DF --> Synth
    Synth --> Lookup
    Lookup --> Cells
    DF --> Sim

Dos rutas a partir del mismo árbol de DeferredFunction:

  • Ruta de síntesisIEEEPrimitives.TryLower emite registros Cell. El netlist refleja la forma real del hardware.
  • Ruta de tiempo de ejecuciónDeferredFunction.Evaluate recorre recursivamente los DF anidados y llama a StandardFunctions.EvaluateFunction para calcular el valor simulado.

Capa 1 — El host conecta el LibraryCompiler

Los hosts construyen LibraryCompiler mediante DI, proporcionando todo lo relacionado con rutas a través del registro de opciones:

// Kmila.Shared / MauiProgram / Web Program — host code, NOT lib code
builder.Services.AddSingleton<LibraryCompiler.Services.LibraryCompiler>(provider =>
    new LibraryCompiler.Services.LibraryCompiler(new LibraryCompilerOptions(
        OpenLibrarySource: (key, ct) =>
            FileSystem.OpenAppPackageFileAsync(key)        // MAUI host
                .ContinueWith(t => (Stream?)t.Result, ct),
        CacheStore:        new MauiAppDataBlobStore(),    // host's IBlobStore
        AdditionalSources: provider.GetRequiredService<UserAssetStore>()
                                  .ListUserLibrarySources(),
        Policy:            PickPolicyByFreeRam())));

Para pruebas / CLI funciona la fábrica más simple FromSyncOpener:

var compiler = LibraryCompiler.Services.LibraryCompiler.FromSyncOpener(
    openSync:    _ => null,                              // null → embedded fallback
    cacheStore:  NullBlobStore.Instance,
    policy:      LibraryBuildPolicy.AlwaysProcessCached);

El constructor de la biblioteca lanza ArgumentException si OpenLibrarySource es null. No hay un valor por defecto silencioso que sondee el sistema de archivos.

Lo que hace la biblioteca en la primera llamada

  1. LibraryResolver registra los builtins IEEE incluidos (std_logic_1164, numeric_std) más cualquier AdditionalSources proporcionado por el host.
  2. En CompileAsync(["std_logic_1164", "numeric_std"]), el resolver ordena topológicamente las dependencias (std_logic_1164 antes de numeric_std).
  3. BuildOrchestrator recorre el orden, consulta la política y, o bien extrae de la caché, o bien ejecuta LibraryBuilder.BuildAsync para cada biblioteca.
  4. LibraryBuilder lee los bytes de origen mediante el callback del host (o recurre al recurso embebido para IEEE), extrae por regex los nombres de funciones/procedimientos/tipos, y devuelve un artefacto CompiledLibrary.
  5. Los nombres de símbolos se propagan a FunctionRegistry para que el código de usuario que los referencia se resuelva limpiamente durante el análisis de expresiones.

Hoy los cuerpos aún no se analizan — los nombres se siembran como stubs FunctionDefinition de caja negra. El sintetizador emite primitivas basándose en la coincidencia del nombre (Capa 4 más abajo).


Capa 2 — El parser ve la expresión del usuario

El recorredor de expresiones del parser (Operation.ParseFunctionCall) llega a to_integer y lo reconoce como una función conocida (vía StandardFunctions.IsStandardFunction o FunctionRegistry.IsKnown).

q <= to_integer ( unsigned ( addr ) ) ;

Flujo de tokens:

q  <=  to_integer  (  unsigned  (  addr  )  )  ;

ParseFunctionCall("to_integer") entra con _currentSyntetizeIndex en to_integer. Recorre:

skip "to_integer"      → index at "("
skip "("               → index at "unsigned", parenDepth = 1
loop iter 1:
    token = "unsigned"
    next-token == "(" AND IsStandardFunction("unsigned") → RECURSE
    ParseFunctionCall("unsigned"):
        skip "unsigned"   → index at "("
        skip "("          → index at "addr", parenDepth = 1 (LOCAL)
        loop iter 1:
            token = "addr"
            next-token = ")", NOT "(", so don't recurse
            addr matches a Variable in scope → args.Add(addr_variable)
        loop iter 2:
            token = ")", parenDepth → 0, break
        ShouldDefer("unsigned") → true
        return DeferredFunction("unsigned", [addr_variable], …)
    args.Add(DeferredFunction("unsigned", …))     ← key: nested DF, not bare string
    continue                                        ← skip outer index++
loop iter 2:
    token = ")", parenDepth → 0, break
ShouldDefer("to_integer") → true
return DeferredFunction("to_integer", [DeferredFunction("unsigned", [addr_var])], …)

El resultado es un árbol estructurado:

DeferredFunction("to_integer", DATATYPES.INTEGER, [
    DeferredFunction("unsigned", DATATYPES.STD_LOGIC_VECTOR, [
        Variable("addr", "01010101", STD_LOGIC_VECTOR, width=8)
    ])
])

Antes de #29, el addr interno se descartaba silenciosamente y la llamada externa cargaba Arguments = ["unsigned"] — una cadena simple. Ahora el operando se preserva y se estructura.

Tabla de DeferredFunction.ShouldDefer (v1.19)

"rising_edge"           => true,
"falling_edge"          => true,
"to_integer"            => true,
"conv_integer"          => true,
"to_unsigned"           => true,
"to_signed"             => true,
"resize"                => true,
"shift_left"            => true,
"shift_right"           => true,
"rotate_left"           => true,
"rotate_right"          => true,
"unsigned"              => true,    // type-name overloads (v1.19)
"signed"                => true,
"std_logic_vector"      => true,
"conv_std_logic_vector" => true,

Las funciones que devuelven true sobreviven al análisis como DeferredFunctions y alcanzan tanto la ruta de síntesis como la de tiempo de ejecución. Devolver false (el valor por defecto para todo lo demás) significa evaluación en línea durante el análisis — usado para clog2, funciones de usuario, etc.


Capa 3 — Ruta de tiempo de ejecución (simulación por delta-ciclos)

Cuando el simulador ejecuta q <= to_integer(unsigned(addr)), Operation.Execute() recorre la lista de items. Al llegar al DeferredFunction externo, llama a .Evaluate():

public Variable Evaluate()
{
    var resolvedArgs = Arguments.Select(arg => arg switch
    {
        Variable v          => (object)v,
        DeferredFunction df => (object)df.Evaluate(),    // RECURSE on nested
        string s            => (object)(LookupByName(s) ?? s),
        _                   => arg,
    }).ToArray();

    object result = StandardFunctions.EvaluateFunction(FunctionName, resolvedArgs, Variables);
    // … wrap result in a Variable …
}

Traza para addr = "01010101" (= 85):

  1. to_integer.Evaluate() externo:
    • unsigned.Evaluate() interno:
      • addr_variable es una Variable → devuelta tal cual en resolvedArgs
      • StandardFunctions.EvaluateFunction("unsigned", [addr_variable], …)
        • case "unsigned": return GetStringValue(addr_variable, vars);"01010101"
      • Se envuelve como Variable("DeferredFunction-unsigned", "01010101", STD_LOGIC_VECTOR)
    • resolvedArgs = [unsigned_result_variable]
    • StandardFunctions.EvaluateFunction("to_integer", [unsigned_result_variable], …)
      • case "to_integer": return ToInteger(GetStringValue(arg0));ToInteger("01010101")
      • Corrección v1.19: ToInteger comprueba primero "all 0/1 multi-char" → Convert.ToInt32("01010101", 2) = 85
    • Se envuelve como Variable("DeferredFunction-to_integer", 85, INTEGER)
  2. Empuja 85 a la pila de operandos
  3. La asignación se completa: q.Value = 85

Antes de #29, la llamada interna del paso 1 recibía args = ["unsigned"] (la cadena simple), que GetStringValue devolvía como el literal "unsigned", que ToInteger("unsigned") no lograba analizar → devolvía 0. Cada dirección de memoria indexada mediante to_integer(unsigned(addr)) era efectivamente una escritura a la ubicación 0.


Capa 4 — Ruta de síntesis (generación de netlist)

ConcurrentAssignSynthesizer.SynthesizeInto recorre la misma lista de items pero para emitir celdas, no valores:

foreach (var item in op.Items)
{
    switch (item)
    {
        case Variable v:
            stack.Push(NetForVariable(v, ctx));
            break;
        case DeferredFunction df:
            var args  = ResolveDeferredArgs(df, ctx);            // recurses on nested DFs
            var lower = IEEEPrimitives.TryLower(df.FunctionName,
                          new IEEEPrimitiveArgs(args, ctx, sourceLine));
            if (lower is { } r) stack.Push(r.OutputNet);
            else stack.Push(ctx.Constant($"@{df.FunctionName}", 1));
            break;
        case Operator opr:
            HandleOperator(opr, stack, ctx, sourceLine);
            break;
    }
}

ResolveDeferredArgs es la pieza recursiva:

foreach (var arg in df.Arguments)
{
    switch (arg)
    {
        case Variable v:           nets.Add(NetForVariable(v, ctx)); break;
        case Net n:                nets.Add(n);                       break;
        case DeferredFunction nested:
            var nestedArgs = ResolveDeferredArgs(nested, ctx);         // recursion
            var nestedR    = IEEEPrimitives.TryLower(nested.FunctionName, …);
            nets.Add(nestedR is { } nr ? nr.OutputNet
                                       : ctx.Constant($"@nested_{nested.FunctionName}", 1));
            break;
        case int i:                nets.Add(ctx.Constant(i.ToString(), Math.Max(1, i))); break;
        // string + default fallthrough …
    }
}

Traza para nuestro ejemplo:

  1. El recorredor ve el DeferredFunction("to_integer", [DF unsigned, [addr_var]]) externo.
  2. Se llama a ResolveDeferredArgs sobre el DF externo:
    • Para su único argumento (el DF unsigned anidado):
      • Recurre: ResolveDeferredArgs(inner)[NetForVariable(addr_var)] = [addr_net]
      • IEEEPrimitives.TryLower("unsigned", [addr_net]) → emite una celda BUF con Cast="unsigned", net de salida = _n0
      • Devuelve _n0
    • Args externos = [_n0]
  3. IEEEPrimitives.TryLower("to_integer", [_n0]) → emite una celda TO_INTEGER con el pin de entrada A cableado a _n0, net de salida = _n1
  4. _n1 se empuja a la pila de operandos.
  5. La asignación externa q <= ... emite una celda BUF que alimenta q desde _n1.

Grafo de celdas final:

addr (PortIn) ──→ BUF (Cast=unsigned, _n0) ──→ TO_INTEGER (_n1) ──→ BUF ──→ q (Signal)

Cada celda lleva la atribución de línea de origen para que el visor de esquemáticos pueda agrupar las celdas de vuelta a su línea VHDL.

Tabla de tipos de celda de IEEEPrimitives

Nombre de función Celda emitida Attrs Ancho de salida
rising_edge EDGE_DETECT Edge=rising 1
falling_edge EDGE_DETECT Edge=falling 1
to_integer (+ conv_integer) TO_INTEGER Width, Signed 32
to_unsigned TO_VECTOR Signed=false, Width declarado
to_signed (+ conv_std_logic_vector) TO_VECTOR Signed=true, Width declarado
resize RESIZE FromWidth, ToWidth solicitado
shift_left / shift_right SHL / SHR preserva
rotate_left / rotate_right ROL / ROR preserva
unsigned, signed, std_logic_vector BUF Cast preserva

Cobertura de síntesis vs. tiempo de ejecución (Fase 5, 2026-07). La tabla anterior es el registro completo de la ruta de síntesis (IEEEPrimitives, sembrado en su constructor estático). Las adiciones de la Fase 5 — std_logic_arith shl/shr/ext/sxt/conv_unsigned/conv_signed, math_real floor/ceil/round/sqrt, std_logic_1164 to_x01/is_x — existen solo en la ruta de tiempo de ejecución (StandardFunctions.EvaluateFunction). Llamar a una de ellas se traduce al marcador-constante + diagnóstico descritos en "Cuando las cosas no se traducen" más abajo: la simulación es correcta, el netlist simplemente muestra un marcador de posición para ese nodo.


Poniéndolo todo junto — muestra de código completa

Integración de host de extremo a extremo con las cuatro capas:

// 1. HOST DI registration
builder.Services.AddSingleton<LibraryCompiler.Services.LibraryCompiler>(provider =>
    new LibraryCompiler.Services.LibraryCompiler(new LibraryCompilerOptions(
        OpenLibrarySource: (key, ct) => FileSystem.OpenAppPackageFileAsync(key)
                                            .ContinueWith(t => (Stream?)t.Result, ct),
        CacheStore:        new MauiAppDataBlobStore(),
        AdditionalSources: provider.GetRequiredService<UserAssetStore>().ListUserLibrarySources(),
        Policy:            LibraryBuildPolicy.AlwaysProcessCached)));

// 2. AT THE START OF A USER PROJECT BUILD
var compiler = provider.GetRequiredService<LibraryCompiler.Services.LibraryCompiler>();
var ctx = new ProjectContext(
    Files:      new[] { "uart.vhd", "spi.vhd", "top.vhd" },
    UseClauses: new[] { "std_logic_1164", "numeric_std" });
var project = await compiler.CompileProjectAsync(ctx);

// 3. SEED FunctionRegistry FROM compiled libs (Interpreter side)
foreach (var lib in project.Libraries)
foreach (var sym in lib.Functions)
    FunctionRegistry.Instance.Register(new FunctionDefinition
    {
        Name        = sym.Name,
        PackageName = lib.LibraryName,
        IsPure      = sym.Kind == LibrarySymbolKind.Function
    });

// 4. PARSE + SYNTH user files (existing Interpreter pipeline)
foreach (var file in project.UserFiles)
{
    var raw = File.ReadAllText(file);
    var (entities, archCount) = ProcessVhdlCode(raw);   // existing entry point
    foreach (var entity in entities)
    {
        // entity.Architecture.Tasks contains Operations whose items
        // include DeferredFunctions for the IEEE calls. The synthesizer
        // emits real cells via IEEEPrimitives.TryLower.
    }
}

El host es dueño de los pasos 1 y 2 (ruta / política / forma del proyecto). El lado del Interpreter maneja el 3 y el 4 automáticamente — el ProcessVhdlCode existente ya llama a IEEELibraryLoader.Instance.EnsureLoaded(), que delega en LibraryCompiler, de modo que incluso los hosts que no construyen un LibraryCompiler directamente obtienen las semillas IEEE.


Cuando las cosas no se traducen

Si un usuario llama a una función que no tiene emisor de primitiva (función de usuario personalizada, función específica del proveedor aún no registrada), la síntesis emite:

stack.Push(ctx.Constant($"@{df.FunctionName}", 1));
ctx.AddDiagnostic("UnknownIEEEFunction",
    $"No IEEE primitive registered for '{df.FunctionName}' — emitted a constant " +
    "placeholder. The simulator's runtime evaluator handles the call but the " +
    "netlist won't reflect the real hardware shape.", sourceLine);

El simulador sigue funcionando (el evaluador de tiempo de ejecución hace su trabajo), el netlist simplemente muestra una constante de marcador de posición. El diagnóstico se expone a través del canal Synthesis.Diagnostic existente hacia el netlist, que el visor de esquemáticos puede representar como un marcador amarillo.

Para registrar un nuevo emisor, véase ../LibraryCompiler/README.md#extending-ieeeprimitives.


Pruebas que ejercitan este pipeline

Suite Qué demuestra
Interpreter/Tests/LibraryCompilerTests.cs (17 casos) Superficie del módulo — resolver, builder, orchestrator, política, almacenes de blobs, compilación de proyectos
Interpreter/Tests/IEEELibraryLoaderTests.cs (6 casos) La fachada heredada aún siembra FunctionRegistry correctamente
Interpreter/Tests/IEEEPrimitivesTests.cs (18 casos) Cada emisor produce el CellKind / ancho / attrs correcto
Interpreter/Tests/IEEELoweringEndToEndTests.cs (6 casos) DeferredFunction → IEEEPrimitives → grafo de celdas
Interpreter/Tests/NestedIEEECallTests.cs (7 casos) Anidamiento en el parser, evaluación en tiempo de ejecución de llamadas anidadas, topología del cableado de síntesis

Ejecútalas todas (xUnit):

cd Interpreter && dotnet test

Estas suites (LibraryCompiler, IEEELibraryLoader, IEEEPrimitives, IEEELoweringEndToEnd, NestedIEEECall) cubren el pipeline IEEE de extremo a extremo y forman parte de la suite de regresión xUnit del módulo (véase ../Interpreter/README.md §7).


Recapitulación del contrato

  • Biblioteca: es dueña de los nombres, tipos, interfaces, la tabla de primitivas, el orchestrator. Nunca decide rutas.
  • Host: es dueño del sistema de archivos, la elección de política, la implementación de caché, el almacenamiento de plugins de usuario. Conecta las implementaciones concretas a la biblioteca a través de LibraryCompilerOptions.
  • Síntesis: consume árboles de DeferredFunction del parser y emite registros Cell a través de la tabla de primitivas.
  • Tiempo de ejecución: consume los mismos árboles de DeferredFunction y calcula valores simulados vía Evaluate() + StandardFunctions.

Dos rutas a partir de la misma representación intermedia, ambas cubiertas por pruebas, ambas produciendo la respuesta correcta.


Redacción del núcleo: 2 de mayo de 2026 (#20–#29). Verificado contra el código y anotado con las adiciones de la Fase 5 de 2026-07 el 2026-09-14. Detalle por versión: los antiguos CHANGELOG_v1.16.mdCHANGELOG_v1.19.md ahora residen bajo _archive/changelogs/ y están fusionados en el consolidado CHANGELOG.md.