Analyzer API¶
Page in flux. Refreshed 2026-04-24 to match pccx-lab HEAD.
Phase 1 retired the pre-split TraceAnalyzer trait and the monolithic
analyzer::builtin_analyzers() list. In its place pccx-core ships a
small generic plugin-registry primitive that every workspace crate
(reports, verification, authoring, evolve, lsp, ai_copilot, …) reuses
for its own trait-object plugins. Callers now register plugins against
a per-crate PluginRegistry<P> instead of calling a fixed builtin list.
This page documents the primitive and how a consuming crate hangs its
plugin trait off it. The 16-entry curated analyzer catalogue from
pre-Phase-1 has not been re-landed yet — when it returns it will live
inside one of the analytics crates (reports or a new pccx-analytics)
and be registered via this primitive. Until then pccx-core exposes the
raw free functions (roofline::analyze, bottleneck::detect, …) and a
thin pccx-reports::render_markdown wrapper.
Primitive¶
// pccx_core::plugin
pub const PLUGIN_API_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy)]
pub struct PluginMetadata {
pub id: &'static str, // stable identifier
pub api_version: u32, // must equal PLUGIN_API_VERSION
pub description: &'static str, // one-line blurb
}
pub trait Plugin {
fn metadata(&self) -> PluginMetadata;
}
pub struct PluginRegistry<P: Plugin> {
/* private Vec<P> */
}
impl<P: Plugin> PluginRegistry<P> {
pub fn new() -> Self;
pub fn register(&mut self, plugin: P) -> Result<(), PluginError>;
pub fn all(&self) -> &[P];
pub fn find(&self, id: &str) -> Option<&P>;
pub fn len(&self) -> usize;
pub fn is_empty(&self) -> bool;
}
register is the only fallible entry point; it rejects any plugin
whose api_version differs from PLUGIN_API_VERSION so out-of-tree
dylibs built against a stale header are refused up front. Duplicate
ids are permitted — first registration wins on find. Thread-safety
is the caller’s responsibility; wrap in a Mutex / RwLock when
sharing across threads.
Why generic over P?¶
A single registry type accommodates every plugin kind each crate
defines — ReportFormat (reports), VerificationGate (verification),
IsaCompiler / ApiCompiler (authoring), SurrogateModel /
EvoOperator / PRMGate (evolve), CompletionProvider /
HoverProvider / LocationProvider (lsp), ContextCompressor /
SubagentRunner (ai_copilot). Each crate’s unstable trait is a
supertrait of Plugin, gets its own PluginRegistry<CrateTrait>
instance, and is iterated independently at the call site.
Registering a plugin¶
A consumer crate defines its own trait, makes it a supertrait of
Plugin, and offers a registry instance to its host:
use pccx_core::plugin::{Plugin, PluginMetadata, PluginRegistry,
PLUGIN_API_VERSION};
// 1. Crate trait — extend `Plugin`.
pub trait ReportFormat: Plugin {
fn render(&self, trace: &NpuTrace) -> String;
}
// 2. Concrete implementation.
pub struct MarkdownReport;
impl Plugin for MarkdownReport {
fn metadata(&self) -> PluginMetadata {
PluginMetadata {
id: "markdown",
api_version: PLUGIN_API_VERSION,
description: "GitHub-flavoured Markdown report renderer",
}
}
}
impl ReportFormat for MarkdownReport {
fn render(&self, trace: &NpuTrace) -> String { /* … */ }
}
// 3. Host constructs a registry keyed on the concrete plugin type
// and registers instances.
let mut reports: PluginRegistry<MarkdownReport> = PluginRegistry::new();
reports.register(MarkdownReport)?;
Crates that want heterogeneous plugins behind one trait object can
either (a) wrap each concrete type and implement Plugin for a thin
enum, or (b) wait for Phase 2/4 — the upcoming dylib loader lands
Box<dyn CrateTrait>-shaped registration when the C-ABI contract
stabilises.
Callers iterate with registry.all() or look up by id with
registry.find("markdown").
Error surface¶
#[derive(Debug, Clone, thiserror::Error)]
pub enum PluginError {
#[error("plugin '{id}' declares API version {got}; \
host expects {expected}")]
ApiMismatch { expected: u32, got: u32, id: &'static str },
}
ApiMismatch is the only runtime error today. Dylib load failures
(symbol missing, C-ABI mismatch, unload panic) will reuse this enum
when the Phase 2/4 dynamic loader arrives — the registry will then
gain load_dylib(path) on top of the in-process register.
Stability¶
Everything in pccx_core::plugin is unstable until pccx-lab v0.3.
The enum is #[non_exhaustive] in spirit — new variants land without a
SemVer major bump during the Phase 1/2 window.
Dylib-loading machinery (libloading + C-ABI register() symbol + safe
drop on unload) is not yet implemented; it lands during Phase 2/4
once an out-of-tree plugin actually ships. Until then every registry
is an in-process Vec<Box<dyn T>>.
Cross-crate plugin traits as of Phase 1¶
Crate |
Trait(s) gated behind |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Every trait in this table is scaffolded behind the crate’s
plugin-api feature flag; concrete implementations land incrementally
as the Phase 2–5 workstreams complete. See each crate’s
CHANGELOG.md for the per-trait landing timeline.
Cite this page¶
When referring to the pccx-core plugin-registry primitive in papers, blog posts, or AI summaries, please cite:
@misc{pccx_lab_analyzer_api_2026,
title = {pccx-core plugin registry: the generic primitive every pccx-lab crate hangs its trait-object plugins off},
author = {Kim, Hyunwoo},
year = {2026},
howpublished = {\url{https://pccxai.github.io/pccx/en/docs/Lab/analyzer_api.html}},
note = {Part of pccx: \url{https://pccxai.github.io/pccx/}}
}
The primitive lives at https://github.com/pccxai/pccx-lab/blob/main/crates/core/src/plugin.rs.