LLVM and MLIR lessons
PowerIO’s design borrows from LLVM and MLIR where their problems overlap with reading, transforming, and writing power system data. This page goes through each lesson taken and where the shipped design applies it, then the mechanisms left out on purpose. Primary references: the MLIR language reference, diagnostics, interfaces, pass management, and dialect definition documents.
Adopted
A small shared foundation under acyclic higher layers. LLVM’s library layering puts Support under IR under the producers. PowerIO’s powerio-core owns sources, diagnostics, errors, the module, and the generic containers; the network crates, the calculation crate, and the matrix crate stack over it in one direction, and CI asserts the edges from cargo metadata (Crate graph).
Source ownership that survives parsing. MLIR’s source manager keeps buffers alive so locations mean something after parsing. A PioModule retains its source, and same format emission returns those bytes unchanged. A diagnostic has a source identifier plus a byte range into those exact bytes, end to end, and the MATPOWER and PSS/E readers attach the range of the record a finding is about, for the failure that ends a read and for warnings alike; the other readers attach no span yet.
Deterministic serialization. serialize is a function of the module alone, so one module serializes to identical text every time, and serializing the module that text deserializes to reproduces the text. Members are written in a fixed order (record fields in declaration order, map keys sorted), diagnostic IDs are minted d0, d1, … in record order for records that have none, and every float is written in the shortest decimal form that reads back to the same value. Two equal modules therefore compare equal as documents, which is what a cache key, a golden file, or a content digest needs.
Entry points that acquire their own input, with configuration as a value. MLIR reads a module through one operation with three input kinds: parseSourceFile(filename, block, config) opens the named file itself, parseSourceFile(sourceMgr, ...) takes a source manager the caller built, and parseSourceString(text, block, config, sourceName) takes content whose name supplies diagnostic locations. LLVM’s object::createBinary has the same pair of a path and a buffer and “autodetect[s] the file type” rather than taking a format argument. PowerIO’s parse, emit, serialize, and deserialize take their input or output through IntoSource and IntoDestination, which accept a file or directory name, content already in memory, and a built Source or Destination, so the ordinary read is parse("case.raw") with one call and one failure point, and detection is the default. Optional configuration is a documented value, as in writeBytecodeToFile(op, os, config = {}) and spirv::serialize(module, binary, options = {}); Rust has no default arguments, so each operation is a pair, parse with parse_with_options.
Its own representation read separately from foreign formats. MLIR reads its own text and bytecode through parseSourceFile, which detects the encoding, and reaches foreign formats through the mlir-translate registry of named translations. PowerIO does the same. serialize and deserialize handle PowerIO IR, parse and emit translate grid exchange formats, and parse refuses a .pio.json by naming deserialize. A future binary IR encoding therefore goes behind deserialize, detected from its own leading bytes, rather than behind a new operation. The naming lines up as well. MLIR spells its own binary encoding spirv::serialize and spirv::deserialize, and LLVM reserves emit for producing a target file from IR (TargetMachine::addPassesToEmitFile).
One value set, so which value a format produces is data. MLIR has one IR and reads foreign formats through registered translations whose result is that IR, rather than a reading operation per category of input. PowerIO’s PioValue is that one set, so a geographic layer is a value in it, powerio.GeoLayer, and the canonical .geo.json, GeoJSON, aliased CSV or JSON records, headerless buscoords CSV, and a PowerWorld .pwd display all reach it through parse. There is no second reading path beside parse, so a document that is not a grid case still parses, serializes, and emits like every other value.
Structured diagnostics with stable severities and attached context. The four severities (error, warning, remark, note) are MLIR’s, with the same meanings: a remark reports on success, a note attaches context to another finding. PowerIO adds a stable dotted code, which is what you branch on, plus targets, related records, and suggested actions.
Typed representations at more than one abstraction level. The value families span reusable networks, calculation instances, and solutions the way a compiler holds IR at several levels; nothing forces the richer levels through the poorer ones.
Explicit transformations with testable boundaries. Each transformation names its input and output types, returns diagnostics, and refuses what it cannot represent; balanced lowering exposes powerio::transform::to_balanced_report so you can inspect its assumptions and refusals before you transform a value. Nothing rewrites as a side effect, and format loss diagnostics belong to the EmitResult returned by emit.
Verification at representation boundaries. Parsers and transformations verify what they produce and report findings rather than repairing silently; repairs are explicit operations that leave history records.
Shared operations instead of per format switches. The parse and emission dispatchers route once, at the facade; matrix calculations, serializers, and inspectors consume the concrete typed values, so a new format adds one parser and one serializer rather than a case in every consumer.
Analysis caches. Factorizations and prepared solver arrays are derived data behind the public results, invalidated when their inputs change, the way pass manager analyses are; IndexedNetwork, the derived index view, stays public in 0.11 because downstream consumers build matrices through it directly.
Registries checked mechanically where tables drift. Structural type names, format tokens, diagnostic codes, and drawn architecture edges are each held to one source by a CI gate, which is the maintainable slice of MLIR’s declarative dialect definitions.
Serialization specified apart from memory. Each PowerIO IR generation has an explicit schema and validation rules, and the Rust structs do not define the public document layout. As in MLIR bytecode and LLVM bitcode, the document has an integer generation and a separate producer string, and the reader decides compatibility from the generation alone. The generation window follows LLVM’s bitcode epoch rule: within one minor release line every release reads what the line wrote, and the floor moves only at a line boundary. PowerIO IR reference defines every structural type field by field, the way the MLIR language reference defines its types, and a test holds the page to the generated schema. PowerIO makes no LLVM or MLIR compatibility promise by analogy.
Scrutiny proportional to permanence. A new core concept (a value family or common module record) needs a registered structural type name, an exact IR representation, and binding coverage. A new format adapter needs none of that.
Not adopted
PowerIO 0.11 has no SSA values, no generic operation tree, no region nesting, no global context, no open runtime dialect registry, no generic pass manager, and no bytecode. The existing Rust types describe power system data more directly than an operation tree would, and none of those mechanisms has a PowerIO use with measured benefit. Public names stay power system names. A bus is a bus, a lowering is named for its concrete result, and no Rust struct is renamed to an operation so it resembles MLIR. A PioContext would be justified only by measured interning or shared allocation needs, and none has appeared.
Two MLIR bytecode mechanisms are also left out while one generation covers the whole document. Per dialect versions would let one value family evolve on its own schedule; PowerIO adopts them only when a family needs a representation change the others do not. Writer back deployment (setDesiredBytecodeVersion) would let a newer build write an older generation for a consumer pinned to it; PowerIO adopts it only when such a consumer appears.