OmniScope Architecture Deep Dive (Part 2): IR Loading — 8 Ways to Wrestle with LLVM IR
OmniScope Architecture Deep Dive (Part 2): IR Loading — 8 Ways to Wrestle with LLVM IR
If I had to find one word to describe the LLVM IR loading experience, it would be "Schrödinger's IR file."
A
.llfile, before you read it, is simultaneously likely to load successfully and also likely to have you chasing a core dump at 3 AM.
The Predicament: Where Did 98% of the Time Go?
Early versions of OmniScope had a particularly mystifying phenomenon:
I launched an analysis on a 20MB .bc file and waited three minutes without seeing results. top showed CPU at 100%, but the analyzer's progress bar was stuck on "Loading IR..."
Wait — IR loading can take this long?
I wrote a simple benchmark:
.bc file (bitcode): 30s total analysis time → loading took ~29.4s (98%)
.ll file (text IR): 30s total analysis time → loading took ~0.1s (0.3%)
98% of the time was spent on IR loading alone.
Even more absurd — .ll and .bc contain exactly the same information, just serialized in different formats. One is 300x faster, the other is slow enough to make you question your life choices.
This was the first "do I flip the table?" dilemma OmniScope faced.
Why Is .bc So Slow?
Bitcode (.bc) is LLVM's binary serialization format. Its problems:
- Version-locked — A
.bcfile produced by LLVM 12 may not be readable by LLVM 22. Experimentally, in many cases it indeed cannot be read. - Requires full C++ LLVM library initialization — Deserializing
.bcrequires booting the entire LLVM backend, building Module, Function, BasicBlock, Instruction… .bcwas not designed for "reading" — It was designed for Link-Time Optimization (LTO). In LTO scenarios, you read once and use it thousands of times; slowness is tolerable. But in analysis scenarios, reading happens once per invocation, and slow is a disaster.
What about .ll? .ll is a text format where every line is human-readable LLVM IR. You can write a pure Rust text parser — no C++ libraries needed.
The conclusion was clear: .ll must be the priority.
But a new problem emerged: the user's LLVM IR itself is evolving.
LLVM IR Format Evolution: From Typed to Opaque
LLVM IR has a trap I repeatedly fell into during development — how pointer types are represented.
Before LLVM 15, IR used typed pointers. Every pointer annotated the type it points to. For example:
define void @foo(i32* %ptr) {
%val = load i32, i32* %ptr
ret void
}
i32* explicitly tells the compiler: this pointer points to a 32-bit integer.
But starting with LLVM 15, LLVM mandates opaque pointers. Pointers no longer carry type information:
define void @foo(ptr %ptr) {
%val = load i32, ptr %ptr
ret void
}
i32* became ptr. This has profound implications for IR loading:
- Text parser must support both formats simultaneously — the parser must recognize both
i32*andptrsyntax - GEP instruction parameter structure changed — typed pointers require extra parameters for result type derivation
- Type inference logic becomes more complex — under opaque pointers, result types must be reverse-inferred from
load/storetarget types
ir_model.rs: Core IR Data Structures
Below the IR loading layer lies the data model defined in crates/omniscope-ir/src/ir_model.rs. All of OmniScope's core IR data structures reside here:
/// File: crates/omniscope-ir/src/ir_model.rs
pub struct IRModuleModel {
pub module_name: String,
pub target_triple: Option<String>,
pub source_filename: Option<String>,
pub data_layout: Option<String>,
pub functions: Vec<IRFunction>,
pub declarations: Vec<IRDeclaration>,
pub global_variables: Vec<IRGlobalVariable>,
}
This is the final output format for all loading paths, and the input for subsequent analysis (dataflow, ownership inference).
Function definition IRFunction:
pub struct IRFunction {
pub name: String,
pub return_type: Option<String>,
pub params: Vec<String>,
pub basic_blocks: Vec<IRBasicBlock>,
pub attributes: Vec<String>,
pub visibility: Option<String>,
pub alignment: Option<u64>,
pub section: Option<String>,
pub gc: Option<String>,
}
Basic block IRBasicBlock:
pub struct IRBasicBlock {
pub label: Option<String>,
pub instructions: Vec<IRInstructionModel>,
}
Instruction IRInstructionModel:
pub struct IRInstructionModel {
pub opcode: String,
pub result: Option<String>,
pub operands: Vec<String>,
pub instruction_type: Option<String>,
pub metadata: HashMap<String, String>,
pub debug_info: Option<DebugInfo>,
}
The design philosophy: keep it as generic as possible rather than binding to any single IR format:
operandsisVec<String>rather than strongly typed — because different loading paths use different IR representationsmetadataisHashMap<String, String>— any metadata can be preservedinstruction_typeisOption<String>— not all instructions have types (e.g.,store)
Supports both JSON and MessagePack serialization:
pub fn load_from_json(path: &Path) -> Result<IRModuleModel> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let model: IRModuleModel = serde_json::from_reader(reader)?;
Ok(model)
}
pub fn load_from_msgpack(path: &Path) -> Result<IRModuleModel> {
let data = std::fs::read(path)?;
let model: IRModuleModel = rmp_serde::from_slice(&data)?;
Ok(model)
}classDiagram
class IRModuleModel {
+String module_name
+Option~String~ target_triple
+Option~String~ source_filename
+Option~String~ data_layout
+Vec~IRFunction~ functions
+Vec~IRDeclaration~ declarations
+Vec~IRGlobalVariable~ global_variables
}
class IRFunction {
+String name
+Option~String~ return_type
+Vec~String~ params
+Vec~IRBasicBlock~ basic_blocks
+Vec~String~ attributes
}
class IRBasicBlock {
+Option~String~ label
+Vec~IRInstructionModel~ instructions
}
class IRInstructionModel {
+String opcode
+Option~String~ result
+Vec~String~ operands
+Option~String~ instruction_type
+HashMap~String, String~ metadata
+Option~DebugInfo~ debug_info
}
class IRGlobalVariable { +String name +String initializer +u64 alignment }
class IRDeclaration { +String name +String return_type +Vec~String~ param_types }
IRModuleModel *-- IRFunction
IRModuleModel *-- IRDeclaration
IRModuleModel *-- IRGlobalVariable
IRFunction *-- IRBasicBlock
IRBasicBlock *-- IRInstructionModelllvm_sys_adapter.rs: Calling LLVM C API from Rust
When the most complete type information is needed, the text parser isn't enough. You can use the llvm-sys path — through crates/omniscope-ir/src/llvm_sys_adapter.rs, directly calling the LLVM C API:
/// RAII wrapper for LLVM C API context management.
/// crates/omniscope-ir/src/llvm_sys_adapter.rs
pub struct ContextGuard { _private: () }
impl ContextGuard {
pub fn new() -> Self {
unsafe { LLVMInitializeAllAsmParsers(); }
ContextGuard { _private: () }
}
}
impl Drop for ContextGuard {
fn drop(&mut self) { unsafe { LLVMShutdown(); } }
}
ContextGuard ensures the LLVM context is properly initialized and destroyed within Rust's lifetime. ModuleGuard automatically frees the LLVM Module on Drop:
pub struct ModuleGuard { module: LLVMModuleRef }
impl ModuleGuard {
pub fn parse_ir(path: &str) -> Result<Self> {
let _ctx = ContextGuard::new();
let mem_buf = MemoryBufferGuard::new(path)?;
let mut module: LLVMModuleRef = ptr::null_mut();
let mut out_msg: *mut c_char = ptr::null_mut();
let result = unsafe {
LLVMParseIRInContext(LLVMGetGlobalContext(), mem_buf.as_ref(), &mut module, &mut out_msg)
};
// ...
Ok(ModuleGuard { module })
}
pub fn get_pointer_type(&self, val: LLVMValueRef) -> Option<String> {
unsafe {
let t = LLVMTypeOf(val);
if LLVMGetTypeKind(t) == LLVMPointerTypeKind {
Some(format!("ptr addrspace({})", LLVMGetPointerAddressSpace(t)))
} else { None }
}
}
}
impl Drop for ModuleGuard {
fn drop(&mut self) { unsafe { LLVMDisposeModule(self.module); } }
}
The greatest value of this path: it can extract precise type information that the text parser cannot obtain, particularly the address space of opaque pointers. But this capability requires --features llvm-backend compilation, not enabled by default.
Rethinking: One File, Eight Ways to Read
Back to the loading strategy itself. I initially thought IR loading was simple — just write "a parser supporting .ll and .bc."
But reality turned out far more complex:
- Do users have
.llfiles? Not necessarily. Commercial compilers may only output.bc. - Do users have
optinstalled? Not necessarily. Docker images often lack it. - Can users compile C++? Not necessarily. Windows users may not have LLVM dev packages.
- Do users have the
ir_extractorbinary? Not necessarily. It requires separate compilation. - What LLVM version do users have? Unknown. Could be LLVM 12 or LLVM 22.
So my understanding of IR loading evolved:
IR loading is not a problem of "supporting two formats" — it's a problem of "loading as reliably as possible in an unreliable environment."
The solution isn't finding a single "best loading method" — it's providing a complete set of loading strategies that back each other up.
I ultimately designed 8 LoadStrategies (defined in crates/omniscope-ir/src/loader_v2.rs, lines 60-95):
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoadStrategy {
DirectCppFfi, // ir_extractor + --slice=ffi, most precise FFI IR
DirectCpp, // ir_extractor, full IR without depending on opt
LlvmSys, // llvm-sys C API, LLVM dev package
CppPass, // opt + SafetyExportPass.so, existing LLVM pass workflow
TextParser, // Pure Rust text parser, zero dependencies
MsgPack, // MessagePack pre-extraction, 5-10x faster than JSON
Auto, // Auto-detect best strategy
AutoFast, // Default strategy, .ll priority
}Auto / AutoFast: Automatic Degradation Chain
/// crates/omniscope-ir/src/loader_v2.rs
/// Probe backends in priority order and fall back gracefully.
fn load_auto(path: &Path) -> Result<(IRModule, LoadStrategy)> {
// Priority: DirectCppFfi > DirectCpp > llvm-sys > cpp pass > text parser
if can_use_direct_cpp_ffi() {
match load_via_direct_cpp_ffi(path) {
Ok(m) if !m.functions.is_empty() => return Ok((m, DirectCppFfi)),
Ok(_) => warn!("DirectCppFfi empty, falling back"),
Err(e) => warn!(error = %e, "DirectCppFfi failed"),
}
}
// ... degradation step by step ...
let module = load_via_text(path)?;
Ok((module, TextParser)) // TextParser as ultimate fallback
}
AutoFast is the default strategy: .ll files use TextParser first (instantaneous), .bc follows the normal Auto degradation chain. For large .ll files (>10MB), it directly takes the TextParser fast path, avoiding the overhead of probing external tools first.
flowchart TD
A[Input file] --> B{Is .ll?}
B -->|Yes| C[TextParser]
C --> D{Success?}
D -->|Yes| E[Return IRModule]
D -->|No| F[Auto degradation chain]
B -->|No .bc/other| F
subgraph AutoChain["Auto Degradation Chain"]
I[DirectCppFfi] --> J{Success?}
J -->|Yes| K[Return]
J -->|No| L[DirectCpp]
L --> M{Success?}
M -->|Yes| N[Return]
M -->|No| O[LlvmSys]
O --> P{Success?}
P -->|Yes| Q[Return]
P -->|No| R[CppPass]
R --> S{Success?}
S -->|Yes| T[Return]
S -->|No| U[TextParser fallback]
end
F --> I
U --> V[Always return or error]Main Entry Point: load_ir()
All strategies are exposed to upper layers through a unified entry point:
/// crates/omniscope-ir/src/loader_v2.rs
/// Primary entry point for CLI and pipeline.
pub fn load_ir(path: &Path, strategy: LoadStrategy) -> Result<LoadedIr> {
if !path.exists() {
bail!("IR file does not exist: {}", path.display());
}
let start = Instant::now();
let (module, actual_strategy) = match strategy {
Auto => load_auto(path)?,
AutoFast => load_auto_fast(path)?,
DirectCppFfi => (load_via_direct_cpp_ffi(path)?, DirectCppFfi),
LlvmSys => (load_via_llvm_sys(path)?, LlvmSys),
CppPass => (load_via_cpp_pass(path)?, CppPass),
DirectCpp => (load_via_direct_cpp(path)?, DirectCpp),
TextParser => (load_via_text(path)?, TextParser),
MsgPack => (load_via_msgpack(path)?, MsgPack),
};
let load_ms = start.elapsed().as_millis() as u64;
Ok(LoadedIr { module, strategy: actual_strategy, load_ms, .. })
}
The LoadedIr struct not only returns the load result, but also includes actual_strategy (the strategy actually used — because Auto performs degradation) and load_ms (loading time in milliseconds).
LLVM Version Search: From 12 to 22
The project's early BUILD_ENV.md said "Please install LLVM 12 (llvm@12)." Now in crates/omniscope-ir/Cargo.toml:
llvm-sys = "221" # LLVM 22
Five years, 10 major versions — each with mutually incompatible LLVM APIs. find_opt() probes from LLVM 22 down to LLVM 17:
/// crates/omniscope-ir/src/loader_v2.rs
/// Return common Homebrew LLVM bin directories (newest version first).
fn homebrew_llvm_bin_dirs() -> Vec<PathBuf> {
[
"/opt/homebrew/opt/llvm@22/bin",
"/opt/homebrew/opt/llvm@21/bin",
"/opt/homebrew/opt/llvm@20/bin",
"/opt/homebrew/opt/llvm@19/bin",
"/opt/homebrew/opt/llvm@18/bin",
"/opt/homebrew/opt/llvm@17/bin",
"/opt/homebrew/opt/llvm/bin",
].iter().map(PathBuf::from).collect()
}flowchart TD
A[Find opt binary] --> B{LLVM_OPT env var?}
B -->|Set| C[Use directly]
B -->|Unset| D{llvm@22?}
D -->|Found| E[Use llvm@22]
D -->|Not found| F{llvm@21?}
F -->|Found| G[Use llvm@21]
F -->|Not found| H{llvm@20..17?}
H -->|Found| I[Use corresponding version]
H -->|Not found| J{llvm-config?}
J -->|Found| K[Use llvm-config path]
J -->|Not found| L{which opt?}
L -->|Found| M[Use opt from PATH]
L -->|Not found| N[Return None]instruction_parser.rs: Instruction-Level Parsing
After TextParser loading, instructions also require additional parsing for dataflow analysis. crates/omniscope-ir/src/instruction_parser.rs provides best-effort instruction classification:
/// Best-effort instruction classification for LLVM IR.
pub fn classify_instruction(opcode: &str, operands: &[String]) -> InstructionKind {
match opcode {
"call" => InstructionKind::Call,
"load" => InstructionKind::Load,
"store" => InstructionKind::Store,
"alloca" => InstructionKind::Alloca,
"getelementptr" => InstructionKind::GEP,
"phi" => InstructionKind::Phi,
"icmp" | "fcmp" => InstructionKind::Compare,
"br" | "switch" | "indirectbr" => InstructionKind::Branch,
"ret" => InstructionKind::Return,
"bitcast" | "ptrtoint" | "inttoptr" => InstructionKind::Cast,
"malloc" | "free" | "calloc" | "realloc" => InstructionKind::MemoryOp,
_ => InstructionKind::Other,
}
}
It also includes callee extraction and atomic operation detection:
pub fn extract_callee(operands: &[String]) -> Option<&str> {
operands.first().map(|s| s.trim_start_matches('@'))
}
pub fn is_atomic_op(instruction: &str) -> bool {
instruction.starts_with("atomic") || instruction.contains(" atomic ")
}
This parser classifies about 15 instruction categories, covering the core instructions needed for dataflow analysis and ownership inference. It is best-effort — unrecognized instruction formats are tagged as Other and do not cause the entire load to fail.
IR Cache System
Each C++ pass or ir_extractor invocation involves process startup, LLVM initialization, and JSON serialization — potentially 5-10 seconds. crates/omniscope-ir/src/ir_cache.rs provides file-fingerprint caching:
/// crates/omniscope-ir/src/ir_cache.rs
/// Compute cache key based on file identity (size + mtime + xxh3).
fn cache_key(path: &Path) -> Option<String> {
let meta = std::fs::metadata(path).ok()?;
let size = meta.len();
let mtime = meta.modified().ok()?;
let mtime_ns = mtime.duration_since(SystemTime::UNIX_EPOCH).ok()?.as_nanos();
let canonical = std::fs::canonicalize(path).ok()?;
let fingerprint = xxh3_fingerprint(&canonical, size, mtime_ns);
Some(fingerprint.to_string())
}
Cache key composition: canonical_path + file_size + mtime + xxh3 fingerprint. xxh3 was chosen over SHA256 intentionally — cache keys don't need cryptographic security, only high-performance (nanosecond-level) collision resistance.
Cache storage path: {cache_dir}/ir/{fingerprint}.json. When files are modified, mtime changes and the cache is automatically invalidated. If the loading strategy is upgraded (e.g., from text parser to DirectCpp), the cache is also automatically invalidated.
ModuleIndex: Pre-Computed Function Metadata
After loading IR, all passes need to frequently query function information. Traversing the IR structure each time is too slow, so crates/omniscope-pass/src/module_index.rs provides a pre-computed index:
/// Pre-computed index over function metadata for fast pass access.
pub struct ModuleIndex {
pub functions: Vec<CachedFunctionMeta>,
}
pub struct CachedFunctionMeta {
pub name: String,
pub language: Language,
pub is_ffi_boundary: bool,
pub bb_count: usize,
pub inst_count: usize,
pub has_alloca: bool,
pub has_unsafe_ops: bool,
pub calls: Vec<CachedCallMeta>,
}
pub struct CachedCallMeta {
pub callee: String,
pub is_indirect: bool,
pub is_ffi: bool,
pub is_safe: bool,
}
ModuleIndex construction is a one-time cost, shared by all subsequent passes. Language detection (Language enum) is done through function name pattern matching, while FFI boundary classification combines boundary definitions from omniscope.toml.
Source Location Mapping: IRLocation Resolution
Analysis results must map back to source locations to produce meaningful reports. crates/omniscope-ir/src/location.rs defines:
pub struct SourceLocation {
pub file: Option<String>,
pub line: u32,
pub column: u32,
pub function: Option<String>,
}
This struct is populated by parsing LLVM IR's debug metadata through logic in crates/omniscope-ir/src/parser.rs:
define void @foo() !dbg !5 { ret void }
!5 = !DILocation(line: 12, column: 5, scope: !6)
!6 = !DIFile(filename: "main.cpp", directory: "/home/user/project")
The parsing logic in parser.rs:
/// Parse DILocation metadata from LLVM IR debug info.
fn parse_dilocation(line: &str) -> Option<SourceLocation> {
let line_val = extract_named_value(line, "line")?.parse().ok()?;
let col_val = extract_named_value(line, "column")?.parse().ok()?;
Some(SourceLocation { file: None, line: line_val, column: col_val, function: None })
}
Every call instruction with debug info maps back to source file line and column numbers. This means analysis results show you main.cpp:42 instead of @some_obscure_ir_name.
memory_pool.rs: Low-Level Memory Allocation Infrastructure
IR loading involves a large number of small object allocations and deallocations. crates/omniscope-core/src/memory_pool.rs provides an arena allocator:
/// Arena-based bump allocator for high-throughput IR loading.
use bumpalo::Bump;
use std::cell::UnsafeCell;
pub struct MemoryPool {
bump: UnsafeCell<Bump>,
}
impl MemoryPool {
pub fn new() -> Self { MemoryPool { bump: UnsafeCell::new(Bump::new()) } }
pub fn alloc<'a, T>(&'a self, val: T) -> &'a T {
unsafe { (*self.bump.get()).alloc(val) }
}
pub fn alloc_slice<'a, T>(&'a self, val: &[T]) -> &'a [T] where T: Copy {
unsafe { (*self.bump.get()).alloc_slice_copy(val) }
}
pub fn reset(&self) { unsafe { (*self.bump.get()).reset() }; }
}
Key design decisions:
- Arena allocation — allocate a large chunk once, allocate small objects individually, reclaim the entire chunk on free. An order of magnitude faster than jemalloc/malloc.
UnsafeCell<Bump>— bypasses Rust's borrow checker because the pool is shared-write but analysis is single-threaded.alloc_slice_copy— avoids individual allocation for large numbers of small Vecs likeIRInstructionModel.
In IR loading scenarios, the text parser produces thousands of small objects (IRFunction, IRBasicBlock, IRInstructionModel) that are directly allocated via the arena, avoiding memory fragmentation.
platform_filters.toml: Edge Case Handling
After IR loading completes, the analysis system needs to distinguish between "safe library calls" and "suspicious user code." The project root's platform_filters.toml defines platform-specific safe API lists:
[macos]
safe_functions = [
"malloc", "free", "calloc", "realloc",
"mmap", "munmap", "shm_open", "shm_unlink",
"dlopen", "dlclose", "dlsym",
"pthread_create", "pthread_join", "pthread_mutex_lock",
]
[linux]
safe_functions = [
"malloc", "free", "calloc", "realloc",
"mmap", "munmap", "brk", "sbrk",
"dlopen", "dlclose", "dlsym",
"pthread_create", "pthread_join",
]
[windows]
safe_functions = [
"HeapAlloc", "HeapFree", "VirtualAlloc", "VirtualFree",
"LoadLibraryA", "LoadLibraryW", "FreeLibrary",
]
[common]
safe_functions = [
"memcpy", "memset", "memmove", "memcmp",
"strlen", "strcmp", "strncmp",
"printf", "fprintf", "sprintf", "snprintf",
]
When a function call is in the safe_functions list, the analyzer skips it, avoiding false positives. This mechanism is a key prerequisite for the next article on noise reduction.
Strategy Comparison and Tradeoffs
| Dimension | TextParser | LlvmSys | CppPass | DirectCpp |
|---|---|---|---|---|
| Startup Speed | Milliseconds | Seconds (LLVM init) | Seconds (process startup) | Milliseconds (binary) |
| IR Completeness | Limited (~90%) | Complete | Complete | Complete |
| Build Deps | None | llvm-sys + LLVM dev | opt + .so plugin | ir_extractor C++ binary |
| Cross-platform | All platforms | Platforms with LLVM dev | Platforms with opt | Platforms with compiler |
| Memory Overhead | Low (arena alloc) | High (LLVM internal state) | Medium (process isolated) | Medium |
| Use Case | Quick prototype, Docker | Precise type info needed | Complex IR features | Production FFI analysis |
| Type Inference | Limited (pattern matching) | Precise (C API) | Precise (LLVM pass) | Precise (C++ pass) |
From an engineering tradeoff perspective, core tradeoffs are:
-
Full load vs on-demand load: Currently full-load — entire IR module read into memory at once. Subsequent analysis has zero latency, but large files consume significant memory (>50MB becomes noticeable). Lazy loading is theoretically possible but not currently needed.
-
Memory vs speed: TextParser + MemoryPool strikes the best balance. LlvmSys, while rich in type information, has 2-3x the memory overhead of TextParser.
-
Compatibility vs precision: TextParser has the best compatibility (zero dependencies, all platforms) but limited precision. LlvmSys has the highest precision but worst compatibility (needs LLVM dev packages). The 8 strategies exist to offer a sliding scale between these two dimensions.
Engineering Philosophy Summary
Looking back at the IR loading layer, key takeaways are:
- LLVM ecosystem fragmentation is unavoidable — version, toolchain, and format polymorphism demands a strategy pattern to cope
- Degradation chains beat single solutions — no universal loading method exists, but a set of mutually-supporting loading methods does
- Caching is the primary optimization for IO-intensive operations — but cache key design must be sound, or it introduces bugs
- Low-level infrastructure (MemoryPool) impacts performance as much as algorithm optimization — without a bump allocator, IR loading for many small objects would be 2-3x slower
- Debug metadata parsing precision determines downstream analysis usability — analysis results without source locations are effectively "no analysis" from the user's perspective
Honest Limitations
1. TextParser IR Coverage
Certain instruction formats introduced in new LLVM versions (callbr, freeze, poison-related operations) may not be recognized by the text parser. Currently falling back to LlvmSys or CppPass. But if you're using LLVM 23+ without dev packages, the text parser may be the only option — encountering new instructions will error out. This is increasingly common in the 2026 LLVM ecosystem.
2. .bc Version Forward Compatibility
Each LLVM major version changes the .bc serialization format. I can only follow up quickly after each new LLVM release, but a lag window is inevitable. The README already says "Use .ll" in bold uppercase, but some users still insist on using .bc.
3. MsgPack Format Has No Standard
MessagePack serialization is OmniScope-custom, not a universal standard. When migrating to other tools, .msgpack files cannot be reused. JSON output preserves interoperability, but JSON is both large and slow.
4. ModuleIndex Monolith
module_index.rs is currently 1709 lines, and the code's own comments say "TODO: this file is too large, should be split." Function metadata indexing, language detection, and FFI boundary classification — all in one file — making maintenance increasingly difficult.
5. Why Not a Single Optimal Solution?
Every time I look at the 8 strategies, I wonder: is this over-engineered? But every reflection leads to the same conclusion: a single optimal solution does not exist. Docker containers lack C++ toolchains, CI environments only deploy ir_extractor, macOS users have Homebrew's LLVM. The 8 strategies are a compromise with different runtime environments, not over-engineering.
Preview of the Next Article
After loading IR, the next step is analysis. But a bigger problem awaits:
"After analysis, I got 4,525 alerts — who's going to read all that?"
Next article preview: Noise Reduction — An Epic Optimization from 5,098 to ~45. From platform_filters to risk scoring and a 9-layer false-positive suppression system, we filter out 99% of noise, leaving only the ~45 high-confidence issues users truly need to care about.
Previous: Why OmniScope — Call Graphs, State Machines, Semantic Trees Next: Noise Reduction — An Epic Optimization from 5,098 to ~45