LibraryCompiler
Compilación de bibliotecas como módulo hermano de Parser / Interpreter / Debugger / TimeMachine. Es dueño de los builtins de IEEE, expone una API portable dirigida por el host y produce artefactos compilados que el resto del simulador (en particular Interpreter.Synthesis.IEEEPrimitives) consume para bajar las llamadas a funciones IEEE a primitivas reales de netlist.
Estado: v1.19 (2 de mayo de 2026). Incluye #22–#26 (extracción + política + proyecto + paralelo + persistencia) más la tubería de bajado de primitivas IEEE agregada en #27–#29.
Tabla de contenidos
- Regla de diseño (estricta)
- Inicio rápido
- Qué hace la biblioteca en realidad
- Modos de política de construcción
- Orquestación lineal vs paralela
- Caché persistente mediante IBlobStore
- Proyectos de múltiples archivos
- Bibliotecas importadas por el usuario (plugins)
- Integración con el Interpreter — bajado de primitivas IEEE
- Extender IEEEPrimitives
- Pruebas
- Fuera de alcance (territorio del host)
Regla de diseño (estricta)
Este módulo toma cero decisiones de sistema de archivos. Sin Environment.SpecialFolder, sin recorrer AppContext.BaseDirectory, sin ~/.kmila/ embebido en el código. Toda cuestión relacionada con rutas la inyecta el host a través de LibraryCompilerOptions:
public sealed record LibraryCompilerOptions(
Func<string, CancellationToken, Task<Stream?>> OpenLibrarySource,
IBlobStore? CacheStore = null,
IReadOnlyList<LibrarySource>? AdditionalSources = null,
LibraryBuildPolicy? Policy = null);
El mismo patrón de DI que JSONFileReader / Traductor / UserAssetStore ya usan en otras partes del simulador. El constructor de la biblioteca lanza ArgumentException si OpenLibrarySource es nulo — no hay un mecanismo de respaldo de "usar el directorio actual por defecto" que pudiera sorprender a un entorno hospedado.
Inicio rápido
Pruebas / CLI / sondeos puntuales
using LibraryCompiler.Services;
using LibraryCompiler.Repositories;
var compiler = LibraryCompiler.Services.LibraryCompiler.FromSyncOpener(
openSync: _ => null, // host returns no source
cacheStore: NullBlobStore.Instance, // no persistence
policy: LibraryBuildPolicy.AlwaysProcessCached);
// Resolves the user's `use IEEE.std_logic_1164.all` / `use IEEE.numeric_std.all`
// clauses, builds each library once, returns the artifacts.
IReadOnlyList<CompiledLibrary> libs =
await compiler.CompileAsync(new[] { "std_logic_1164", "numeric_std" });
foreach (var lib in libs)
Console.WriteLine($"{lib.LibraryName}: {lib.Functions.Count} symbols, hash {lib.ContentHash[..8]}");
Cableado en el host MAUI
// In MauiProgram.CreateMauiApp() (HOST side — out of scope for this module)
builder.Services.AddSingleton<LibraryCompiler>(provider =>
{
var policy = ProbeFreeRamAndPickPolicy(); // host's heuristic
return new LibraryCompiler(new LibraryCompilerOptions(
OpenLibrarySource: (key, ct) => FileSystem.OpenAppPackageFileAsync(key)
.ContinueWith(t => (Stream?)t.Result, ct),
CacheStore: new MauiAppDataBlobStore(), // host's IBlobStore impl
AdditionalSources: provider.GetRequiredService<UserAssetStore>().ListUserLibrarySources(),
Policy: policy));
});
La biblioteca nunca ve FileSystem.AppDataDirectory directamente — el host lo envuelve dentro del callback / blob store y los entrega por ahí.
Qué hace la biblioteca en realidad
flowchart LR
User[(User VHDL)]
Compiler[LibraryCompiler<br/>facade]
Resolver[LibraryResolver<br/>name → source<br/>topo sort]
Builder[LibraryBuilder<br/>regex symbol extract]
Orch[BuildOrchestrator<br/>linear / parallel<br/>policy enforcement]
Cache[in-memory dict]
Blob[/IBlobStore<br/>host-supplied/]
Out[("CompiledLibrary[]")]
User -->|use IEEE.numeric_std.all| Compiler
Compiler --> Resolver
Resolver -->|"topo-ordered LibrarySource[]"| Orch
Orch -->|cache hit| Cache
Orch -->|cache hit| Blob
Orch -->|cache miss| Builder
Builder -.embedded resource.-> Builder
Builder -.host callback.-> Builder
Builder --> Cache
Builder --> Blob
Cache --> Out
Blob --> Out
| Cuestión | Responsable |
|---|---|
| Resolución de nombre lógico → clave de fuente + orden topológico | LibraryResolver |
| Lectura de bytes de la fuente (callback del host o recurso embebido) | LibraryBuilder.ReadSourceBytesAsync |
| Orquestación lineal vs paralela | BuildOrchestrator.BuildAsync |
| Caché de artefactos en memoria | BuildOrchestrator._processCache |
| Aplicación de la política de caché (OnDemand / ProcessCached / Persistent) | BuildOrchestrator.BuildOneAsync |
| (De)serialización de lectura/escritura de persistencia | BuildOrchestrator.TryRead/WriteFromBlobAsync |
| Decisiones de ruta del sistema de archivos / directorio de datos de la app | HOST (nunca la biblioteca) |
| Heurística de RAM libre que elige la política por defecto | HOST (nunca la biblioteca) |
| Ubicación de almacenamiento e interfaz de los plugins del usuario | HOST (nunca la biblioteca) |
Implementaciones concretas de IBlobStore |
HOST (la biblioteca solo incluye las implementaciones de referencia InMemory + Null) |
Modos de política de construcción
public enum LibraryBuildMode
{
OnDemand, // re-parse every call, drop after read
ProcessCached, // session-scoped in-memory retention
Persistent // session + IBlobStore round-trip
}
public sealed class LibraryBuildPolicy
{
public LibraryBuildMode Default { get; init; } = LibraryBuildMode.ProcessCached;
public IReadOnlyDictionary<string, LibraryBuildMode> PerLibrary { get; init; }
public LibraryBuildMode ResolveFor(string libraryName) => …;
}
| Modo | RAM | Disco | Cuándo |
|---|---|---|---|
| OnDemand | reanaliza en cada llamada, descarta tras la lectura | ninguno | dispositivo de poca RAM; muchas compilaciones puntuales |
| ProcessCached | retención en memoria acotada a la sesión | ninguno | sesión típica de editor interactivo (por defecto) |
| Persistent | sesión + ida y vuelta a IBlobStore |
decisión del host | escritorio / estación de trabajo de desarrollo, construcciones repetidas de proyecto completo |
Anulaciones por biblioteca
var policy = new LibraryBuildPolicy
{
Default = LibraryBuildMode.OnDemand, // tight RAM budget
PerLibrary = new Dictionary<string, LibraryBuildMode>(StringComparer.OrdinalIgnoreCase)
{
["std_logic_1164"] = LibraryBuildMode.ProcessCached, // cache the hot one
["numeric_std"] = LibraryBuildMode.ProcessCached,
// every user plugin stays OnDemand by default
}
};
Constructores de conveniencia: LibraryBuildPolicy.AlwaysOnDemand, AlwaysProcessCached, AlwaysPersistent.
La biblioteca no elige el valor por defecto por ti. El host inspecciona el dispositivo (RAM libre, cuota de disco, ajuste del usuario) y decide. Si el host no suministra una política, la biblioteca usa ProcessCached por defecto — pero emite una entrada de configuración para que la elección sea auditable.
Orquestación lineal vs paralela
// Deterministic, single thread, topological order.
var libs = await compiler.CompileAsync(useClauses, BuildExecutionMode.Linear, ct);
// Concurrent, bounded by orchestrator.MaxParallelism (default = ProcessorCount/2).
// Output is sorted by library name on emit so diagnostics stay deterministic.
var libs = await compiler.CompileAsync(useClauses, BuildExecutionMode.Parallel, ct);
MaxParallelism usa por defecto Environment.ProcessorCount / 2 para respetar el presupuesto del GC de estación de trabajo establecido en #7. Los hosts pueden ajustarlo hacia arriba o hacia abajo:
compiler.Orchestrator.MaxParallelism = 4;
Usa el modo lineal para las ejecuciones de "fixture único" del editor (deterministas, salida de diagnóstico más simple). Usa el modo paralelo para construcciones de proyecto completo donde 5–10 bibliotecas de usuario + IEEE necesitan síntesis al mismo tiempo.
Caché persistente mediante IBlobStore
La biblioteca define la interfaz, el host escribe las implementaciones concretas. Esta es la costura que mantiene a la biblioteca portable entre MAUI / Web / escritorio.
public interface IBlobStore
{
Task<bool> ExistsAsync(string key, CancellationToken ct);
Task<byte[]?> ReadAsync(string key, CancellationToken ct);
Task WriteAsync(string key, byte[] payload, CancellationToken ct);
Task DeleteAsync(string key, CancellationToken ct);
IAsyncEnumerable<string> EnumerateKeysAsync(CancellationToken ct);
}
Implementaciones de referencia que incluye la biblioteca
| Clase | Uso |
|---|---|
InMemoryBlobStore |
pruebas, persistencia solo de sesión sin disco |
NullBlobStore.Instance |
persistencia deshabilitada (singleton) |
Implementaciones concretas que escribe el host
| Clase (host) | Respaldo | Dónde |
|---|---|---|
FileSystemBlobStore(rootPath) |
sistema de archivos del SO | escritorio / árbol de desarrollo |
MauiAppDataBlobStore() |
FileSystem.AppDataDirectory |
móvil / iOS / Android |
IsolatedStorageBlobStore() |
IsolatedStorageFile |
en sandbox |
La biblioteca usa una clave de hash de contenido (libcache/<library_name>.json que contiene el SHA-256 de la fuente) y registros CompiledLibrary serializados en JSON. El desalojo es decisión del host — la biblioteca emite suficiente información para que el host calcule el uso y elimine entradas; el presupuesto de tamaño vive en los ajustes del host.
Proyectos de múltiples archivos
var ctx = new ProjectContext(
Files: new[] { "src/uart.vhd", "src/spi.vhd", "src/top.vhd" },
UseClauses: new[] { "std_logic_1164", "numeric_std", "my_pkg" },
TopEntity: "top");
var project = await compiler.CompileProjectAsync(ctx);
// project.Libraries → CompiledLibrary[] (IEEE + user libs)
// project.UserFiles → pass-through to the existing Parser pipeline
// project.Diagnostics → aggregated lib-side warnings / errors
Los diseños de FPGA reales abarcan muchos archivos .vhd. CompileProjectAsync es el punto de entrada del botón "Build" del editor. El host preescanea los archivos del usuario en busca de cláusulas use (regex económico), ensambla un ProjectContext, y el compilador devuelve un resultado unificado que alimenta la tubería existente Parser → Synthesizer.
Bibliotecas importadas por el usuario (plugins)
La biblioteca no tiene ruta ni convención de plugins. El host enumera lo que considere una "biblioteca de usuario" y suministra el resultado a través de LibraryCompilerOptions.AdditionalSources:
var custom = new LibrarySource(
LibraryName: "my_crc",
Vendor: "WORK",
SourceKey: "/Users/me/Documents/Kmila/Plugins/my_crc/crc32.vhd", // opaque to the lib
DependsOn: new[] { "numeric_std" });
var compiler = new LibraryCompiler(new LibraryCompilerOptions(
OpenLibrarySource: (key, ct) => File.OpenRead(key) is var s ? Task.FromResult<Stream?>(s) : Task.FromResult<Stream?>(null),
AdditionalSources: new[] { custom }));
Una vez registrada, use my_crc.crc32_pkg.all se resuelve a través de la misma maquinaria que IEEE.
Integración con el Interpreter — bajado de primitivas IEEE
Esta es la parte divertida. v1.17–v1.19 conectaron el catálogo de símbolos del LibraryCompiler con la ruta de síntesis del Interpreter de modo que las llamadas a funciones IEEE en el código del usuario emiten primitivas reales de netlist, no constantes de marcador de posición opacas.
La tubería
flowchart TB
UserSrc["q <= to_integer(unsigned(addr));"]
Parser[Parser tokenises + structures]
OpInner["Operation.ParseFunctionCall<br/>(unsigned)"]
OpOuter["Operation.ParseFunctionCall<br/>(to_integer)"]
DFInner["DeferredFunction unsigned<br/>Args=[addr_var]"]
DFOuter["DeferredFunction to_integer<br/>Args=[DFInner]"]
Synth[ConcurrentAssignSynthesizer<br/>SynthesizeInto]
Resolve[ResolveDeferredArgs<br/>recursive]
Lookup[IEEEPrimitives.TryLower]
BUF[BUF cell<br/>Cast=unsigned]
TI[TO_INTEGER cell<br/>Width=8]
Wire[wired:<br/>BUF.Y → TI.A]
UserSrc --> Parser
Parser --> OpOuter
OpOuter -->|nested call detected| OpInner
OpInner --> DFInner
OpOuter --> DFOuter
DFOuter --> Synth
Synth --> Resolve
Resolve -->|nested DF detected| Lookup
Lookup --> BUF
Lookup --> TI
BUF --> Wire
TI --> Wire
Qué ocurre con q <= to_integer(unsigned(addr))
Lado del Parser —
Operation.ParseFunctionCall("to_integer")recorre los argumentos a profundidad 1. Cuando veunsignedseguido de(, se llama a sí mismo recursivamente para consumir la llamada anidada. La llamada interna devuelve unDeferredFunction("unsigned", [addr_variable]). ElArguments == [DeferredFunction("unsigned", …)]de la llamada externa — sin pérdida de operandos.DeferredFunction.ShouldDefer — tanto
to_integercomounsigneddevuelventrue, por lo que ambos permanecen estructurados (no se evalúan en línea a literales). Este es el cambio que hizo posible #29.Lado de ejecución —
DeferredFunction.Evaluate()evalúa recursivamente los DF anidados hasta sus resultadosVariable, y luego los entrega aStandardFunctions.EvaluateFunction.to_integer(unsigned(addr))ahora calcula el entero correcto para el patrón de bits enaddr.Lado de síntesis —
ConcurrentAssignSynthesizer.ResolveDeferredArgsbaja recursivamente los DF anidados a través deIEEEPrimitives.TryLower. Elunsignedinterno se baja a una celdaBUFconCast="unsigned"; su net de salida se convierte en el pin de entrada de la celda externaTO_INTEGER. Netlist real cableado.
Tabla de tipos de celda
| Llamada VHDL del usuario | Celda emitida | Notas |
|---|---|---|
rising_edge(clk) |
EDGE_DETECT (Edge=rising) |
pin de entrada CLK, pin de salida Y |
falling_edge(clk) |
EDGE_DETECT (Edge=falling) |
|
to_integer(v) (+ alias conv_integer) |
TO_INTEGER |
salida de 32 bits, atributo Width |
to_unsigned(i, w) |
TO_VECTOR (Signed=false) |
salida de bus de ancho |
to_signed(i, w) (+ alias conv_std_logic_vector) |
TO_VECTOR (Signed=true) |
|
resize(v, n) |
RESIZE |
atributos FromWidth + ToWidth |
shift_left(v, n) / shift_right(v, n) |
SHL / SHR |
conserva el ancho del operando |
rotate_left(v, n) / rotate_right(v, n) |
ROL / ROR |
|
unsigned(stdvec) / signed(stdvec) / std_logic_vector(...) |
BUF (atributo Cast) |
cables de conversión de tipo |
Extender IEEEPrimitives
La tabla de primitivas vive en Interpreter/Synthesis/IEEEPrimitives.cs. Las bibliotecas de proveedores (por ejemplo, Xilinx UNISIM, primitivas de Intel Cyclone) reutilizan la misma maquinaria. Para agregar una primitiva nueva:
internal sealed class MyAddSubPrimitive : IIEEEPrimitive
{
public string CanonicalName => "addsub";
public IReadOnlyList<string> Aliases => Array.Empty<string>();
public CellKind PrimaryCellKind => CellKind.ADD;
public IEEEPrimitiveResult Lower(IEEEPrimitiveArgs a)
{
if (a.Args.Count < 3) return new IEEEPrimitiveResult(a.Ctx.Constant("@addsub_arity", 1), 0);
var lhs = a.Args[0]; var rhs = a.Args[1]; var sel = a.Args[2];
var outNet = a.Ctx.NewInternal(width: lhs.Width, sourceLine: a.SourceLine);
a.Ctx.AddCell(
kind: CellKind.ADD, // would be a new CellKind.ADDSUB in real code
inputs: new[] { new Pin("L", lhs.Id), new Pin("R", rhs.Id), new Pin("SEL", sel.Id) },
outputs: new[] { new Pin("Y", outNet.Id) },
sourceLine: a.SourceLine,
attrs: new Dictionary<string, string> { ["Op"] = "addsub" });
return new IEEEPrimitiveResult(outNet, 1);
}
}
// At host startup (or vendor library init):
IEEEPrimitives.Register(new MyAddSubPrimitive());
DeferredFunction.ShouldDefer // ← also extend if you want it deferred at parse time
Tanto IEEEPrimitiveArgs como la interfaz IIEEEPrimitive son internal porque referencian el SynthesisContext interno. Los paquetes de proveedores que necesiten registrarse desde fuera del ensamblado del Interpreter deberían agregar un atributo InternalsVisibleTo o vivir como un subespacio de nombres dentro de Interpreter.
Pruebas
Dentro del módulo: 17 casos en Interpreter/Tests/LibraryCompilerTests.cs ejercitan toda la superficie (resolver, builder, compiler, orchestrator, los 3 modos de política, la anulación por biblioteca, ambos blob stores, compilación de proyecto, reinicio).
Integración transversal con la ruta de síntesis:
| Suite | Casos | Qué cubre |
|---|---|---|
IEEELibraryLoaderTests |
6 | La fachada heredada IEEELibraryLoader sigue sembrando correctamente FunctionRegistry |
IEEEPrimitivesTests |
18 | Cada emisor produce el CellKind / ancho / atributos correctos |
IEEELoweringEndToEndTests |
6 | De extremo a extremo: un árbol de DeferredFunction hecho a mano alimentado al sintetizador produce las celdas esperadas |
NestedIEEECallTests |
7 | Anidamiento del parser, evaluación en ejecución de llamadas anidadas (to_integer(unsigned("01010101")) == 85), topología de cableado de síntesis |
Ejecuta todas:
cd Interpreter && dotnet bin/Debug/net9.0/Interpreter.dll --test-delta
Conteo total de pruebas del Interpreter bajo --test-delta: 82 / 82.
Fuera de alcance (territorio del host)
El contrato es consistente: la biblioteca define interfaces, el host escribe las implementaciones concretas. Lo siguiente vive en Kmila.Shared/Services/ (o código futuro del host), no en este módulo:
- Implementaciones concretas de
IBlobStore(FileSystemBlobStore,MauiAppDataBlobStore,IsolatedStorageBlobStore). - Interfaz de "Importar biblioteca" / extensión
UserAssetStore.ListUserLibrarySources(). - Sonda de RAM libre que elige el
LibraryBuildModepor defecto. - Política de desalojo LRU / cuota para la caché persistente.
- Interruptores del editor para
--no-cache, "Limpiar caché",--build-mode linear|parallel.
Diagramas de auditoría (2026-05-09)
Estos tres diagramas se produjeron durante la auditoría entre módulos porque el
módulo LibraryCompiler es posterior a TT1 y anteriormente estaba ausente de
Documentacion/TT1/diagrams/. Las fuentes viven allí como
fig_5_12_1_libcompiler_resolver_dag.mmd, fig_5_12_2_libcompiler_cache_roundtrip.mmd,
fig_5_12_3_libcompiler_source_cascade.mmd.
fig_5_12_1_libcompiler_resolver_dag — Resolución topológica de dependencias
flowchart TD
A([CompileAsync nombres: A, B]) --> B[LibraryResolver.ResolveTransitive]
B --> C[Set visited]
C --> D[Por cada nombre raiz]
D --> E[Visit nombre]
E --> F{Esta en visited?}
F -->|Si| G[Skip nodo]
F -->|No| H[Anadir a visited]
H --> I[Lookup LibrarySource]
I --> J{Existe?}
J -->|No| K[Diagnostico:<br/>libreria no resuelta]
J -->|Si| L[Por cada dep en source.DependsOn]
L --> E
J -->|Si| M[Anadir source a orden post-DFS]
M --> N{Mas dependencias?}
N -->|Si| L
N -->|No| O[Retorno: orden topologico]
G --> O
O --> P{Mas raices?}
P -->|Si| D
P -->|No| Q([Lista ordenada IReadOnlyList~LibrarySource~])
fig_5_12_2_libcompiler_cache_roundtrip — Ida y vuelta de caché con invalidación por hash
flowchart TD
A([BuildOrchestrator.BuildOneAsync source]) --> B{Politica?}
B -->|OnDemand| C[LibraryBuilder.BuildAsync]
B -->|ProcessCached| D{En _processCache?}
B -->|Persistent| E[Leer hash de fuente actual]
D -->|Si| F([Hit memoria: retornar CompiledLibrary])
D -->|No| C
E --> G{Existe blob libcache/name.json?}
G -->|No| H[Cold build]
G -->|Si| I[Leer JSON del IBlobStore]
I --> J[Comparar hash almacenado vs hash actual]
J --> K{Coinciden?}
K -->|Si| L([Hit persistente: retornar CompiledLibrary])
K -->|No| M[Invalidar blob y rebuild]
H --> C
M --> C
C --> N[LibraryBuilder lee bytes via callback]
N --> O[Extrae symbols por regex]
O --> P[Calcula SHA-256 del source]
P --> Q[Construye CompiledLibrary]
Q --> R{Politica = ProcessCached o Persistent?}
R -->|ProcessCached| S[_processCache name = lib]
R -->|Persistent| T[_processCache name = lib]
T --> U[Serializar JSON y escribir<br/>libcache/name.json en IBlobStore]
R -->|OnDemand| V([Retornar lib sin cachear])
S --> V
U --> V
fig_5_12_3_libcompiler_source_cascade — Respaldo de fuente en tres niveles
flowchart TD
A([LibraryBuilder.ReadSourceBytesAsync]) --> B{Host inyecto<br/>OpenLibrarySource?}
B -->|Si| C[Llamar callback host con source.Key]
C --> D{Devuelve bytes?}
D -->|Si| E([Bytes del host: usar])
D -->|No| F[Continuar cascada]
B -->|No| F
F --> G[Buscar embedded resource<br/>Builtins/IEEE/name.vhd]
G --> H{Resource existe?}
H -->|Si| I([Bytes embebidos: usar])
H -->|No| J{Es libreria IEEE conocida?}
J -->|Si| K[BuildFallback hardcoded<br/>symbol list]
J -->|No| L[Diagnostico:<br/>fuente no disponible]
K --> M([CompiledLibrary stub: usar])
L --> N([CompiledLibrary vacio + diag])
Módulo agregado en v1.16. Integración del bajado de primitivas IEEE completada en v1.19. Consulta Documentacion/CHANGELOG_v1.16.md … v1.19.md para el detalle por versión.