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

  1. Regla de diseño (estricta)
  2. Inicio rápido
  3. Qué hace la biblioteca en realidad
  4. Modos de política de construcción
  5. Orquestación lineal vs paralela
  6. Caché persistente mediante IBlobStore
  7. Proyectos de múltiples archivos
  8. Bibliotecas importadas por el usuario (plugins)
  9. Integración con el Interpreter — bajado de primitivas IEEE
  10. Extender IEEEPrimitives
  11. Pruebas
  12. 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 &lt;= 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))

  1. Lado del ParserOperation.ParseFunctionCall("to_integer") recorre los argumentos a profundidad 1. Cuando ve unsigned seguido de (, se llama a sí mismo recursivamente para consumir la llamada anidada. La llamada interna devuelve un DeferredFunction("unsigned", [addr_variable]). El Arguments == [DeferredFunction("unsigned", …)] de la llamada externa — sin pérdida de operandos.

  2. DeferredFunction.ShouldDefer — tanto to_integer como unsigned devuelven true, por lo que ambos permanecen estructurados (no se evalúan en línea a literales). Este es el cambio que hizo posible #29.

  3. Lado de ejecuciónDeferredFunction.Evaluate() evalúa recursivamente los DF anidados hasta sus resultados Variable, y luego los entrega a StandardFunctions.EvaluateFunction. to_integer(unsigned(addr)) ahora calcula el entero correcto para el patrón de bits en addr.

  4. Lado de síntesisConcurrentAssignSynthesizer.ResolveDeferredArgs baja recursivamente los DF anidados a través de IEEEPrimitives.TryLower. El unsigned interno se baja a una celda BUF con Cast="unsigned"; su net de salida se convierte en el pin de entrada de la celda externa TO_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 LibraryBuildMode por 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.mdv1.19.md para el detalle por versión.