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íaIEEEPrimitives.TryLowero tiempo de ejecución víaDeferredFunction.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 aStandardFunctions.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 devuelvenullsilenciosamente — emite una advertenciaIEEE-STUBy un marcador de posición"0".
Resumen
flowchart LR
UserVHDL["User VHDL<br/>q <= 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íntesis —
IEEEPrimitives.TryLoweremite registrosCell. El netlist refleja la forma real del hardware. - Ruta de tiempo de ejecución —
DeferredFunction.Evaluaterecorre recursivamente los DF anidados y llama aStandardFunctions.EvaluateFunctionpara 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
LibraryResolverregistra los builtins IEEE incluidos (std_logic_1164,numeric_std) más cualquierAdditionalSourcesproporcionado por el host.- En
CompileAsync(["std_logic_1164", "numeric_std"]), el resolver ordena topológicamente las dependencias (std_logic_1164antes denumeric_std). BuildOrchestratorrecorre el orden, consulta la política y, o bien extrae de la caché, o bien ejecutaLibraryBuilder.BuildAsyncpara cada biblioteca.LibraryBuilderlee 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 artefactoCompiledLibrary.- Los nombres de símbolos se propagan a
FunctionRegistrypara 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):
to_integer.Evaluate()externo:unsigned.Evaluate()interno:addr_variablees una Variable → devuelta tal cual en resolvedArgsStandardFunctions.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)
- Empuja 85 a la pila de operandos
- 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:
- El recorredor ve el
DeferredFunction("to_integer", [DF unsigned, [addr_var]])externo. - Se llama a
ResolveDeferredArgssobre el DF externo:- Para su único argumento (el
DF unsignedanidado):- Recurre:
ResolveDeferredArgs(inner)→[NetForVariable(addr_var)]=[addr_net] IEEEPrimitives.TryLower("unsigned", [addr_net])→ emite una celdaBUFconCast="unsigned", net de salida =_n0- Devuelve
_n0
- Recurre:
- Args externos =
[_n0]
- Para su único argumento (el
IEEEPrimitives.TryLower("to_integer", [_n0])→ emite una celdaTO_INTEGERcon el pin de entrada A cableado a_n0, net de salida =_n1_n1se empuja a la pila de operandos.- La asignación externa
q <= ...emite una celdaBUFque alimentaqdesde_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_arithshl/shr/ext/sxt/conv_unsigned/conv_signed, math_realfloor/ceil/round/sqrt, std_logic_1164to_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
DeferredFunctiondel parser y emite registrosCella través de la tabla de primitivas. - Tiempo de ejecución: consume los mismos árboles de
DeferredFunctiony calcula valores simulados víaEvaluate()+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.md … CHANGELOG_v1.19.md ahora residen bajo
_archive/changelogs/ y están fusionados en el consolidado
CHANGELOG.md.