diff --git a/.gitattributes b/.gitattributes index 0e0ad135..b3dabd4b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ crates/compass-cli/assets/vendor/pierre-diffs-v1.2.12.js -whitespace +crates/compass-output/assets/viewer/graph.js -whitespace +crates/compass-cli/assets/compass-skill/** text eol=lf diff --git a/.github/workflows/compass-ci.yml b/.github/workflows/compass-ci.yml index 98ddc5f1..5944db74 100644 --- a/.github/workflows/compass-ci.yml +++ b/.github/workflows/compass-ci.yml @@ -19,6 +19,27 @@ defaults: working-directory: . jobs: + code-graph-v1-fixtures: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "24.13.1" + cache: npm + - name: Install pinned Rust toolchain + uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master + with: + toolchain: 1.97.1 + components: rustfmt, clippy + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: . + - run: npm ci + - run: npx playwright install --with-deps chromium + - name: Qualify Compass code graph v1 + run: ./scripts/qualify_code_graph_v1.sh --fixtures-only + javascript-and-vscode: runs-on: ubuntu-24.04 steps: @@ -81,7 +102,10 @@ jobs: run: cargo nextest run --workspace --lib --bins --locked - name: Verify publishable crate archives - run: cargo package --workspace --locked --no-verify + run: | + cargo package --workspace --locked --no-verify \ + --exclude compass-transcribe \ + --exclude compass-whisper - name: Verify cargo install and standalone launch shell: bash @@ -140,6 +164,11 @@ jobs: - name: Native tests run: cargo test --workspace --lib --bins --locked --target ${{ matrix.target }} + - name: Code graph deterministic publication and SQLite recovery + run: | + cargo test -p compass-core --test code_graph_v1_determinism --locked --target ${{ matrix.target }} + cargo test -p compass-query --test index_recovery --locked --target ${{ matrix.target }} + - name: Test PowerShell installer if: runner.os == 'Windows' shell: pwsh @@ -148,6 +177,26 @@ jobs: ./scripts/test_install_ps1.ps1 ` -CompassBinary "target/${{ matrix.target }}/release/compass.exe" + code-graph-client-platforms: + strategy: + fail-fast: false + matrix: + runner: [ubuntu-24.04, macos-15, windows-2025] + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "24.13.1" + cache: npm + - run: npm ci + - name: Source containment and process cancellation + run: >- + npm exec -w editors/vscode -- + vitest run + src/views/sourceResolution.test.ts + src/cli/processManager.test.ts + dependency-audit: runs-on: ubuntu-24.04 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 465bf846..85b2c18d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## Unreleased +- Introduce the first supported Compass code graph contract, + `compass.graph/1`, as a strict versioned NetworkX-compatible multigraph with + structural, framework, enterprise, messaging, job, schema, configuration, + and database kinds. +- Add explicit `routes_to` bindings for supported server and file-routing + frameworks, preserving middleware order and attributable heuristic wiring. +- Add the shared `compass.query/1` search, callers, callees, impact, explore, + and node-trail contract across CLI, MCP, the viewer, and VS Code. +- Hard-cut over graph persistence: artifacts without `compass.graph/1` are not + loaded through an adapter. Run `compass update` to rebuild them. - Correct C function identities by resolving the callable declarator before generic declaration names, including macro-heavy SQLite declarations. - Preserve repeated Markdown sections and rationale entries as distinct diff --git a/Cargo.lock b/Cargo.lock index b1857f66..9973be48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1045,6 +1045,7 @@ dependencies = [ name = "compass-files" version = "0.1.10" dependencies = [ + "atomicwrites", "glob", "md-5 0.10.6", "rayon", @@ -1139,14 +1140,16 @@ name = "compass-ingest" version = "0.1.10" dependencies = [ "compass-files", - "compass-transcribe", "regex", "serde_json", + "sha1", + "sha2 0.10.9", "tempfile", "thiserror 2.0.19", "time", "ureq", "url", + "wait-timeout", ] [[package]] @@ -1165,7 +1168,6 @@ version = "0.1.10" dependencies = [ "compass-files", "compass-ir", - "compass-model", "compass-program", "compass-tree-sitter-language-pack", "flate2", @@ -1192,6 +1194,7 @@ version = "0.1.10" dependencies = [ "axum", "compass-core", + "compass-files", "compass-graph", "compass-model", "compass-prs", @@ -1283,6 +1286,7 @@ dependencies = [ name = "compass-prs" version = "0.1.10" dependencies = [ + "compass-files", "compass-model", "regex", "serde_json", @@ -1296,12 +1300,21 @@ dependencies = [ name = "compass-query" version = "0.1.10" dependencies = [ + "compass-analysis", "compass-cypher", + "compass-graph", "compass-graphdb", + "compass-ir", + "compass-languages", "compass-model", + "libc", "regex", + "rusqlite", + "rustix 1.1.4", "serde", "serde_json", + "sha2 0.10.9", + "tempfile", "thiserror 2.0.19", "unicode-normalization", ] @@ -1326,12 +1339,16 @@ name = "compass-resolve" version = "0.1.10" dependencies = [ "ahash", + "compass-graph", "compass-languages", "compass-model", "rayon", "regex", + "serde", "serde_json", "sha1", + "tempfile", + "thiserror 2.0.19", ] [[package]] @@ -1376,6 +1393,7 @@ dependencies = [ name = "compass-transcribe" version = "0.1.10" dependencies = [ + "compass-ingest", "compass-whisper", "rubato", "serde", @@ -2075,8 +2093,6 @@ dependencies = [ [[package]] name = "gemm-common" version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88027625910cc9b1085aaaa1c4bc46bb3a36aad323452b33c25b5e4e7c8e2a3e" dependencies = [ "bytemuck", "dyn-stack", diff --git a/Cargo.toml b/Cargo.toml index cd785337..023b8026 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ members = [ "vendor/compass-tree-sitter-language-pack", ] resolver = "3" -exclude = ["fuzz"] +exclude = ["fuzz", "vendor/gemm-common"] [workspace.package] version = "0.1.10" @@ -71,6 +71,8 @@ oxidize-pdf = { version = "4.1.1", default-features = false, features = ["compre ctrlc = "3.5.2" strsim = "0.11" rayon = "1" +rusqlite = { version = "0.31.0", features = ["bundled", "modern_sqlite"] } +rustix = { version = "1.1", features = ["fs"] } rand = "0.8.7" realfft = "3.5.0" regex = "1" @@ -125,3 +127,6 @@ codegen-units = 1 lto = "thin" panic = "abort" strip = "symbols" + +[patch.crates-io] +gemm-common = { path = "vendor/gemm-common" } diff --git a/Makefile b/Makefile index 33e58f14..92ab36ed 100644 --- a/Makefile +++ b/Makefile @@ -129,6 +129,10 @@ test-product: ## Run the Compass CLI product contract test-release-scripts: ## Test packaging and download installer scripts sh scripts/test_release_scripts.sh +.PHONY: qualify-code-graph-v1 +qualify-code-graph-v1: ## Run the complete Compass code graph v1 fixture release gate + ./scripts/qualify_code_graph_v1.sh --fixtures-only + # ── Lint & Format ────────────────────────────────────────────────── .PHONY: fmt diff --git a/crates/compass-analysis/Cargo.toml b/crates/compass-analysis/Cargo.toml index 87bfe0cf..fdd950eb 100644 --- a/crates/compass-analysis/Cargo.toml +++ b/crates/compass-analysis/Cargo.toml @@ -12,8 +12,8 @@ keywords.workspace = true categories.workspace = true [dependencies] -compass-ir = { path = "../compass-ir" } -compass-model = { path = "../compass-model" } +compass-ir = { path = "../compass-ir", version = "0.1.4" } +compass-model = { path = "../compass-model", version = "0.1.4" } rayon.workspace = true serde.workspace = true sha2.workspace = true diff --git a/crates/compass-analysis/src/universal_call_graph.rs b/crates/compass-analysis/src/universal_call_graph.rs index 1f7c7b7e..aea330b5 100644 --- a/crates/compass-analysis/src/universal_call_graph.rs +++ b/crates/compass-analysis/src/universal_call_graph.rs @@ -595,11 +595,7 @@ fn coverage( } fn line(node: &NodeRecord, key: &str) -> Option { - node.attributes.get(key).and_then(|value| { - value - .as_u64() - .or_else(|| value.as_i64().and_then(|value| value.try_into().ok())) - }) + node.unsigned(key) } fn source_line(location: &str) -> Option { diff --git a/crates/compass-cargo/src/lib.rs b/crates/compass-cargo/src/lib.rs index adb63f14..1ee9f2b8 100644 --- a/crates/compass-cargo/src/lib.rs +++ b/crates/compass-cargo/src/lib.rs @@ -105,11 +105,26 @@ pub fn introspect_cargo(root: &Path) -> Result Result Result) -> io::Resul Ok(()) } +fn canonical_text(input: &str) -> String { + input.replace("\r\n", "\n").replace('\r', "\n") +} + fn main() -> Result<(), Box> { println!( "cargo:rustc-env=COMPASS_BUILD_TARGET={}", @@ -45,17 +49,24 @@ fn main() -> Result<(), Box> { for directory in ["compass-skill", "compass-integrations"] { collect(&root, &root.join(directory), &mut files)?; } + let output = PathBuf::from(env::var("OUT_DIR")?); + let embedded_root = output.join("install-assets"); let mut generated = String::from("static EMBEDDED_ASSETS: &[EmbeddedAsset] = &[\n"); for relative in files { let source = root.join(&relative); + let embedded = embedded_root.join(&relative); + if let Some(parent) = embedded.parent() { + fs::create_dir_all(parent)?; + } + let body = fs::read_to_string(&source)?; + fs::write(&embedded, canonical_text(&body))?; generated.push_str(&format!( " EmbeddedAsset {{ path: {:?}, bytes: include_bytes!({:?}) }},\n", relative.to_string_lossy().replace('\\', "/"), - source + embedded )); } generated.push_str("];\n"); - let output = PathBuf::from(env::var("OUT_DIR")?); fs::write(output.join("install_assets.rs"), generated)?; Ok(()) } diff --git a/crates/compass-cli/src/call_graph_commands.rs b/crates/compass-cli/src/call_graph_commands.rs index fdcff69b..44681443 100644 --- a/crates/compass-cli/src/call_graph_commands.rs +++ b/crates/compass-cli/src/call_graph_commands.rs @@ -16,7 +16,20 @@ pub(crate) fn command(frontend: Frontend, args: &[String]) -> Outcome { Ok(options) => options, Err(error) => return Outcome::failure_with_code(format!("error: {error}"), 2), }; - let graph = match GraphDocument::load(&options.graph) { + let resolved_graph = match compass_files::BuildGuard::resolve_requested_artifact(&options.graph) + { + Ok(path) => path, + Err(error) => { + return Outcome::failure_with_code( + format!( + "error: could not resolve graph {}: {error}", + options.graph.display() + ), + 3, + ); + } + }; + let graph = match GraphDocument::load(&resolved_graph) { Ok(graph) => graph, Err(error) => { return Outcome::failure_with_code( diff --git a/crates/compass-cli/src/code_query_commands.rs b/crates/compass-cli/src/code_query_commands.rs new file mode 100644 index 00000000..225df65e --- /dev/null +++ b/crates/compass-cli/src/code_query_commands.rs @@ -0,0 +1,181 @@ +use std::path::PathBuf; + +use compass_model::query_contract::{ + CallRequest, CodeQueryLimits, CodeQueryResponse, ExploreRequest, ImpactRequest, + NodeTrailRequest, SearchRequest, +}; +use compass_query::open; + +use crate::Outcome; + +pub(crate) fn command(operation: &str, args: &[String]) -> Outcome { + match execute(operation, args) { + Ok(response) => { + let format = option(args, "--format").unwrap_or("text"); + if format == "json" { + match serde_json::to_string_pretty(&response) { + Ok(json) => Outcome::success(json), + Err(error) => Outcome::failure(format!("error: {error}")), + } + } else if format == "text" { + Outcome::success(render_text(&response)) + } else { + Outcome::failure("error: --format must be json or text".to_owned()) + } + } + Err(error) => Outcome::failure(format!("error: {error}")), + } +} + +fn execute(operation: &str, args: &[String]) -> Result { + let positional = positional(args); + let requested_graph = + PathBuf::from(option(args, "--graph").unwrap_or("compass-out/graph.json")); + let cache = option(args, "--cache") + .map(PathBuf::from) + .unwrap_or_else(|| { + requested_graph + .parent() + .unwrap_or_else(|| std::path::Path::new(".")) + .join("cache") + }); + let graph = resolve_generation_artifact(requested_graph)?; + let program = option(args, "--program") + .map(PathBuf::from) + .map(resolve_generation_artifact) + .transpose()?; + let engine = open(&graph, program.as_deref(), &cache).map_err(|error| error.to_string())?; + let limits = limits(args)?; + match operation { + "search" => engine.search(SearchRequest { + query: required(&positional, 0, "search ")?.to_owned(), + limits, + }), + "callers" => engine.callers(CallRequest { + symbol: required(&positional, 0, "callers ")?.to_owned(), + limits, + }), + "callees" => engine.callees(CallRequest { + symbol: required(&positional, 0, "callees ")?.to_owned(), + limits, + }), + "impact" => engine.impact(ImpactRequest { + symbol: required(&positional, 0, "impact ")?.to_owned(), + include_heuristic: args.iter().any(|arg| arg == "--include-heuristic"), + limits, + }), + "explore" => engine.explore(ExploreRequest { + symbols: positional, + root: option(args, "--root").unwrap_or_default().to_owned(), + limits, + }), + "node" => engine.node_trail(NodeTrailRequest { + source: required(&positional, 0, "node ")?.to_owned(), + target: required(&positional, 1, "node ")?.to_owned(), + include_heuristic: args.iter().any(|arg| arg == "--include-heuristic"), + limits, + }), + _ => unreachable!(), + } + .map_err(|error| error.to_string()) +} + +fn resolve_generation_artifact(path: PathBuf) -> Result { + compass_files::BuildGuard::resolve_requested_artifact(&path).map_err(|error| error.to_string()) +} + +fn limits(args: &[String]) -> Result { + let defaults = CodeQueryLimits::default(); + Ok(CodeQueryLimits { + max_depth: number(args, "--max-depth", defaults.max_depth)?, + max_nodes: number(args, "--max-nodes", defaults.max_nodes)?, + max_edges: number(args, "--max-edges", defaults.max_edges)?, + max_paths: number(args, "--max-paths", defaults.max_paths)?, + max_candidates: number(args, "--max-candidates", defaults.max_candidates)?, + max_source_bytes: number(args, "--max-source-bytes", defaults.max_source_bytes)?, + max_response_bytes: number(args, "--max-response-bytes", defaults.max_response_bytes)?, + }) +} + +fn number( + args: &[String], + name: &str, + default: T, +) -> Result { + option(args, name).map_or(Ok(default), |value| { + value + .parse() + .map_err(|_| format!("{name} requires a positive integer")) + }) +} + +fn option<'a>(args: &'a [String], name: &str) -> Option<&'a str> { + args.iter() + .position(|argument| argument == name) + .and_then(|index| args.get(index + 1)) + .map(String::as_str) +} + +fn positional(args: &[String]) -> Vec { + let value_options = [ + "--graph", + "--program", + "--cache", + "--format", + "--root", + "--max-depth", + "--max-nodes", + "--max-edges", + "--max-paths", + "--max-candidates", + "--max-source-bytes", + "--max-response-bytes", + ]; + let mut values = Vec::new(); + let mut skip = false; + for argument in args { + if skip { + skip = false; + } else if value_options.contains(&argument.as_str()) { + skip = true; + } else if !argument.starts_with("--") { + values.push(argument.clone()); + } + } + values +} + +fn required<'a>(values: &'a [String], index: usize, usage: &str) -> Result<&'a str, String> { + values + .get(index) + .map(String::as_str) + .ok_or_else(|| format!("usage: compass {usage} [OPTIONS]")) +} + +fn render_text(response: &CodeQueryResponse) -> String { + let mut lines = vec![format!( + "{:?}: {} node(s), {} edge(s), {} path(s)", + response.operation, + response.nodes.len(), + response.edges.len(), + response.paths.len() + )]; + lines.extend(response.nodes.iter().map(|node| { + format!( + "{} [{}] {}", + node.qualified_name, + node.kind.as_str(), + node.source + .as_ref() + .map(|source| format!("{}:{}", source.file, source.start_line)) + .unwrap_or_default() + ) + })); + lines.extend( + response + .diagnostics + .iter() + .map(|diagnostic| format!("! {:?}: {}", diagnostic.code, diagnostic.message)), + ); + lines.join("\n") +} diff --git a/crates/compass-cli/src/dedup_commands.rs b/crates/compass-cli/src/dedup_commands.rs index dab266d6..87d1a88f 100644 --- a/crates/compass-cli/src/dedup_commands.rs +++ b/crates/compass-cli/src/dedup_commands.rs @@ -192,7 +192,7 @@ fn format_environment_keys(keys: &[&str]) -> String { #[cfg(test)] mod tests { use super::*; - use compass_model::NodeRecord; + use compass_graph::RawNodeRecord as NodeRecord; use serde_json::{Map, Value}; use std::io::{Read, Write}; use std::net::TcpListener; diff --git a/crates/compass-cli/src/help.rs b/crates/compass-cli/src/help.rs index c73fd4c2..b2c1df98 100644 --- a/crates/compass-cli/src/help.rs +++ b/crates/compass-cli/src/help.rs @@ -87,6 +87,12 @@ const GROUPS: &[Group] = &[ Group { title: "Explore", commands: &[ + "search", + "callers", + "callees", + "impact", + "explore", + "node", "call-graph", "query", "program", @@ -210,6 +216,42 @@ const PAGES: &[Page] = &[ ["compass merge-semantic --cached --new --out "], "Options:\n --cached Existing semantic result\n --new Newly extracted semantic result\n --out Required merged output path\n\nExamples:\n compass merge-semantic --cached old.json --new fresh.json --out semantic.json" ), + page!( + "search", + "Search typed code symbols by name", + ["compass search [OPTIONS]"], + "Arguments:\n Symbol name or qualified name\n\nOptions:\n --graph Typed graph [default: compass-out/graph.json]\n --program Optional Program IR enrichment\n --cache Query-index cache directory\n --max-candidates Candidate bound\n --format Output format [default: text]\n\nExamples:\n compass search PaymentService\n compass search checkout --format json\n\nNotes:\n Search uses the versioned compass.query/1 response contract in both formats." + ), + page!( + "callers", + "List direct callers of a typed symbol", + ["compass callers [OPTIONS]"], + "Arguments:\n Symbol ID, name, or qualified name\n\nOptions:\n --graph Typed graph [default: compass-out/graph.json]\n --program Optional Program IR enrichment\n --cache Query-index cache directory\n --max-nodes Node bound\n --max-edges Edge bound\n --format Output format [default: text]\n\nExamples:\n compass callers PaymentService.charge\n compass callers sym:checkout --format json" + ), + page!( + "callees", + "List direct callees of a typed symbol", + ["compass callees [OPTIONS]"], + "Arguments:\n Symbol ID, name, or qualified name\n\nOptions:\n --graph Typed graph [default: compass-out/graph.json]\n --program Optional Program IR enrichment\n --cache Query-index cache directory\n --max-nodes Node bound\n --max-edges Edge bound\n --format Output format [default: text]\n\nExamples:\n compass callees CheckoutController.create\n compass callees sym:checkout --format json" + ), + page!( + "impact", + "Compute the bounded transitive impact of a symbol", + ["compass impact [OPTIONS]"], + "Arguments:\n Changed symbol ID, name, or qualified name\n\nOptions:\n --graph Typed graph [default: compass-out/graph.json]\n --program Optional Program IR enrichment\n --cache Query-index cache directory\n --max-depth Traversal radius\n --max-nodes Node bound\n --max-edges Edge bound\n --include-heuristic Traverse heuristic evidence\n --format Output format [default: text]\n\nExamples:\n compass impact PaymentGateway --max-depth 3\n compass impact sym:gateway --include-heuristic --format json" + ), + page!( + "explore", + "Return related source grouped by file with connecting paths", + ["compass explore [SYMBOL...] [OPTIONS]"], + "Arguments:\n Symbol IDs, names, or qualified names\n\nOptions:\n --root Repository root used to read source\n --graph Typed graph [default: compass-out/graph.json]\n --program Optional Program IR enrichment\n --cache Query-index cache directory\n --max-paths Path bound\n --max-source-bytes Source-byte bound\n --max-response-bytes Serialized response bound\n --format Output format [default: text]\n\nExamples:\n compass explore CheckoutController PaymentGateway --root .\n compass explore sym:a sym:b --root . --format json" + ), + page!( + "node", + "Show a bounded evidence trail between two symbols", + ["compass node [OPTIONS]"], + "Arguments:\n Trail origin symbol\n Trail destination symbol\n\nOptions:\n --graph Typed graph [default: compass-out/graph.json]\n --program Optional Program IR enrichment\n --cache Query-index cache directory\n --max-depth Traversal radius\n --max-paths Path bound\n --include-heuristic Include heuristic evidence\n --format Output format [default: text]\n\nExamples:\n compass node route:/checkout CheckoutController.create\n compass node sym:a sym:b --include-heuristic --format json" + ), page!( "query", "Search the graph with natural language or CompassQL", @@ -1005,7 +1047,7 @@ mod tests { #[test] fn catalog_has_unique_complete_public_roots() { let roots = root_commands(); - assert_eq!(roots.len(), 39); + assert_eq!(roots.len(), 45); for root in roots { let matches = PAGES.iter().filter(|page| page.path == root).count(); assert_eq!(matches, 1, "{root}"); diff --git a/crates/compass-cli/src/history_build.rs b/crates/compass-cli/src/history_build.rs index 4540802c..44f8cda8 100644 --- a/crates/compass-cli/src/history_build.rs +++ b/crates/compass-cli/src/history_build.rs @@ -870,7 +870,11 @@ impl CompleteGraphBuilder for NativeCompleteGraphBuilder { if !self.is_default_current_snapshot_profile() { return Ok(None); } - let Some(output_dir) = self.working_tree_seed.as_ref() else { + let Some(output_container) = self.working_tree_seed.as_ref() else { + return Ok(None); + }; + let Ok(output_dir) = compass_files::BuildGuard::resolve_active_directory(output_container) + else { return Ok(None); }; if !graph_stamp_matches(&output_dir.join("graph.json"), commit) { @@ -891,7 +895,7 @@ impl CompleteGraphBuilder for NativeCompleteGraphBuilder { return Ok(None); } let Ok(completed) = CompletedGraphArtifacts::load( - output_dir, + &output_dir, CompletionEvidence { extraction_succeeded: true, allow_partial: false, @@ -913,6 +917,9 @@ impl CompleteGraphBuilder for NativeCompleteGraphBuilder { if program.program.providers != expected_providers { return Ok(None); } + if !analysis_ids_match_graph(&completed) { + return Ok(None); + } let code_files = detection .files .get("code") @@ -970,7 +977,9 @@ impl CompleteGraphBuilder for NativeCompleteGraphBuilder { }); } - let output_dir = output_root.join("compass-out"); + let output_container = output_root.join("compass-out"); + let output_dir = compass_files::BuildGuard::resolve_active_directory(&output_container) + .map_err(|error| MaterializeError::Builder(error.to_string()))?; let detection = detect( checkout, &DetectOptions { @@ -1026,11 +1035,7 @@ impl CompleteGraphBuilder for NativeCompleteGraphBuilder { ) -> Result>, MaterializeError> { let mut communities = compass_graph::Communities::new(); for node in &completed.artifacts.document.nodes { - let community = node - .attributes - .get("community") - .and_then(serde_json::Value::as_u64) - .unwrap_or_default() as usize; + let community = node.unsigned("community").unwrap_or_default() as usize; communities .entry(community) .or_default() @@ -1078,16 +1083,53 @@ impl NativeCompleteGraphBuilder { } } +fn analysis_ids_match_graph(completed: &CompletedGraphArtifacts) -> bool { + let graph_ids = completed + .artifacts + .document + .nodes + .iter() + .map(|node| node.id.as_str()) + .collect::>(); + let Some(communities) = completed + .artifacts + .analysis + .as_ref() + .and_then(|analysis| analysis.get("communities")) + .and_then(serde_json::Value::as_object) + else { + return false; + }; + let analysis_ids = communities + .values() + .filter_map(serde_json::Value::as_array) + .flatten() + .filter_map(serde_json::Value::as_str) + .collect::>(); + analysis_ids == graph_ids +} + fn graph_stamp_matches(path: &Path, commit: &compass_history::CommitId) -> bool { #[derive(serde::Deserialize)] struct GraphStamp { - built_at_commit: Option, + graph: GraphStampMetadata, + } + + #[derive(serde::Deserialize)] + struct GraphStampMetadata { + build: GraphBuildStamp, + } + + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct GraphBuildStamp { + source_commit: Option, } fs::File::open(path) .ok() .and_then(|file| serde_json::from_reader::<_, GraphStamp>(file).ok()) - .and_then(|stamp| stamp.built_at_commit) + .and_then(|stamp| stamp.graph.build.source_commit) .as_deref() == Some(commit.as_str()) } @@ -1202,7 +1244,7 @@ mod tests { } #[test] - fn native_builder_promotes_an_exact_current_code_only_snapshot() + fn native_builder_rejects_snapshot_with_pre_v1_analysis_ids() -> Result<(), Box> { let (directory, commit, output) = current_snapshot_fixture()?; let options = @@ -1213,16 +1255,10 @@ mod tests { None, ); - let promoted = builder.promote_current(directory.path(), &commit)?; - assert!( - promoted.is_some_and(|completed| completed.artifacts.document.nodes.iter().any( - |node| node - .attributes - .get("source_file") - .and_then(serde_json::Value::as_str) - == Some("service.rs") - )) + builder + .promote_current(directory.path(), &commit)? + .is_none() ); Ok(()) } diff --git a/crates/compass-cli/src/history_commands.rs b/crates/compass-cli/src/history_commands.rs index 40f85a93..50d6d838 100644 --- a/crates/compass-cli/src/history_commands.rs +++ b/crates/compass-cli/src/history_commands.rs @@ -63,7 +63,7 @@ pub(crate) fn load_graph_at( let cache_key = serde_json::json!({ "schema": "compass.history.graph_query_key/1", "realization": preferred.id.to_string(), - "projection_version": 1, + "projection_version": 2, }); let document = cache .read(DerivedCacheNamespace::Viewer, &cache_key, 256 * 1024 * 1024) @@ -73,11 +73,10 @@ pub(crate) fn load_graph_at( let document = match document { Some(document) => document, None => { - let reader = history - .reader(&preferred.id) - .map_err(|error| error.to_string())?; - let document = compass_output::historical_graph_document(&reader) + let artifacts = history + .artifacts(&preferred.id) .map_err(|error| error.to_string())?; + let document = artifacts.artifacts.document; if let Ok(value) = serde_json::to_value(&document) && let Ok(bytes) = compass_history::canonical_json_bytes(&value) { @@ -688,8 +687,7 @@ fn execute(frontend: Frontend, args: &[String]) -> Result Result Outcome { results.push(result); } - let graph_exists = scope.root().join("compass-out/graph.json").is_file(); + let output = scope.root().join("compass-out"); + let graph_exists = compass_files::BuildGuard::resolve_artifact(&output, "graph.json") + .is_ok_and(|path| path.is_file()); let mut next_actions = Vec::new(); if !graph_exists && scope.is_project() { next_actions @@ -3965,6 +3967,12 @@ mod tests { fn canonical_compass_skill_package_is_native() { let body = asset_text(SKILL_ASSET).unwrap_or_default(); assert!(body.starts_with("---\nname: compass\n")); + assert!( + EMBEDDED_ASSETS + .iter() + .all(|asset| !asset.bytes.contains(&b'\r')), + "embedded text assets must use canonical LF line endings" + ); assert!(body.contains("references/query.md")); assert!(body.contains("compass query")); assert!(!body.contains("python -m"), "stale token python -m"); diff --git a/crates/compass-cli/src/integration_commands.rs b/crates/compass-cli/src/integration_commands.rs index 5398de97..98affec9 100644 --- a/crates/compass-cli/src/integration_commands.rs +++ b/crates/compass-cli/src/integration_commands.rs @@ -317,7 +317,7 @@ pub(super) fn command_hook_guard(_frontend: Frontend, args: &[String]) -> Outcom if kind == "gemini" { let mut payload = Map::new(); payload.insert("decision".to_owned(), Value::String("allow".to_owned())); - if graph_path().is_file() { + if graph_path().is_ok_and(|path| path.is_file()) { payload.insert( "additionalContext".to_owned(), Value::String(GEMINI_NUDGE_TEXT.to_owned()), @@ -358,7 +358,7 @@ fn search_guard(tool: &Map) -> Option { let bash_search = ["grep", "ripgrep", "rg ", "find ", "fd ", "ack ", "ag "] .iter() .any(|token| command.contains(token)); - ((grep_tool || bash_search) && graph_path().is_file()) + ((grep_tool || bash_search) && graph_path().is_ok_and(|path| path.is_file())) .then(|| pretool_payload("additionalContext", SEARCH_NUDGE_TEXT)) } @@ -408,7 +408,7 @@ fn read_guard( { return None; } - let graph = graph_path(); + let graph = graph_path().ok()?; let graph_modified = graph .metadata() .and_then(|metadata| metadata.modified()) @@ -574,7 +574,10 @@ fn target_is_indexed(file_path: &str, root: &Path) -> bool { if file_path.is_empty() { return true; } - let manifest_path = output_root().join("manifest.json"); + let output = output_root(); + let Ok(manifest_path) = crate::resolve_output_artifact(&output, "manifest.json") else { + return false; + }; let Ok(metadata) = manifest_path.metadata() else { return true; }; @@ -941,8 +944,9 @@ fn output_root() -> PathBuf { PathBuf::from(std::env::var("COMPASS_OUT").unwrap_or_else(|_| "compass-out".to_owned())) } -fn graph_path() -> PathBuf { - output_root().join("graph.json") +fn graph_path() -> Result { + let output = output_root(); + crate::resolve_output_artifact(&output, "graph.json") } fn project_root() -> PathBuf { diff --git a/crates/compass-cli/src/label_commands.rs b/crates/compass-cli/src/label_commands.rs index 59e86891..213db76e 100644 --- a/crates/compass-cli/src/label_commands.rs +++ b/crates/compass-cli/src/label_commands.rs @@ -37,10 +37,16 @@ pub(super) fn command_label(_frontend: Frontend, args: &[String]) -> Outcome { Err(error) => return Outcome::failure(error), }; let output_name = std::env::var("COMPASS_OUT").unwrap_or_else(|_| "compass-out".to_owned()); - let graph_path = parsed + let requested_graph = parsed .graph_override .clone() .unwrap_or_else(|| parsed.root.join(&output_name).join("graph.json")); + let graph_path = match compass_files::BuildGuard::resolve_requested_artifact(&requested_graph) { + Ok(path) => path, + Err(error) => { + return Outcome::failure(format!("error: could not resolve graph: {error}")); + } + }; if !graph_path.exists() { let message = format!( "error: no graph found at {} — run `compass extract {}` first", @@ -49,16 +55,10 @@ pub(super) fn command_label(_frontend: Frontend, args: &[String]) -> Outcome { ); return Outcome::failure(message); } - let output_dir = if parsed.graph_override.is_some() - && graph_path.parent().and_then(Path::file_name) == Path::new(&output_name).file_name() - { - graph_path - .parent() - .unwrap_or_else(|| Path::new(".")) - .to_path_buf() - } else { - parsed.root.join(&output_name) - }; + let output_dir = graph_path + .parent() + .unwrap_or_else(|| Path::new(".")) + .to_path_buf(); let environment = std::env::vars().collect::>(); let global_providers = home_directory() diff --git a/crates/compass-cli/src/lib.rs b/crates/compass-cli/src/lib.rs index 8e308d64..c7ba25b3 100644 --- a/crates/compass-cli/src/lib.rs +++ b/crates/compass-cli/src/lib.rs @@ -2,6 +2,7 @@ mod call_graph_commands; mod capability_commands; +mod code_query_commands; mod dedup_commands; mod help; mod history_batch; @@ -118,6 +119,11 @@ pub struct Outcome { html_output: Option, } +pub(crate) fn resolve_output_artifact(output: &Path, name: &str) -> Result { + compass_files::BuildGuard::resolve_requested_artifact(&output.join(name)) + .map_err(|error| error.to_string()) +} + impl Outcome { #[must_use] pub fn from_command_output(code: u8, stdout: String, stderr: String) -> Self { @@ -341,6 +347,12 @@ pub fn run(frontend: Frontend, arguments: impl IntoIterator) -> "history" => history_commands::command(frontend, &args), "call-graph" => call_graph_commands::command(frontend, &args), "capabilities" => capability_commands::command(frontend, &args), + "search" => code_query_commands::command("search", &args), + "callers" => code_query_commands::command("callers", &args), + "callees" => code_query_commands::command("callees", &args), + "impact" => code_query_commands::command("impact", &args), + "explore" => code_query_commands::command("explore", &args), + "node" => code_query_commands::command("node", &args), "history-worker" => history_commands::command_worker(frontend, &args), "diff" => semantic_diff_commands::command(frontend, &args), "query" => query_commands::command_query(frontend, &args), @@ -620,7 +632,10 @@ fn parse_mcp_options(args: &[String]) -> Result, String> { let graph_path = graph_flag .filter(|path| !path.as_os_str().is_empty()) .or_else(|| positional.filter(|path| !path.as_os_str().is_empty())) - .unwrap_or_else(default_graph_path); + .unwrap_or_else(|| { + PathBuf::from(std::env::var("COMPASS_OUT").unwrap_or_else(|_| "compass-out".to_owned())) + .join("graph.json") + }); Ok(Some(McpOptions { graph_path, transport, @@ -1116,6 +1131,10 @@ fn command_diagnose(frontend: Frontend, args: &[String]) -> Outcome { index += 1; } let _ = frontend; + let graph_path = match compass_files::BuildGuard::resolve_requested_artifact(&graph_path) { + Ok(path) => path, + Err(error) => return Outcome::failure(format!("error: {error}")), + }; match diagnose_graph_file(&graph_path, directed, max_examples, extract_path.as_deref()) { Ok(summary) if json_output => { match serde_json::to_string_pretty(&format_diagnostic_json(&summary)) { @@ -1210,9 +1229,15 @@ fn command_cluster_only(_frontend: Frontend, args: &[String]) -> Outcome { index += 1; } let output_name = std::env::var("COMPASS_OUT").unwrap_or_else(|_| "compass-out".to_owned()); - let graph_path = graph_override + let requested_graph = graph_override .clone() .unwrap_or_else(|| root.join(&output_name).join("graph.json")); + let graph_path = match compass_files::BuildGuard::resolve_requested_artifact(&requested_graph) { + Ok(path) => path, + Err(error) => { + return Outcome::failure(format!("error: could not resolve graph: {error}")); + } + }; if !graph_path.exists() { return Outcome::failure(format!( "error: no graph found at {} — run `compass extract {}` first", @@ -1220,16 +1245,10 @@ fn command_cluster_only(_frontend: Frontend, args: &[String]) -> Outcome { root.display() )); } - let output_dir = if graph_override.is_some() - && graph_path.parent().and_then(Path::file_name) == Path::new(&output_name).file_name() - { - graph_path - .parent() - .unwrap_or_else(|| Path::new(".")) - .to_path_buf() - } else { - root.join(&output_name) - }; + let output_dir = graph_path + .parent() + .unwrap_or_else(|| Path::new(".")) + .to_path_buf(); match cluster_existing_graph(&ClusterExistingOptions { graph_path, output_dir: output_dir.clone(), @@ -1312,6 +1331,12 @@ fn command_tree(frontend: Frontend, args: &[String]) -> Outcome { } index += 1; } + graph_path = match compass_files::BuildGuard::resolve_requested_artifact(&graph_path) { + Ok(path) => path, + Err(error) => { + return Outcome::failure(format!("error: could not resolve graph: {error}")); + } + }; if !graph_path.is_file() { return Outcome::failure(format!( "error: graph.json not found at {}", @@ -1395,12 +1420,20 @@ fn command_merge_graphs(args: &[String]) -> Outcome { .to_owned(), ); } - for path in &paths { + let mut resolved_paths = Vec::with_capacity(paths.len()); + for path in paths { + let path = match compass_files::BuildGuard::resolve_requested_artifact(&path) { + Ok(path) => path, + Err(error) => { + return Outcome::failure(format!("error: could not resolve graph: {error}")); + } + }; if !path.exists() { return Outcome::failure(format!("error: not found: {}", path.display())); } + resolved_paths.push(path); } - match merge_graphs(&paths, &output) { + match merge_graphs(&resolved_paths, &output) { Ok(result) => { let mut lines = Vec::new(); if result.naive_tags_collided { @@ -1421,7 +1454,13 @@ fn command_merge_graphs(args: &[String]) -> Outcome { } fn command_benchmark(args: &[String]) -> Outcome { - let graph_path = args.first().map_or_else(default_graph_path, PathBuf::from); + let requested = args.first().map_or_else(default_graph_path, PathBuf::from); + let graph_path = match compass_files::BuildGuard::resolve_requested_artifact(&requested) { + Ok(path) => path, + Err(error) => { + return Outcome::failure(format!("error: could not resolve graph: {error}")); + } + }; let document = match compass_model::GraphDocument::load(&graph_path) { Ok(document) => document, Err(error) => return Outcome::failure(format!("error: {error}")), @@ -1533,6 +1572,7 @@ fn command_build_with_validation_inner( let mut token_budget = None; let mut max_concurrency = None; let mut max_workers = None; + let mut max_source_bytes = None; let mut api_timeout = None; let mut allow_partial = false; let mut timing = false; @@ -1716,13 +1756,32 @@ fn command_build_with_validation_inner( Err(error) => return extract_parse_failure(frontend, error), }; } + "--max-source-bytes" if index + 1 < args.len() => { + max_source_bytes = match parse_positive_u64(&args[index + 1], "--max-source-bytes") + { + Ok(value) => Some(value), + Err(error) => return extract_parse_failure(frontend, error), + }; + index += 1; + } + value if value.starts_with("--max-source-bytes=") => { + max_source_bytes = match parse_positive_u64(&value[19..], "--max-source-bytes") { + Ok(value) => Some(value), + Err(error) => return extract_parse_failure(frontend, error), + }; + } + "--max-source-bytes" => { + return Outcome::failure( + "error: --max-source-bytes requires a positive integer".to_owned(), + ); + } "--timing" => timing = true, "--dedup-llm" if extract => dedup_llm = true, "-h" | "--help" => { return Outcome::success(if extract { extract_help() } else { - "Usage: compass update [path] [--program-artifact PATH] [--no-cluster] [--force] [--no-viz] [--timing]".to_owned() + "Usage: compass update [path] [--program-artifact PATH] [--max-source-bytes N] [--no-cluster] [--force] [--no-viz] [--timing]".to_owned() }); } value if value.starts_with('-') => { @@ -1784,17 +1843,20 @@ fn command_build_with_validation_inner( options.program_artifacts = program_artifacts; options.precomputed_detection = precomputed_detection; apply_max_workers_override(&mut options, max_workers); + if let Some(max_source_bytes) = max_source_bytes { + options.max_source_bytes = max_source_bytes; + } let output_name = std::env::var("COMPASS_OUT").unwrap_or_else(|_| "compass-out".to_owned()); + let output_container = options + .output_root + .as_deref() + .map(absolute_cli_path) + .unwrap_or_else(|| root.clone()) + .join(output_name); let extract_incremental = extract && !force - && options - .output_root - .as_deref() - .map(absolute_cli_path) - .unwrap_or_else(|| root.clone()) - .join(output_name) - .join("graph.json") - .is_file(); + && compass_files::BuildGuard::resolve_artifact(&output_container, "graph.json") + .is_ok_and(|path| path.is_file()); let mut dedup_environment = std::env::vars().collect::>(); if let Some(timeout) = api_timeout { dedup_environment.insert("COMPASS_API_TIMEOUT".to_owned(), timeout.to_string()); @@ -2430,6 +2492,18 @@ fn parse_positive_usize(value: &str, option: &str) -> Result { .ok_or_else(|| format!("error: {option} must be > 0 (got {parsed})")) } +fn parse_positive_u64(value: &str, option: &str) -> Result { + let parsed = value.parse::().map_err(|_| { + format!( + "error: {option} must be a positive integer (got {})", + python_string_repr(value) + ) + })?; + (parsed > 0) + .then_some(parsed) + .ok_or_else(|| format!("error: {option} must be > 0 (got {parsed})")) +} + fn parse_positive_f64(value: &str, option: &str) -> Result { let parsed = value.parse::().map_err(|_| { format!( @@ -2551,7 +2625,7 @@ fn executable_on_path(name: &str) -> bool { } fn extract_help() -> String { - "Usage: compass extract [PATH] [--program-artifact PATH] [--code-only] [--cargo] [--google-workspace] [--postgres DSN] [--backend NAME] [--model MODEL] [--mode deep] [--token-budget N] [--max-concurrency N] [--max-workers N] [--api-timeout SECONDS] [--allow-partial] [--dedup-llm] [--timing] [--out DIR] [--no-cluster] [--force] [--no-viz] [--no-gitignore] [--exclude PATTERN] [--resolution N] [--exclude-hubs N]".to_owned() + "Usage: compass extract [PATH] [--program-artifact PATH] [--code-only] [--cargo] [--google-workspace] [--postgres DSN] [--backend NAME] [--model MODEL] [--mode deep] [--token-budget N] [--max-concurrency N] [--max-workers N] [--max-source-bytes N] [--api-timeout SECONDS] [--allow-partial] [--dedup-llm] [--timing] [--out DIR] [--no-cluster] [--force] [--no-viz] [--no-gitignore] [--exclude PATTERN] [--resolution N] [--exclude-hubs N]".to_owned() } fn saved_graph_root() -> Option { @@ -2804,6 +2878,12 @@ fn command_export(frontend: Frontend, args: &[String]) -> Outcome { _ => index += 1, } } + graph_path = match compass_files::BuildGuard::resolve_requested_artifact(&graph_path) { + Ok(path) => path, + Err(error) => { + return Outcome::failure(format!("error: could not resolve graph: {error}")); + } + }; if graph_explicit { let output_dir = graph_path.parent().unwrap_or_else(|| Path::new(".")); if !labels_explicit { @@ -3639,16 +3719,20 @@ fn frontend_name(frontend: Frontend) -> &'static str { } fn load(path: &Path, force_directed: bool) -> Result { + let path = compass_files::BuildGuard::resolve_requested_artifact(path) + .map_err(|error| Outcome::failure(format!("error: could not resolve graph: {error}")))?; let result = if force_directed { - LoadedGraph::load_directed(path) + LoadedGraph::load_directed(&path) } else { - LoadedGraph::load(path) + LoadedGraph::load(&path) }; result.map_err(graph_load_outcome) } fn load_affected(path: &Path) -> Result { - LoadedGraph::load_for_affected(path).map_err(graph_load_outcome) + let path = compass_files::BuildGuard::resolve_requested_artifact(path) + .map_err(|error| Outcome::failure(format!("error: could not resolve graph: {error}")))?; + LoadedGraph::load_for_affected(&path).map_err(graph_load_outcome) } fn graph_load_outcome(error: GraphError) -> Outcome { @@ -3837,6 +3921,28 @@ mod mcp_option_tests { Ok(()) } + #[test] + fn shared_cli_artifact_resolution_distinguishes_legacy_from_malformed_pointer() + -> Result<(), Box> { + let directory = tempfile::tempdir()?; + let output = directory.path().join("compass-out"); + fs::create_dir_all(&output)?; + fs::write(output.join("graph.json"), "{}")?; + + assert_eq!( + resolve_output_artifact(&output, "graph.json")?, + output.join("graph.json"), + "pointer absence must retain documented legacy compatibility" + ); + + fs::write(output.join(".compass-active-generation"), "../escape")?; + let Err(error) = resolve_output_artifact(&output, "graph.json") else { + return Err("a malformed pointer must never fall back to the legacy graph".into()); + }; + assert!(error.contains("generation"), "{error}"); + Ok(()) + } + #[test] fn oversized_session_timeout_is_an_error_not_a_panic() { assert_eq!( @@ -4115,4 +4221,16 @@ mod mcp_option_tests { apply_max_workers_override(&mut options, Some(4)); assert_eq!(options.max_workers, Some(4)); } + + #[test] + fn source_size_limit_parser_rejects_zero_negative_and_invalid_values() { + assert_eq!( + parse_positive_u64("16777216", "--max-source-bytes"), + Ok(16_777_216) + ); + for invalid in ["0", "-1", "large"] { + assert!(parse_positive_u64(invalid, "--max-source-bytes").is_err()); + } + assert!(extract_help().contains("[--max-source-bytes N]")); + } } diff --git a/crates/compass-cli/src/program_commands.rs b/crates/compass-cli/src/program_commands.rs index 780b78bb..d4fc89c5 100644 --- a/crates/compass-cli/src/program_commands.rs +++ b/crates/compass-cli/src/program_commands.rs @@ -57,8 +57,9 @@ struct CommonOptions { } fn default_program_path() -> PathBuf { - PathBuf::from(std::env::var("COMPASS_OUT").unwrap_or_else(|_| "compass-out".to_owned())) - .join("program.json") + let output = + PathBuf::from(std::env::var("COMPASS_OUT").unwrap_or_else(|_| "compass-out".to_owned())); + output.join("program.json") } fn parse_common( @@ -109,12 +110,14 @@ fn parse_format(value: &str) -> Result { } pub(crate) fn load_program(path: &Path) -> Result { - let metadata = fs::metadata(path) - .map_err(|error| format!("could not read {}: {error}", path.display()))?; + let resolved = compass_files::BuildGuard::resolve_requested_artifact(path) + .map_err(|error| error.to_string())?; + let metadata = fs::metadata(&resolved) + .map_err(|error| format!("could not read {}: {error}", resolved.display()))?; if !metadata.is_file() { return Err(format!( "Program IR is not a regular file: {}", - path.display() + resolved.display() )); } if metadata.len() > MAX_PROGRAM_BYTES { @@ -122,10 +125,10 @@ pub(crate) fn load_program(path: &Path) -> Result { "Program IR exceeds the {MAX_PROGRAM_BYTES}-byte safety limit" )); } - let bytes = - fs::read(path).map_err(|error| format!("could not read {}: {error}", path.display()))?; + let bytes = fs::read(&resolved) + .map_err(|error| format!("could not read {}: {error}", resolved.display()))?; let analysis: AnalysisBundle = serde_json::from_slice(&bytes) - .map_err(|error| format!("invalid Program IR JSON at {}: {error}", path.display()))?; + .map_err(|error| format!("invalid Program IR JSON at {}: {error}", resolved.display()))?; analysis .validate() .map_err(|error| format!("invalid Program IR at {}: {error}", path.display()))?; @@ -541,7 +544,10 @@ fn call_graph(analysis: &AnalysisBundle, args: &[String], format: Format) -> Out return usage_error("provide exactly one of --symbol and --at"); }; let graph = match graph_path { - Some(path) => match GraphDocument::load(&path) { + Some(path) => match compass_files::BuildGuard::resolve_requested_artifact(&path) + .map_err(|error| error.to_string()) + .and_then(|resolved| GraphDocument::load(&resolved).map_err(|error| error.to_string())) + { Ok(graph) => Some(graph), Err(error) => { return Outcome::failure_with_code( diff --git a/crates/compass-cli/src/prs_commands.rs b/crates/compass-cli/src/prs_commands.rs index a33ce926..3851e4b3 100644 --- a/crates/compass-cli/src/prs_commands.rs +++ b/crates/compass-cli/src/prs_commands.rs @@ -89,8 +89,7 @@ pub(super) fn command_prs(frontend: Frontend, args: &[String]) -> Outcome { for pr in &mut prs { pr.worktree_path = worktrees.get(&pr.branch).cloned(); } - let needs_impact = - parsed.graph.exists() && (parsed.number.is_some() || parsed.triage || parsed.conflicts); + let needs_impact = parsed.number.is_some() || parsed.triage || parsed.conflicts; let community_labels = if needs_impact { attach_graph_impact(&runner, &mut prs, &parsed.graph, parsed.repo.as_deref()) } else { diff --git a/crates/compass-cli/src/result_commands.rs b/crates/compass-cli/src/result_commands.rs index a755d201..c20a24bc 100644 --- a/crates/compass-cli/src/result_commands.rs +++ b/crates/compass-cli/src/result_commands.rs @@ -146,12 +146,14 @@ pub(super) fn command_reflect(frontend: Frontend, args: &[String]) -> Outcome { } index += 1; } - if graph.is_none() { - let default_graph = PathBuf::from(&output_root).join("graph.json"); - if default_graph.exists() { - graph = Some(default_graph); + let requested_graph = graph.unwrap_or_else(|| PathBuf::from(&output_root).join("graph.json")); + graph = match compass_files::BuildGuard::resolve_requested_artifact(&requested_graph) { + Ok(path) if path.exists() => Some(path), + Ok(_) => None, + Err(error) => { + return Outcome::failure(format!("error: could not resolve graph: {error}")); } - } + }; if let Some(graph_path) = graph.as_deref() { let parent = graph_path.parent().unwrap_or_else(|| Path::new(".")); analysis.get_or_insert_with(|| parent.join(".compass_analysis.json")); diff --git a/crates/compass-cli/src/semantic_diff_commands.rs b/crates/compass-cli/src/semantic_diff_commands.rs index d8ff1739..664bf284 100644 --- a/crates/compass-cli/src/semantic_diff_commands.rs +++ b/crates/compass-cli/src/semantic_diff_commands.rs @@ -676,11 +676,7 @@ impl ChangeSink for DirectChanges { ) { return Ok(()); } - let source_file = value - .and_then(|value| value.get("source_file")) - .and_then(serde_json::Value::as_str) - .unwrap_or_default() - .to_owned(); + let source_file = value.map(graph_edge_source_file).unwrap_or_default(); self.dependencies.push(DependencyDelta { source: source.to_owned(), target: target.to_owned(), @@ -720,9 +716,11 @@ fn graph_node_delta( ) -> GraphNodeDelta { GraphNodeDelta { id: id.to_owned(), - label: graph_string(value, "label"), - kind: graph_string(value, "kind"), - source_file: graph_string(value, "source_file"), + label: graph_string(value, "label") + .or_else(|| graph_string(value, "name")) + .unwrap_or_default(), + kind: graph_string(value, "kind").unwrap_or_default(), + source_file: value.map(graph_node_source_file).unwrap_or_default(), changed_fields, } } @@ -740,17 +738,60 @@ fn graph_edge_delta( target: target.to_owned(), relation: relation.to_owned(), key: key.to_owned(), - source_file: graph_string(value, "source_file"), + source_file: value.map(graph_edge_source_file).unwrap_or_default(), changed_fields, } } -fn graph_string(value: Option<&serde_json::Value>, key: &str) -> String { +fn graph_string(value: Option<&serde_json::Value>, key: &str) -> Option { value .and_then(|value| value.get(key)) .and_then(serde_json::Value::as_str) + .or_else(|| { + value + .and_then(|value| value.get("attributes")) + .and_then(|attributes| attributes.get(key)) + .and_then(serde_json::Value::as_str) + }) + .map(str::to_owned) +} + +fn graph_node_source_file(value: &serde_json::Value) -> String { + graph_string(Some(value), "source_file") + .or_else(|| nested_string(value, &["source", "file"])) .unwrap_or_default() - .to_owned() +} + +fn graph_edge_source_file(value: &serde_json::Value) -> String { + graph_string(Some(value), "source_file") + .or_else(|| nested_string(value, &["relationshipSite", "file"])) + .or_else(|| nested_string(value, &["relationship_site", "file"])) + .or_else(|| { + value + .get("evidence") + .and_then(serde_json::Value::as_array) + .and_then(|items| items.iter().find_map(evidence_source_file)) + }) + .unwrap_or_default() +} + +fn evidence_source_file(value: &serde_json::Value) -> Option { + nested_string(value, &["wiringSite", "file"]) + .or_else(|| nested_string(value, &["wiring_site", "file"])) + .or_else(|| { + value + .get("anchors") + .and_then(serde_json::Value::as_array) + .and_then(|anchors| anchors.first()) + .and_then(|anchor| nested_string(anchor, &["file"])) + }) +} + +fn nested_string(value: &serde_json::Value, path: &[&str]) -> Option { + path.iter() + .try_fold(value, |current, key| current.get(*key)) + .and_then(serde_json::Value::as_str) + .map(str::to_owned) } fn meaningful_graph_fields( @@ -884,6 +925,53 @@ mod tests { ); } + #[test] + fn typed_graph_records_decode_into_flat_semantic_deltas() { + let node = serde_json::json!({ + "id": "node", + "kind": "function", + "name": "serve", + "qualifiedName": "api::serve", + "source": { + "file": "src/api.rs", + "startByte": 10, + "endByte": 20, + "startLine": 2, + "startColumn": 0, + "endLine": 2, + "endColumn": 10 + } + }); + let edge = serde_json::json!({ + "source": "route", + "target": "node", + "kind": "routes_to", + "relationshipSite": { + "file": "src/routes.rs", + "startByte": 1, + "endByte": 5, + "startLine": 1, + "startColumn": 1, + "endLine": 1, + "endColumn": 5 + } + }); + + let node_delta = graph_node_delta("node", Some(&node), Vec::new()); + assert_eq!(node_delta.label, "serve"); + assert_eq!(node_delta.kind, "function"); + assert_eq!(node_delta.source_file, "src/api.rs"); + let edge_delta = graph_edge_delta( + "route", + "node", + "routes_to", + "edge", + Some(&edge), + Vec::new(), + ); + assert_eq!(edge_delta.source_file, "src/routes.rs"); + } + #[test] fn dependency_attribute_churn_is_not_reported_as_a_semantic_change() { let delta = |change| DependencyDelta { diff --git a/crates/compass-cli/tests/cargo_extract.rs b/crates/compass-cli/tests/cargo_extract.rs index 628dfda4..da9623ac 100644 --- a/crates/compass-cli/tests/cargo_extract.rs +++ b/crates/compass-cli/tests/cargo_extract.rs @@ -2,6 +2,8 @@ use std::error::Error; use std::path::Path; use std::process::Command; +use compass_files::BuildGuard; + fn seed(root: &Path) -> Result<(), Box> { std::fs::create_dir_all(root.join("crates/app/src"))?; std::fs::create_dir_all(root.join("crates/core/src"))?; @@ -45,18 +47,14 @@ fn cargo_extract_emits_workspace_dependency_facts() -> Result<(), Box ); assert!(String::from_utf8_lossy(&output.stdout).contains("Cargo: 2 nodes, 1 edges")); - let graph: serde_json::Value = serde_json::from_slice(&std::fs::read( - project.path().join("compass-out/graph.json"), - )?)?; + let graph_path = + BuildGuard::resolve_artifact(&project.path().join("compass-out"), "graph.json")?; + let graph: serde_json::Value = serde_json::from_slice(&std::fs::read(graph_path)?)?; let crate_nodes = graph["nodes"] .as_array() .into_iter() .flatten() - .filter(|node| { - node["id"] - .as_str() - .is_some_and(|id| id.starts_with("crate:")) - }) + .filter(|node| node["kind"] == "package") .count(); let dependency_edges = graph .get("links") @@ -64,7 +62,7 @@ fn cargo_extract_emits_workspace_dependency_facts() -> Result<(), Box .and_then(serde_json::Value::as_array) .into_iter() .flatten() - .filter(|edge| edge["relation"] == "crate_depends_on") + .filter(|edge| edge["kind"] == "depends_on") .count(); assert_eq!(crate_nodes, 2); assert_eq!(dependency_edges, 1); diff --git a/crates/compass-cli/tests/code_query_cli.rs b/crates/compass-cli/tests/code_query_cli.rs new file mode 100644 index 00000000..0365cf15 --- /dev/null +++ b/crates/compass-cli/tests/code_query_cli.rs @@ -0,0 +1,168 @@ +mod support; + +use std::error::Error; +use std::ffi::OsString; + +use compass_cli::{Frontend, run}; +use compass_files::BuildGuard; +use serde_json::Value; + +#[test] +fn typed_query_commands_share_the_versioned_json_contract() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + let graph = support::write_typed_graph(directory.path())?; + let cache = directory.path().join("cache"); + let graph = graph.to_string_lossy().into_owned(); + let cache = cache.to_string_lossy().into_owned(); + let root = directory.path().to_string_lossy().into_owned(); + for (command, positional, operation) in [ + ("search", vec!["Target"], "search"), + ("callers", vec!["Target"], "callers"), + ("callees", vec!["Caller"], "callees"), + ("impact", vec!["Target"], "impact"), + ("explore", vec!["Caller", "Target"], "explore"), + ("node", vec!["Caller", "Target"], "node_trail"), + ] { + let mut args = vec![OsString::from(command)]; + args.extend(positional.into_iter().map(OsString::from)); + args.extend([ + OsString::from("--graph"), + OsString::from(&graph), + OsString::from("--cache"), + OsString::from(&cache), + OsString::from("--root"), + OsString::from(&root), + OsString::from("--format"), + OsString::from("json"), + ]); + let outcome = run(Frontend::Compass, args); + assert_eq!(outcome.code, 0, "{command}: {}", outcome.stderr); + let response: Value = serde_json::from_str(&outcome.stdout)?; + assert_eq!(response["schema"], "compass.query/1"); + assert_eq!(response["operation"], operation); + } + Ok(()) +} + +#[test] +fn typed_query_text_is_a_projection_of_the_same_response() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + let graph = support::write_typed_graph(directory.path())?; + let outcome = run( + Frontend::Compass, + [ + OsString::from("search"), + OsString::from("Target"), + OsString::from("--graph"), + graph.into_os_string(), + ], + ); + assert_eq!(outcome.code, 0, "{}", outcome.stderr); + assert!(outcome.stdout.contains("Search:")); + assert!(outcome.stdout.contains("Fixture.Target")); + Ok(()) +} + +#[test] +fn typed_query_resolves_the_active_generation_from_the_public_path() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + let output = directory.path().join("compass-out"); + let guard = BuildGuard::begin(&output)?; + support::write_typed_graph(guard.staging_directory())?; + guard.commit_with_artifacts(&["graph.json"])?; + + let outcome = run( + Frontend::Compass, + [ + OsString::from("search"), + OsString::from("Target"), + OsString::from("--graph"), + output.join("graph.json").into_os_string(), + ], + ); + assert_eq!(outcome.code, 0, "{}", outcome.stderr); + assert!(outcome.stdout.contains("Fixture.Target")); + Ok(()) +} + +#[test] +fn typed_query_prefers_active_generation_over_stale_legacy_graph() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + let output = directory.path().join("compass-out"); + support::write_typed_graph(&output)?; + let guard = BuildGuard::begin(&output)?; + support::write_typed_graph(guard.staging_directory())?; + guard.commit_with_artifacts(&["graph.json"])?; + std::fs::write(output.join("graph.json"), b"{\"stale\":true}")?; + + let outcome = run( + Frontend::Compass, + [ + OsString::from("search"), + OsString::from("Target"), + OsString::from("--graph"), + output.join("graph.json").into_os_string(), + ], + ); + assert_eq!(outcome.code, 0, "{}", outcome.stderr); + assert!(outcome.stdout.contains("Fixture.Target")); + Ok(()) +} + +#[test] +fn typed_query_fails_closed_on_a_malformed_generation_pointer() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + let output = directory.path().join("compass-out"); + support::write_typed_graph(&output)?; + std::fs::write(output.join(".compass-active-generation"), "../escape")?; + + let outcome = run( + Frontend::Compass, + [ + OsString::from("search"), + OsString::from("Target"), + OsString::from("--graph"), + output.join("graph.json").into_os_string(), + ], + ); + assert_ne!(outcome.code, 0); + assert!(outcome.stderr.contains("generation")); + Ok(()) +} + +#[test] +fn natural_query_reads_legacy_only_when_the_generation_pointer_is_absent() +-> Result<(), Box> { + let directory = tempfile::tempdir()?; + let output = directory.path().join("compass-out"); + support::write_typed_graph(&output)?; + + let legacy = run( + Frontend::Compass, + [ + OsString::from("query"), + OsString::from("Target"), + OsString::from("--graph"), + output.join("graph.json").into_os_string(), + ], + ); + assert_eq!(legacy.code, 0, "{}", legacy.stderr); + + std::fs::write(output.join(".compass-active-generation"), "../escape")?; + let malformed = run( + Frontend::Compass, + [ + OsString::from("query"), + OsString::from("Target"), + OsString::from("--graph"), + output.join("graph.json").into_os_string(), + ], + ); + assert_ne!(malformed.code, 0); + assert!( + malformed.stderr.contains("generation"), + "{}", + malformed.stderr + ); + Ok(()) +} diff --git a/crates/compass-cli/tests/compass_product.rs b/crates/compass-cli/tests/compass_product.rs index d428b2c9..cef86e12 100644 --- a/crates/compass-cli/tests/compass_product.rs +++ b/crates/compass-cli/tests/compass_product.rs @@ -2,6 +2,7 @@ use std::error::Error; use std::path::Path; use std::process::Command; +use compass_files::BuildGuard; use serde_json::Value; fn run_update(root: &Path, configure: impl FnOnce(&mut Command)) -> Result<(), Box> { @@ -27,7 +28,9 @@ fn update_writes_to_compass_out_by_default() -> Result<(), Box> { let root = tempfile::tempdir()?; run_update(root.path(), |_| {})?; - assert!(root.path().join("compass-out/graph.json").is_file()); + assert!( + BuildGuard::resolve_artifact(&root.path().join("compass-out"), "graph.json")?.is_file() + ); assert!(!root.path().join("graphify-out").exists()); Ok(()) } @@ -41,7 +44,9 @@ fn compass_out_overrides_the_output_and_graphify_out_is_ignored() -> Result<(), .env("GRAPHIFY_OUT", "legacy-output"); })?; - assert!(root.path().join("chosen-output/graph.json").is_file()); + assert!( + BuildGuard::resolve_artifact(&root.path().join("chosen-output"), "graph.json")?.is_file() + ); assert!(!root.path().join("legacy-output").exists()); Ok(()) } diff --git a/crates/compass-cli/tests/coverage_paths.rs b/crates/compass-cli/tests/coverage_paths.rs index 6c92fda2..1ca66513 100644 --- a/crates/compass-cli/tests/coverage_paths.rs +++ b/crates/compass-cli/tests/coverage_paths.rs @@ -38,6 +38,97 @@ fn write_graph_fixture(path: &Path) -> Result<(), Box> { Ok(()) } +fn write_diagnostic_graph(path: &Path, node_count: usize) -> Result<(), Box> { + let nodes = (0..node_count) + .map(|index| serde_json::json!({"id":format!("node-{index}")})) + .collect::>(); + fs::write( + path, + serde_json::to_vec(&serde_json::json!({ + "directed":true, + "multigraph":false, + "nodes":nodes, + "links":[] + }))?, + )?; + Ok(()) +} + +fn diagnostic_node_count(graph: &Path) -> Result> { + let outcome = invoke_owned( + Frontend::Compass, + &[ + "diagnose".to_owned(), + "multigraph".to_owned(), + "--graph".to_owned(), + graph.to_string_lossy().into_owned(), + "--json".to_owned(), + ], + ); + if outcome.code != 0 { + return Err(outcome.stderr.into()); + } + let body: serde_json::Value = serde_json::from_str(&outcome.stdout)?; + body["summary"]["node_count"] + .as_u64() + .and_then(|value| usize::try_from(value).ok()) + .ok_or_else(|| "missing diagnostic node count".into()) +} + +#[test] +fn diagnose_rereads_the_active_generation_for_an_explicit_public_graph() +-> Result<(), Box> { + let directory = tempfile::tempdir()?; + let output = directory.path(); + let public = output.join("graph.json"); + write_diagnostic_graph(&public, 1)?; + let generations = output.join(".compass-generations"); + let first = generations.join("generation-first"); + let second = generations.join("generation-second"); + fs::create_dir_all(&first)?; + fs::create_dir_all(&second)?; + write_diagnostic_graph(&first.join("graph.json"), 2)?; + write_diagnostic_graph(&second.join("graph.json"), 3)?; + + fs::write( + output.join(".compass-active-generation"), + "generation-first", + )?; + assert_eq!(diagnostic_node_count(&public)?, 2); + fs::write( + output.join(".compass-active-generation"), + "generation-second", + )?; + assert_eq!(diagnostic_node_count(&public)?, 3); + Ok(()) +} + +#[test] +fn diagnose_uses_legacy_only_when_the_generation_pointer_is_absent() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + let public = directory.path().join("graph.json"); + write_diagnostic_graph(&public, 1)?; + assert_eq!(diagnostic_node_count(&public)?, 1); + + fs::write( + directory.path().join(".compass-active-generation"), + "../escape", + )?; + let outcome = invoke_owned( + Frontend::Compass, + &[ + "diagnose".to_owned(), + "multigraph".to_owned(), + "--graph".to_owned(), + public.to_string_lossy().into_owned(), + "--json".to_owned(), + ], + ); + assert_ne!(outcome.code, 0); + assert!(outcome.stderr.contains("generation"), "{}", outcome.stderr); + Ok(()) +} + #[test] fn frontend_roots_versions_help_and_unknown_commands_are_total() { assert_eq!(invoke(Frontend::Compass, &[]).code, 0); diff --git a/crates/compass-cli/tests/dedup_extract.rs b/crates/compass-cli/tests/dedup_extract.rs index 3796312b..817e8c95 100644 --- a/crates/compass-cli/tests/dedup_extract.rs +++ b/crates/compass-cli/tests/dedup_extract.rs @@ -6,6 +6,7 @@ use std::process::Command; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use compass_files::BuildGuard; use serde_json::Value; mod support; @@ -130,18 +131,21 @@ fn dedup_llm_resolves_ambiguous_semantic_entities() -> Result<(), Box .map_err(|error| format!("provider failed: {error}"))?; assert!(saw_tiebreak.load(Ordering::SeqCst)); - let graph: Value = serde_json::from_slice(&fs::read( - corpus.path().join("compass-out").join("graph.json"), - )?)?; + let graph_path = + BuildGuard::resolve_artifact(&corpus.path().join("compass-out"), "graph.json")?; + let graph: Value = serde_json::from_slice(&fs::read(graph_path)?)?; let surviving = graph["nodes"] .as_array() .ok_or("graph nodes are not an array")? .iter() .filter(|node| { - matches!( - node["id"].as_str(), - Some("customer_account_management" | "customer_identity_management") - ) + node["kind"] == "resource" + && node["details"]["type"] == "resource" + && node["details"]["data"]["resourceKind"] == "concept" + && matches!( + node["name"].as_str(), + Some("Customer Account Management" | "Customer Identity Management") + ) }) .count(); assert_eq!(surviving, 1, "ambiguous concepts were not merged: {graph}"); diff --git a/crates/compass-cli/tests/history_cli.rs b/crates/compass-cli/tests/history_cli.rs index ecd2c19e..bcdbbe38 100644 --- a/crates/compass-cli/tests/history_cli.rs +++ b/crates/compass-cli/tests/history_cli.rs @@ -1637,7 +1637,10 @@ fn explicit_rebuild_ignores_the_mutable_current_snapshot() -> Result<(), Box>(); + assert!( + typed_node_deltas.iter().any(|node| { + node["label"] + .as_str() + .is_some_and(|label| !label.is_empty()) + && node["source_file"] == "service.rs" + }), + "history-backed typed node deltas lost their name/source projection: {typed_node_deltas:?}" + ); let repository = Repository::discover(directory.path())?; let history = HistoryStore::open_existing(&repository)?.ok_or("missing history store")?; let versions = history.list(None)?; @@ -2331,7 +2352,10 @@ fn normal_graph_export_and_historical_queries_are_semantically_identical() "{}", String::from_utf8_lossy(&extracted.stderr) ); - let graph_path = directory.path().join("compass-out/graph.json"); + let graph_path = compass_files::BuildGuard::resolve_artifact( + &directory.path().join("compass-out"), + "graph.json", + )?; let original: serde_json::Value = serde_json::from_slice(&std::fs::read(&graph_path)?)?; let built = run(compass, directory.path(), &["history", "build", "HEAD"])?; @@ -2407,7 +2431,14 @@ fn normalize_graph(mut graph: serde_json::Value) -> serde_json::Value { .get_mut(field) .and_then(serde_json::Value::as_array_mut) { - records.sort_by_key(serde_json::Value::to_string); + records.sort_by_key(|record| { + record + .get("id") + .or_else(|| record.get("key")) + .and_then(serde_json::Value::as_str) + .unwrap_or_default() + .to_owned() + }); } } graph diff --git a/crates/compass-cli/tests/hook_cli.rs b/crates/compass-cli/tests/hook_cli.rs index f2a032ec..f0984986 100644 --- a/crates/compass-cli/tests/hook_cli.rs +++ b/crates/compass-cli/tests/hook_cli.rs @@ -173,7 +173,8 @@ fn native_hook_refresh_honors_recorded_scan_root() -> Result<(), Box> "{}", String::from_utf8_lossy(&output.stderr) ); - let graph = std::fs::read_to_string(output_root.join("graph.json"))?; + let graph_path = compass_files::BuildGuard::resolve_artifact(&output_root, "graph.json")?; + let graph = std::fs::read_to_string(graph_path)?; assert!(graph.contains("hook_target")); assert!(!source_root.join("compass-out/graph.json").exists()); Ok(()) diff --git a/crates/compass-cli/tests/init_cli.rs b/crates/compass-cli/tests/init_cli.rs index dda44133..88fc9f20 100644 --- a/crates/compass-cli/tests/init_cli.rs +++ b/crates/compass-cli/tests/init_cli.rs @@ -4,7 +4,7 @@ use std::fs; use std::io::Cursor; use std::process::{Command, Stdio}; -use compass_files::ProjectConfig; +use compass_files::{BuildGuard, ProjectConfig}; use compass_model::GraphDocument; use serde_json::Value; @@ -38,7 +38,10 @@ fn init_persists_scope_and_builds_only_matching_files() -> Result<(), Box Result<(), Box> { assert!(forced.status.success()); let config = ProjectConfig::load(root.path())?.ok_or("missing forced config")?; assert_eq!(config.build.include, ["main.rs"]); - let graph = GraphDocument::load(&root.path().join("compass-out/graph.json"))?; + let graph = GraphDocument::load(&BuildGuard::resolve_artifact( + &root.path().join("compass-out"), + "graph.json", + )?)?; assert!(graph.nodes.iter().all(|node| node.label() != "other()")); let other = tempfile::tempdir()?; @@ -112,7 +118,10 @@ fn update_reuses_scope_and_invalid_config_never_widens_it() -> Result<(), Box(values: [&str; N]) -> Vec { values.into_iter().map(OsString::from).collect() @@ -23,7 +25,10 @@ fn native_update_emits_and_reports_program_analysis() -> Result<(), Box Result<(), Box> { + let directory = tempfile::tempdir()?; + fs::write(directory.path().join("healthy.rs"), "pub fn healthy() {}\n")?; + fs::write( + directory.path().join("generated.rs"), + "pub fn generated() {}\n".repeat(64), + )?; + let root = directory.path().to_string_lossy(); + let outcome = run( + Frontend::Compass, + arguments([ + "update", + root.as_ref(), + "--max-source-bytes=64", + "--no-cluster", + "--no-viz", + ]), + ); + assert_eq!(outcome.code, 0, "{}", outcome.stderr); + + let output = directory.path().join("compass-out"); + let graph_path = BuildGuard::resolve_artifact(&output, "graph.json")?; + let graph = GraphDocument::load(&graph_path)?; + let generated = graph + .graph + .files + .iter() + .find(|file| file.path == "generated.rs") + .ok_or("missing oversized source inventory")?; + assert_eq!(generated.extraction_status, ExtractionStatus::Partial); + assert!(graph.graph.diagnostics.iter().any(|diagnostic| { + diagnostic.code == "partial_extraction" + && diagnostic + .message + .contains("configured 64 byte extraction limit") + })); + let program: serde_json::Value = serde_json::from_slice(&fs::read( + BuildGuard::resolve_artifact(&output, "program.json")?, + )?)?; + assert!( + program["program"]["modules"] + .as_array() + .is_some_and(|modules| { + modules + .iter() + .all(|module| module["source_file"] != "generated.rs") + }) + ); + Ok(()) +} + #[test] fn program_commands_inspect_explain_and_query_canonical_ir() -> Result<(), Box> { let directory = tempfile::tempdir()?; @@ -143,7 +200,9 @@ fn program_commands_inspect_explain_and_query_canonical_ir() -> Result<(), Box Result<(), Box> { + let directory = tempfile::tempdir()?; + let bin = directory.path().join("bin"); + std::fs::create_dir_all(&bin)?; + write_executable( + &bin.join("gh"), + r#"#!/bin/sh +if [ "$1" = "pr" ] && [ "$2" = "list" ]; then + printf '%s\n' '[{"number":101,"title":"Move handler","headRefName":"feature-handler","baseRefName":"main","author":{"login":"alice"},"isDraft":false,"reviewDecision":"","statusCheckRollup":[{"conclusion":"SUCCESS","status":"COMPLETED"}],"updatedAt":"2026-07-19T08:00:00Z"}]' + exit 0 +fi +if [ "$1" = "pr" ] && [ "$2" = "diff" ]; then + printf '%s\n' 'src/lib.rs' + exit 0 +fi +exit 1 +"#, + )?; + let output = directory.path().join("compass-out"); + let generation = output + .join(".compass-generations") + .join("generation-current"); + std::fs::create_dir_all(&generation)?; + std::fs::write( + generation.join("graph.json"), + serde_json::to_vec(&serde_json::json!({ + "directed":false, + "multigraph":false, + "graph":{}, + "nodes":[{ + "id":"handler", + "label":"Handler", + "source_file":"src/lib.rs", + "community":7 + }], + "links":[] + }))?, + )?; + std::fs::write( + output.join(".compass-active-generation"), + "generation-current", + )?; + let public = output.join("graph.json"); + assert!(!public.exists()); + let public_arg = public.to_string_lossy().into_owned(); + + let path = std::env::join_paths([bin, PathBuf::from("/usr/bin"), PathBuf::from("/bin")])?; + let detail = Command::new(env!("CARGO_BIN_EXE_compass")) + .current_dir(directory.path()) + .env("PATH", &path) + .env("NO_COLOR", "1") + .args(["prs", "101", "--base", "main", "--graph", &public_arg]) + .output()?; + assert!( + detail.status.success(), + "{}", + String::from_utf8_lossy(&detail.stderr) + ); + assert!( + String::from_utf8_lossy(&detail.stdout).contains("1 node / 1 community"), + "{}", + String::from_utf8_lossy(&detail.stdout) + ); + + std::fs::write( + output.join(".compass-active-generation"), + "../invalid-generation", + )?; + let malformed = Command::new(env!("CARGO_BIN_EXE_compass")) + .current_dir(directory.path()) + .env("PATH", path) + .env("NO_COLOR", "1") + .args(["prs", "101", "--base", "main", "--graph", &public_arg]) + .output()?; + assert!(malformed.status.success()); + assert!( + !String::from_utf8_lossy(&malformed.stdout).contains("Graph impact:"), + "{}", + String::from_utf8_lossy(&malformed.stdout) + ); + Ok(()) +} diff --git a/crates/compass-cli/tests/semantic_extract.rs b/crates/compass-cli/tests/semantic_extract.rs index fb42bdce..c47dddb3 100644 --- a/crates/compass-cli/tests/semantic_extract.rs +++ b/crates/compass-cli/tests/semantic_extract.rs @@ -115,22 +115,36 @@ fn native_semantic_extract_uses_provider_then_runs_warm_without_network() .map_err(|error| format!("provider failed: {error}"))?; let output = corpus.path().join("compass-out"); - let graph: Value = serde_json::from_slice(&fs::read(output.join("graph.json"))?)?; - assert!(graph["nodes"].as_array().is_some_and(|nodes| { - nodes - .iter() - .any(|node| node["id"] == "guide_domain_rule" && node["_origin"] == "semantic") - })); + let active = compass_files::BuildGuard::resolve_active_directory(&output)?; + let graph: Value = serde_json::from_slice(&fs::read(active.join("graph.json"))?)?; + assert!( + graph["nodes"].as_array().is_some_and(|nodes| { + nodes.iter().any(|node| { + node["name"] == "Domain rule" + && node["kind"] == "resource" + && node["details"]["type"] == "resource" + && node["details"]["data"]["resourceKind"] == "concept" + && node["evidence"].as_array().is_some_and(|evidence| { + evidence.iter().any(|item| { + item["origin"] == "heuristic" + && item["rule"] == "semantic-extraction" + && item["wiringSite"]["file"] == "guide.md" + }) + }) + }) + }), + "{graph:#}" + ); let analysis: Value = - serde_json::from_slice(&fs::read(output.join(".compass_analysis.json"))?)?; + serde_json::from_slice(&fs::read(active.join(".compass_analysis.json"))?)?; assert_eq!( analysis["tokens"], serde_json::json!({"input":17,"output":9}) ); let marker: Value = - serde_json::from_slice(&fs::read(output.join(".compass_semantic_marker"))?)?; + serde_json::from_slice(&fs::read(active.join(".compass_semantic_marker"))?)?; assert_eq!(marker["output_tokens"], 9); - let manifest: Value = serde_json::from_slice(&fs::read(output.join("manifest.json"))?)?; + let manifest: Value = serde_json::from_slice(&fs::read(active.join("manifest.json"))?)?; assert!( manifest["guide.md"]["semantic_hash"] .as_str() @@ -156,11 +170,12 @@ fn native_semantic_extract_uses_provider_then_runs_warm_without_network() String::from_utf8_lossy(&warm.stdout), String::from_utf8_lossy(&warm.stderr) ); - let warm_graph: Value = serde_json::from_slice(&fs::read(output.join("graph.json"))?)?; - assert!( - warm_graph["nodes"] - .as_array() - .is_some_and(|nodes| { nodes.iter().any(|node| node["id"] == "guide_domain_rule") }) - ); + let warm_graph_path = compass_files::BuildGuard::resolve_artifact(&output, "graph.json")?; + let warm_graph: Value = serde_json::from_slice(&fs::read(warm_graph_path)?)?; + assert!(warm_graph["nodes"].as_array().is_some_and(|nodes| { + nodes + .iter() + .any(|node| node["name"] == "Domain rule" && node["kind"] == "resource") + })); Ok(()) } diff --git a/crates/compass-cli/tests/support/mod.rs b/crates/compass-cli/tests/support/mod.rs index 5fd2b9f4..21af638d 100644 --- a/crates/compass-cli/tests/support/mod.rs +++ b/crates/compass-cli/tests/support/mod.rs @@ -1,8 +1,17 @@ #![allow(dead_code)] -use std::path::Path; +use std::fs; +use std::path::{Path, PathBuf}; use std::process::Command; +use compass_model::code_graph::{ + BuildMetadata, EdgeKind, EdgeRecord, ExtractionStatus, FileRecord, GraphDocument, NodeKind, + NodeRecord, +}; +use compass_model::identity::{edge_id, file_id}; +use compass_model::provenance::{EvidenceConfidence, EvidenceOrigin, Provenance, SourceAnchor}; +use sha2::{Digest, Sha256}; + pub fn compass_executable() -> &'static Path { Path::new(env!("CARGO_BIN_EXE_compass")) } @@ -14,3 +23,86 @@ pub fn compass_command() -> Command { pub fn command(executable: &Path) -> Command { Command::new(executable) } + +pub fn write_typed_graph(root: &Path) -> Result> { + let graph_path = root.join("graph.json"); + let source_path = root.join("src/lib.rs"); + let source = b"code"; + fs::create_dir_all(source_path.parent().unwrap_or(root))?; + fs::write(&source_path, source)?; + let anchor = SourceAnchor { + file: "src/lib.rs".to_owned(), + start_byte: 0, + end_byte: 4, + start_line: 1, + start_column: 0, + end_line: 1, + end_column: 4, + }; + let evidence = Provenance { + origin: EvidenceOrigin::Ast, + extractor: "cli-test".to_owned(), + confidence: EvidenceConfidence::Exact, + rule: None, + anchors: vec![anchor.clone()], + wiring_site: None, + score: None, + candidates: Vec::new(), + }; + let mut graph = GraphDocument::empty_v1(BuildMetadata { + builder_version: "test".to_owned(), + schema_fingerprint: "sha256:test".to_owned(), + source_tree_digest: "sha256:test".to_owned(), + configuration_digest: "sha256:test".to_owned(), + generation_id: "sha256:test".to_owned(), + source_commit: None, + }); + graph.graph.files.push(FileRecord { + id: file_id("src/lib.rs"), + path: "src/lib.rs".to_owned(), + language: Some("rust".to_owned()), + content_digest: format!("sha256:{:x}", Sha256::digest(source)), + byte_size: 4, + generated: false, + extraction_status: ExtractionStatus::Extracted, + extractor_versions: vec!["cli-test".to_owned()], + coverage: Vec::new(), + diagnostics: Vec::new(), + }); + graph.nodes = ["Caller", "Target"] + .into_iter() + .map(|name| NodeRecord { + id: format!("n:{}", name.to_ascii_lowercase()), + kind: NodeKind::Function, + roles: Vec::new(), + name: name.to_owned(), + qualified_name: format!("Fixture.{name}"), + language: Some("rust".to_owned()), + framework: None, + source: Some(anchor.clone()), + details: None, + evidence: vec![evidence.clone()], + coverage: Vec::new(), + diagnostics: Vec::new(), + community: None, + }) + .collect(); + let id = edge_id("n:caller", EdgeKind::Calls, "n:target", Some(&anchor), None); + graph.links.push(EdgeRecord { + id: id.clone(), + key: id, + source: "n:caller".to_owned(), + target: "n:target".to_owned(), + kind: EdgeKind::Calls, + occurrence_rule: None, + relationship_site: Some(anchor), + details: None, + evidence: vec![evidence], + weight: None, + context: None, + deferred: false, + diagnostics: Vec::new(), + }); + fs::write(&graph_path, serde_json::to_vec_pretty(&graph)?)?; + Ok(graph_path) +} diff --git a/crates/compass-cli/tests/watch_cli.rs b/crates/compass-cli/tests/watch_cli.rs index 0efbbecd..4ffc9d21 100644 --- a/crates/compass-cli/tests/watch_cli.rs +++ b/crates/compass-cli/tests/watch_cli.rs @@ -2,6 +2,8 @@ use std::error::Error; use std::io::{BufRead, BufReader, Read}; use std::process::{Command, Stdio}; +use compass_files::BuildGuard; + #[cfg(unix)] #[test] fn native_watch_synchronizes_before_reporting_ready() -> Result<(), Box> { @@ -40,6 +42,9 @@ fn native_watch_synchronizes_before_reporting_ready() -> Result<(), Box String { + let mut digest = Sha256::new(); + for component in [ + compass_model::code_graph::CODE_GRAPH_SCHEMA_V1, + compass_graph::V1_PUBLICATION_SEMANTICS_VERSION, + compass_languages::EXTRACTION_SEMANTICS_VERSION, + compass_files::AST_CACHE_VERSION, + ] { + digest.update(component.as_bytes()); + digest.update([0]); + } + digest.update(compass_files::CACHE_ENCODING_VERSION.to_le_bytes()); + format!("sha256:{:x}", digest.finalize()) +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub(crate) struct ArtifactSeal { pub bytes: u64, @@ -77,6 +92,12 @@ pub(crate) struct BuildProfile { pub resolution: f64, pub exclude_hubs: Option, pub program_analysis: bool, + #[serde(default = "default_max_source_bytes")] + pub max_source_bytes: u64, +} + +const fn default_max_source_bytes() -> u64 { + crate::pipeline::DEFAULT_MAX_SOURCE_BYTES } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] @@ -148,7 +169,7 @@ impl BuildState { ); Ok(Self { schema: BUILD_STATE_SCHEMA.to_owned(), - producer: env!("CARGO_PKG_VERSION").to_owned(), + producer: current_build_fingerprint(), profile, manifest: manifest?, graph: graph?, @@ -182,6 +203,7 @@ pub(crate) fn load_verified( Err(_) => return Ok(None), }; if state.schema != BUILD_STATE_SCHEMA + || state.producer != current_build_fingerprint() || state.profile != *profile || !state.manifest.matches(manifest_path) || !state.graph.matches(&output_dir.join("graph.json")) @@ -241,6 +263,7 @@ mod tests { resolution: 1.0, exclude_hubs: None, program_analysis: true, + max_source_bytes: default_max_source_bytes(), }; let state = BuildState::capture( output, @@ -267,6 +290,12 @@ mod tests { document["schema"] = serde_json::Value::String("unsupported".to_owned()); fs::write(&state_path, serde_json::to_vec(&document)?)?; assert!(load_verified(output, &profile, &manifest, true)?.is_none()); + + state.save(output)?; + let mut document: serde_json::Value = serde_json::from_slice(&fs::read(&state_path)?)?; + document["producer"] = serde_json::Value::String("legacy-builder".to_owned()); + fs::write(&state_path, serde_json::to_vec(&document)?)?; + assert!(load_verified(output, &profile, &manifest, true)?.is_none()); Ok(()) } } diff --git a/crates/compass-core/src/history.rs b/crates/compass-core/src/history.rs index cc014e88..5a97df57 100644 --- a/crates/compass-core/src/history.rs +++ b/crates/compass-core/src/history.rs @@ -579,15 +579,10 @@ fn validate_completed( profile: &BuildProfile, worktree: &WorktreeGuard, ) -> Result<(), MaterializeError> { - let built_at = completed - .artifacts - .document - .extras - .get("built_at_commit") - .and_then(serde_json::Value::as_str); + let built_at = source_commit(&completed.artifacts.document); if built_at != Some(commit.as_str()) { return Err(MaterializeError::Incomplete(format!( - "built_at_commit is {:?}, expected {commit}", + "sourceCommit is {:?}, expected {commit}", built_at ))); } @@ -667,15 +662,10 @@ fn validate_promoted( commit: &CommitId, profile: &BuildProfile, ) -> Result<(), MaterializeError> { - let built_at = completed - .artifacts - .document - .extras - .get("built_at_commit") - .and_then(serde_json::Value::as_str); + let built_at = source_commit(&completed.artifacts.document); if built_at != Some(commit.as_str()) { return Err(MaterializeError::Incomplete(format!( - "built_at_commit is {:?}, expected {commit}", + "sourceCommit is {:?}, expected {commit}", built_at ))); } @@ -699,6 +689,21 @@ fn validate_promoted( Ok(()) } +fn source_commit(document: &compass_model::GraphDocument) -> Option<&str> { + document + .graph + .get("build") + .and_then(serde_json::Value::as_object) + .and_then(|build| build.get("sourceCommit")) + .and_then(serde_json::Value::as_str) + .or_else(|| { + document + .extras + .get("built_at_commit") + .and_then(serde_json::Value::as_str) + }) +} + fn attach_source_inventory( completed: &mut CompletedGraphArtifacts, repository: &Repository, diff --git a/crates/compass-core/src/lib.rs b/crates/compass-core/src/lib.rs index 95c7a963..33375ed5 100644 --- a/crates/compass-core/src/lib.rs +++ b/crates/compass-core/src/lib.rs @@ -24,8 +24,9 @@ pub use history::{ pub use merge::{MergeResult, merge_graphs}; pub use pipeline::{ BuildFileProgress, BuildOptions, BuildPurpose, BuildResult, BuildTimings, CoreError, - SemanticLayer, build_graph_with_layers, build_graph_with_layers_and_progress, - build_graph_with_layers_and_tiebreaker, build_graph_with_semantic, build_local_graph, + DEFAULT_MAX_SOURCE_BYTES, SemanticLayer, build_graph_with_layers, + build_graph_with_layers_and_progress, build_graph_with_layers_and_tiebreaker, + build_graph_with_semantic, build_local_graph, }; pub use watch::{ WatchBackend, WatchBuildReason, WatchError, WatchOptions, WatchStatus, watch_local_graph, @@ -63,13 +64,8 @@ impl ExportInputs { if communities.is_empty() { for node in &document.nodes { let community = node - .attributes - .get("community") - .and_then(|value| { - value - .as_u64() - .or_else(|| value.as_str().and_then(|text| text.parse().ok())) - }) + .unsigned("community") + .or_else(|| node.string("community").parse().ok()) .and_then(|value| usize::try_from(value).ok()); if let Some(community) = community { communities @@ -184,8 +180,11 @@ impl LoadedGraph { #[must_use] pub fn default_graph_path() -> PathBuf { - PathBuf::from(std::env::var("COMPASS_OUT").unwrap_or_else(|_| "compass-out".to_owned())) - .join("graph.json") + let output = + PathBuf::from(std::env::var("COMPASS_OUT").unwrap_or_else(|_| "compass-out".to_owned())); + let requested = output.join("graph.json"); + compass_files::BuildGuard::resolve_requested_artifact(&requested) + .unwrap_or_else(|_| output.join(".compass-invalid-active-generation")) } fn load_learning_overlay(graph_path: &Path) -> HashMap> { diff --git a/crates/compass-core/src/pipeline.rs b/crates/compass-core/src/pipeline.rs index 3205d0ce..9d9efc7f 100644 --- a/crates/compass-core/src/pipeline.rs +++ b/crates/compass-core/src/pipeline.rs @@ -7,23 +7,39 @@ use std::time::{Duration, Instant}; use ahash::{AHashMap, AHashSet}; use compass_files::{ BuildGuard, BuildScope, Cache, CacheKind, CacheOptions, DetectOptions, Detection, IgnorePolicy, - Manifest, ManifestKind, detect, write_json_ascii_atomic, write_json_atomic, write_text_atomic, + Manifest, ManifestKind, detect, write_json_atomic, write_text_atomic, }; use compass_graph::{ - ClusterOptions, EntityTiebreaker, build_owned_with_tiebreaker as build_document, cluster, - dedupe_edges, dedupe_nodes, graph_insights, label_communities_by_hub, - remap_communities_to_previous, score_communities, + ClusterOptions, EntityTiebreaker, GRAPH_DIAGNOSTICS_EXTENSION, InventoryEvidence, + build_owned_with_tiebreaker as build_document, canonical_edge_kind, canonical_raw_edge_sites, + cluster, dedupe_edges, dedupe_nodes, extraction_from_v1, graph_insights, + label_communities_by_hub, normalize_document_v1_with_inventory, remap_communities_to_previous, + score_communities, +}; +use compass_languages::{ + EXTRACTION_QUALITY_EXTENSION, EXTRACTION_QUALITY_PARTIAL, EXTRACTION_QUALITY_REASON_EXTENSION, + Engine, Extraction, ExtractorKind, RawEdgeRecord, RawNodeRecord, Registry, file_stem, make_id, +}; +use compass_model::code_graph::{ + DiagnosticSeverity, ExtractionStatus, GraphDiagnostic, GraphDocument as V1GraphDocument, + NodeKind, +}; +use compass_model::provenance::{ + COALESCED_NODE_EVIDENCE_ATTRIBUTE, CONSUME_INCREMENTAL_ENDPOINT_REMAP_ATTRIBUTE, + EndpointRewriteEvidence, EndpointRewriteRule, EvidenceOrigin, OCCURRENCE_RULE_ATTRIBUTE, + Provenance, SEMANTIC_LAYER_EXTRACTOR, SourceAnchor, TRUSTED_EDGE_RECORD_ATTRIBUTE, + append_endpoint_rewrite_evidence, }; -use compass_languages::{Engine, Extraction, Registry, file_stem, make_id}; use compass_model::{EdgeRecord, GraphDocument, NodeRecord}; use compass_output::{ - DetectionSummary, HtmlOptions, JsonExportOptions, OutputError, ReportOptions, TokenCost, - generate_report, graph_view_model_document, write_html, write_json, + DetectionSummary, HtmlOptions, OutputError, ReportOptions, TokenCost, generate_report, + graph_view_model_document, write_html, }; use compass_resolve::{merge_decl_def_classes, resolve_owned_with_root}; use rayon::prelude::*; use serde::{Deserialize, Serialize}; use serde_json::json; +use sha2::{Digest, Sha256}; use crate::build_state::{ ArtifactSeal, BUILD_STATE_FILE, BuildProfile, BuildState, SavedStats, load_verified, @@ -34,6 +50,13 @@ use crate::program::{ }; use crate::raw_guard::enforce_incomplete_raw_guard; +/// Default upper bound for one source file entering the parser pipeline. +/// +/// Syntax and semantic extractors can retain several multiples of the source +/// size while building records. Callers may raise this bound explicitly for +/// repositories with intentionally large generated sources. +pub const DEFAULT_MAX_SOURCE_BYTES: u64 = 16 * 1024 * 1024; + #[derive(Clone, Debug)] pub struct BuildOptions { pub root: PathBuf, @@ -42,8 +65,8 @@ pub struct BuildOptions { /// Explicit repository-private cache root used by exact history builds. pub cache_root: Option, pub force: bool, - /// Preserve validated extraction and completed-output fast paths while - /// retaining force authorization for output replacement. + /// Reuse validated AST and Program cache entries while retaining force + /// authorization for output replacement. pub reuse_cache_on_force: bool, pub no_cluster: bool, pub no_viz: bool, @@ -63,6 +86,11 @@ pub struct BuildOptions { /// Maximum number of worker threads used by the deterministic AST stages. /// `None` uses the host CPU count in a build-local Rayon pool. pub max_workers: Option, + /// Maximum size of one source file admitted to AST and Program analysis. + /// + /// Oversized files remain in the inventory with partial coverage and an + /// explicit reason; they are not read into memory. + pub max_source_bytes: u64, /// Override the commit recorded in update artifacts. /// /// This is primarily useful for reproducible builds and compatibility @@ -84,7 +112,10 @@ const OUTPUT_STATS_FILE: &str = ".compass_output_stats.json"; const GRAPH_OVERVIEW_FILE: &str = "graph-overview.json"; const GRAPH_OVERVIEW_SCHEMA: &str = "compass.graph-overview/1"; const GRAPH_OVERVIEW_NODE_LIMIT: isize = 5_000; - +const MAX_INCREMENTAL_REMAP_DIAGNOSTICS: usize = 100; +const INCREMENTAL_REMAP_DROP_DIAGNOSTIC: &str = "dropped_incremental_remap_without_wiring_site"; +const INCREMENTAL_REMAP_TRUNCATION_DIAGNOSTIC: &str = + "incremental_remap_without_wiring_site_truncated"; #[derive(Clone, Debug, Deserialize, Serialize)] struct OutputStats { graph_bytes: u64, @@ -119,6 +150,7 @@ impl BuildOptions { // Large builds use a local host-sized pool. Keeping this unset also // lets CLI callers provide an explicit memory/throughput bound. max_workers: None, + max_source_bytes: DEFAULT_MAX_SOURCE_BYTES, built_at_commit: None, purpose: BuildPurpose::Update, precomputed_detection: None, @@ -126,6 +158,14 @@ impl BuildOptions { } } +const fn cache_reuse_enabled(force: bool, reuse_cache_on_force: bool) -> bool { + !force || reuse_cache_on_force +} + +const fn prior_published_graph_input_enabled(force: bool) -> bool { + !force +} + #[derive(Clone, Debug)] pub struct BuildResult { pub root: PathBuf, @@ -335,23 +375,24 @@ fn build_graph_inner( .output_root .as_deref() .map_or_else(|| root.clone(), absolutize); - let output_dir = output_root.join(&output_name); - fs::create_dir_all(&output_dir).map_err(|source| compass_files::FileError::Io { - path: output_dir.clone(), + let reuse_cached_analysis = cache_reuse_enabled(options.force, options.reuse_cache_on_force); + let read_prior_published_graph = prior_published_graph_input_enabled(options.force); + let output_container = output_root.join(&output_name); + fs::create_dir_all(&output_container).map_err(|source| compass_files::FileError::Io { + path: output_container.clone(), source, })?; - let prior_build_complete = BuildGuard::ensure_complete(&output_dir).is_ok(); - let guard = BuildGuard::begin(&output_dir)?; + let prior_build_complete = BuildGuard::ensure_complete(&output_container).is_ok(); + let guard = BuildGuard::begin(&output_container)?; + let output_dir = guard.staging_directory().to_path_buf(); + if !options.program_analysis { + remove_if_exists(&output_dir.join("program.json"))?; + } if options.force || !prior_build_complete { remove_if_exists(&output_dir.join(BUILD_STATE_FILE))?; } let manifest_path = output_dir.join("manifest.json"); let prior_manifest = Manifest::load(&manifest_path, Some(&root)); - let has_confirmed_deletion = prior_manifest - .entries() - .keys() - .any(|path| !Path::new(path).exists()); - let detect_options = DetectOptions { scan_filesystem: options.scan_filesystem, gitignore: options.gitignore, @@ -407,9 +448,7 @@ fn build_graph_inner( timings.detect = stage_started.elapsed(); stage_started = Instant::now(); let mut internal_started = Instant::now(); - let mut semantic_documents = if options.purpose == BuildPurpose::Update - || (options.purpose == BuildPurpose::Extract && !options.force) - { + let mut semantic_documents = if read_prior_published_graph { semantic_document_sources(&output_dir.join("graph.json"), &root) } else { HashSet::new() @@ -439,7 +478,7 @@ fn build_graph_inner( ); let manifest_unchanged = options.purpose == BuildPurpose::Update - && (!options.force || options.reuse_cache_on_force) + && read_prior_published_graph && prior_manifest.is_unchanged(&detection.files, ManifestKind::Ast); let build_profile = build_profile(options); let has_program_artifacts = @@ -465,10 +504,10 @@ fn build_graph_inner( remove_if_exists(&output_dir.join(GRAPH_OVERVIEW_FILE))?; } remove_if_exists(&output_dir.join("needs_update"))?; - guard.commit()?; + let published_output_dir = commit_generation(guard, &output_container)?; return Ok(BuildResult { root, - output_dir: output_dir.clone(), + output_dir: published_output_dir, detection, files_considered: state.stats.files, files_extracted: 0, @@ -526,10 +565,10 @@ fn build_graph_inner( stats.communities, unchanged_program.as_ref(), )?; - guard.commit()?; + let published_output_dir = commit_generation(guard, &output_container)?; return Ok(BuildResult { root, - output_dir: output_dir.clone(), + output_dir: published_output_dir, detection, files_considered: sources.len(), files_extracted: 0, @@ -569,8 +608,17 @@ fn build_graph_inner( let mut cache = Cache::open(&root, cache_options)?; let mut extractions = BTreeMap::::new(); let mut missing = Vec::new(); - if !options.force || options.reuse_cache_on_force { + if reuse_cached_analysis { for path in &sources { + if fs::metadata(path).is_ok_and(|metadata| { + metadata.is_file() && metadata.len() > options.max_source_bytes + }) { + extractions.insert( + path.clone(), + oversized_source_extraction(path, options.max_source_bytes)?, + ); + continue; + } let cached = cache.load(path, &CacheKind::Ast, None, false)?; if let Some(value) = cached { let extraction = @@ -584,7 +632,18 @@ fn build_graph_inner( } } } else { - missing.clone_from(&sources); + for path in &sources { + if fs::metadata(path).is_ok_and(|metadata| { + metadata.is_file() && metadata.len() > options.max_source_bytes + }) { + extractions.insert( + path.clone(), + oversized_source_extraction(path, options.max_source_bytes)?, + ); + } else { + missing.push(path.clone()); + } + } } let worker_count = options.max_workers.unwrap_or_else(default_ast_workers); let worker_pool = if missing.len() >= 256 || sources.len() >= 256 { @@ -606,25 +665,15 @@ fn build_graph_inner( // cannot silently serialize cold extraction. let completed_files = Mutex::new(0_usize); let total_files = missing.len(); - let extract_source = - |engine: &mut Engine, path: &PathBuf| -> Result<_, compass_languages::ExtractError> { - let bytes = fs::read(path).map_err(|source| compass_files::FileError::Io { - path: path.clone(), - source, - })?; - let source_file = path - .strip_prefix(&root) - .unwrap_or(path) - .to_string_lossy() - .replace('\\', "/"); - let language = Registry::resolve(path).map_or("", |spec| spec.name); - let combined = engine.extract_source_combined(path, &source_file, &bytes)?; - let prepared = combined.program.map(|batch| PreparedSyntaxInput { - source_file, - language: language.to_owned(), - bytes: bytes.clone(), - batch, - }); + let extract_source = |engine: &mut Engine, + path: &PathBuf| + -> Result<_, compass_languages::ExtractError> { + let metadata = fs::metadata(path).map_err(|source| compass_files::FileError::Io { + path: path.clone(), + source, + })?; + if metadata.len() > options.max_source_bytes { + let graph = oversized_source_extraction(path, options.max_source_bytes)?; if let Some(progress) = progress { let mut completed = completed_files .lock() @@ -636,33 +685,120 @@ fn build_graph_inner( path: path.clone(), }); } - let source = ( - path.to_string_lossy().into_owned(), - String::from_utf8_lossy(&bytes).into_owned(), - ); - Ok((path.clone(), combined.graph, source, prepared)) - }; - let fresh = if missing.len() < 256 { + return Ok(( + path.clone(), + graph, + (path.to_string_lossy().into_owned(), String::new()), + None, + )); + } + let bytes = fs::read(path).map_err(|source| compass_files::FileError::Io { + path: path.clone(), + source, + })?; + let source_file = path + .strip_prefix(&root) + .unwrap_or(path) + .to_string_lossy() + .replace('\\', "/"); + let language = Registry::resolve(path).map_or("", |spec| spec.name); + let mut combined = engine.extract_source_combined(path, &source_file, &bytes)?; + let empty_structured_document = bytes.is_empty() + && Registry::resolve(path).is_some_and(|spec| { + matches!( + spec.kind, + ExtractorKind::JsonConfig | ExtractorKind::ProjectXml | ExtractorKind::Xaml + ) + }); + if empty_structured_document && combined.graph.error.is_none() { + combined.graph.error = Some(format!("{language} extraction failed: empty document")); + } else if bytes.is_empty() && combined.graph.error.is_none() { + combined.graph.nodes.clear(); + combined.graph.edges.clear(); + combined.graph.hyperedges.clear(); + combined.graph.framework_facts.clear(); + combined.graph.raw_calls = None; + combined.program = None; + } + let prepared = combined.program.map(|batch| PreparedSyntaxInput { + source_file, + language: language.to_owned(), + bytes: bytes.clone(), + batch, + }); + if let Some(progress) = progress { + let mut completed = completed_files + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + *completed += 1; + progress(BuildFileProgress { + current: *completed, + total: total_files, + path: path.clone(), + }); + } + let source = ( + path.to_string_lossy().into_owned(), + String::from_utf8_lossy(&bytes).into_owned(), + ); + Ok((path.clone(), combined.graph, source, prepared)) + }; + let fresh_outcomes = if missing.len() < 256 { let mut engine = Engine::default(); missing .iter() .map(|path| extract_source(&mut engine, path)) - .collect::, _>>()? + .collect::>() } else { let extract = || { missing .par_iter() .map_init(Engine::default, extract_source) - .collect::, _>>() + .collect::>() }; if let Some(pool) = &worker_pool { - pool.install(extract)? + pool.install(extract) } else { - extract()? + extract() } }; + let mut extraction_failures = BTreeMap::new(); + let mut fresh = missing + .iter() + .cloned() + .zip(fresh_outcomes) + .filter_map(|(path, outcome)| match outcome { + Ok(extracted) => Some(extracted), + Err(error) => { + extraction_failures.insert( + canonical_identity(&path), + portable_diagnostic_reason(&error.to_string(), &path, &root), + ); + None + } + }) + .collect::>(); + let mut extraction_partials = BTreeMap::new(); + for (path, extraction) in &mut extractions { + prepare_extraction_for_publication( + path, + extraction, + &root, + &mut extraction_failures, + &mut extraction_partials, + ); + } + for (path, extraction, _, _) in &mut fresh { + prepare_extraction_for_publication( + path, + extraction, + &root, + &mut extraction_failures, + &mut extraction_partials, + ); + } profile_internal("tree-sitter combined extraction", &mut internal_started); - let prepared = if options.force && !options.reuse_cache_on_force { + let prepared = if !reuse_cached_analysis { fresh .iter() .filter_map(|(_, _, _, prepared)| prepared.clone()) @@ -674,7 +810,7 @@ fn build_graph_inner( let program_root = root.clone(); let program_sources = sources.clone(); let mut program_options = options.clone(); - program_options.force = options.force && !options.reuse_cache_on_force; + program_options.force = !reuse_cached_analysis; let program_cache_root = options.cache_root.clone(); let program_output_cache_root = output_cache_root.map(Path::to_path_buf); let program_output_dir = output_dir.clone(); @@ -779,14 +915,7 @@ fn build_graph_inner( }) .map_err(|error| CoreError::WorkerPool(error.to_string()))?; profile_internal("portable AST ID remapping", &mut internal_started); - let read_source = |path: &PathBuf| { - fs::read(path).ok().map(|bytes| { - ( - path.to_string_lossy().into_owned(), - String::from_utf8_lossy(&bytes).into_owned(), - ) - }) - }; + let read_source = |path: &PathBuf| read_source_text_with_limit(path, options.max_source_bytes); let read_cached_source = |path: &PathBuf| { (!fresh_paths.contains(path)) .then(|| read_source(path)) @@ -820,9 +949,7 @@ fn build_graph_inner( }; profile_internal("wait for Program analysis", &mut internal_started); stage_started = Instant::now(); - if options.purpose == BuildPurpose::Update - || (options.purpose == BuildPurpose::Extract && !options.force) - { + if read_prior_published_graph { let refreshed = semantic .map(|layer| { let mut refreshed = canonical_source_set(&layer.refreshed_files, &root); @@ -894,10 +1021,10 @@ fn build_graph_inner( 0, program.as_ref(), )?; - guard.commit()?; + let published_output_dir = commit_generation(guard, &output_container)?; return Ok(BuildResult { root, - output_dir, + output_dir: published_output_dir, detection, files_considered: sources.len(), files_extracted: 0, @@ -932,16 +1059,27 @@ fn build_graph_inner( } if options.no_cluster { let (nodes, edges) = (dedupe_nodes(&resolved.nodes), dedupe_edges(&resolved.edges)); - let tokens = semantic_tokens(semantic); enforce_incomplete_raw_guard(semantic, &output_dir.join("graph.json"), &root, nodes.len())?; - write_raw_graph( - &output_dir.join("graph.json"), - &resolved, - &nodes, - &edges, - options.purpose, - tokens, + let document = build_document(resolved, true, true, Some(&root), tiebreaker)?; + let configuration_digest = graph_configuration_digest(options, &output_dir)?; + let source_commit = options + .built_at_commit + .clone() + .or_else(|| git_commit(&root)); + let published = normalize_document_v1_with_inventory( + &document, + &root, + configuration_digest, + source_commit.as_deref(), + detection_inventory( + &detection, + semantic, + &extraction_failures, + &extraction_partials, + &root, + ), )?; + write_json_atomic(output_dir.join("graph.json"), &published, false)?; remove_if_exists(&output_dir.join(GRAPH_OVERVIEW_FILE))?; save_output_stats(&output_dir, nodes.len(), edges.len(), 0, false)?; write_semantic_marker(&output_dir, semantic)?; @@ -970,11 +1108,11 @@ fn build_graph_inner( 0, program.as_ref(), )?; - guard.commit()?; + let published_output_dir = commit_generation(guard, &output_container)?; timings.publish = stage_started.elapsed(); return Ok(BuildResult { root, - output_dir, + output_dir: published_output_dir, detection, files_considered: sources.len(), files_extracted: missing.len(), @@ -1007,7 +1145,7 @@ fn build_graph_inner( timings, }); } - let document = build_document(resolved, false, true, Some(&root), tiebreaker)?; + let document = build_document(resolved, true, true, Some(&root), tiebreaker)?; profile_internal("graph document build and dedup", &mut internal_started); timings.graph_assembly = stage_started.elapsed(); stage_started = Instant::now(); @@ -1056,10 +1194,10 @@ fn build_graph_inner( communities, program.as_ref(), )?; - guard.commit()?; + let published_output_dir = commit_generation(guard, &output_container)?; return Ok(BuildResult { root, - output_dir: output_dir.clone(), + output_dir: published_output_dir, detection, files_considered: sources.len(), files_extracted: missing.len(), @@ -1134,27 +1272,26 @@ fn build_graph_inner( .and_then(|directory| git_commit(&directory)) }); - let incomplete_semantic = semantic.is_some_and(|layer| semantic_is_incomplete(layer, &root)); let graph_output = || -> Result { let started = Instant::now(); let mut output_profile_started = Instant::now(); - write_json( + let configuration_digest = graph_configuration_digest(options, &output_dir)?; + let published = published_v1_document( &document, &communities, - output_dir.join("graph.json"), - &JsonExportOptions { - force: semantic.map_or(options.force || has_confirmed_deletion, |layer| { - !incomplete_semantic || layer.allow_partial - }), - built_at_commit: commit.as_deref(), - community_labels: (options.purpose == BuildPurpose::Update && !labels.is_empty()) - .then_some(&labels), + &labels, + &root, + PublicationEvidence { + detection: &detection, + semantic, + extraction_failures: &extraction_failures, + extraction_partials: &extraction_partials, }, + configuration_digest, + commit.as_deref(), )?; - profile_internal( - "graph.json borrowed publication", - &mut output_profile_started, - ); + write_json_atomic(output_dir.join("graph.json"), &published, false)?; + profile_internal("graph.json v1 publication", &mut output_profile_started); if options.purpose == BuildPurpose::Update { write_text_atomic( output_dir.join(".compass_root"), @@ -1315,10 +1452,10 @@ fn build_graph_inner( program.as_ref(), )?; profile_internal("Program output and build seals", &mut internal_started); - guard.commit()?; + let published_output_dir = commit_generation(guard, &output_container)?; Ok(BuildResult { root, - output_dir, + output_dir: published_output_dir, detection, files_considered: sources.len(), files_extracted: missing.len(), @@ -1352,6 +1489,30 @@ fn build_graph_inner( }) } +fn oversized_source_extraction( + path: &Path, + max_source_bytes: u64, +) -> Result { + let metadata = fs::metadata(path).map_err(|source| compass_files::FileError::Io { + path: path.to_path_buf(), + source, + })?; + let mut extraction = Extraction::default(); + extraction.extensions.insert( + EXTRACTION_QUALITY_EXTENSION.to_owned(), + serde_json::Value::String(EXTRACTION_QUALITY_PARTIAL.to_owned()), + ); + extraction.extensions.insert( + EXTRACTION_QUALITY_REASON_EXTENSION.to_owned(), + serde_json::Value::String(format!( + "source is {} bytes, exceeding the configured {} byte extraction limit", + metadata.len(), + max_source_bytes + )), + ); + Ok(extraction) +} + fn program_modules(program: Option<&ProgramBuild>) -> usize { program.map_or(0, |program| program.analysis.program.modules.len()) } @@ -1376,6 +1537,7 @@ fn build_profile(options: &BuildOptions) -> BuildProfile { resolution: options.resolution, exclude_hubs: options.exclude_hubs, program_analysis: options.program_analysis, + max_source_bytes: options.max_source_bytes, } } @@ -1433,6 +1595,15 @@ fn publish_build_state( state.save(output_dir) } +fn commit_generation(guard: BuildGuard, output_container: &Path) -> Result { + let mut artifacts = vec!["graph.json", "manifest.json", BUILD_STATE_FILE]; + if guard.staging_directory().join("program.json").is_file() { + artifacts.push("program.json"); + } + guard.commit_with_artifacts(&artifacts)?; + Ok(BuildGuard::resolve_active_directory(output_container)?) +} + #[cfg(target_os = "macos")] fn default_ast_workers() -> usize { num_cpus::get().max(num_cpus::get_physical()) @@ -1520,21 +1691,18 @@ fn finalize_ast_extraction(extraction: &mut Extraction, root: &Path) { || { extraction.nodes.par_iter_mut().for_each(|node| { normalize_source_attribute_cached(&mut node.attributes, root, &canonical_sources); - node.attributes.remove("origin_file"); node.attributes.remove("_callable"); - node.attributes.insert( - "_origin".to_owned(), - serde_json::Value::String("ast".to_owned()), - ); + node.attributes + .entry("_origin".to_owned()) + .or_insert_with(|| serde_json::Value::String("ast".to_owned())); }); }, || { extraction.edges.par_iter_mut().for_each(|edge| { normalize_source_attribute_cached(&mut edge.attributes, root, &canonical_sources); - edge.attributes.insert( - "_origin".to_owned(), - serde_json::Value::String("ast".to_owned()), - ); + edge.attributes + .entry("_origin".to_owned()) + .or_insert_with(|| serde_json::Value::String("ast".to_owned())); }); }, ); @@ -1550,28 +1718,54 @@ fn prepare_portable_ast_cache_entry(extraction: &mut Extraction, source: &Path, }; let portable = relative.to_string_lossy().replace('\\', "/"); let set_portable = |attributes: &mut serde_json::Map| { + let normalize_path = |value: &str| { + let path = Path::new(value); + if path.is_absolute() { + let canonical = canonicalize_allow_missing(path); + canonical.strip_prefix(root).map_or_else( + |_| portable_out_of_root_source(&canonical, root), + |relative| relative.to_string_lossy().replace('\\', "/"), + ) + } else { + value.replace('\\', "/") + } + }; for key in ["source_file", "origin_file"] { - if attributes + let Some(value) = attributes .get(key) .and_then(serde_json::Value::as_str) - .is_some_and(|value| !value.is_empty()) - { - attributes.insert(key.to_owned(), serde_json::Value::String(portable.clone())); + .filter(|value| !value.is_empty()) + else { + continue; + }; + let normalized = normalize_path(value); + if normalized != value { + attributes.insert(key.to_owned(), serde_json::Value::String(normalized)); } } + if let Some(anchor) = attributes + .get_mut("origin_source_anchor") + .and_then(serde_json::Value::as_object_mut) + && let Some(value) = anchor + .get("file") + .and_then(serde_json::Value::as_str) + .map(str::to_owned) + { + anchor.insert( + "file".to_owned(), + serde_json::Value::String(normalize_path(&value)), + ); + } if let Some(target) = attributes .get("target_file") .and_then(serde_json::Value::as_str) .map(str::to_owned) { - let target = Path::new(&target); - let canonical = fs::canonicalize(target).unwrap_or_else(|_| target.to_path_buf()); - if let Ok(relative) = canonical.strip_prefix(root) { - attributes.insert( - "target_file".to_owned(), - serde_json::Value::String(relative.to_string_lossy().replace('\\', "/")), - ); - } + let normalized = normalize_path(&target); + attributes.insert( + "target_file".to_owned(), + serde_json::Value::String(normalized), + ); } }; for node in &mut extraction.nodes { @@ -1696,6 +1890,10 @@ fn finalize_semantic_extraction(extraction: &mut Extraction, root: &Path) { "_origin".to_owned(), serde_json::Value::String("semantic".to_owned()), ); + node.attributes.insert( + "extractor".to_owned(), + serde_json::Value::String(SEMANTIC_LAYER_EXTRACTOR.to_owned()), + ); } for edge in &mut extraction.edges { normalize_source_attribute(&mut edge.attributes, root); @@ -1703,6 +1901,10 @@ fn finalize_semantic_extraction(extraction: &mut Extraction, root: &Path) { "_origin".to_owned(), serde_json::Value::String("semantic".to_owned()), ); + edge.attributes.insert( + "extractor".to_owned(), + serde_json::Value::String(SEMANTIC_LAYER_EXTRACTOR.to_owned()), + ); } for hyperedge in &mut extraction.hyperedges { let Some(attributes) = hyperedge.as_object_mut() else { @@ -1713,6 +1915,10 @@ fn finalize_semantic_extraction(extraction: &mut Extraction, root: &Path) { "_origin".to_owned(), serde_json::Value::String("semantic".to_owned()), ); + attributes.insert( + "extractor".to_owned(), + serde_json::Value::String(SEMANTIC_LAYER_EXTRACTOR.to_owned()), + ); } } @@ -2004,131 +2210,733 @@ fn preserve_semantic_layer( root: &Path, refreshed: &HashSet, ) { - let Ok(existing) = GraphDocument::load(graph_path) else { + let Ok(existing) = V1GraphDocument::load(graph_path) else { return; }; - let ast_ids = extraction + let mut existing_raw = extraction_from_v1(&existing); + let current_ast_by_key = extraction + .nodes + .iter() + .filter_map(|node| raw_node_match_key(node).map(|key| (key, node.id.clone()))) + .collect::>(); + let typed_ast_remap = existing + .nodes + .iter() + .filter(|node| node_has_origin(node, EvidenceOrigin::Ast)) + .filter_map(|node| { + typed_node_match_key(node) + .and_then(|key| current_ast_by_key.get(&key)) + .map(|current| (node.id.clone(), current.clone())) + }) + .collect::>(); + let mut dropped_edges = HashSet::new(); + let mut remapped_edges = HashSet::new(); + let mut remap_diagnostics = Vec::new(); + let mut dropped_without_site = 0_usize; + for (index, edge) in existing_raw.edges.iter_mut().enumerate() { + let source = typed_ast_remap + .get(&edge.source) + .cloned() + .unwrap_or_else(|| edge.source.clone()); + let target = typed_ast_remap + .get(&edge.target) + .cloned() + .unwrap_or_else(|| edge.target.clone()); + if source == edge.source && target == edge.target { + continue; + } + if !has_exact_remap_site(&edge.attributes) { + dropped_edges.insert(index); + dropped_without_site = dropped_without_site.saturating_add(1); + if remap_diagnostics.len() < MAX_INCREMENTAL_REMAP_DIAGNOSTICS { + let mut related_ids = edge + .attributes + .get(compass_model::provenance::TRUSTED_EDGE_RECORD_ATTRIBUTE) + .and_then(|record| record.get("id")) + .and_then(serde_json::Value::as_str) + .map(str::to_owned) + .into_iter() + .collect::>(); + related_ids.extend([edge.source.clone(), edge.target.clone()]); + remap_diagnostics.push(GraphDiagnostic { + severity: DiagnosticSeverity::Warning, + code: INCREMENTAL_REMAP_DROP_DIAGNOSTIC.to_owned(), + message: "dropped preserved edge whose AST endpoint changed without an authoritative producer wiring site".to_owned(), + anchor: None, + related_ids, + }); + } + continue; + } + edge.source = source; + edge.target = target; + append_endpoint_rewrite_evidence( + &mut edge.attributes, + EndpointRewriteEvidence { + rule: EndpointRewriteRule::IncrementalAstEndpointRemap, + score: 1.0, + }, + ); + remapped_edges.insert(index); + } + if dropped_without_site > MAX_INCREMENTAL_REMAP_DIAGNOSTICS { + remap_diagnostics.push(GraphDiagnostic { + severity: DiagnosticSeverity::Warning, + code: INCREMENTAL_REMAP_TRUNCATION_DIAGNOSTIC.to_owned(), + message: format!( + "omitted {} additional incremental remap diagnostics", + dropped_without_site - MAX_INCREMENTAL_REMAP_DIAGNOSTICS + ), + anchor: None, + related_ids: Vec::new(), + }); + } + let mut fresh_edge_sites = HashMap::>::new(); + let raw_edge_sites = canonical_raw_edge_sites(&extraction.edges, root); + for (edge, site) in extraction.edges.iter().zip(raw_edge_sites) { + let Some(key) = incremental_edge_key(edge) else { + continue; + }; + let Some(site) = site else { + continue; + }; + fresh_edge_sites.entry(key).or_default().push(site); + } + for sites in fresh_edge_sites.values_mut() { + sites.sort_by(|left, right| { + incremental_anchor_key(left).cmp(&incremental_anchor_key(right)) + }); + } + let mut mixed_edges = HashMap::>::new(); + for (index, (raw, typed)) in existing_raw + .edges + .iter() + .zip(existing.links.iter()) + .enumerate() + { + if dropped_edges.contains(&index) + || !typed + .evidence + .iter() + .any(|evidence| evidence.origin == EvidenceOrigin::Ast) + || !typed.evidence.iter().any(is_semantic_layer_evidence) + { + continue; + } + let Some(key) = incremental_edge_key(raw) else { + continue; + }; + let Some(site) = typed.relationship_site.clone() else { + continue; + }; + mixed_edges.entry(key).or_default().push((index, site)); + } + let mut refreshed_mixed_sites = HashMap::::new(); + let mut semantic_only_mixed_edges = HashSet::::new(); + for (key, mut prior) in mixed_edges { + let Some(current) = fresh_edge_sites.get(&key) else { + semantic_only_mixed_edges.extend(prior.into_iter().map(|(index, _)| index)); + continue; + }; + prior.sort_by(|left, right| { + incremental_anchor_key(&left.1) + .cmp(&incremental_anchor_key(&right.1)) + .then_with(|| left.0.cmp(&right.0)) + }); + // Exact shared sites are authoritative even when occurrence cardinality changed. + // Equal residual cardinality has one stable sorted bijection (the existing moved-site + // behavior). Unequal residual cardinality is ambiguous: fresh occurrences stay AST-only, + // while prior occurrences may survive only through semantic-only revalidation below. + let mut matched_current = vec![false; current.len()]; + let mut unmatched_prior = Vec::new(); + for (index, prior_site) in prior { + let exact = current + .iter() + .enumerate() + .position(|(current_index, site)| { + !matched_current[current_index] + && incremental_anchor_key(&prior_site) == incremental_anchor_key(site) + }); + if let Some(current_index) = exact { + matched_current[current_index] = true; + refreshed_mixed_sites.insert(index, current[current_index].clone()); + } else { + unmatched_prior.push((index, prior_site)); + } + } + let unmatched_current = current + .iter() + .enumerate() + .filter(|(index, _)| !matched_current[*index]) + .map(|(_, site)| site) + .collect::>(); + if unmatched_prior.len() == unmatched_current.len() { + for ((index, _), site) in unmatched_prior.into_iter().zip(unmatched_current) { + refreshed_mixed_sites.insert(index, site.clone()); + } + } else { + semantic_only_mixed_edges.extend(unmatched_prior.into_iter().map(|(index, _)| index)); + } + } + let prior_diagnostics = existing_raw.extensions.remove(GRAPH_DIAGNOSTICS_EXTENSION); + append_graph_diagnostics(extraction, prior_diagnostics, remap_diagnostics); + for prior in &existing.nodes { + let Some(current_raw_id) = typed_ast_remap.get(&prior.id) else { + continue; + }; + let Some(current) = extraction + .nodes + .iter_mut() + .find(|node| node.id == *current_raw_id) + else { + continue; + }; + let fresh_site = raw_source_anchor(¤t.attributes, root); + let semantic_evidence = prior + .evidence + .iter() + .filter(|evidence| is_semantic_layer_evidence(evidence)) + .cloned() + .map(|mut evidence| { + rebind_preserved_node_evidence( + &mut evidence, + prior.source.as_ref(), + fresh_site.as_ref(), + ); + evidence + }) + .filter_map(|evidence| serde_json::to_value(evidence).ok()) + .collect::>(); + if semantic_evidence.is_empty() { + continue; + } + let evidence = current + .attributes + .entry(COALESCED_NODE_EVIDENCE_ATTRIBUTE.to_owned()) + .or_insert_with(|| serde_json::Value::Array(Vec::new())); + let Some(evidence) = evidence.as_array_mut() else { + continue; + }; + evidence.extend(semantic_evidence); + evidence.sort_by_cached_key(serde_json::Value::to_string); + evidence.dedup(); + } + let preserved_node_ids = existing .nodes .iter() + .filter(|node| { + !typed_ast_remap.contains_key(&node.id) + && node.evidence.iter().any(is_semantic_layer_evidence) + }) .map(|node| node.id.as_str()) - .collect::>(); - let mut preserved_nodes = existing + .collect::>(); + let mut preserved_nodes = existing_raw .nodes .into_iter() .filter(|node| { - !ast_ids.contains(node.id.as_str()) - && node - .attributes - .get("_origin") - .and_then(serde_json::Value::as_str) - != Some("ast") + preserved_node_ids.contains(node.id.as_str()) && !source_in_set(node.attributes.get("source_file"), root, refreshed) && !source_was_deleted(node.attributes.get("source_file"), root) }) .collect::>(); + for node in &mut preserved_nodes { + strip_preserved_ast_node_evidence(&mut node.attributes); + } let all_ids = extraction .nodes .iter() .map(|node| node.id.clone()) .chain(preserved_nodes.iter().map(|node| node.id.clone())) .collect::>(); - let mut preserved_edges = existing - .links - .into_iter() - .filter(|edge| { - all_ids.contains(&edge.source) - && all_ids.contains(&edge.target) - && edge - .attributes - .get("_origin") - .and_then(serde_json::Value::as_str) - != Some("ast") - && !source_in_set(edge.attributes.get("source_file"), root, refreshed) - && !source_was_deleted(edge.attributes.get("source_file"), root) - }) - .collect::>(); - extraction.nodes.append(&mut preserved_nodes); - extraction.edges.append(&mut preserved_edges); - let new_hyperedge_ids = extraction - .hyperedges - .iter() - .filter_map(|value| value.get("id").and_then(serde_json::Value::as_str)) - .collect::>(); - let existing_hyperedges = existing - .extras - .get("hyperedges") - .or_else(|| existing.graph.get("hyperedges")) - .and_then(serde_json::Value::as_array) - .cloned() - .unwrap_or_default() + let mut preserved_edges = existing_raw + .edges .into_iter() - .filter(|hyperedge| { - let Some(object) = hyperedge.as_object() else { - return false; - }; - if object - .get("id") - .and_then(serde_json::Value::as_str) - .is_some_and(|id| new_hyperedge_ids.contains(id)) - || source_in_set(object.get("source_file"), root, refreshed) - || source_was_deleted(object.get("source_file"), root) + .zip(existing.links) + .enumerate() + .filter_map(|(index, (mut raw, typed))| { + if dropped_edges.contains(&index) { + return None; + } + let preserve = typed.evidence.iter().any(is_semantic_layer_evidence) + && all_ids.contains(&raw.source) + && all_ids.contains(&raw.target) + && !source_in_set(raw.attributes.get("source_file"), root, refreshed) + && !source_was_deleted(raw.attributes.get("source_file"), root); + if !preserve { + return None; + } + if let Some(site) = refreshed_mixed_sites.get(&index) { + refresh_preserved_mixed_edge(&mut raw.attributes, &raw.source, &raw.target, site); + } else if semantic_only_mixed_edges.contains(&index) + && !retain_preserved_mixed_edge_as_semantic_only( + &mut raw.attributes, + remapped_edges.contains(&index), + ) { - return false; + return None; } - ["nodes", "members", "node_ids"] - .into_iter() - .find_map(|key| object.get(key).and_then(serde_json::Value::as_array)) - .is_none_or(|members| { - members.iter().all(|member| { - member - .as_str() - .is_some_and(|member| all_ids.contains(member)) - }) - }) + Some(raw) }) .collect::>(); - extraction.hyperedges.extend(existing_hyperedges); + extraction + .nodes + .extend(preserved_nodes.drain(..).map(|node| RawNodeRecord { + id: node.id, + attributes: node.attributes, + })); + extraction + .edges + .extend(preserved_edges.drain(..).map(|edge| RawEdgeRecord { + source: edge.source, + target: edge.target, + attributes: edge.attributes, + })); } -fn canonical_source_set(paths: &[PathBuf], root: &Path) -> HashSet { - paths - .iter() - .map(|path| { - let absolute = if path.is_absolute() { - path.clone() - } else { - root.join(path) - }; - canonical_identity(&absolute) - }) - .collect() +type IncrementalEdgeKey = (String, String, String, Option); + +fn incremental_anchor_key(anchor: &SourceAnchor) -> (&str, u64, u64) { + (&anchor.file, anchor.start_byte, anchor.end_byte) } -fn source_in_set( - value: Option<&serde_json::Value>, - root: &Path, - sources: &HashSet, -) -> bool { - let Some(source) = value.and_then(serde_json::Value::as_str) else { - return false; +fn incremental_edge_key(edge: &RawEdgeRecord) -> Option { + let relation = edge.attributes.get("relation")?.as_str()?; + Some(( + edge.source.clone(), + edge.target.clone(), + canonical_edge_kind(relation)?.as_str().to_owned(), + edge.attributes + .get(OCCURRENCE_RULE_ATTRIBUTE) + .and_then(serde_json::Value::as_str) + .map(str::to_owned), + )) +} + +fn refresh_preserved_mixed_edge( + attributes: &mut serde_json::Map, + source: &str, + target: &str, + fresh_site: &SourceAnchor, +) { + let Some(record) = attributes.get_mut(TRUSTED_EDGE_RECORD_ATTRIBUTE) else { + return; }; - let path = Path::new(source); - let absolute = if path.is_absolute() { - path.to_path_buf() - } else { - root.join(path) + let Ok(mut edge) = + serde_json::from_value::(record.clone()) + else { + return; }; - sources.contains(&canonical_identity(&absolute)) + let prior_site = edge.relationship_site.clone(); + edge.evidence.retain(|evidence| { + is_semantic_layer_evidence(evidence) + && evidence.rule.as_deref() + != Some(EndpointRewriteRule::IncrementalAstEndpointRemap.as_str()) + }); + for evidence in &mut edge.evidence { + if evidence.wiring_site.as_ref() == prior_site.as_ref() { + evidence.wiring_site = Some(fresh_site.clone()); + } + for anchor in &mut evidence.anchors { + if Some(&*anchor) == prior_site.as_ref() { + anchor.clone_from(fresh_site); + } + } + } + edge.source = source.to_owned(); + edge.target = target.to_owned(); + edge.relationship_site = Some(fresh_site.clone()); + *record = serde_json::to_value(edge).unwrap_or(serde_json::Value::Null); + rebind_raw_anchor(attributes, fresh_site); } -fn semantic_source_set(fragment: &serde_json::Value, root: &Path) -> HashSet { - ["nodes", "edges", "hyperedges"] - .into_iter() - .filter_map(|bucket| fragment.get(bucket).and_then(serde_json::Value::as_array)) - .flatten() - .filter_map(|item| item.get("source_file").and_then(serde_json::Value::as_str)) - .map(|source| { - let path = Path::new(source); +fn retain_preserved_mixed_edge_as_semantic_only( + attributes: &mut serde_json::Map, + remapped_in_current_pass: bool, +) -> bool { + if remapped_in_current_pass + && !attributes + .get("_endpoint_rewrite_rules") + .and_then(serde_json::Value::as_array) + .is_some_and(|rewrites| { + rewrites.iter().any(|rewrite| { + rewrite.get("rule").and_then(serde_json::Value::as_str) + == Some(EndpointRewriteRule::IncrementalAstEndpointRemap.as_str()) + }) + }) + { + return false; + } + let Some(record) = attributes.get_mut(TRUSTED_EDGE_RECORD_ATTRIBUTE) else { + return false; + }; + let Ok(mut edge) = + serde_json::from_value::(record.clone()) + else { + return false; + }; + edge.evidence.retain(|evidence| { + is_semantic_layer_evidence(evidence) + && evidence.rule.as_deref() + != Some(EndpointRewriteRule::IncrementalAstEndpointRemap.as_str()) + }); + let Some(primary) = edge + .evidence + .iter() + .find(|evidence| { + evidence + .rule + .as_deref() + .and_then(EndpointRewriteRule::from_wire_name) + .is_none() + }) + .cloned() + else { + // Endpoint rewrites describe transport, not an independently justified relationship. + // Without a non-AST producer assertion, an unmatched prior occurrence is stale. + return false; + }; + edge.relationship_site = None; + *record = serde_json::to_value(edge).unwrap_or(serde_json::Value::Null); + for key in [ + "source_file", + "source_location", + "source_anchor", + "start_byte", + "end_byte", + "line_start", + "line_end", + "column_start", + "column_end", + "_coalesced_edge_evidence", + ] { + attributes.remove(key); + } + if !remapped_in_current_pass { + attributes.remove("_endpoint_rewrite_rules"); + attributes.remove(CONSUME_INCREMENTAL_ENDPOINT_REMAP_ATTRIBUTE); + } else { + attributes.insert( + CONSUME_INCREMENTAL_ENDPOINT_REMAP_ATTRIBUTE.to_owned(), + serde_json::Value::Bool(true), + ); + } + project_preserved_semantic_evidence(attributes, &primary); + true +} + +fn project_preserved_semantic_evidence( + attributes: &mut serde_json::Map, + evidence: &compass_model::provenance::Provenance, +) { + attributes.insert( + "_origin".to_owned(), + serde_json::Value::String(evidence.origin.as_str().to_owned()), + ); + attributes.insert( + "confidence".to_owned(), + serde_json::Value::String(evidence.confidence.legacy_str().to_owned()), + ); + attributes.insert( + "extractor".to_owned(), + serde_json::Value::String(evidence.extractor.clone()), + ); + if let Some(rule) = &evidence.rule { + attributes.insert("rule".to_owned(), serde_json::Value::String(rule.clone())); + } else { + attributes.remove("rule"); + } + if let Some(score) = evidence.score { + attributes.insert("confidence_score".to_owned(), score.into()); + } else { + attributes.remove("confidence_score"); + } + if evidence.candidates.is_empty() { + attributes.remove("candidates"); + } else { + attributes.insert( + "candidates".to_owned(), + serde_json::to_value(&evidence.candidates).unwrap_or(serde_json::Value::Null), + ); + } + let semantic_site = evidence + .wiring_site + .as_ref() + .or_else(|| evidence.anchors.first()); + if let Some(site) = semantic_site { + attributes.insert( + "source_file".to_owned(), + serde_json::Value::String(site.file.clone()), + ); + attributes.insert( + "source_anchor".to_owned(), + serde_json::to_value(site).unwrap_or(serde_json::Value::Null), + ); + } +} + +fn rebind_raw_anchor( + attributes: &mut serde_json::Map, + fresh_site: &SourceAnchor, +) { + attributes.insert( + "source_file".to_owned(), + serde_json::Value::String(fresh_site.file.clone()), + ); + attributes.insert( + "source_location".to_owned(), + serde_json::Value::String(format!( + "L{}:{}-L{}:{}", + fresh_site.start_line, + fresh_site.start_column, + fresh_site.end_line, + fresh_site.end_column + )), + ); + attributes.insert( + "source_anchor".to_owned(), + serde_json::to_value(fresh_site).unwrap_or(serde_json::Value::Null), + ); + for (key, value) in [ + ("start_byte", fresh_site.start_byte.into()), + ("end_byte", fresh_site.end_byte.into()), + ("line_start", fresh_site.start_line.into()), + ("line_end", fresh_site.end_line.into()), + ("column_start", fresh_site.start_column.into()), + ("column_end", fresh_site.end_column.into()), + ] { + if attributes.contains_key(key) { + attributes.insert(key.to_owned(), value); + } + } +} + +fn strip_preserved_ast_node_evidence(attributes: &mut serde_json::Map) { + let Some(record) = attributes.get_mut(compass_model::provenance::TRUSTED_NODE_RECORD_ATTRIBUTE) + else { + return; + }; + let Ok(mut node) = + serde_json::from_value::(record.clone()) + else { + return; + }; + node.evidence.retain(is_semantic_layer_evidence); + *record = serde_json::to_value(node).unwrap_or(serde_json::Value::Null); +} + +fn rebind_preserved_node_evidence( + evidence: &mut compass_model::provenance::Provenance, + prior_site: Option<&SourceAnchor>, + fresh_site: Option<&SourceAnchor>, +) { + let (Some(prior_site), Some(fresh_site)) = (prior_site, fresh_site) else { + return; + }; + if evidence.wiring_site.as_ref() == Some(prior_site) { + evidence.wiring_site = Some(fresh_site.clone()); + } + for anchor in &mut evidence.anchors { + if anchor == prior_site { + anchor.clone_from(fresh_site); + } + } +} + +fn raw_source_anchor( + attributes: &serde_json::Map, + root: &Path, +) -> Option { + let mut site = attributes + .get("source_anchor") + .and_then(|value| serde_json::from_value::(value.clone()).ok()) + .or_else(|| { + Some(SourceAnchor { + file: attributes.get("source_file")?.as_str()?.to_owned(), + start_byte: attributes.get("start_byte")?.as_u64()?, + end_byte: attributes.get("end_byte")?.as_u64()?, + start_line: u32::try_from(attributes.get("line_start")?.as_u64()?).ok()?, + start_column: u32::try_from(attributes.get("column_start")?.as_u64()?).ok()?, + end_line: u32::try_from(attributes.get("line_end")?.as_u64()?).ok()?, + end_column: u32::try_from(attributes.get("column_end")?.as_u64()?).ok()?, + }) + })?; + let path = Path::new(&site.file); + if path.is_absolute() + && let Ok(relative) = path.strip_prefix(root) + { + site.file = relative.to_string_lossy().replace('\\', "/"); + } + Some(site) +} + +fn has_exact_remap_site(attributes: &serde_json::Map) -> bool { + attributes + .get("source_anchor") + .and_then(|value| serde_json::from_value::(value.clone()).ok()) + .is_some_and(|site| site.is_valid() && site.start_byte < site.end_byte) +} + +fn append_graph_diagnostics( + extraction: &mut Extraction, + prior: Option, + diagnostics: Vec, +) { + let mut values = extraction + .extensions + .remove(GRAPH_DIAGNOSTICS_EXTENSION) + .and_then(|value| value.as_array().cloned()) + .unwrap_or_default(); + values.extend( + prior + .and_then(|value| value.as_array().cloned()) + .unwrap_or_default(), + ); + values.extend( + diagnostics + .into_iter() + .filter_map(|diagnostic| serde_json::to_value(diagnostic).ok()), + ); + let mut remap_drops = Vec::new(); + let mut unrelated = Vec::new(); + let mut omitted = 0_usize; + for value in values { + match value + .get("code") + .and_then(serde_json::Value::as_str) + .unwrap_or_default() + { + INCREMENTAL_REMAP_DROP_DIAGNOSTIC => remap_drops.push(value), + INCREMENTAL_REMAP_TRUNCATION_DIAGNOSTIC => { + omitted = omitted.saturating_add(remap_truncation_count(&value)); + } + _ => unrelated.push(value), + } + } + remap_drops.sort_by_cached_key(serde_json::Value::to_string); + remap_drops.dedup(); + if remap_drops.len() > MAX_INCREMENTAL_REMAP_DIAGNOSTICS { + omitted = omitted.saturating_add( + remap_drops + .len() + .saturating_sub(MAX_INCREMENTAL_REMAP_DIAGNOSTICS), + ); + remap_drops.truncate(MAX_INCREMENTAL_REMAP_DIAGNOSTICS); + } + unrelated.append(&mut remap_drops); + if omitted > 0 + && let Ok(summary) = serde_json::to_value(GraphDiagnostic { + severity: DiagnosticSeverity::Warning, + code: INCREMENTAL_REMAP_TRUNCATION_DIAGNOSTIC.to_owned(), + message: format!("omitted {omitted} additional incremental remap diagnostics"), + anchor: None, + related_ids: Vec::new(), + }) + { + unrelated.push(summary); + } + unrelated.sort_by_cached_key(serde_json::Value::to_string); + unrelated.dedup(); + extraction.extensions.insert( + GRAPH_DIAGNOSTICS_EXTENSION.to_owned(), + serde_json::Value::Array(unrelated), + ); +} + +fn remap_truncation_count(value: &serde_json::Value) -> usize { + value + .get("message") + .and_then(serde_json::Value::as_str) + .and_then(|message| { + message + .strip_prefix("omitted ") + .and_then(|message| { + message.strip_suffix(" additional incremental remap diagnostics") + }) + .and_then(|count| count.parse().ok()) + }) + .unwrap_or_default() +} + +fn raw_node_match_key(node: &RawNodeRecord) -> Option<(String, String, String)> { + Some(( + node.attributes.get("source_file")?.as_str()?.to_owned(), + node.label().to_owned(), + node.attributes + .get("symbol_kind") + .or_else(|| node.attributes.get("type")) + .and_then(serde_json::Value::as_str) + .unwrap_or("symbol") + .to_owned(), + )) +} + +fn typed_node_match_key( + node: &compass_model::code_graph::NodeRecord, +) -> Option<(String, String, String)> { + Some(( + node.source_file()?.to_owned(), + node.label().to_owned(), + node.kind.as_str().to_owned(), + )) +} + +fn node_has_origin(node: &compass_model::code_graph::NodeRecord, origin: EvidenceOrigin) -> bool { + node.evidence + .iter() + .any(|evidence| evidence.origin == origin) +} + +fn is_semantic_layer_evidence(evidence: &Provenance) -> bool { + evidence.extractor == SEMANTIC_LAYER_EXTRACTOR +} + +fn node_has_semantic_layer_evidence(node: &compass_model::code_graph::NodeRecord) -> bool { + node.evidence.iter().any(is_semantic_layer_evidence) +} + +fn edge_has_semantic_layer_evidence(edge: &compass_model::code_graph::EdgeRecord) -> bool { + edge.evidence.iter().any(is_semantic_layer_evidence) +} + +fn canonical_source_set(paths: &[PathBuf], root: &Path) -> HashSet { + paths + .iter() + .map(|path| { + let absolute = if path.is_absolute() { + path.clone() + } else { + root.join(path) + }; + canonical_identity(&absolute) + }) + .collect() +} + +fn source_in_set( + value: Option<&serde_json::Value>, + root: &Path, + sources: &HashSet, +) -> bool { + let Some(source) = value.and_then(serde_json::Value::as_str) else { + return false; + }; + let path = Path::new(source); + let absolute = if path.is_absolute() { + path.to_path_buf() + } else { + root.join(path) + }; + sources.contains(&canonical_identity(&absolute)) +} + +fn semantic_source_set(fragment: &serde_json::Value, root: &Path) -> HashSet { + ["nodes", "edges", "hyperedges"] + .into_iter() + .filter_map(|bucket| fragment.get(bucket).and_then(serde_json::Value::as_array)) + .flatten() + .filter_map(|item| item.get("source_file").and_then(serde_json::Value::as_str)) + .map(|source| { + let path = Path::new(source); let absolute = if path.is_absolute() { path.to_path_buf() } else { @@ -2144,7 +2952,7 @@ fn stale_semantic_sources( root: &Path, detected: &BTreeMap>, ) -> HashSet { - let Ok(existing) = GraphDocument::load(graph_path) else { + let Ok(existing) = V1GraphDocument::load(graph_path) else { return HashSet::new(); }; let live = detected @@ -2155,45 +2963,23 @@ fn stale_semantic_sources( let mut stale = existing .nodes .iter() - .filter(|node| { - node.attributes - .get("_origin") - .and_then(serde_json::Value::as_str) - != Some("ast") - }) - .filter_map(|node| semantic_source_under_root(node.attributes.get("source_file"), root)) + .filter(|node| node_has_semantic_layer_evidence(node)) + .filter_map(|node| semantic_path_under_root(node.source_file(), root)) .filter(|source| !live.contains(source)) .collect::>(); stale.extend( existing .links .iter() - .filter(|edge| { - edge.attributes - .get("_origin") - .and_then(serde_json::Value::as_str) - != Some("ast") - }) - .filter_map(|edge| semantic_source_under_root(edge.attributes.get("source_file"), root)) - .filter(|source| !live.contains(source)), - ); - let hyperedges = existing - .extras - .get("hyperedges") - .or_else(|| existing.graph.get("hyperedges")) - .and_then(serde_json::Value::as_array); - stale.extend( - hyperedges - .into_iter() - .flatten() - .filter_map(|hyperedge| semantic_source_under_root(hyperedge.get("source_file"), root)) + .filter(|edge| edge_has_semantic_layer_evidence(edge)) + .filter_map(|edge| semantic_path_under_root(edge.source_file(), root)) .filter(|source| !live.contains(source)), ); stale } -fn semantic_source_under_root(value: Option<&serde_json::Value>, root: &Path) -> Option { - let source = value.and_then(serde_json::Value::as_str)?; +fn semantic_path_under_root(source: Option<&str>, root: &Path) -> Option { + let source = source?; let path = Path::new(source); let absolute = if path.is_absolute() { path.to_path_buf() @@ -2307,36 +3093,21 @@ fn save_build_manifest( } fn semantic_document_sources(graph_path: &Path, root: &Path) -> HashSet { - let Ok(existing) = GraphDocument::load(graph_path) else { + let Ok(existing) = V1GraphDocument::load(graph_path) else { return HashSet::new(); }; existing .nodes .into_iter() - .filter(|node| { - node.attributes - .get("_origin") - .and_then(serde_json::Value::as_str) - != Some("ast") - && matches!( - node.attributes - .get("file_type") - .and_then(serde_json::Value::as_str), - Some("document" | "concept" | "rationale" | "paper") - ) - }) + .filter(|node| node_has_semantic_layer_evidence(node) && node.kind == NodeKind::Resource) .filter_map(|node| { - node.attributes - .get("source_file") - .and_then(serde_json::Value::as_str) - .map(Path::new) - .map(|path| { - if path.is_absolute() { - canonical_identity(path) - } else { - canonical_identity(&root.join(path)) - } - }) + node.source_file().map(Path::new).map(|path| { + if path.is_absolute() { + canonical_identity(path) + } else { + canonical_identity(&root.join(path)) + } + }) }) .collect() } @@ -2345,6 +3116,207 @@ fn canonical_identity(path: &Path) -> PathBuf { fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) } +fn graph_configuration_digest( + options: &BuildOptions, + output_dir: &Path, +) -> Result { + let bytes = serde_json::to_vec(&build_profile(options)).map_err(|source| { + CoreError::SerializeExtraction { + path: output_dir.join("graph.json"), + source, + } + })?; + Ok(format!("sha256:{:x}", Sha256::digest(bytes))) +} + +struct PublicationEvidence<'a> { + detection: &'a Detection, + semantic: Option<&'a SemanticLayer>, + extraction_failures: &'a BTreeMap, + extraction_partials: &'a BTreeMap, +} + +fn published_v1_document( + document: &GraphDocument, + communities: &compass_graph::Communities, + labels: &BTreeMap, + root: &Path, + evidence: PublicationEvidence<'_>, + configuration_digest: String, + source_commit: Option<&str>, +) -> Result { + let mut publication_source = document.clone(); + let node_communities = communities + .iter() + .flat_map(|(community, members)| { + members + .iter() + .map(move |member| (member.as_str(), *community)) + }) + .collect::>(); + for node in &mut publication_source.nodes { + let Some(&community_index) = node_communities.get(node.id.as_str()) else { + continue; + }; + let community = u64::try_from(community_index) + .map_err(|_| CoreError::InvalidBuildState("community ID exceeds u64".to_owned()))?; + node.attributes + .insert("community".to_owned(), serde_json::Value::from(community)); + if let Some(label) = labels.get(&community_index) { + node.attributes.insert( + "community_name".to_owned(), + serde_json::Value::String(label.clone()), + ); + } + } + Ok(normalize_document_v1_with_inventory( + &publication_source, + root, + configuration_digest, + source_commit, + detection_inventory( + evidence.detection, + evidence.semantic, + evidence.extraction_failures, + evidence.extraction_partials, + root, + ), + )?) +} + +fn detection_inventory( + detection: &Detection, + semantic: Option<&SemanticLayer>, + extraction_failures: &BTreeMap, + extraction_partials: &BTreeMap, + root: &Path, +) -> Vec { + let mut inventory = detection + .files + .iter() + .flat_map(|(category, paths)| { + paths.iter().map(move |path| { + let absolute = if Path::new(path).is_absolute() { + PathBuf::from(path) + } else { + root.join(path) + }; + let identity = canonical_identity(&absolute); + let spec = Registry::resolve(Path::new(path)); + let status = if extraction_failures.contains_key(&identity) { + ExtractionStatus::ParseFailure + } else if extraction_partials.contains_key(&identity) + || semantic.is_some_and(|layer| { + layer.partial_files.iter().any(|partial| { + canonical_identity(partial) == canonical_identity(&absolute) + }) + }) + { + ExtractionStatus::Partial + } else if source_is_generated(&absolute) { + ExtractionStatus::Generated + } else { + match category.as_str() { + "image" | "video" => ExtractionStatus::Binary, + "code" | "document" if spec.is_some() => ExtractionStatus::Extracted, + _ => ExtractionStatus::Unsupported, + } + }; + InventoryEvidence { + path: PathBuf::from(path), + language: spec.map(|spec| spec.name.to_owned()), + producer: spec.map_or_else( + || "compass.files.detect".to_owned(), + |spec| format!("compass.languages.{}", spec.name), + ), + status, + reason: extraction_failures + .get(&identity) + .or_else(|| extraction_partials.get(&identity)) + .cloned() + .or_else(|| { + (status == ExtractionStatus::Unsupported) + .then(|| format!("no extractor for detected {category} file")) + }), + } + }) + }) + .collect::>(); + inventory.extend(detection.unclassified.iter().map(|path| InventoryEvidence { + path: PathBuf::from(path), + language: None, + producer: "compass.files.detect".to_owned(), + status: ExtractionStatus::Unsupported, + reason: Some("unclassified by detector".to_owned()), + })); + inventory.extend(detection.ignored.iter().map(|path| InventoryEvidence { + path: PathBuf::from(path), + language: None, + producer: "compass.files.detect".to_owned(), + status: ExtractionStatus::Excluded, + reason: Some("excluded by ignore policy".to_owned()), + })); + inventory +} + +fn prepare_extraction_for_publication( + path: &Path, + extraction: &mut Extraction, + root: &Path, + failures: &mut BTreeMap, + partials: &mut BTreeMap, +) { + let identity = canonical_identity(path); + if let Some(error) = extraction.error.take() { + failures.insert(identity, portable_diagnostic_reason(&error, path, root)); + *extraction = Extraction::default(); + extraction.raw_calls = None; + return; + } + if extraction + .extensions + .get(EXTRACTION_QUALITY_EXTENSION) + .and_then(serde_json::Value::as_str) + != Some(EXTRACTION_QUALITY_PARTIAL) + { + return; + } + let reason = extraction + .extensions + .get(EXTRACTION_QUALITY_REASON_EXTENSION) + .and_then(serde_json::Value::as_str) + .unwrap_or("extraction completed with parser recovery"); + partials.insert(identity, portable_diagnostic_reason(reason, path, root)); + extraction.edges.clear(); + extraction.hyperedges.clear(); + extraction.framework_facts.clear(); + extraction.raw_calls = None; +} + +fn portable_diagnostic_reason(reason: &str, path: &Path, root: &Path) -> String { + let root_text = root.to_string_lossy(); + let path_text = path.to_string_lossy(); + let relative = path + .strip_prefix(root) + .unwrap_or(path) + .to_string_lossy() + .replace('\\', "/"); + let sanitized = reason + .replace(root_text.as_ref(), ".") + .replace(path_text.as_ref(), &relative) + .replace('\\', "/"); + sanitized.chars().take(512).collect() +} + +fn source_is_generated(path: &Path) -> bool { + fs::read(path).ok().is_some_and(|bytes| { + let head = String::from_utf8_lossy(&bytes[..bytes.len().min(2_048)]); + ["DO NOT EDIT", "@generated", "Generated by"] + .iter() + .any(|marker| head.contains(marker)) + }) +} + fn source_was_deleted(value: Option<&serde_json::Value>, root: &Path) -> bool { let Some(source) = value.and_then(serde_json::Value::as_str) else { return false; @@ -2358,63 +3330,6 @@ fn source_was_deleted(value: Option<&serde_json::Value>, root: &Path) -> bool { Registry::resolve(path).is_some() && !absolute.exists() } -fn write_raw_graph( - path: &Path, - extraction: &Extraction, - nodes: &[NodeRecord], - edges: &[EdgeRecord], - purpose: BuildPurpose, - tokens: (u64, u64), -) -> Result<(), CoreError> { - let document = RawGraphOutput { - extraction, - nodes, - edges, - purpose, - tokens, - }; - write_json_ascii_atomic(path, &document, true, purpose == BuildPurpose::Update)?; - Ok(()) -} - -struct RawGraphOutput<'a> { - extraction: &'a Extraction, - nodes: &'a [NodeRecord], - edges: &'a [EdgeRecord], - purpose: BuildPurpose, - tokens: (u64, u64), -} - -impl Serialize for RawGraphOutput<'_> { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use serde::ser::SerializeMap; - - if self.purpose == BuildPurpose::Extract { - let mut map = serializer.serialize_map(Some(5))?; - map.serialize_entry("nodes", self.nodes)?; - map.serialize_entry("edges", self.edges)?; - map.serialize_entry("hyperedges", &self.extraction.hyperedges)?; - map.serialize_entry("input_tokens", &self.tokens.0)?; - map.serialize_entry("output_tokens", &self.tokens.1)?; - map.end() - } else { - let fields = 4 + usize::from(!self.extraction.hyperedges.is_empty()); - let mut map = serializer.serialize_map(Some(fields))?; - map.serialize_entry("input_tokens", &0_u64)?; - map.serialize_entry("output_tokens", &0_u64)?; - map.serialize_entry("nodes", self.nodes)?; - map.serialize_entry("links", self.edges)?; - if !self.extraction.hyperedges.is_empty() { - map.serialize_entry("hyperedges", &self.extraction.hyperedges)?; - } - map.end() - } - } -} - fn report_root_label(path: &Path) -> String { if path.is_absolute() { return path @@ -2756,14 +3671,54 @@ fn absolutize_from(root: &Path, path: &Path) -> PathBuf { } } +fn read_source_text_with_limit(path: &Path, max_source_bytes: u64) -> Option<(String, String)> { + fs::metadata(path) + .ok() + .filter(|metadata| metadata.is_file() && metadata.len() <= max_source_bytes) + .and_then(|_| fs::read(path).ok()) + .map(|bytes| { + ( + path.to_string_lossy().into_owned(), + String::from_utf8_lossy(&bytes).into_owned(), + ) + }) +} + #[cfg(test)] mod tests { use std::error::Error; + use compass_model::code_graph::GraphDocument as V1GraphDocument; use serde_json::{Map, Value}; use super::*; + #[test] + fn force_cache_reuse_never_authorizes_prior_published_graph_input() { + assert!(cache_reuse_enabled(false, false)); + assert!(cache_reuse_enabled(false, true)); + assert!(!cache_reuse_enabled(true, false)); + assert!(cache_reuse_enabled(true, true)); + assert!(prior_published_graph_input_enabled(false)); + assert!(!prior_published_graph_input_enabled(true)); + } + + #[test] + fn resolver_source_text_enforces_the_pre_read_byte_limit() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + let source = directory.path().join("oversized.py"); + fs::write(&source, b"0123456789")?; + + assert!(read_source_text_with_limit(&source, 9).is_none()); + assert_eq!( + read_source_text_with_limit(&source, 10) + .map(|(_, body)| body) + .as_deref(), + Some("0123456789") + ); + Ok(()) + } + #[test] fn precomputed_detection_cannot_cross_repository_roots() -> Result<(), Box> { let detected_root = tempfile::tempdir()?; @@ -2781,7 +3736,10 @@ mod tests { let mut options = BuildOptions::new(build_root.path()); options.precomputed_detection = Some(detection); - let error = build_local_graph(&options).expect_err("mismatched detection must fail"); + let error = match build_local_graph(&options) { + Ok(_) => return Err("mismatched detection unexpectedly succeeded".into()), + Err(error) => error, + }; assert!(matches!(error, CoreError::DetectionRootMismatch)); Ok(()) } @@ -2799,14 +3757,14 @@ mod tests { let prefix_symbol_id = make_id(&[&file_stem(&source)]); let mut extraction = Extraction { nodes: vec![ - NodeRecord { + RawNodeRecord { id: file_id, attributes: Map::from_iter([( "source_file".to_owned(), Value::String(source_text.clone()), )]), }, - NodeRecord { + RawNodeRecord { id: prefix_symbol_id.clone(), attributes: Map::from_iter([( "source_file".to_owned(), @@ -2814,7 +3772,7 @@ mod tests { )]), }, ], - edges: vec![EdgeRecord { + edges: vec![RawEdgeRecord { source: "caller".to_owned(), target: prefix_symbol_id.clone(), attributes: Map::new(), @@ -2846,14 +3804,14 @@ mod tests { let old_id = make_id(&[&external_text]); let source_id = "webapi".to_owned(); let mut extraction = Extraction { - nodes: vec![NodeRecord { + nodes: vec![RawNodeRecord { id: old_id.clone(), attributes: Map::from_iter([( "source_file".to_owned(), Value::String(external_text), )]), }], - edges: vec![EdgeRecord { + edges: vec![RawEdgeRecord { source: source_id.clone(), target: old_id, attributes: Map::from_iter([( @@ -2878,7 +3836,7 @@ mod tests { fn astro_import_identities_do_not_include_checkout_root() -> Result<(), Box> { let first = tempfile::tempdir()?; let second = tempfile::tempdir()?; - let build = |root: &Path| -> Result> { + let build = |root: &Path| -> Result> { let source = root.join("src"); fs::create_dir_all(&source)?; fs::create_dir_all(root.join(".hidden"))?; @@ -2889,14 +3847,14 @@ mod tests { fs::write(root.join(".hidden/Layout.astro"), "\n")?; let mut options = BuildOptions::new(root); options.no_viz = true; - build_local_graph(&options)?; - Ok(GraphDocument::load( - &root.join("compass-out").join("graph.json"), + let result = build_local_graph(&options)?; + Ok(V1GraphDocument::load( + &result.output_dir.join("graph.json"), )?) }; let first_graph = build(first.path())?; let second_graph = build(second.path())?; - let identities = |document: &GraphDocument| { + let identities = |document: &V1GraphDocument| { let mut nodes = document .nodes .iter() @@ -2909,7 +3867,7 @@ mod tests { ( edge.source.clone(), edge.target.clone(), - edge.string("relation"), + edge.relation().to_owned(), ) }) .collect::>(); @@ -2922,7 +3880,12 @@ mod tests { .nodes .iter() .chain(&second_graph.nodes) - .flat_map(|node| [node.id.clone(), node.string("source_file")]) + .flat_map(|node| { + [ + node.id.clone(), + node.source_file().unwrap_or_default().to_owned(), + ] + }) .chain( first_graph .links @@ -2932,7 +3895,7 @@ mod tests { [ edge.source.clone(), edge.target.clone(), - edge.string("source_file"), + edge.source_file().unwrap_or_default().to_owned(), ] }), ) @@ -2945,7 +3908,7 @@ mod tests { let punctuation_symbols = first_graph .nodes .iter() - .filter(|node| node.string("label") == "[]") + .filter(|node| node.label() == "[]") .collect::>(); assert!( !punctuation_symbols.is_empty(), @@ -2954,7 +3917,7 @@ mod tests { assert!( punctuation_symbols .iter() - .all(|node| node.id.starts_with("src_page_")) + .all(|node| node.id.starts_with("sha256:")) ); Ok(()) } @@ -2996,10 +3959,14 @@ mod tests { assert_eq!(overview["model"]["schema"], "compass.viewer.graph/1"); assert!(cold.output_dir.join("manifest.json").is_file()); assert!(!cold.output_dir.join(".compass_incomplete").exists()); - let cold_graph = GraphDocument::load(&cold.output_dir.join("graph.json"))?; + let cold_graph = V1GraphDocument::load(&cold.output_dir.join("graph.json"))?; assert!(cold_graph.nodes.iter().all(|node| { - node.attributes.get("_origin").and_then(Value::as_str) == Some("ast") - && !Path::new(&node.string("source_file")).is_absolute() + node.evidence + .first() + .is_some_and(|evidence| evidence.origin == EvidenceOrigin::Ast) + && node + .source_file() + .is_none_or(|source| !Path::new(source).is_absolute()) })); let cold_graph_bytes = fs::read(cold.output_dir.join("graph.json"))?; let cold_report_bytes = fs::read(cold.output_dir.join("GRAPH_REPORT.md"))?; @@ -3022,13 +3989,13 @@ mod tests { let changed = build_local_graph(&options)?; assert_eq!(changed.files_extracted, 1); assert_eq!(changed.files_cached, 1); - let changed_graph = GraphDocument::load(&changed.output_dir.join("graph.json"))?; + let changed_graph = V1GraphDocument::load(&changed.output_dir.join("graph.json"))?; let changed_graph_bytes = fs::read(changed.output_dir.join("graph.json"))?; assert_ne!( changed_graph_bytes, cold_graph_bytes, "a body-only edit must update definition hashes" ); - let identities = |document: &GraphDocument| { + let identities = |document: &V1GraphDocument| { ( document .nodes @@ -3038,23 +4005,18 @@ mod tests { document .links .iter() - .map(|edge| { - ( - edge.source.clone(), - edge.target.clone(), - edge.string("relation"), - ) - }) + .map(|edge| (edge.source.clone(), edge.target.clone(), edge.relation())) .collect::>(), ) }; assert_eq!(identities(&changed_graph), identities(&cold_graph)); - let implementation_hash = |document: &GraphDocument| { + let implementation_hash = |document: &V1GraphDocument| { document .nodes .iter() .find(|node| node.label() == "work()") - .map(|node| node.string("implementation_hash")) + .and_then(|node| node.digest("implementation_hash")) + .map(str::to_owned) }; assert_ne!( implementation_hash(&changed_graph), @@ -3071,12 +4033,12 @@ mod tests { fs::remove_file(root.join("helper.py"))?; let deleted = build_local_graph(&options)?; assert_eq!(deleted.files_considered, 1); - let graph = GraphDocument::load(&deleted.output_dir.join("graph.json"))?; + let graph = V1GraphDocument::load(&deleted.output_dir.join("graph.json"))?; assert!( graph .nodes .iter() - .all(|node| node.string("source_file") != "helper.py") + .all(|node| node.source_file() != Some("helper.py")) ); Ok(()) } @@ -3086,29 +4048,31 @@ mod tests { let directory = tempfile::tempdir()?; let root = directory.path(); fs::write(root.join("main.py"), "def before():\n return 1\n")?; + fs::write(root.join("domain.md"), "# Domain rule\n")?; let mut options = BuildOptions::new(root); options.no_viz = true; - let first = build_local_graph(&options)?; - let graph_path = first.output_dir.join("graph.json"); - let mut graph = GraphDocument::load(&graph_path)?; - let mut attributes = Map::new(); - attributes.insert("label".to_owned(), Value::String("Domain rule".to_owned())); - attributes.insert("file_type".to_owned(), Value::String("concept".to_owned())); - graph.nodes.push(NodeRecord { - id: "semantic_domain_rule".to_owned(), - attributes, - }); - write_json_atomic(&graph_path, &graph, true)?; + let semantic = SemanticLayer { + fragment: json!({ + "nodes": [{ + "id": "semantic_domain_rule", + "label": "Domain rule", + "file_type": "concept", + "source_file": "domain.md", + }], + "edges": [], + "hyperedges": [], + "failed_chunks": 0, + }), + refreshed_files: vec![PathBuf::from("domain.md")], + partial_files: Vec::new(), + allow_partial: false, + }; + build_graph_with_semantic(&options, &semantic)?; fs::write(root.join("main.py"), "def after():\n return 2\n")?; - build_local_graph(&options)?; - let graph = GraphDocument::load(&graph_path)?; - assert!( - graph - .nodes - .iter() - .any(|node| node.id == "semantic_domain_rule") - ); + let updated = build_local_graph(&options)?; + let graph = V1GraphDocument::load(&updated.output_dir.join("graph.json"))?; + assert!(graph.nodes.iter().any(|node| node.label() == "Domain rule")); assert!(graph.nodes.iter().any(|node| node.label() == "after()")); assert!(!graph.nodes.iter().any(|node| node.label() == "before()")); Ok(()) @@ -3121,46 +4085,39 @@ mod tests { fs::write(root.join("guide.md"), "# Guide\n\nLocal structure.\n")?; let mut options = BuildOptions::new(root); options.no_viz = true; - let first = build_local_graph(&options)?; - let graph_path = first.output_dir.join("graph.json"); - let semantic = GraphDocument { - directed: false, - multigraph: false, - graph: Map::new(), - nodes: vec![NodeRecord { - id: "semantic_guide".to_owned(), - attributes: Map::from_iter([ - ( - "label".to_owned(), - Value::String("Guide concept".to_owned()), - ), - ("file_type".to_owned(), Value::String("concept".to_owned())), - ( - "source_file".to_owned(), - Value::String("guide.md".to_owned()), - ), - ]), - }], - links: Vec::new(), - extras: BTreeMap::new(), + let semantic = SemanticLayer { + fragment: json!({ + "nodes": [{ + "id": "semantic_guide", + "label": "Guide concept", + "file_type": "concept", + "source_file": "guide.md", + }], + "edges": [], + "hyperedges": [], + "failed_chunks": 0, + }), + refreshed_files: vec![PathBuf::from("guide.md")], + partial_files: Vec::new(), + allow_partial: false, }; - write_json_atomic(&graph_path, &semantic, true)?; + build_graph_with_semantic(&options, &semantic)?; - build_local_graph(&options)?; - let graph = GraphDocument::load(&graph_path)?; - assert_eq!(graph.nodes.len(), 1); - assert_eq!(graph.nodes[0].id, "semantic_guide"); - assert!( + let updated = build_local_graph(&options)?; + let graph = V1GraphDocument::load(&updated.output_dir.join("graph.json"))?; + assert_eq!( graph .nodes .iter() - .all(|node| node.attributes.get("_origin").is_none()) + .filter(|node| node.label() == "Guide concept") + .count(), + 1 ); Ok(()) } #[test] - fn no_cluster_schema_tracks_command_purpose() -> Result<(), Box> { + fn no_cluster_always_publishes_the_v1_contract() -> Result<(), Box> { let extract_dir = tempfile::tempdir()?; fs::write( extract_dir.path().join("main.py"), @@ -3172,9 +4129,11 @@ mod tests { let result = build_local_graph(&extract)?; let value: Value = serde_json::from_slice(&fs::read(result.output_dir.join("graph.json"))?)?; - assert!(value.get("edges").is_some()); - assert!(value.get("links").is_none()); - assert!(value.get("directed").is_none()); + assert_eq!(value["graph"]["schema"], "compass.graph/1"); + assert_eq!(value["directed"], true); + assert_eq!(value["multigraph"], true); + assert!(value.get("links").is_some()); + assert!(value.get("edges").is_none()); assert!(!result.output_dir.join("GRAPH_REPORT.md").exists()); assert!(!result.output_dir.join(".compass_analysis.json").exists()); @@ -3184,8 +4143,10 @@ mod tests { update.no_cluster = true; let result = build_local_graph(&update)?; let bytes = fs::read(result.output_dir.join("graph.json"))?; - assert_eq!(bytes.last(), Some(&b'\n')); let value: Value = serde_json::from_slice(&bytes)?; + assert_eq!(value["graph"]["schema"], "compass.graph/1"); + assert_eq!(value["directed"], true); + assert_eq!(value["multigraph"], true); assert!(value.get("links").is_some()); assert!(value.get("edges").is_none()); assert!(result.output_dir.join(".compass_root").is_file()); @@ -3193,7 +4154,7 @@ mod tests { } #[test] - fn code_sources_precede_deterministic_documents() -> Result<(), Box> { + fn code_and_deterministic_document_sources_are_both_published() -> Result<(), Box> { let directory = tempfile::tempdir()?; fs::write( directory.path().join("zzz.py"), @@ -3208,30 +4169,20 @@ mod tests { options.no_viz = true; let result = build_local_graph(&options)?; - let graph = GraphDocument::load(&result.output_dir.join("graph.json"))?; - let code_positions = graph + let graph = V1GraphDocument::load(&result.output_dir.join("graph.json"))?; + let code_nodes = graph .nodes .iter() - .enumerate() - .filter_map(|(position, node)| { - (node.string("source_file") == "zzz.py").then_some(position) - }) + .filter(|node| node.source_file() == Some("zzz.py")) .collect::>(); - let document_positions = graph + let document_nodes = graph .nodes .iter() - .enumerate() - .filter_map(|(position, node)| { - (node.string("source_file") == "aaa.md").then_some(position) - }) + .filter(|node| node.source_file() == Some("aaa.md")) .collect::>(); - assert!(!code_positions.is_empty()); - assert!(!document_positions.is_empty()); - assert!( - code_positions.iter().max() < document_positions.iter().min(), - "Python compatibility requires every code extraction to precede deterministic document extraction" - ); + assert!(!code_nodes.is_empty()); + assert!(!document_nodes.is_empty()); Ok(()) } @@ -3251,7 +4202,7 @@ mod tests { options.no_viz = true; let result = build_local_graph(&options)?; - let graph = GraphDocument::load(&result.output_dir.join("graph.json"))?; + let graph = V1GraphDocument::load(&result.output_dir.join("graph.json"))?; assert!( graph .nodes @@ -3262,7 +4213,7 @@ mod tests { graph .nodes .iter() - .all(|node| node.string("source_file") != "vendor-treadmill") + .all(|node| node.source_file() != Some("vendor-treadmill")) ); Ok(()) } @@ -3282,12 +4233,12 @@ mod tests { let graph_path = cold.output_dir.join("graph.json"); let graph_bytes = fs::read(&graph_path)?; let manifest_bytes = fs::read(cold.output_dir.join("manifest.json"))?; - fs::remove_dir_all(cold.output_dir.join("cache"))?; + fs::remove_dir_all(directory.path().join("compass-out").join("cache"))?; let warm = build_local_graph(&options)?; assert_eq!(warm.files_extracted, 0); assert_eq!(warm.files_cached, 1); - assert_eq!(fs::read(graph_path)?, graph_bytes); + assert_eq!(fs::read(warm.output_dir.join("graph.json"))?, graph_bytes); assert_eq!( fs::read(warm.output_dir.join("manifest.json"))?, manifest_bytes @@ -3356,8 +4307,8 @@ mod tests { }; let first = build_graph_with_semantic(&options, &first_layer)?; let graph_path = first.output_dir.join("graph.json"); - let graph = GraphDocument::load(&graph_path)?; - assert!(graph.nodes.iter().any(|node| node.id == "old_concept")); + let graph = V1GraphDocument::load(&graph_path)?; + assert!(graph.nodes.iter().any(|node| node.label() == "Old concept")); let manifest: Value = serde_json::from_slice(&fs::read(first.output_dir.join("manifest.json"))?)?; assert!( @@ -3392,15 +4343,19 @@ mod tests { partial_files: Vec::new(), allow_partial: false, }; - build_graph_with_semantic(&options, &second_layer)?; - let graph = GraphDocument::load(&graph_path)?; - assert!(!graph.nodes.iter().any(|node| node.id == "old_concept")); - assert!(graph.nodes.iter().any(|node| node.id == "new_concept")); - let Some(semantic) = graph.nodes.iter().find(|node| node.id == "new_concept") else { + let second = build_graph_with_semantic(&options, &second_layer)?; + let graph = V1GraphDocument::load(&second.output_dir.join("graph.json"))?; + assert!(!graph.nodes.iter().any(|node| node.label() == "Old concept")); + assert!(graph.nodes.iter().any(|node| node.label() == "New concept")); + let Some(semantic) = graph + .nodes + .iter() + .find(|node| node.label() == "New concept") + else { return Err("new semantic node was not written".into()); }; - assert_eq!(semantic.string("source_file"), "diagram.png"); - assert_eq!(semantic.string("_origin"), "semantic"); + assert_eq!(semantic.source_file(), Some("diagram.png")); + assert!(node_has_origin(semantic, EvidenceOrigin::Heuristic)); Ok(()) } @@ -3418,8 +4373,8 @@ mod tests { let complete = SemanticLayer { fragment: json!({ "nodes": [ - {"id":"concept_a", "source_file":"diagram.png"}, - {"id":"concept_b", "source_file":"diagram.png"} + {"id":"concept_a", "label":"Concept A", "file_type":"concept", "source_file":"diagram.png"}, + {"id":"concept_b", "label":"Concept B", "file_type":"concept", "source_file":"diagram.png"} ], "edges": [], "hyperedges": [], @@ -3436,7 +4391,7 @@ mod tests { let original = fs::read(&graph_path)?; let mut incomplete = SemanticLayer { fragment: json!({ - "nodes": [{"id":"concept_a", "source_file":"diagram.png"}], + "nodes": [{"id":"concept_a", "label":"Concept A", "file_type":"concept", "source_file":"diagram.png"}], "edges": [], "hyperedges": [], "input_tokens": 2, @@ -3455,15 +4410,12 @@ mod tests { assert_eq!(fs::read(&graph_path)?, original); incomplete.allow_partial = true; - build_graph_with_semantic(&options, &incomplete)?; - let graph = GraphDocument::load(&graph_path)?; - assert!(graph.nodes.iter().any(|node| node.id == "concept_a")); - assert!(!graph.nodes.iter().any(|node| node.id == "concept_b")); - let raw: Value = serde_json::from_slice(&fs::read(&graph_path)?)?; - assert_eq!(raw["input_tokens"], 2); - assert_eq!(raw["output_tokens"], 1); + let partial = build_graph_with_semantic(&options, &incomplete)?; + let graph = V1GraphDocument::load(&partial.output_dir.join("graph.json"))?; + assert!(graph.nodes.iter().any(|node| node.label() == "Concept A")); + assert!(!graph.nodes.iter().any(|node| node.label() == "Concept B")); let manifest: Value = - serde_json::from_slice(&fs::read(first.output_dir.join("manifest.json"))?)?; + serde_json::from_slice(&fs::read(partial.output_dir.join("manifest.json"))?)?; assert_eq!(manifest["diagram.png"]["semantic_hash"], ""); Ok(()) } @@ -3481,8 +4433,8 @@ mod tests { let complete = SemanticLayer { fragment: json!({ "nodes": [ - {"id":"concept_a", "source_file":"diagram.png"}, - {"id":"concept_b", "source_file":"diagram.png"} + {"id":"concept_a", "label":"Concept A", "file_type":"concept", "source_file":"diagram.png"}, + {"id":"concept_b", "label":"Concept B", "file_type":"concept", "source_file":"diagram.png"} ], "edges": [], "hyperedges": [], @@ -3492,11 +4444,11 @@ mod tests { partial_files: Vec::new(), allow_partial: false, }; - let first = build_graph_with_semantic(&options, &complete)?; + build_graph_with_semantic(&options, &complete)?; let smaller = SemanticLayer { fragment: json!({ - "nodes": [{"id":"concept_a", "source_file":"diagram.png"}], + "nodes": [{"id":"concept_a", "label":"Concept A", "file_type":"concept", "source_file":"diagram.png"}], "edges": [], "hyperedges": [], "failed_chunks": 0, @@ -3505,11 +4457,10 @@ mod tests { partial_files: Vec::new(), allow_partial: false, }; - build_graph_with_semantic(&options, &smaller)?; - let graph_path = first.output_dir.join("graph.json"); - let graph = GraphDocument::load(&graph_path)?; - assert!(graph.nodes.iter().any(|node| node.id == "concept_a")); - assert!(!graph.nodes.iter().any(|node| node.id == "concept_b")); + let smaller = build_graph_with_semantic(&options, &smaller)?; + let graph = V1GraphDocument::load(&smaller.output_dir.join("graph.json"))?; + assert!(graph.nodes.iter().any(|node| node.label() == "Concept A")); + assert!(!graph.nodes.iter().any(|node| node.label() == "Concept B")); fs::remove_file(&image)?; let empty = SemanticLayer { @@ -3523,11 +4474,11 @@ mod tests { partial_files: Vec::new(), allow_partial: false, }; - build_graph_with_semantic(&options, &empty)?; - let graph = GraphDocument::load(&graph_path)?; - assert!(!graph.nodes.iter().any(|node| node.id == "concept_a")); + let empty = build_graph_with_semantic(&options, &empty)?; + let graph = V1GraphDocument::load(&empty.output_dir.join("graph.json"))?; + assert!(!graph.nodes.iter().any(|node| node.label() == "Concept A")); let manifest: Value = - serde_json::from_slice(&fs::read(first.output_dir.join("manifest.json"))?)?; + serde_json::from_slice(&fs::read(empty.output_dir.join("manifest.json"))?)?; assert!(manifest.get("diagram.png").is_none()); Ok(()) } diff --git a/crates/compass-core/src/program.rs b/crates/compass-core/src/program.rs index f1282d04..1ab81458 100644 --- a/crates/compass-core/src/program.rs +++ b/crates/compass-core/src/program.rs @@ -99,7 +99,7 @@ pub(crate) fn build_program( }); inputs } else { - read_sources(root, sources)? + read_sources(root, sources, options.max_source_bytes)? }; let source_digests = inputs .iter() @@ -324,7 +324,7 @@ pub(crate) fn load_current_program( return Ok(None); } - let inputs = read_sources(root, sources)?; + let inputs = read_sources(root, sources, options.max_source_bytes)?; let source_digests = inputs .iter() .map(|input| (input.source_file.clone(), input.digest.clone())) @@ -399,7 +399,7 @@ pub(crate) fn current_provider_manifest( sources: &[PathBuf], options: &BuildOptions, ) -> Result, CoreError> { - let inputs = read_sources(root, sources)?; + let inputs = read_sources(root, sources, options.max_source_bytes)?; let source_digests = inputs .iter() .map(|input| (input.source_file.clone(), input.digest.clone())) @@ -448,9 +448,20 @@ pub(crate) fn write_program(output_dir: &Path, canonical_bytes: &[u8]) -> Result Ok(()) } -fn read_sources(root: &Path, sources: &[PathBuf]) -> Result, CoreError> { +fn read_sources( + root: &Path, + sources: &[PathBuf], + max_source_bytes: u64, +) -> Result, CoreError> { let mut inputs = Vec::with_capacity(sources.len()); for path in sources { + let metadata = fs::metadata(path).map_err(|source| FileError::Io { + path: path.clone(), + source, + })?; + if metadata.len() > max_source_bytes { + continue; + } let canonical = fs::canonicalize(path).map_err(|source| FileError::Io { path: path.clone(), source, diff --git a/crates/compass-core/src/watch.rs b/crates/compass-core/src/watch.rs index c45fd6b4..0cc16fa6 100644 --- a/crates/compass-core/src/watch.rs +++ b/crates/compass-core/src/watch.rs @@ -922,7 +922,9 @@ mod tests { assert!(complete, "watch statuses: {:?}", statuses.lock()); assert!(root.join("compass-out/needs_update").is_file()); - let graph = compass_model::GraphDocument::load(&root.join("compass-out/graph.json"))?; + let graph_path = + compass_files::BuildGuard::resolve_artifact(&root.join("compass-out"), "graph.json")?; + let graph = compass_model::code_graph::GraphDocument::load(&graph_path)?; assert!( graph .nodes @@ -936,16 +938,22 @@ mod tests { .collect::>() ); let statuses = statuses.lock().map_err(|_| "status mutex poisoned")?; - assert!(statuses.iter().any(|status| { + let saw_deterministic = statuses.iter().any(|status| { matches!( status, - WatchStatus::Batch { - deterministic: 1, - semantic: 1, - .. - } + WatchStatus::Batch { deterministic, .. } if *deterministic > 0 ) - })); + }); + let saw_semantic = statuses.iter().any(|status| { + matches!( + status, + WatchStatus::Batch { semantic, .. } if *semantic > 0 + ) + }); + assert!( + saw_deterministic && saw_semantic, + "watch statuses: {statuses:?}" + ); Ok(()) } @@ -1048,7 +1056,9 @@ mod tests { stop.store(true, Ordering::Release); handle.join().map_err(|_| "watch thread panicked")??; - let graph = compass_model::GraphDocument::load(&root.join("compass-out/graph.json"))?; + let graph_path = + compass_files::BuildGuard::resolve_artifact(&root.join("compass-out"), "graph.json")?; + let graph = compass_model::code_graph::GraphDocument::load(&graph_path)?; assert!( graph .nodes @@ -1106,7 +1116,10 @@ mod tests { stop.store(true, Ordering::Release); handle.join().map_err(|_| "watch thread panicked")??; - assert!(root.join("compass-out/graph.json").is_file()); + assert!( + compass_files::BuildGuard::resolve_artifact(&root.join("compass-out"), "graph.json")? + .is_file() + ); let statuses = statuses.lock().map_err(|_| "status mutex poisoned")?; assert!(statuses.iter().any(|status| matches!( status, diff --git a/crates/compass-core/tests/code_graph_v1_determinism.rs b/crates/compass-core/tests/code_graph_v1_determinism.rs new file mode 100644 index 00000000..7e1e8ade --- /dev/null +++ b/crates/compass-core/tests/code_graph_v1_determinism.rs @@ -0,0 +1,534 @@ +use std::collections::BTreeSet; +use std::error::Error; +use std::fs; +use std::path::Path; + +use compass_core::{BuildOptions, BuildPurpose, build_local_graph}; +use compass_model::code_graph::{ + EdgeKind, GraphDocument, NodeDetails, NodeKind, NodeRole, RouteStage, +}; +use compass_model::provenance::{EvidenceConfidence, EvidenceOrigin, ResolutionState}; + +const SOURCE: &str = r#" +pub struct Store; + +impl Store { + pub fn load(&self) -> usize { 1 } +} + +pub fn handler(store: &Store) -> usize { + store.load() +} +"#; + +fn build(root: &Path) -> Result<(Vec, bool), Box> { + let mut options = BuildOptions::new(root); + options.no_cluster = true; + options.no_viz = true; + options.max_workers = Some(2); + options.built_at_commit = Some("0123456789012345678901234567890123456789".to_owned()); + let result = build_local_graph(&options)?; + let path = result.output_dir.join("graph.json"); + let bytes = fs::read(&path)?; + GraphDocument::load(&path)?; + Ok((bytes, result.outputs_changed)) +} + +#[test] +fn clean_warm_restored_and_checkout_root_builds_are_byte_identical() -> Result<(), Box> { + let first = tempfile::tempdir()?; + let second = tempfile::tempdir()?; + for root in [first.path(), second.path()] { + fs::create_dir_all(root.join("src"))?; + fs::write(root.join("src/lib.rs"), SOURCE)?; + } + + let (cold, cold_changed) = build(first.path())?; + assert!(cold_changed); + let (warm, warm_changed) = build(first.path())?; + assert!(!warm_changed); + assert_eq!(warm, cold); + + let (other_root, _) = build(second.path())?; + assert_eq!(other_root, cold); + + fs::write( + first.path().join("src/lib.rs"), + format!("{SOURCE}\npub fn changed() {{}}\n"), + )?; + let (changed, changed_output) = build(first.path())?; + assert!(changed_output); + assert_ne!(changed, cold); + + fs::write(first.path().join("src/lib.rs"), SOURCE)?; + let (restored, restored_output) = build(first.path())?; + assert!(restored_output); + assert_eq!(restored, cold); + Ok(()) +} + +#[test] +fn edit_restore_does_not_preserve_recomputable_heuristic_facts() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + let root = directory.path(); + fs::create_dir_all(root.join("src"))?; + fs::write(root.join("src/lib.rs"), SOURCE)?; + fs::write( + root.join("MissingReference.csproj"), + r#" + + + + +"#, + )?; + + let (clean, _) = build(root)?; + fs::write( + root.join("src/lib.rs"), + format!("{SOURCE}\npub fn temporary_edit() {{}}\n"), + )?; + let (edited, _) = build(root)?; + assert_ne!(edited, clean); + + fs::write(root.join("src/lib.rs"), SOURCE)?; + let (restored, _) = build(root)?; + let clean_graph: GraphDocument = serde_json::from_slice(&clean)?; + let restored_graph: GraphDocument = serde_json::from_slice(&restored)?; + assert_eq!(restored_graph.nodes.len(), clean_graph.nodes.len()); + assert_eq!(restored_graph.links.len(), clean_graph.links.len()); + assert_eq!( + restored_graph.graph.coverage.len(), + clean_graph.graph.coverage.len() + ); + assert_eq!( + restored_graph.graph.diagnostics.len(), + clean_graph.graph.diagnostics.len() + ); + assert_eq!(restored_graph.graph.build, clean_graph.graph.build); + assert_eq!(restored_graph.graph.files, clean_graph.graph.files); + assert_eq!(restored_graph.graph.coverage, clean_graph.graph.coverage); + assert_eq!( + restored_graph.graph.diagnostics, + clean_graph.graph.diagnostics + ); + assert_eq!(restored_graph.nodes, clean_graph.nodes); + assert_eq!(restored_graph.links, clean_graph.links); + assert!( + restored == clean, + "recomputable resolver and graph heuristics must not feed back from the prior graph" + ); + Ok(()) +} + +#[test] +fn production_pipeline_preserves_framework_domain_kinds_and_route_targets() +-> Result<(), Box> { + let directory = tempfile::tempdir()?; + let root = directory.path(); + for (relative, source) in [ + ( + "src/orders.ts", + r#"import { Controller } from '@nestjs/common'; +import { EventPattern, MessagePattern } from '@nestjs/microservices'; +@Controller() +export class OrdersConsumer { + @MessagePattern('orders.created') + handleCreated() {} + @EventPattern('orders.cancelled') + handleCancelled() {} +} +"#, + ), + ( + "src/OrderEvents.java", + r#"import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +class OrderEvents { + @KafkaListener(topics = "orders.created") + public void consume(String event) {} + @RabbitListener(queues = "orders.queue") + public void consumeQueue(String event) {} +} +"#, + ), + ( + "src/jobs.py", + r#"from celery import shared_task +@shared_task +def refresh_inventory(): + pass +"#, + ), + ( + "src/admin/routes.tsx", + r#"import { createBrowserRouter } from "react-router-dom"; +import Screen from "./AdminPage"; +export const router = createBrowserRouter([{ path: "/admin", Component: Screen }]); +"#, + ), + ( + "src/admin/AdminPage.tsx", + "export default function AdminPage() { return null; }\n", + ), + ( + "src/public/routes.tsx", + r#"import { createBrowserRouter } from "react-router-dom"; +import Screen from "./PublicPage"; +export const router = createBrowserRouter([{ path: "/public", Component: Screen }]); +"#, + ), + ( + "src/public/PublicPage.tsx", + "export default function PublicPage() { return null; }\n", + ), + ( + "nuxt/middleware/auth.ts", + "export default defineNuxtRouteMiddleware(() => {});\n", + ), + ( + "src/server.ts", + r#"import express from "express"; +const app = express(); +app.get("/staged", authenticate, missingMiddleware, show); +app.get("/conflict", firstHandler); +app.get("/conflict", secondHandler); +function authenticate() {} +function show() {} +function firstHandler() {} +function secondHandler() {} +"#, + ), + ] { + let path = root.join(relative); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, source)?; + } + + let mut options = BuildOptions::new(root); + options.no_cluster = true; + options.no_viz = true; + options.max_workers = Some(2); + options.built_at_commit = Some("0123456789012345678901234567890123456789".to_owned()); + let result = build_local_graph(&options)?; + let graph = GraphDocument::load(&result.output_dir.join("graph.json"))?; + + for kind in [ + NodeKind::Event, + NodeKind::Message, + NodeKind::Topic, + NodeKind::Queue, + NodeKind::Job, + ] { + assert!( + graph.nodes.iter().any(|node| node.kind == kind), + "missing {kind:?}" + ); + } + for (route_path, target_source) in [ + ("/admin", "src/admin/AdminPage.tsx"), + ("/public", "src/public/PublicPage.tsx"), + ] { + let route = graph + .nodes + .iter() + .find(|node| { + node.kind == NodeKind::Route + && node.details.as_ref().is_some_and(|details| { + matches!( + details, + compass_model::code_graph::NodeDetails::Route(details) + if details.path == route_path + ) + }) + }) + .ok_or_else(|| format!("missing route {route_path}"))?; + let target = graph + .links + .iter() + .find(|edge| edge.kind == EdgeKind::RoutesTo && edge.source == route.id) + .and_then(|edge| graph.nodes.iter().find(|node| node.id == edge.target)) + .ok_or_else(|| format!("missing route target for {route_path}"))?; + assert_eq!( + target.source.as_ref().map(|source| source.file.as_str()), + Some(target_source) + ); + } + assert!(graph.nodes.iter().any(|node| { + node.roles.contains(&NodeRole::Middleware) + && node + .source + .as_ref() + .is_some_and(|source| source.file == "nuxt/middleware/auth.ts") + })); + let staged_route = graph + .nodes + .iter() + .find(|node| { + matches!( + node.details.as_ref(), + Some(NodeDetails::Route(details)) if details.path == "/staged" + ) + }) + .ok_or("missing staged route")?; + let Some(NodeDetails::Route(staged)) = staged_route.details.as_ref() else { + return Err("missing staged route details".into()); + }; + assert_eq!( + staged + .stages + .iter() + .map(|stage| ( + stage.stage, + stage.position, + stage.reference.as_str(), + stage.resolution, + stage.target.is_some(), + stage.candidates.len(), + )) + .collect::>(), + vec![ + ( + RouteStage::Middleware, + 0, + "authenticate", + ResolutionState::Exact, + true, + 1, + ), + ( + RouteStage::Middleware, + 1, + "missingMiddleware", + ResolutionState::Unresolved, + false, + 0, + ), + ( + RouteStage::Handler, + 2, + "show", + ResolutionState::Exact, + true, + 1, + ), + ] + ); + assert_eq!( + graph + .links + .iter() + .filter(|edge| { edge.kind == EdgeKind::RoutesTo && edge.source == staged_route.id }) + .count(), + 2 + ); + let conflict_routes = graph + .nodes + .iter() + .filter(|node| { + matches!( + node.details.as_ref(), + Some(NodeDetails::Route(details)) if details.path == "/conflict" + ) + }) + .collect::>(); + assert_eq!(conflict_routes.len(), 2); + let mut conflict_references = BTreeSet::new(); + for route in conflict_routes { + let anchor = route.source.as_ref().ok_or("missing conflict anchor")?; + let Some(NodeDetails::Route(details)) = route.details.as_ref() else { + return Err("missing conflict route details".into()); + }; + let [stage] = details.stages.as_slice() else { + return Err("conflict route must have one handler stage".into()); + }; + assert_eq!(stage.stage, RouteStage::Handler); + assert_eq!(stage.position, 0); + assert_eq!(stage.resolution, ResolutionState::Exact); + assert_eq!(stage.candidates.len(), 1); + let target = stage + .target + .as_deref() + .ok_or("missing conflict stage target")?; + assert_eq!(stage.candidates[0].node_id, target); + conflict_references.insert(stage.reference.as_str()); + + let edges = graph + .links + .iter() + .filter(|edge| edge.kind == EdgeKind::RoutesTo && edge.source == route.id) + .collect::>(); + let [edge] = edges.as_slice() else { + return Err("conflict route must have one authoritative edge".into()); + }; + assert_eq!(edge.target, target); + assert_eq!(edge.relationship_site.as_ref(), Some(anchor)); + assert!( + edge.evidence + .iter() + .all(|evidence| evidence.confidence == EvidenceConfidence::Exact) + ); + let target_node = graph + .nodes + .iter() + .find(|node| node.id == target) + .ok_or("missing conflict target node")?; + assert_eq!( + target_node.name.trim_end_matches("()"), + stage.reference.as_str() + ); + } + assert_eq!( + conflict_references, + BTreeSet::from(["firstHandler", "secondHandler"]) + ); + Ok(()) +} + +#[test] +fn force_update_is_a_clean_byte_identical_rebuild() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + let root = directory.path(); + for (relative, source) in [ + ( + "project/urls.py", + r#"from django.urls import path +from . import views +urlpatterns = [path("health/", views.health, name="health")] +"#, + ), + ( + "project/views.py", + "def health(request):\n return \"ok\"\n", + ), + ("guide.md", "# Service guide\n\n[Routes](project/urls.py)\n"), + ] { + let path = root.join(relative); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, source)?; + } + + let mut options = BuildOptions::new(root); + options.no_cluster = true; + options.no_viz = true; + options.max_workers = Some(2); + options.built_at_commit = Some("0123456789012345678901234567890123456789".to_owned()); + + let clean = build_local_graph(&options)?; + let clean_path = clean.output_dir.join("graph.json"); + let clean_bytes = fs::read(&clean_path)?; + let clean_graph = GraphDocument::load(&clean_path)?; + assert!(clean_graph.nodes.iter().any(|node| { + node.kind == NodeKind::Resource + && node.source_file() == Some("guide.md") + && node + .evidence + .iter() + .any(|evidence| evidence.origin == EvidenceOrigin::Artifact) + })); + assert!( + clean_graph + .nodes + .iter() + .any(|node| node.kind == NodeKind::Route) + ); + assert!(clean_graph.links.iter().any(|edge| { + edge.evidence + .iter() + .any(|evidence| evidence.origin == EvidenceOrigin::Convention) + })); + + options.force = true; + let forced = build_local_graph(&options)?; + let forced_path = forced.output_dir.join("graph.json"); + let forced_bytes = fs::read(&forced_path)?; + let forced_graph = GraphDocument::load(&forced_path)?; + + assert_eq!(forced_bytes, clean_bytes); + assert_eq!(forced_graph.graph.files, clean_graph.graph.files); + assert_eq!(forced_graph.nodes, clean_graph.nodes); + assert_eq!(forced_graph.links, clean_graph.links); + assert!(forced_graph.links.iter().all(|edge| { + edge.evidence + .iter() + .all(|evidence| evidence.rule.as_deref() != Some("incremental-ast-endpoint-remap")) + })); + Ok(()) +} + +#[test] +fn force_extract_with_cache_reuse_has_no_prior_published_semantic_input() +-> Result<(), Box> { + let directory = tempfile::tempdir()?; + let root = directory.path(); + fs::create_dir_all(root.join("src"))?; + fs::write(root.join("src/lib.rs"), "pub fn answer() -> usize { 42 }\n")?; + fs::write( + root.join("README.md"), + "# Example\n\nThe implementation is in [Rust](src/lib.rs).\n", + )?; + + let mut options = BuildOptions::new(root); + options.purpose = BuildPurpose::Extract; + options.no_cluster = true; + options.no_viz = true; + options.program_analysis = true; + options.max_workers = Some(2); + options.built_at_commit = Some("0123456789012345678901234567890123456789".to_owned()); + + let clean = build_local_graph(&options)?; + let clean_path = clean.output_dir.join("graph.json"); + let clean_bytes = fs::read(&clean_path)?; + let clean_graph = GraphDocument::load(&clean_path)?; + let clean_document_coverage = clean_graph + .graph + .coverage + .iter() + .filter(|coverage| coverage.capability == "node:document") + .cloned() + .collect::>(); + assert!(!clean_document_coverage.is_empty()); + assert!(clean_graph.nodes.iter().any(|node| { + node.kind == NodeKind::Resource + && node.source_file() == Some("README.md") + && node + .evidence + .iter() + .any(|evidence| evidence.origin == EvidenceOrigin::Artifact) + })); + + options.force = true; + options.reuse_cache_on_force = true; + let forced = build_local_graph(&options)?; + let forced_path = forced.output_dir.join("graph.json"); + let forced_bytes = fs::read(&forced_path)?; + let forced_graph = GraphDocument::load(&forced_path)?; + let forced_document_coverage = forced_graph + .graph + .coverage + .iter() + .filter(|coverage| coverage.capability == "node:document") + .cloned() + .collect::>(); + + assert_eq!(forced.files_considered, clean.files_considered); + assert_eq!(forced.files_extracted, 0); + assert_eq!(forced.files_cached, clean.files_considered); + assert_eq!(forced.program_syntax_analyzed, 0); + assert!(forced.program_syntax_reused > 0); + assert_eq!(forced_bytes, clean_bytes); + assert_eq!(forced_graph.graph.files, clean_graph.graph.files); + assert_eq!(forced_graph.nodes, clean_graph.nodes); + assert_eq!(forced_graph.links, clean_graph.links); + assert_eq!(forced_document_coverage, clean_document_coverage); + assert!(forced_graph.links.iter().all(|edge| { + edge.evidence + .iter() + .all(|evidence| evidence.rule.as_deref() != Some("incremental-ast-endpoint-remap")) + })); + Ok(()) +} diff --git a/crates/compass-core/tests/code_graph_v1_publication_resilience.rs b/crates/compass-core/tests/code_graph_v1_publication_resilience.rs new file mode 100644 index 00000000..3d52eef6 --- /dev/null +++ b/crates/compass-core/tests/code_graph_v1_publication_resilience.rs @@ -0,0 +1,520 @@ +use std::collections::BTreeSet; +use std::error::Error; +use std::fs; +use std::path::Path; + +use compass_core::{BuildOptions, build_local_graph}; +use compass_files::{Cache, CacheOptions}; +use compass_languages::{Extraction, Registry}; +use compass_model::code_graph::{CoverageStatus, ExtractionStatus, GraphDocument}; +use compass_model::provenance::EvidenceOrigin; +use sha2::{Digest, Sha256}; + +fn build(root: &Path) -> Result> { + let mut options = BuildOptions::new(root); + options.no_cluster = true; + options.no_viz = true; + options.max_workers = Some(2); + options.built_at_commit = Some("0123456789012345678901234567890123456789".to_owned()); + let result = build_local_graph(&options)?; + Ok(GraphDocument::load(&result.output_dir.join("graph.json"))?) +} + +fn write(root: &Path, relative: &str, source: &str) -> Result<(), Box> { + let path = root.join(relative); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, source)?; + Ok(()) +} + +#[test] +fn zero_byte_registered_source_is_truthful_inventory() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + write(directory.path(), "src/healthy.rs", "pub fn healthy() {}\n")?; + write(directory.path(), "src/empty.rs", "")?; + write(directory.path(), "config/Empty.csproj", "")?; + write(directory.path(), "ui/Empty.xaml", "")?; + write(directory.path(), "config/empty.json", "")?; + + let graph = build(directory.path())?; + let empty = graph + .graph + .files + .iter() + .find(|file| file.path == "src/empty.rs") + .ok_or("missing empty-file inventory")?; + assert_eq!(empty.language.as_deref(), Some("rust")); + assert_eq!(empty.byte_size, 0); + assert_eq!(empty.extraction_status, ExtractionStatus::Extracted); + assert!(graph.graph.coverage.iter().any(|coverage| { + coverage.file_id.as_deref() == Some(empty.id.as_str()) + && coverage.capability == "file_inventory" + && coverage.producer == "compass.languages.rust" + && coverage.status == CoverageStatus::Complete + })); + assert!( + graph.nodes.iter().all(|node| { + node.source + .as_ref() + .is_none_or(|anchor| anchor.file != "src/empty.rs") + }), + "zero-byte input invented a non-empty AST anchor" + ); + for (path, language) in [ + ("config/Empty.csproj", "project-xml"), + ("ui/Empty.xaml", "xaml"), + ("config/empty.json", "json"), + ] { + let failed = graph + .graph + .files + .iter() + .find(|file| file.path == path) + .ok_or_else(|| format!("missing empty-file inventory for {path}"))?; + assert_eq!(failed.language.as_deref(), Some(language)); + assert_eq!(failed.byte_size, 0); + assert_eq!(failed.extraction_status, ExtractionStatus::ParseFailure); + assert!(graph.graph.coverage.iter().any(|coverage| { + coverage.file_id.as_deref() == Some(failed.id.as_str()) + && coverage.status == CoverageStatus::Failed + })); + assert!(graph.graph.diagnostics.iter().any(|diagnostic| { + diagnostic.code == "extractor_failure" && diagnostic.message.contains(path) + })); + assert!(graph.nodes.iter().all(|node| { + node.source + .as_ref() + .is_none_or(|anchor| anchor.file != path) + })); + assert!(graph.links.iter().all(|edge| { + edge.relationship_site + .as_ref() + .is_none_or(|anchor| anchor.file != path) + })); + } + Ok(()) +} + +#[test] +fn missing_dotnet_references_are_external_and_do_not_abort() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + write(directory.path(), "src/healthy.rs", "pub fn healthy() {}\n")?; + write( + directory.path(), + "src/App/App.csproj", + r#" + + + + + +"#, + )?; + write( + directory.path(), + "src/Lib/Lib.csproj", + r#""#, + )?; + + let graph = build(directory.path())?; + let paths = graph + .graph + .files + .iter() + .map(|file| file.path.as_str()) + .collect::>(); + assert!(paths.contains(&"src/App/App.csproj")); + assert!(paths.contains(&"src/Lib/Lib.csproj")); + assert!(!paths.contains(&"src/Missing/Missing.csproj")); + assert!(graph.graph.diagnostics.iter().any(|diagnostic| { + diagnostic.code == "unresolved_external_reference" + && diagnostic.message.contains("src/Missing/Missing.csproj") + && diagnostic + .anchor + .as_ref() + .is_some_and(|anchor| anchor.file == "src/App/App.csproj") + && !diagnostic + .message + .contains(directory.path().to_string_lossy().as_ref()) + })); + let missing = graph + .nodes + .iter() + .find(|node| node.qualified_name == "Missing.csproj") + .ok_or("missing unresolved external identity")?; + assert!(missing.source.is_none()); + assert!(missing.evidence.iter().any(|evidence| { + evidence.origin == EvidenceOrigin::Heuristic + && evidence.rule.as_deref() == Some("external-symbol-placeholder") + })); + Ok(()) +} + +#[test] +fn parser_recovery_is_partial_deterministic_and_publishes_no_exact_edges() +-> Result<(), Box> { + let directory = tempfile::tempdir()?; + write(directory.path(), "src/healthy.rs", "pub fn healthy() {}\n")?; + write( + directory.path(), + "src/recovered.rs", + "pub fn recovered( { healthy();\n", + )?; + + let first = build(directory.path())?; + let second = build(directory.path())?; + let recovered = first + .graph + .files + .iter() + .find(|file| file.path == "src/recovered.rs") + .ok_or("missing recovered input")?; + assert_eq!(recovered.language.as_deref(), Some("rust")); + assert_eq!(recovered.extraction_status, ExtractionStatus::Partial); + assert!(first.graph.coverage.iter().any(|coverage| { + coverage.file_id.as_deref() == Some(recovered.id.as_str()) + && coverage.status == CoverageStatus::Partial + && coverage.producer == "compass.languages.rust" + })); + assert!(first.graph.diagnostics.iter().any(|diagnostic| { + diagnostic.code == "parser_recovery" && diagnostic.message.contains("src/recovered.rs") + })); + assert!(first.links.iter().all(|edge| { + edge.relationship_site + .as_ref() + .is_none_or(|anchor| anchor.file != "src/recovered.rs") + })); + assert_eq!(first.graph.diagnostics, second.graph.diagnostics); + assert_eq!(first.graph.coverage, second.graph.coverage); + Ok(()) +} + +#[test] +fn legacy_markerless_ast_cache_is_reextracted_conservatively() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + let source = directory.path().join("src/recovered.rs"); + write( + directory.path(), + "src/recovered.rs", + "pub fn recovered( { missing();\n", + )?; + + let mut cache = Cache::open(directory.path(), CacheOptions::output_directory(None))?; + cache.save_portable_ast_batch(&[(source, Extraction::default())])?; + drop(cache); + let ast_root = directory.path().join("compass-out/cache/ast"); + fs::rename(ast_root.join("v2"), ast_root.join("v1"))?; + + let graph = build(directory.path())?; + let recovered = graph + .graph + .files + .iter() + .find(|file| file.path == "src/recovered.rs") + .ok_or("missing recovered input")?; + assert_eq!(recovered.extraction_status, ExtractionStatus::Partial); + assert!(graph.graph.diagnostics.iter().any(|diagnostic| { + diagnostic.code == "parser_recovery" && diagnostic.message.contains("src/recovered.rs") + })); + assert!(!ast_root.join("v1").exists()); + Ok(()) +} + +#[test] +fn sealed_legacy_build_state_cannot_skip_current_publication() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + write(directory.path(), "src/main.rs", "pub fn current() {}\n")?; + let mut options = BuildOptions::new(directory.path()); + options.no_cluster = true; + options.no_viz = true; + options.built_at_commit = Some("0123456789012345678901234567890123456789".to_owned()); + let first = build_local_graph(&options)?; + let graph_path = first.output_dir.join("graph.json"); + let state_path = first.output_dir.join(".compass_build_state.json"); + + let mut graph: serde_json::Value = serde_json::from_slice(&fs::read(&graph_path)?)?; + let files = graph["graph"]["files"] + .as_array_mut() + .ok_or("graph files are not an array")?; + let main = files + .iter_mut() + .find(|file| file["path"] == "src/main.rs") + .ok_or("missing main inventory")?; + main["language"] = serde_json::Value::String("legacy-poison".to_owned()); + let graph_bytes = serde_json::to_vec_pretty(&graph)?; + fs::write(&graph_path, &graph_bytes)?; + + let mut state: serde_json::Value = serde_json::from_slice(&fs::read(&state_path)?)?; + state["producer"] = serde_json::Value::String("legacy-builder".to_owned()); + state["graph"]["bytes"] = serde_json::Value::from(graph_bytes.len()); + state["graph"]["sha256"] = + serde_json::Value::String(format!("{:x}", Sha256::digest(&graph_bytes))); + fs::write(&state_path, serde_json::to_vec_pretty(&state)?)?; + + let second = build_local_graph(&options)?; + let graph = GraphDocument::load(&second.output_dir.join("graph.json"))?; + let main = graph + .graph + .files + .iter() + .find(|file| file.path == "src/main.rs") + .ok_or("missing rebuilt main inventory")?; + assert_eq!(main.language.as_deref(), Some("rust")); + Ok(()) +} + +#[test] +fn failed_file_isolated_from_healthy_file_and_exact_relationships() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + write( + directory.path(), + "src/healthy.rs", + "pub fn target() {}\npub fn healthy() { target(); }\n", + )?; + write( + directory.path(), + "ui/Broken.xaml", + " + ) : ( + + {sourceLabel(item, source)} + + ) + )} + {item.candidates.length > 0 && ( +
+ Retained candidates +
    + {item.candidates.map((candidate) => ( +
  • + {candidate.nodeId} + {candidate.reason} + {candidate.confidence} +
  • + ))} +
+
+ )} + + + ); + })} + + + ); +} + +function EvidenceIcon({ item }: { item: CodeEvidenceRecord }) { + if (item.resolution === "unresolved") return ; + if (item.resolution === "ambiguous" || item.confidence === "ambiguous") { + return ; + } + if (item.origin === "heuristic") return ; + if (item.origin === "config") return ; + if (item.origin === "convention") return ; + return ; +} + +function evidenceStatus(item: CodeEvidenceRecord): { key: string; label: string } { + if (item.resolution === "unresolved") return { key: "unresolved", label: "Unresolved" }; + if (item.resolution === "ambiguous" || item.confidence === "ambiguous") { + return { key: "ambiguous", label: "Ambiguous" }; + } + if (item.origin === "heuristic") return { key: "heuristic", label: "Heuristic" }; + if (item.origin === "config") return { key: "config", label: "Configuration" }; + if (item.origin === "convention") return { key: "convention", label: "Convention" }; + return { key: "exact", label: "Exact" }; +} + +function sourceLabel(item: CodeEvidenceRecord, source: CodeSourceAnchor): string { + const prefix = item.origin === "heuristic" ? "Wired at" : "Evidence at"; + return `${prefix} ${source.file}:${source.startLine}`; +} + +function diagnosticLabel(code: CodeQueryDiagnostic["code"]): string { + return `${code.replaceAll("_", " ")}: `; +} diff --git a/packages/compass-viewer/src/graph/CompassGraph.tsx b/packages/compass-viewer/src/graph/CompassGraph.tsx index f975c337..d91c259a 100644 --- a/packages/compass-viewer/src/graph/CompassGraph.tsx +++ b/packages/compass-viewer/src/graph/CompassGraph.tsx @@ -7,6 +7,7 @@ import { type CSSProperties } from "react"; import type { GraphViewModel, SourceLocation } from "../contracts/graph"; +import type { CodeQueryResponse } from "../contracts/codeQuery"; import { GraphInspector } from "./GraphInspector"; import { GraphTransitionScreen } from "./GraphTransitionScreen"; import { GraphToolbar } from "./GraphToolbar"; @@ -38,6 +39,7 @@ const CHANGE_TYPES: Array<{ export type GraphHost = { openSource(source: SourceLocation, revision?: string): void; openCommunity?(communityId: number): void; + queryNode?(operation: "callers" | "callees" | "impact", symbol: string): void; }; export type CommunityGraphDetail = { @@ -58,6 +60,7 @@ export type CompassGraphProps = { communityError?: string | undefined; onBackToOverview?: (() => void) | undefined; sourceRevisions?: GraphSourceRevisions | undefined; + queryResult?: CodeQueryResponse | undefined; initialInspectorLayout?: Partial | undefined; onInspectorLayoutChange?: ((layout: InspectorLayout) => void) | undefined; }; @@ -70,6 +73,7 @@ export function CompassGraph({ communityError, onBackToOverview, sourceRevisions, + queryResult, initialInspectorLayout, onInspectorLayoutChange }: CompassGraphProps) { @@ -94,6 +98,7 @@ export function CompassGraph({ onBackToOverview={communityDetail ? onBackToOverview : undefined} bounded={communityDetail?.bounded} sourceRevisions={sourceRevisions} + queryResult={queryResult} inspectorLayout={inspectorLayout} onInspectorLayoutChange={updateInspectorLayout} /> @@ -109,6 +114,7 @@ function CompassGraphView({ onBackToOverview, bounded, sourceRevisions, + queryResult, inspectorLayout, onInspectorLayoutChange }: { @@ -120,6 +126,7 @@ function CompassGraphView({ onBackToOverview?: (() => void) | undefined; bounded?: CommunityGraphDetail["bounded"]; sourceRevisions?: GraphSourceRevisions | undefined; + queryResult?: CodeQueryResponse | undefined; inspectorLayout: InspectorLayout; onInspectorLayoutChange(layout: InspectorLayout): void; }) { @@ -333,10 +340,12 @@ function CompassGraphView({ hiddenCommunities={state.hiddenCommunities} comparisonMode={comparisonMode} sourceRevisions={sourceRevisions} + queryResult={queryResult} onQueryChange={(query) => dispatch({ type: "search", query })} onFocus={focus} onOpenSource={host.openSource} onOpenCommunity={detailCommunityId === undefined ? host.openCommunity : undefined} + onQueryNode={host.queryNode} onToggleCommunity={(communityId) => dispatch({ type: "toggleCommunity", communityId diff --git a/packages/compass-viewer/src/graph/GraphInspector.tsx b/packages/compass-viewer/src/graph/GraphInspector.tsx index f5d0c1f8..d1bf0780 100644 --- a/packages/compass-viewer/src/graph/GraphInspector.tsx +++ b/packages/compass-viewer/src/graph/GraphInspector.tsx @@ -12,8 +12,10 @@ import type { GraphViewModel, SourceLocation } from "../contracts/graph"; +import type { CodeQueryResponse } from "../contracts/codeQuery"; import { ChangeEvidence, type GraphSourceRevisions } from "./ChangeEvidence"; import { ChangedSymbolList } from "./ChangedSymbolList"; +import { CodeEvidence } from "./CodeEvidence"; import { navigableSource } from "./sourceNavigation"; function lineRange(node: GraphNode): string | undefined { @@ -74,10 +76,12 @@ export function GraphInspector({ hiddenCommunities, comparisonMode, sourceRevisions, + queryResult, onQueryChange, onFocus, onOpenSource, onOpenCommunity, + onQueryNode, onToggleCommunity, onSetAllVisible, collapsed, @@ -92,10 +96,15 @@ export function GraphInspector({ hiddenCommunities: ReadonlySet; comparisonMode: boolean; sourceRevisions?: GraphSourceRevisions | undefined; + queryResult?: CodeQueryResponse | undefined; onQueryChange(query: string): void; onFocus(nodeId: string): void; onOpenSource(source: SourceLocation, revision?: string): void; onOpenCommunity?: ((communityId: number) => void) | undefined; + onQueryNode?: (( + operation: "callers" | "callees" | "impact", + symbol: string + ) => void) | undefined; onToggleCommunity(communityId: number): void; onSetAllVisible(visible: boolean): void; collapsed: boolean; @@ -117,6 +126,17 @@ export function GraphInspector({ () => new Map(model.nodes.map((node) => [node.id, node])), [model.nodes] ); + const selectedQueryNode = selected + ? queryResult?.nodes.find((node) => node.id === selected.id) + : undefined; + const selectedCodeEvidence = selectedQueryNode?.evidence ?? selected?.codeEvidence ?? []; + const relationshipCodeEvidence = selected + ? (queryResult + ? queryResult.edges + .filter((edge) => edge.source === selected.id || edge.target === selected.id) + .flatMap((edge) => edge.evidence) + : connectedEdges.flatMap((edge) => edge.codeEvidence ?? [])) + : []; const choose = (node: GraphNode) => { onFocus(node.id); @@ -315,6 +335,31 @@ export function GraphInspector({ {selected.signature && ( {selected.signature} )} + {onQueryNode && ( +
+ + + +
+ )} + + {model.stats.aggregated && selected.memberCount !== undefined && onOpenCommunity && ( diff --git a/packages/compass-viewer/src/graph/edgeLabels.test.ts b/packages/compass-viewer/src/graph/edgeLabels.test.ts index 6c1a6f38..259bc408 100644 --- a/packages/compass-viewer/src/graph/edgeLabels.test.ts +++ b/packages/compass-viewer/src/graph/edgeLabels.test.ts @@ -22,6 +22,18 @@ describe("formatGraphEdgeLabel", () => { ] as const)("formats %o as %s", (edge, expected) => { expect(formatGraphEdgeLabel(edge)).toBe(expected); }); + + it("keeps route stages and enterprise relations explicit", () => { + expect(formatGraphEdgeLabel({ + relation: "routes_to", + details: { + type: "route", + data: { stage: "middleware", position: 1, operation: "GET" } + } + })).toBe("routes to · middleware 2 · GET"); + expect(formatGraphEdgeLabel({ relation: "publishes" })).toBe("publishes"); + expect(formatGraphEdgeLabel({ relation: "maps_to" })).toBe("maps to"); + }); }); describe("shouldShowGraphEdgeLabel", () => { diff --git a/packages/compass-viewer/src/graph/edgeLabels.ts b/packages/compass-viewer/src/graph/edgeLabels.ts index 3b869b0c..ecdce13f 100644 --- a/packages/compass-viewer/src/graph/edgeLabels.ts +++ b/packages/compass-viewer/src/graph/edgeLabels.ts @@ -1,16 +1,42 @@ import type { GraphEdge } from "../contracts/graph"; +const RELATION_LABELS: Readonly> = { + routes_to: "routes to", + publishes: "publishes", + subscribes: "subscribes", + produces: "produces", + consumes: "consumes", + schedules: "schedules", + triggers: "triggers", + handles: "handles", + registers: "registers", + reads: "reads", + writes: "writes", + maps_to: "maps to", + depends_on: "depends on" +}; + export type EdgeLabelVisibility = { hoveredEdgeId: string | null; }; export function formatGraphEdgeLabel( - edge: Pick + edge: Pick ): string { const relation = edge.relation.trim(); + const relationLabel = RELATION_LABELS[relation] ?? relation; + const route = edge.details?.type === "route" ? edge.details.data : undefined; + const routeStage = route + ? `${route.stage}${route.position === null || route.position === undefined + ? "" + : ` ${route.position + 1}`}` + : undefined; + const semanticLabel = [relationLabel, routeStage, route?.operation] + .filter((part) => part !== undefined && part !== "") + .join(" · "); const confidence = edge.confidence?.trim().toLocaleUpperCase(); - if (relation && confidence) return `${relation} [${confidence}]`; - if (relation) return relation; + if (semanticLabel && confidence) return `${semanticLabel} [${confidence}]`; + if (semanticLabel) return semanticLabel; return confidence ? `[${confidence}]` : ""; } diff --git a/packages/compass-viewer/src/index.ts b/packages/compass-viewer/src/index.ts index f60ddda2..c3711d26 100644 --- a/packages/compass-viewer/src/index.ts +++ b/packages/compass-viewer/src/index.ts @@ -2,9 +2,11 @@ export * from "./calls/CallGraph"; export * from "./calls/state"; export * from "./contracts/callGraph"; export * from "./contracts/callflow"; +export * from "./contracts/codeQuery"; export * from "./contracts/graph"; export * from "./contracts/history"; export * from "./graph/CompassGraph"; +export * from "./graph/CodeEvidence"; export * from "./graph/CompassBrandMark"; export * from "./graph/GraphTransitionScreen"; export * from "./graph/inspectorLayout"; diff --git a/packages/compass-viewer/src/theme.css b/packages/compass-viewer/src/theme.css index 883cd92d..88cf9d69 100644 --- a/packages/compass-viewer/src/theme.css +++ b/packages/compass-viewer/src/theme.css @@ -1298,6 +1298,228 @@ button:not(:disabled), margin-top: 16px; } +.compass-code-evidence { + margin-top: 16px; +} + +.compass-code-query-actions { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 5px; + margin-top: 10px; +} + +.compass-code-query-actions button { + padding: 5px 4px; + border: 1px solid var(--compass-line); + border-radius: 3px; + background: transparent; + color: var(--vscode-textLink-foreground, var(--compass-focus)); + font-size: 9px; +} + +.compass-code-query-actions button:focus-visible { + outline: 2px solid var(--vscode-focusBorder, var(--compass-focus)); + outline-offset: 1px; +} + +.compass-code-evidence-ledger { + display: grid; + gap: 6px; +} + +.compass-code-evidence-entry { + display: grid; + grid-template-columns: 24px minmax(0, 1fr); + gap: 8px; + padding: 8px; + border: 1px solid var(--compass-line); + border-left-width: 3px; + border-radius: 3px; + background: var(--vscode-editorWidget-background, var(--background)); +} + +.compass-code-evidence-entry[data-status="exact"], +.compass-code-evidence-entry[data-status="config"], +.compass-code-evidence-entry[data-status="convention"] { + border-left-color: var(--vscode-testing-iconPassed, #2ea043); +} + +.compass-code-evidence-entry[data-status="heuristic"] { + border-left-color: var(--vscode-charts-blue, #58a6ff); +} + +.compass-code-evidence-entry[data-status="ambiguous"], +.compass-code-evidence-entry[data-status="unresolved"] { + border-left-color: var(--vscode-editorWarning-foreground, #d29922); +} + +.compass-code-evidence-icon { + display: grid; + width: 24px; + height: 24px; + place-items: center; + border-radius: 50%; + background: var(--vscode-badge-background, var(--muted)); + color: var(--vscode-badge-foreground, var(--foreground)); +} + +.compass-code-evidence-icon svg { + width: 13px; + height: 13px; +} + +.compass-code-evidence-copy { + min-width: 0; +} + +.compass-code-evidence-status { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; +} + +.compass-code-evidence-status strong { + color: var(--foreground); + font-size: 10px; +} + +.compass-code-evidence-status span { + color: var(--compass-faint); + font-size: 8px; + text-transform: uppercase; +} + +.compass-code-evidence-copy dl { + display: grid; + gap: 3px; + margin: 7px 0 0; +} + +.compass-code-evidence-copy dl > div { + display: grid; + grid-template-columns: 62px minmax(0, 1fr); + gap: 6px; +} + +.compass-code-evidence-copy dt { + color: var(--compass-faint); + font-size: 8px; + text-transform: uppercase; +} + +.compass-code-evidence-copy dd { + min-width: 0; + margin: 0; + overflow-wrap: anywhere; + color: var(--foreground); + font: 9px/1.35 var(--compass-font-mono); +} + +.compass-code-evidence-source { + display: inline-flex; + align-items: center; + gap: 5px; + max-width: 100%; + margin-top: 7px; + padding: 0; + border: 0; + background: transparent; + color: var(--vscode-textLink-foreground, var(--compass-focus)); + font: 9px/1.35 var(--compass-font-mono); + overflow-wrap: anywhere; + text-align: left; +} + +button.compass-code-evidence-source { + cursor: pointer; +} + +button.compass-code-evidence-source:focus-visible { + border-radius: 2px; + outline: 2px solid var(--vscode-focusBorder, var(--compass-focus)); + outline-offset: 2px; +} + +.compass-code-evidence-source svg { + width: 11px; + min-width: 11px; + height: 11px; +} + +.compass-code-candidates { + margin-top: 8px; + padding-top: 7px; + border-top: 1px solid var(--compass-line); +} + +.compass-code-candidates > span { + color: var(--compass-faint); + font-size: 8px; + text-transform: uppercase; +} + +.compass-code-candidates ul { + display: grid; + gap: 5px; + margin: 6px 0 0; + padding: 0; + list-style: none; +} + +.compass-code-candidates li { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 1px 6px; +} + +.compass-code-candidates code { + overflow-wrap: anywhere; + color: var(--foreground); + font: 9px/1.35 var(--compass-font-mono); +} + +.compass-code-candidates li > span { + grid-column: 1 / -1; + color: var(--muted-foreground); + font-size: 9px; +} + +.compass-code-candidates small { + grid-column: 2; + grid-row: 1; + color: var(--compass-faint); + font-size: 8px; + text-transform: uppercase; +} + +.compass-code-diagnostic { + display: flex; + gap: 7px; + margin: 0 0 6px; + padding: 7px 8px; + border: 1px solid var(--vscode-editorWarning-foreground, #d29922); + border-radius: 3px; + color: var(--muted-foreground); + font-size: 9px; +} + +.compass-code-diagnostic[data-severity="info"] { + border-color: var(--vscode-charts-blue, #58a6ff); +} + +.compass-code-diagnostic svg { + width: 12px; + min-width: 12px; + height: 12px; +} + +.compass-code-diagnostic strong { + color: var(--foreground); + text-transform: capitalize; +} + .compass-evidence-heading { display: flex; align-items: baseline; diff --git a/scripts/check_code_graph_v1_coverage.py b/scripts/check_code_graph_v1_coverage.py new file mode 100755 index 00000000..7fe028c5 --- /dev/null +++ b/scripts/check_code_graph_v1_coverage.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""CLI adapter for the importable Compass code-graph v1 semantic oracle.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +from code_graph_v1_oracle import ( + QualificationError, + canonical_bytes, + load_json, + load_manifest, + manifest_fingerprint, + qualify_graph, + qualification_summary, +) + +ROOT = Path(__file__).resolve().parent.parent +DEFAULT_MANIFEST = ROOT / "tests/qualification/code-graph-v1-semantic.json" +DEFAULT_CORPUS = ROOT / "tests/qualification/code-graph-v1-corpus.json" + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST) + parser.add_argument("--corpus-manifest", type=Path, default=DEFAULT_CORPUS) + parser.add_argument("--fixture-root", type=Path, default=ROOT) + parser.add_argument("--graph", type=Path) + parser.add_argument("--compare", nargs=2, type=Path) + parser.add_argument("--compass-revision") + parser.add_argument("--comparisons", type=Path) + args = parser.parse_args() + + corpus = load_json(args.corpus_manifest) + if corpus.get("schema") != "compass.code-graph-qualification-corpus/1": + raise QualificationError( + f"corpus_manifest_schema [{args.corpus_manifest}]: unsupported schema" + ) + if set(corpus) != {"schema", "files"} or not isinstance(corpus["files"], list): + raise QualificationError( + f"corpus_manifest_shape [{args.corpus_manifest}]: expected schema and files" + ) + corpus_sources: set[str] = set() + corpus_ids: set[str] = set() + for item in corpus["files"]: + if not isinstance(item, dict) or set(item) != {"id", "path", "language", "contents"}: + raise QualificationError( + f"corpus_manifest_file [{args.corpus_manifest}]: invalid file record" + ) + path = Path(item["path"]) + if ( + not item["id"] + or item["id"] in corpus_ids + or not item["language"] + or path.is_absolute() + or ".." in path.parts + or item["path"] in corpus_sources + ): + raise QualificationError( + f"corpus_manifest_file [{item['id']}]: duplicate or unsafe identity" + ) + corpus_ids.add(item["id"]) + corpus_sources.add(item["path"]) + manifest = load_manifest(args.manifest, args.fixture_root, corpus_sources) + producer_version = manifest["languages"]["producerVersion"] + manifest["_languageExpectations"] = [ + { + "id": item["id"], + "source": item["path"], + "language": item["language"], + "producerVersion": producer_version, + } + for item in corpus.get("files", []) + ] + if args.compare: + left, right = (path.read_bytes() for path in args.compare) + if left != right: + raise QualificationError( + f"deterministic_graph_bytes [{args.compare[0]}]: differs from {args.compare[1]}" + ) + fingerprint = manifest_fingerprint((args.manifest, args.corpus_manifest)) + if args.graph is None: + print(canonical_bytes({ + "schema": manifest["schema"], + "fixtureManifestFingerprint": fingerprint, + "flows": len(manifest["flows"]), + "negatives": len(manifest["negatives"]), + "nodeKinds": len(manifest["nodeProducers"]), + "edgeKinds": len(manifest["edgeProducers"]), + "languages": len(manifest["_languageExpectations"]), + }).decode(), end="") + return 0 + + graph_bytes = args.graph.read_bytes() + graph = json.loads(graph_bytes) + assertions = qualify_graph(graph, manifest, args.fixture_root.resolve()) + comparisons = load_json(args.comparisons) if args.comparisons else {} + summary = qualification_summary( + compass_revision=args.compass_revision or "unknown", + manifest_digest=fingerprint, + graph_bytes=graph_bytes, + graph=graph, + assertions=assertions, + comparisons=comparisons, + ) + print(canonical_bytes(summary).decode(), end="") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (OSError, json.JSONDecodeError, QualificationError, ValueError) as error: + print(f"code-graph-v1 qualification failed: {error}", file=sys.stderr) + raise SystemExit(1) diff --git a/scripts/code_graph_v1_oracle.py b/scripts/code_graph_v1_oracle.py new file mode 100755 index 00000000..65dfb1ce --- /dev/null +++ b/scripts/code_graph_v1_oracle.py @@ -0,0 +1,878 @@ +#!/usr/bin/env python3 +"""Executable semantic oracle for the published ``compass.graph/1`` contract.""" + +from __future__ import annotations + +import hashlib +import json +import re +from collections import Counter, defaultdict +from pathlib import Path +from typing import Any, Iterable + +SCHEMA = "compass.code-graph-qualification/2" +GRAPH_SCHEMA = "compass.graph/1" +NODE_KINDS = ( + "file", "module", "package", "namespace", "class", "struct", "interface", + "trait", "protocol", "enum", "enum_member", "type_alias", "function", + "method", "constructor", "property", "field", "variable", "constant", + "parameter", "import", "export", "macro", "annotation", "route", + "component", "event", "message", "topic", "queue", "job", "resource", + "schema", "query", "migration", "config_key", "database", + "database_schema", "database_table", "database_view", "database_column", + "database_index", "database_constraint", "database_procedure", + "database_trigger", +) +EDGE_KINDS = ( + "contains", "calls", "imports", "exports", "extends", "implements", + "references", "type_of", "returns", "instantiates", "overrides", + "decorates", "routes_to", "reads", "writes", "aliases", "registers", + "handles", "publishes", "subscribes", "produces", "consumes", "schedules", + "triggers", "tests", "depends_on", "documents", "maps_to", +) +DETAIL_TYPES = { + "file": {"file"}, + "symbol": set(NODE_KINDS[1:24]) | {"migration"}, + "import_export": {"import", "export"}, + "route": {"route"}, + "component": {"component"}, + "resource": {"resource"}, + "messaging": {"event", "message", "topic", "queue"}, + "job": {"job"}, + "schema": {"schema"}, + "query": {"query"}, + "config": {"config_key"}, + "database": set(NODE_KINDS[36:]), +} +TRUSTED_ORIGINS = {"ast", "config", "convention", "artifact"} +ALL_ORIGINS = TRUSTED_ORIGINS | {"heuristic"} +CONFIDENCES = {"exact", "inferred", "ambiguous"} +RESOLUTIONS = {"exact", "ambiguous", "unresolved"} +STAGES = {"handler", "middleware"} +ENTERPRISE_KINDS = { + "event", "message", "topic", "queue", "job", "resource", "schema", "query", + "migration", "config_key", "database", "database_schema", "database_table", + "database_view", "database_column", "database_index", "database_constraint", + "database_procedure", "database_trigger", +} +KNOWN_PRODUCER = re.compile( + r"^compass\.(?:languages|frameworks|resolve|graph|postgres|semantic)\.[a-z0-9_.-]+$" +) + +TYPE_KINDS = {"class", "struct", "interface", "trait", "protocol", "enum", "type_alias"} +CALLABLE = {"function", "method", "constructor", "database_procedure"} +CONTAINER = { + "file", "module", "package", "namespace", "class", "struct", "interface", + "trait", "protocol", "enum", "component", "resource", "schema", "database", + "database_schema", "database_table", "database_view", +} +CONTAINS_FILE_TARGETS = set(NODE_KINDS[1:36]) | {"database"} +CONTAINS_SCOPE_TARGETS = set(NODE_KINDS[:36]) +CONTAINS_TYPE_TARGETS = { + "class", "struct", "interface", "trait", "protocol", "enum", "enum_member", + "type_alias", "function", "method", "constructor", "property", "field", + "variable", "constant", "parameter", "macro", "annotation", "component", +} +CONTAINS_CALLABLE_TARGETS = { + "class", "struct", "interface", "trait", "protocol", "enum", "type_alias", + "function", "method", "constructor", "property", "field", "variable", + "constant", "parameter", +} +EXECUTABLE = CALLABLE | {"component", "job", "query", "database_trigger"} +DATA = { + "property", "field", "variable", "constant", "parameter", "resource", + "schema", "query", "config_key", "database", "database_schema", + "database_table", "database_view", "database_column", +} +VALUE_KINDS = {"property", "field", "variable", "constant", "parameter", "import", "export", "type_alias"} +MESSAGE_KINDS = {"event", "message", "topic", "queue"} + + +class QualificationError(ValueError): + """A bounded, identity-bearing qualification failure.""" + + +def fail(invariant: str, identity: str, message: str) -> None: + raise QualificationError(f"{invariant} [{identity}]: {message}") + + +def canonical_bytes(value: Any) -> bytes: + return (json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode() + + +def digest_bytes(data: bytes) -> str: + return f"sha256:{hashlib.sha256(data).hexdigest()}" + + +def load_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8")) + + +def manifest_fingerprint(paths: Iterable[Path]) -> str: + digest = hashlib.sha256() + for path in sorted(paths): + digest.update(path.name.encode()) + digest.update(b"\0") + digest.update(path.read_bytes()) + digest.update(b"\0") + return f"sha256:{digest.hexdigest()}" + + +def _require(record: dict[str, Any], fields: set[str], identity: str) -> None: + missing = sorted(field for field in fields if field not in record) + if missing: + fail("manifest_missing_field", identity, f"missing {', '.join(missing)}") + + +def load_manifest( + path: Path, + fixture_root: Path, + declared_sources: set[str] | None = None, +) -> dict[str, Any]: + manifest = load_json(path) + if not isinstance(manifest, dict) or manifest.get("schema") != SCHEMA: + fail("manifest_schema", str(path), f"expected {SCHEMA}") + allowed = { + "schema", "flows", "negatives", "nodeProducers", "edgeProducers", + "languages", "occurrences", "coverage", "limits", + } + unknown = sorted(set(manifest) - allowed) + if unknown: + fail("manifest_unknown_field", str(path), f"unknown {', '.join(unknown)}") + for name in allowed - {"schema"}: + if name not in manifest: + fail("manifest_missing_field", str(path), f"missing {name}") + + ids: set[str] = set() + selectors: set[tuple[Any, ...]] = set() + flow_fields = { + "id", "framework", "routeFramework", "operation", "path", "routeSource", + "handler", "handlerSource", "relationship", "stage", "position", + "handlerKind", "handlerLanguage", "resolution", "origins", "producer", + "rules", "allowHeuristic", "candidates", + } + for flow in manifest["flows"]: + identity = str(flow.get("id", "")) + _require(flow, flow_fields, identity) + if set(flow) != flow_fields: + fail("manifest_unknown_field", identity, f"flow fields {sorted(set(flow) - flow_fields)}") + _unique_id(ids, identity) + if flow["relationship"] != "routes_to": + fail("manifest_enum", identity, "relationship must be routes_to") + if flow["stage"] not in STAGES or flow["resolution"] not in RESOLUTIONS: + fail("manifest_enum", identity, "unknown stage or resolution") + if flow["handlerKind"] not in NODE_KINDS: + fail("manifest_enum", identity, f"unknown handler kind {flow['handlerKind']}") + if not set(flow["origins"]) <= ALL_ORIGINS or not flow["origins"]: + fail("manifest_enum", identity, "unknown or empty origins") + selector = ( + flow["routeFramework"], flow["operation"], flow["path"], + flow["routeSource"], flow["stage"], flow["position"], + ) + if selector in selectors: + fail("manifest_duplicate_selector", identity, repr(selector)) + selectors.add(selector) + _source_exists(fixture_root, flow["routeSource"], identity, declared_sources) + _source_exists(fixture_root, flow["handlerSource"], identity, declared_sources) + if not isinstance(flow["handler"], dict) or set(flow["handler"]) not in ( + {"qualifiedName"}, {"qualifiedNameTemplate"}, + ): + fail("manifest_handler_identity", identity, "handler must contain exactly one qualified identity") + + negative_fields = {"id", "framework", "source", "routeFramework"} + for item in manifest["negatives"]: + identity = str(item.get("id", "")) + _require(item, negative_fields, identity) + if set(item) != negative_fields: + fail("manifest_unknown_field", identity, "negative fields differ from contract") + _unique_id(ids, identity) + _source_exists(fixture_root, item["source"], identity, declared_sources) + + producer_fields = { + "id", "kind", "source", "qualifiedName", "producer", "origins", + "detailType", + } + for group, vocabulary in (("nodeProducers", NODE_KINDS), ("edgeProducers", EDGE_KINDS)): + seen_kinds: set[str] = set() + for item in manifest[group]: + identity = str(item.get("id", "")) + _require(item, producer_fields, identity) + if set(item) != producer_fields: + fail("manifest_unknown_field", identity, f"{group} fields differ from contract") + _unique_id(ids, identity) + if item["kind"] not in vocabulary: + fail("manifest_enum", identity, f"unknown {group} kind {item['kind']}") + if item["kind"] in seen_kinds: + fail("manifest_duplicate_kind", identity, item["kind"]) + seen_kinds.add(item["kind"]) + _source_exists(fixture_root, item["source"], identity, declared_sources) + missing = sorted(set(vocabulary) - seen_kinds) + if missing: + fail("manifest_missing_vocabulary", group, ", ".join(missing)) + language_contract = manifest["languages"] + if not isinstance(language_contract, dict) or set(language_contract) != { + "source", "producerVersion", + } or language_contract["source"] != "corpus": + fail("manifest_language_contract", str(path), "languages must bind to corpus") + if not isinstance(manifest["occurrences"], list): + fail("manifest_occurrences", str(path), "occurrences must be an array") + occurrence_fields = { + "id", "kind", "source", "sourceQualifiedName", "targetQualifiedName", + "minimum", + } + for item in manifest["occurrences"]: + identity = str(item.get("id", "")) + _require(item, occurrence_fields, identity) + if set(item) != occurrence_fields or item["kind"] not in EDGE_KINDS: + fail("manifest_occurrences", identity, "invalid occurrence expectation") + _unique_id(ids, identity) + _source_exists(fixture_root, item["source"], identity, declared_sources) + if not isinstance(item["minimum"], int) or item["minimum"] < 2: + fail("manifest_occurrences", identity, "minimum must be at least two") + if not isinstance(manifest["coverage"], list) or not manifest["coverage"]: + fail("manifest_coverage", str(path), "coverage expectations must be non-empty") + for item in manifest["coverage"]: + identity = str(item.get("id", "")) + _unique_id(ids, identity) + _source_exists(fixture_root, item.get("source", ""), identity, declared_sources) + expected = ( + {"id", "source", "forbidCompleteWhen"}, + {"id", "source", "extractionStatus"}, + {"id", "source", "diagnosticCode"}, + ) + if set(item) not in expected: + fail("manifest_coverage", identity, "invalid coverage expectation") + if not isinstance(manifest["limits"], dict) or set(manifest["limits"]) != {"maxDiagnostics"}: + fail("manifest_limits", str(path), "limits must contain maxDiagnostics") + return manifest + + +def _unique_id(ids: set[str], identity: str) -> None: + if not identity or identity == "" or identity in ids: + fail("manifest_duplicate_id", identity, "ID is empty or duplicated") + ids.add(identity) + + +def _source_exists( + root: Path, + relative: str, + identity: str, + declared_sources: set[str] | None, +) -> None: + path = Path(relative) + declared = declared_sources is not None and relative in declared_sources + if path.is_absolute() or ".." in path.parts or (not declared and not (root / path).is_file()): + fail("manifest_fixture_source", identity, f"missing or unsafe source {relative}") + + +def _detail_type(record: dict[str, Any]) -> str | None: + details = record.get("details") + return details.get("type") if isinstance(details, dict) else None + + +def _anchor(anchor: Any, files: dict[str, dict[str, Any]], owner: str) -> None: + if not isinstance(anchor, dict): + fail("invalid_anchor", owner, "anchor is not an object") + fields = ("file", "startByte", "endByte", "startLine", "startColumn", "endLine", "endColumn") + if any(field not in anchor for field in fields): + fail("invalid_anchor", owner, "anchor fields are incomplete") + file = anchor["file"] + if file not in files: + fail("invalid_anchor", owner, f"unknown file {file!r}") + values = [anchor[field] for field in fields[1:]] + if any(not isinstance(value, int) or value < 0 for value in values): + fail("invalid_anchor", owner, "anchor coordinates must be non-negative integers") + if anchor["endByte"] < anchor["startByte"] or anchor["endByte"] > files[file]["byteSize"]: + fail("invalid_anchor", owner, f"byte range outside {file}") + start = (anchor["startLine"], anchor["startColumn"]) + end = (anchor["endLine"], anchor["endColumn"]) + if start > end or anchor["startLine"] == 0: + fail("invalid_anchor", owner, "line/column range is invalid") + + +def _evidence( + records: Any, + files: dict[str, dict[str, Any]], + owner: str, + *, + allow_empty_direct: bool = False, +) -> None: + if not isinstance(records, list) or not records: + fail("unknown_producer", owner, "missing provenance") + for index, evidence in enumerate(records): + identity = f"{owner}:evidence:{index}" + producer = evidence.get("extractor", "") + if producer.endswith(".unknown") or not KNOWN_PRODUCER.fullmatch(producer): + fail("unknown_producer", identity, repr(producer)) + origin = evidence.get("origin") + confidence = evidence.get("confidence") + if origin not in ALL_ORIGINS or confidence not in CONFIDENCES: + fail("unsupported_provenance", identity, f"{origin}/{confidence}") + anchors = evidence.get("anchors", []) + wiring = evidence.get("wiringSite") + if origin in {"ast", "config", "artifact"} and not anchors: + fail("invalid_anchor", identity, "direct evidence requires an anchor") + if origin == "convention" and (not anchors or not evidence.get("rule")): + fail("unsupported_provenance", identity, "convention requires rule and anchor") + if origin == "heuristic" and (not evidence.get("rule") or not wiring): + fail("heuristic_wiring", identity, "rule and exact wiring site required") + for anchor in anchors: + _anchor(anchor, files, identity) + if ( + origin in {"ast", "config", "artifact"} + and anchor["startByte"] == anchor["endByte"] + and not allow_empty_direct + ): + fail("invalid_anchor", identity, "direct anchor is empty") + if wiring is not None: + _anchor(wiring, files, identity) + if wiring["startByte"] == wiring["endByte"]: + fail("heuristic_wiring", identity, "wiring site is empty") + candidates = evidence.get("candidates", []) + if len(candidates) > 20: + fail("candidate_bound", identity, "more than 20 candidates") + candidate_ids = [candidate.get("nodeId") for candidate in candidates] + if candidate_ids != sorted(set(candidate_ids)): + fail("candidate_order", identity, "candidate IDs are not unique and sorted") + + +def endpoint_allowed(source: dict[str, Any], edge: dict[str, Any], target: dict[str, Any]) -> bool: + s, kind, t = source["kind"], edge["kind"], target["kind"] + if kind == "contains": + return ( + (s == "schema" and t == "config_key") + or (s == "file" and t in CONTAINS_FILE_TARGETS) + or (s in {"module", "package", "namespace"} and t in CONTAINS_SCOPE_TARGETS) + or (s in TYPE_KINDS | {"component", "schema"} and t in CONTAINS_TYPE_TARGETS) + or (s in CALLABLE | {"type_alias"} and t in CONTAINS_CALLABLE_TARGETS) + or (s == "resource" and t in {"file", "resource", "config_key"}) + or ( + s == "database" + and t in { + "database_schema", "database_table", "database_view", + "database_index", "database_trigger", + } + ) + or ( + s == "database_schema" + and t in { + "database_table", "database_view", "database_procedure", + "database_trigger", + } + ) + or ( + s in {"database_table", "database_view"} + and t in { + "database_column", "database_index", "database_constraint", + "database_trigger", + } + ) + ) + if kind == "calls": + return s in CALLABLE | TYPE_KINDS | {"file", "module", "variable"} and t in CALLABLE | {"variable", "import", "type_alias"} + if kind == "imports": + return (s in {"file", "module", "package", "namespace", "import"} | CALLABLE and t in CONTAINER | CALLABLE | TYPE_KINDS | {"import", "export", "type_alias", "variable", "constant", "resource", "config_key"}) or (s == "config_key" and t == "resource") + if kind == "exports": + return s in CONTAINER | {"export"} and t in CONTAINER | CALLABLE | TYPE_KINDS | {"import", "export", "type_alias", "variable", "constant"} + if kind == "extends": + return s in TYPE_KINDS and t in TYPE_KINDS + if kind == "implements": + return s in TYPE_KINDS and t in {"interface", "trait", "protocol"} + if kind == "type_of": + return s in VALUE_KINDS and t in TYPE_KINDS + if kind == "returns": + return s in CALLABLE and t in TYPE_KINDS | { + "variable", "import", "schema", "database_table", "database_view", + } + if kind == "instantiates": + return s in CALLABLE | TYPE_KINDS | {"variable"} and t in {"class", "struct", "enum", "component", "database_procedure"} + if kind == "overrides": + return s in CALLABLE and t in CALLABLE + if kind == "decorates": + return s in {"annotation", "macro"} and t in CALLABLE | TYPE_KINDS | VALUE_KINDS | {"component", "route", "resource"} + if kind == "routes_to": + return s == "route" and t in {"file", "function", "method", "class", "component"} + if kind == "maps_to": + return s in {"class", "struct", "schema", "database_table", "database_view"} and t in {"database_table", "database_view"} + if kind == "reads": + return (s in EXECUTABLE and t in DATA) or (s == "database_view" and t == "database_table") + if kind == "writes": + return s in EXECUTABLE and t in DATA + if kind == "aliases": + return s in {"import", "export", "type_alias"} and t in CALLABLE | TYPE_KINDS | {"import", "export", "type_alias", "variable", "constant"} + if kind == "registers": + return s in EXECUTABLE | CONTAINER and t in CALLABLE | CONTAINER | {"component", "route", "event", "message", "topic", "queue", "job"} + if kind in {"handles", "publishes", "produces"}: + return s in EXECUTABLE and t in MESSAGE_KINDS + if kind in {"subscribes", "consumes"}: + return s in {"function", "method", "component", "job", "queue"} and t in MESSAGE_KINDS + if kind in {"schedules", "triggers"}: + return (s in EXECUTABLE and t in {"function", "method", "job", "event", "database_trigger"}) or (kind == "triggers" and s == "database_trigger" and t == "database_table") + if kind == "tests": + return ( + s in {"file", "function", "method", "class"} + and "test" in source.get("roles", []) + and ( + t in CALLABLE + or t in TYPE_KINDS + or t in CONTAINER + or t in { + "route", "component", "event", "message", "topic", "queue", + "job", "query", "migration", "database_procedure", + "database_trigger", + } + ) + ) + if kind == "documents": + return s == "resource" and ( + t in CALLABLE + or t in TYPE_KINDS + or t in CONTAINER + or t in { + "route", "component", "event", "message", "topic", "queue", + "job", "schema", "query", "migration", "database_procedure", + "database_trigger", + } + ) + if kind == "references": + reference_source = CONTAINER | CALLABLE | TYPE_KINDS | { + "file", "property", "field", "variable", "constant", "import", + "export", "type_alias", "resource", "schema", "query", "config_key", + "database_table", "database_view", "database_column", + "database_procedure", "database_trigger", + } + reference_target = reference_source | { + "database", "database_schema", "database_index", + "database_constraint", + } + return s in reference_source and t in reference_target + if kind == "depends_on": + dependency = CONTAINER | CALLABLE | TYPE_KINDS | { + "file", "import", "export", "type_alias", "resource", "schema", + "query", "config_key", "database", "database_schema", + "database_table", "database_view", "database_procedure", + "database_trigger", + } + return s in dependency and t in dependency + return False + + +def validate_graph(graph: dict[str, Any], manifest: dict[str, Any]) -> dict[str, Any]: + if graph.get("directed") is not True or graph.get("multigraph") is not True: + fail("graph_envelope", "graph", "directed multigraph required") + metadata = graph.get("graph", {}) + if metadata.get("schema") != GRAPH_SCHEMA: + fail("graph_schema", "graph", f"expected {GRAPH_SCHEMA}") + files = {item["path"]: item for item in metadata.get("files", [])} + nodes = graph.get("nodes") + edges = graph.get("links") + if not isinstance(nodes, list) or not isinstance(edges, list): + fail("graph_envelope", "graph", "nodes and links must be arrays") + node_index: dict[str, dict[str, Any]] = {} + edge_ids: set[str] = set() + for node in nodes: + identity = node.get("id", "") + if identity in node_index: + fail("duplicate_node_id", identity, "durable node ID repeated") + if node.get("kind") not in NODE_KINDS: + fail("node_kind", identity, repr(node.get("kind"))) + detail = _detail_type(node) + if detail is not None and node["kind"] not in DETAIL_TYPES.get(detail, set()): + fail("typed_details", identity, f"{detail} is incompatible with {node['kind']}") + if node.get("source") is not None: + _anchor(node["source"], files, identity) + source_file = (node.get("source") or {}).get("file") + allow_empty = ( + node.get("kind") == "file" + and source_file in files + and files[source_file].get("byteSize") == 0 + ) + _evidence(node.get("evidence"), files, identity, allow_empty_direct=allow_empty) + node_index[identity] = node + pair_sites: dict[tuple[str, str, str], set[tuple[Any, ...]]] = defaultdict(set) + for edge in edges: + identity = edge.get("id", "") + if identity in edge_ids or identity != edge.get("key"): + fail("duplicate_edge_id", identity, "edge ID duplicated or differs from key") + edge_ids.add(identity) + if edge.get("kind") not in EDGE_KINDS: + fail("edge_kind", identity, repr(edge.get("kind"))) + if edge.get("source") not in node_index or edge.get("target") not in node_index: + fail("dangling_endpoint", identity, f"{edge.get('source')} -> {edge.get('target')}") + if edge["source"] == edge["target"] and edge["kind"] != "calls": + fail("non_recursive_self_loop", identity, edge["kind"]) + source, target = node_index[edge["source"]], node_index[edge["target"]] + if not endpoint_allowed(source, edge, target): + fail("impossible_endpoint", identity, f"{source['kind']} -{edge['kind']}-> {target['kind']}") + if edge.get("relationshipSite") is not None: + _anchor(edge["relationshipSite"], files, identity) + _evidence(edge.get("evidence"), files, identity) + site = edge.get("relationshipSite") or {} + pair_sites[(edge["source"], edge["target"], edge["kind"])].add( + (site.get("file"), site.get("startByte"), site.get("endByte"), edge.get("occurrenceRule")) + ) + _validate_coverage(metadata, files, manifest) + _validate_external_placeholders(nodes, edges, node_index) + _validate_global_hubs(nodes, edges, node_index) + summary = Counter() + summary.update({"invariants": len(nodes) + len(edges)}) + return { + "node_index": node_index, + "files": files, + "pair_sites": pair_sites, + "invariant_assertions": summary["invariants"], + } + + +def _validate_coverage(metadata: dict[str, Any], files: dict[str, dict[str, Any]], manifest: dict[str, Any]) -> None: + diagnostics = metadata.get("diagnostics", []) + limit = manifest["limits"]["maxDiagnostics"] + if len(diagnostics) > limit: + fail("diagnostic_bound", "graph", f"{len(diagnostics)} > {limit}") + coverage = metadata.get("coverage", []) + by_file = defaultdict(list) + for record in coverage: + by_file[record.get("fileId")].append(record) + for path, file in files.items(): + if file.get("extractionStatus") in {"partial", "parse_failure", "unsupported", "excluded"}: + if any(record.get("status") == "complete" for record in by_file[file.get("id")]): + fail("false_coverage", path, f"{file.get('extractionStatus')} file marked complete") + + +def _validate_global_hubs(nodes: list[dict[str, Any]], edges: list[dict[str, Any]], index: dict[str, dict[str, Any]]) -> None: + incident = defaultdict(set) + for edge in edges: + for endpoint, other in ((edge["source"], edge["target"]), (edge["target"], edge["source"])): + node = index[other] + source = (node.get("source") or {}).get("file") + incident[endpoint].add((node.get("language"), source)) + for node in nodes: + if node.get("source") is None and node.get("name") == node.get("qualifiedName"): + scopes = incident[node["id"]] + languages = {language for language, _ in scopes if language} + files = {source for _, source in scopes if source} + if len(languages) > 1 or len(files) > 1: + fail("global_unresolved_hub", node["id"], f"{len(languages)} languages/{len(files)} files") + + +def _validate_external_placeholders( + nodes: list[dict[str, Any]], + edges: list[dict[str, Any]], + index: dict[str, dict[str, Any]], +) -> None: + scopes: set[tuple[Any, ...]] = set() + for node in nodes: + records = [ + item for item in node.get("evidence", []) + if item.get("rule") == "external-symbol-placeholder" + ] + if not records: + continue + if len(records) != 1: + fail("external_placeholder", node["id"], "requires one placeholder provenance") + evidence = records[0] + wiring = evidence.get("wiringSite") or {} + scope = ( + node.get("language"), wiring.get("file"), wiring.get("startByte"), + wiring.get("endByte"), node.get("qualifiedName"), + ) + if None in scope or scope in scopes: + fail("external_placeholder_scope", node["id"], repr(scope)) + scopes.add(scope) + if ( + evidence.get("origin") != "heuristic" + or evidence.get("confidence") != "inferred" + or not KNOWN_PRODUCER.fullmatch(evidence.get("extractor", "")) + or not isinstance(node.get("details"), dict) + ): + fail("external_placeholder", node["id"], "not typed inferred heuristic evidence") + incident = [ + edge for edge in edges + if node["id"] in (edge["source"], edge["target"]) + ] + if not incident: + fail("external_placeholder", node["id"], "orphan placeholder") + for edge in incident: + edge_file = (edge.get("relationshipSite") or {}).get("file") + if edge.get("deferred") is not True or edge_file != wiring.get("file"): + fail( + "external_placeholder_deferred", + edge["id"], + f"deferred={edge.get('deferred')} scope={edge_file!r}", + ) + + +def _expand_identity(identity: dict[str, str], fixture_root: Path) -> str: + if "qualifiedName" in identity: + return identity["qualifiedName"] + return identity["qualifiedNameTemplate"].replace("{fixtureRoot}", fixture_root.as_posix()) + + +def assert_flows(graph: dict[str, Any], manifest: dict[str, Any], fixture_root: Path) -> dict[str, int]: + nodes = graph["nodes"] + index = {node["id"]: node for node in nodes} + edges = graph["links"] + by_framework = Counter() + resolutions = Counter() + for flow in manifest["flows"]: + identity = flow["id"] + matches = [] + for node in nodes: + data = (node.get("details") or {}).get("data", {}) + source = (node.get("source") or {}).get("file") + if ( + node.get("kind") == "route" + and node.get("framework") == flow["routeFramework"] + and data.get("operation") == flow["operation"] + and data.get("path") == flow["path"] + and source == flow["routeSource"] + ): + matches.append(node) + if len(matches) != 1: + fail("flow_route_selector", identity, f"matched {len(matches)} route occurrences") + route = matches[0] + data = route["details"]["data"] + if data.get("resolution") != flow["resolution"]: + fail("flow_resolution", identity, f"{data.get('resolution')} != {flow['resolution']}") + stage_matches = [ + stage for stage in data.get("stages", []) + if stage.get("stage") == flow["stage"] and stage.get("position") == flow["position"] + ] + if len(stage_matches) != 1: + fail("flow_stage", identity, f"matched {len(stage_matches)} stages") + stage = stage_matches[0] + expected_candidates = flow["candidates"] + actual_candidates = sorted(candidate["nodeId"] for candidate in stage.get("candidates", [])) + if expected_candidates and actual_candidates != sorted(expected_candidates): + fail("flow_candidates", identity, f"{actual_candidates!r}") + route_edges = [ + edge for edge in edges + if edge["source"] == route["id"] + and edge["kind"] == "routes_to" + and (edge.get("details") or {}).get("data", {}).get("stage") == flow["stage"] + and (edge.get("details") or {}).get("data", {}).get("position") == flow["position"] + ] + if len(route_edges) != 1: + fail("flow_edge_selector", identity, f"matched {len(route_edges)} routes_to edges") + edge = route_edges[0] + target = index[edge["target"]] + expected_name = _expand_identity(flow["handler"], fixture_root) + actual_source = (target.get("source") or {}).get("file") + if target.get("qualifiedName") != expected_name or actual_source != flow["handlerSource"]: + fail("flow_target_mismatch", identity, f"{target.get('qualifiedName')} @ {actual_source}") + if target.get("kind") != flow["handlerKind"] or target.get("language") != flow["handlerLanguage"]: + fail( + "flow_target_mismatch", + identity, + f"{target.get('kind')}/{target.get('language')}", + ) + if stage.get("target") not in (None, edge["target"]): + fail("flow_stage", identity, f"stage target {stage.get('target')} != {edge['target']}") + evidence = edge["evidence"] + if not any( + item["extractor"] == flow["producer"] + and item["origin"] in flow["origins"] + and item.get("rule") in flow["rules"] + for item in evidence + ): + fail("flow_provenance", identity, "producer/origin/rule mismatch") + if not flow["allowHeuristic"] and any(item["origin"] == "heuristic" for item in evidence): + fail("flow_provenance", identity, "heuristic evidence forbidden") + if flow["resolution"] == "exact" and not any( + item["confidence"] == "exact" and item["origin"] in TRUSTED_ORIGINS for item in evidence + ): + fail("false_exact", identity, edge["id"]) + by_framework[flow["framework"]] += 1 + resolutions[flow["resolution"]] += 1 + return { + "flows": sum(by_framework.values()), + "frameworks": len(by_framework), + **{f"resolution_{key}": value for key, value in sorted(resolutions.items())}, + } + + +def assert_negatives(graph: dict[str, Any], manifest: dict[str, Any]) -> dict[str, int]: + count = 0 + for item in manifest["negatives"]: + route_ids = { + node["id"] for node in graph["nodes"] + if node.get("kind") == "route" + and node.get("framework") == item["routeFramework"] + and (node.get("source") or {}).get("file") == item["source"] + and (node.get("details") or {}).get("data", {}).get("resolution") == "exact" + } + edge_ids = [ + edge["id"] for edge in graph["links"] + if edge["kind"] == "routes_to" and edge["source"] in route_ids + and any(evidence.get("confidence") == "exact" for evidence in edge.get("evidence", [])) + ] + if route_ids or edge_ids: + fail("framework_negative", item["id"], f"unexpected nodes {sorted(route_ids)} edges {edge_ids}") + count += 1 + return {"negatives": count} + + +def assert_vocabulary(graph: dict[str, Any], manifest: dict[str, Any]) -> dict[str, int]: + counts = {} + for group, records, key in ( + ("nodeProducers", graph["nodes"], "node_kinds"), + ("edgeProducers", graph["links"], "edge_kinds"), + ): + passed = 0 + for expectation in manifest[group]: + candidates = [] + for record in records: + source = ( + (record.get("source") or {}).get("file") + if group == "nodeProducers" + else (record.get("relationshipSite") or {}).get("file") + ) + qualified = record.get("qualifiedName") if group == "nodeProducers" else record.get("id") + if ( + record.get("kind") == expectation["kind"] + and source == expectation["source"] + and (expectation["qualifiedName"] == "*" or qualified == expectation["qualifiedName"]) + and _detail_type(record) == expectation["detailType"] + and any( + evidence.get("extractor") == expectation["producer"] + and evidence.get("origin") in expectation["origins"] + for evidence in record.get("evidence", []) + ) + ): + candidates.append(record) + if not candidates: + fail("runtime_vocabulary_producer", expectation["id"], expectation["kind"]) + passed += 1 + counts[key] = passed + if {node["kind"] for node in graph["nodes"]}.isdisjoint(ENTERPRISE_KINDS): + fail("enterprise_kinds", "graph", "no enterprise/domain nodes") + missing_enterprise = sorted(ENTERPRISE_KINDS - {node["kind"] for node in graph["nodes"]}) + if missing_enterprise: + fail("enterprise_kinds", "graph", ", ".join(missing_enterprise)) + return counts + + +def assert_languages(graph: dict[str, Any], manifest: dict[str, Any]) -> dict[str, int]: + files = {item["path"]: item for item in graph["graph"].get("files", [])} + passed = 0 + for item in manifest.get("_languageExpectations", []): + file = files.get(item["source"]) + if file is None: + fail("language_matrix", item["id"], "file absent from inventory") + if file.get("language") != item["language"]: + fail("language_matrix", item["id"], f"{file.get('language')} != {item['language']}") + if item["producerVersion"] not in file.get("extractorVersions", []): + fail("producer_version", item["id"], repr(file.get("extractorVersions"))) + passed += 1 + return {"languages": passed} + + +def assert_occurrences(graph: dict[str, Any], manifest: dict[str, Any]) -> dict[str, int]: + passed = 0 + index = {node["id"]: node for node in graph["nodes"]} + for item in manifest["occurrences"]: + matches = [ + edge for edge in graph["links"] + if edge["kind"] == item["kind"] + and index[edge["source"]].get("qualifiedName") == item["sourceQualifiedName"] + and index[edge["target"]].get("qualifiedName") == item["targetQualifiedName"] + and (edge.get("relationshipSite") or {}).get("file") == item["source"] + ] + sites = { + ( + edge["relationshipSite"]["startByte"], + edge["relationshipSite"]["endByte"], + edge.get("occurrenceRule"), + ) + for edge in matches if edge.get("relationshipSite") + } + if len(matches) < item["minimum"] or len(sites) < item["minimum"]: + fail("repeated_occurrence_loss", item["id"], f"{len(matches)} edges/{len(sites)} sites") + passed += 1 + return {"occurrences": passed} + + +def assert_coverage(graph: dict[str, Any], manifest: dict[str, Any]) -> dict[str, int]: + metadata = graph["graph"] + files = {item["path"]: item for item in metadata.get("files", [])} + diagnostics = metadata.get("diagnostics", []) + passed = 0 + for item in manifest["coverage"]: + identity = item["id"] + file = files.get(item["source"]) + if "extractionStatus" in item: + if file is None or file.get("extractionStatus") != item["extractionStatus"]: + fail( + "coverage_expectation", + identity, + f"{None if file is None else file.get('extractionStatus')} != {item['extractionStatus']}", + ) + elif "forbidCompleteWhen" in item: + if file is None: + fail("coverage_expectation", identity, "file absent from inventory") + status = file.get("extractionStatus") + if status in item["forbidCompleteWhen"]: + records = [ + record for record in metadata.get("coverage", []) + if record.get("fileId") == file.get("id") + ] + if any(record.get("status") == "complete" for record in records): + fail("false_coverage", identity, f"{status} file marked complete") + else: + matches = [ + diagnostic for diagnostic in diagnostics + if diagnostic.get("code") == item["diagnosticCode"] + and ( + diagnostic.get("file") == item["source"] + or (diagnostic.get("anchor") or {}).get("file") == item["source"] + or item["source"] in str(diagnostic) + ) + ] + if not matches: + fail("coverage_diagnostic", identity, item["diagnosticCode"]) + passed += 1 + return {"coverage_expectations": passed} + + +def qualify_graph(graph: dict[str, Any], manifest: dict[str, Any], fixture_root: Path) -> dict[str, Any]: + invariants = validate_graph(graph, manifest) + summary: dict[str, Any] = {} + summary.update(assert_flows(graph, manifest, fixture_root)) + summary.update(assert_negatives(graph, manifest)) + summary.update(assert_vocabulary(graph, manifest)) + summary.update(assert_languages(graph, manifest)) + summary.update(assert_occurrences(graph, manifest)) + summary.update(assert_coverage(graph, manifest)) + summary["invariants"] = invariants["invariant_assertions"] + summary["coverage_records"] = len(graph["graph"].get("coverage", [])) + summary["diagnostics"] = len(graph["graph"].get("diagnostics", [])) + return dict(sorted(summary.items())) + + +def qualification_summary( + *, + compass_revision: str, + manifest_digest: str, + graph_bytes: bytes, + graph: dict[str, Any], + assertions: dict[str, Any], + comparisons: dict[str, bool], +) -> dict[str, Any]: + resolutions = Counter( + (node.get("details") or {}).get("data", {}).get("resolution", "unknown") + for node in graph["nodes"] if node.get("kind") == "route" + ) + return { + "schema": "compass.code-graph-qualification-summary/1", + "compassRevision": compass_revision, + "fixtureManifestFingerprint": manifest_digest, + "graphDigest": digest_bytes(graph_bytes), + "runMode": "fixtures-only", + "assertions": dict(sorted(assertions.items())), + "routeResolutions": dict(sorted(resolutions.items())), + "coverage": { + "records": len(graph["graph"].get("coverage", [])), + "diagnostics": len(graph["graph"].get("diagnostics", [])), + }, + "byteComparisons": dict(sorted(comparisons.items())), + } diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 03d67a7c..440e43b1 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -49,8 +49,7 @@ try { if ($Expected -notmatch "^[0-9a-f]{64}$") { throw "invalid SHA-256 file for $Archive" } - $Actual = (Get-FileHash (Join-Path $Temporary $Archive) -Algorithm SHA256).Hash ` - .ToLowerInvariant() + $Actual = (Get-FileHash (Join-Path $Temporary $Archive) -Algorithm SHA256).Hash.ToLowerInvariant() if ($Actual -ne $Expected) { throw "checksum verification failed for $Archive" } diff --git a/scripts/publish_crates.sh b/scripts/publish_crates.sh index 3ae6feef..790113b9 100755 --- a/scripts/publish_crates.sh +++ b/scripts/publish_crates.sh @@ -21,7 +21,11 @@ if [[ "$(git describe --tags --exact-match 2>/dev/null || true)" != "compass-v$v exit 2 fi -cargo package --workspace --locked --no-verify +# Private inference crates remain workspace-native, but have no registry graph +# and therefore are intentionally excluded from the release package set. +cargo package --workspace --locked --no-verify \ + --exclude compass-transcribe \ + --exclude compass-whisper # Prove that compass-languages' normalized registry manifest points at the # published static adapter by package name. A path-only dependency would work @@ -36,6 +40,27 @@ if ! rg -q 'package = "compass-tree-sitter-language-pack"' \ exit 2 fi +# A publishable registry crate may only depend on other registry crates. Compute +# the complete dependency-first order from workspace metadata so newly added +# product crates cannot be silently omitted from a release. +mapfile -t crates < <( + cargo metadata --no-deps --format-version 1 \ + | python3 scripts/publishable_crates.py +) + +# Normalized manifests are Cargo's actual registry contracts. Keep inference +# crates out of both the ingest boundary and the installable CLI package. +for packaged_crate in compass-ingest compass-cli; do + normalized_manifest="$( + tar -xOf "target/package/$packaged_crate-$version.crate" \ + "$packaged_crate-$version/Cargo.toml" + )" + if rg -q 'compass-(transcribe|whisper)' <<<"$normalized_manifest"; then + echo "error: packaged $packaged_crate reaches an internal inference crate" >&2 + exit 2 + fi +done + # A registry install cannot inherit this repository's .cargo/config.toml. The # adapter therefore owns Compass's compile-time grammar selection and must be # published before compass-languages. Its version follows the pinned upstream @@ -49,30 +74,6 @@ fi # Cargo waits for each new package to become available in the registry index, # so downstream crates can be published immediately in topological order. -crates=( - compass-model - compass-graphdb - compass-files - compass-media - compass-whisper - compass-cargo - compass-google-workspace - compass-prs - compass-query - compass-reflect - compass-global - compass-semantic - compass-transcribe - compass-ingest - compass-languages - compass-postgres - compass-graph - compass-resolve - compass-output - compass-core - compass-mcp - compass-cli -) for crate in "${crates[@]}"; do cargo publish --locked -p "$crate" done diff --git a/scripts/publishable_crates.py b/scripts/publishable_crates.py new file mode 100644 index 00000000..98c0e506 --- /dev/null +++ b/scripts/publishable_crates.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Print publishable Compass workspace crates in dependency-first order.""" + +from __future__ import annotations + +import json +import sys + + +def main() -> int: + metadata = json.load(sys.stdin) + workspace_members = set(metadata["workspace_members"]) + workspace_packages = { + package["name"]: package + for package in metadata["packages"] + if package["id"] in workspace_members + } + internal = { + name + for name, package in workspace_packages.items() + if package["publish"] == [] + } + publishable = { + name: package + for name, package in workspace_packages.items() + if name.startswith("compass-") + and name != "compass-tree-sitter-language-pack" + and package["publish"] != [] + } + + for name, package in publishable.items(): + dependencies = { + dependency["name"] + for dependency in package["dependencies"] + if dependency["kind"] != "dev" + } + forbidden = dependencies & internal + if forbidden: + joined = ", ".join(sorted(forbidden)) + raise SystemExit(f"{name} depends on internal crate(s): {joined}") + + remaining = set(publishable) + ordered: list[str] = [] + while remaining: + ready = sorted( + name + for name in remaining + if not ( + { + dependency["name"] + for dependency in publishable[name]["dependencies"] + if dependency["kind"] != "dev" + } + & remaining + ) + ) + if not ready: + joined = ", ".join(sorted(remaining)) + raise SystemExit(f"publishable workspace dependency cycle: {joined}") + ordered.extend(ready) + remaining.difference_update(ready) + + print("\n".join(ordered)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/qualify_code_graph_v1.sh b/scripts/qualify_code_graph_v1.sh new file mode 100755 index 00000000..9abca973 --- /dev/null +++ b/scripts/qualify_code_graph_v1.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +set -euo pipefail + +QUALIFY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +QUALIFY_TMP="$(mktemp -d "${TMPDIR:-/tmp}/compass-code-graph-v1.XXXXXX")" +trap 'chmod -R u+w "$QUALIFY_TMP" 2>/dev/null || true; rm -rf -- "$QUALIFY_TMP"' EXIT + +usage() { + echo "usage: $0 --fixtures-only" >&2 + exit 2 +} + +[[ "${1:-}" == "--fixtures-only" ]] || usage +shift +[[ "$#" -eq 0 ]] || usage + +cd "$QUALIFY_ROOT" +MANIFEST="$QUALIFY_ROOT/tests/qualification/code-graph-v1-semantic.json" +CORPUS_MANIFEST="$QUALIFY_ROOT/tests/qualification/code-graph-v1-corpus.json" +CORPUS="$QUALIFY_TMP/corpus" +OUTPUT_PARENT="$QUALIFY_TMP/output" + +echo "[code-graph-v1] validate strict manifests" +python3 scripts/check_code_graph_v1_coverage.py \ + --manifest "$MANIFEST" \ + --corpus-manifest "$CORPUS_MANIFEST" + +echo "[code-graph-v1] enforce in-process scale ceilings" +cargo test --locked -p compass-core --lib \ + resolver_source_text_enforces_the_pre_read_byte_limit +cargo test --locked -p compass-query --lib \ + bounded_matching_scales_with_response_budget_on_500k_edges +cargo test --locked -p compass-resolve --lib \ + ambiguous_terminal_lookup_is_bounded_by_candidate_budget +cargo test --locked -p compass-core --test pipeline_scale \ + cold_and_warm_in_process_builds_stay_within_enterprise_ceiling +cargo test --locked -p compass-query --test code_query_scale \ + enterprise_queries_stay_within_in_process_ceiling +cargo test --locked -p compass-resolve --test framework_resolution_scale \ + shared_production_framework_resolution_stays_within_enterprise_ceiling + +echo "[code-graph-v1] build qualifying production binary once" +cargo build --locked -p compass-cli --bin compass +COMPASS_BIN="$QUALIFY_ROOT/target/debug/compass" + +mkdir -p "$CORPUS/fixtures/code-graph" +cp -R "$QUALIFY_ROOT/fixtures/code-graph/." "$CORPUS/fixtures/code-graph/" +python3 - "$CORPUS_MANIFEST" "$CORPUS" <<'PY' +import json +import pathlib +import sys + +manifest = json.loads(pathlib.Path(sys.argv[1]).read_text()) +root = pathlib.Path(sys.argv[2]) +for fixture in manifest["files"]: + path = root / fixture["path"] + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(fixture["contents"].encode("utf-8")) +PY + +active_graph() { + python3 - "$1" <<'PY' +import pathlib +import sys + +output = pathlib.Path(sys.argv[1]) / "compass-out" +pointer = output / ".compass-active-generation" +generation = pointer.read_text().strip() +if not generation.startswith("generation-") or "/" in generation or "\\" in generation: + raise SystemExit(f"invalid active generation {generation!r}") +active = output / ".compass-generations" / generation +if not active.is_dir() or (active / ".compass-build-incomplete").exists(): + raise SystemExit(f"incomplete active generation {active}") +print(active / "graph.json") +PY +} + +fixture_digest() { + python3 - "$QUALIFY_ROOT/fixtures/code-graph" <<'PY' +import hashlib +import pathlib +import sys + +root = pathlib.Path(sys.argv[1]) +digest = hashlib.sha256() +for path in sorted(item for item in root.rglob("*") if item.is_file()): + digest.update(path.relative_to(root).as_posix().encode()) + digest.update(b"\0") + digest.update(path.read_bytes()) + digest.update(b"\0") +print(digest.hexdigest()) +PY +} + +fixture_digest_before="$(fixture_digest)" + +run_update() { + local mode="$1" + shift + "$COMPASS_BIN" update "$CORPUS" \ + --out "$OUTPUT_PARENT" --no-cluster --no-viz --no-gitignore "$@" \ + >"$QUALIFY_TMP/$mode.log" + active_graph "$OUTPUT_PARENT" +} + +echo "[code-graph-v1] clean production update" +clean_graph="$(run_update clean)" +cp "$clean_graph" "$QUALIFY_TMP/clean.graph.json" + +echo "[code-graph-v1] unchanged warm production update" +warm_graph="$(run_update warm)" +cp "$warm_graph" "$QUALIFY_TMP/warm.graph.json" +cmp "$QUALIFY_TMP/clean.graph.json" "$QUALIFY_TMP/warm.graph.json" + +echo "[code-graph-v1] forced clean production rebuild" +rebuild_graph="$(run_update rebuild --force)" +cp "$rebuild_graph" "$QUALIFY_TMP/rebuild.graph.json" +cmp "$QUALIFY_TMP/clean.graph.json" "$QUALIFY_TMP/rebuild.graph.json" + +edit_path="$CORPUS/fixtures/code-graph/routes/rust/axum.rs" +cp "$edit_path" "$QUALIFY_TMP/axum.original" +printf '\n' >>"$edit_path" +run_update incremental-edit >/dev/null +cp "$QUALIFY_TMP/axum.original" "$edit_path" +restored_graph="$(run_update incremental-restore)" +cp "$restored_graph" "$QUALIFY_TMP/restored.graph.json" +cmp "$QUALIFY_TMP/clean.graph.json" "$QUALIFY_TMP/restored.graph.json" + +echo "[code-graph-v1] alternate-checkout production update" +CHECKOUT_CORPUS="$QUALIFY_TMP/alternate/corpus" +CHECKOUT_OUTPUT="$QUALIFY_TMP/alternate/output" +mkdir -p "$CHECKOUT_CORPUS" +cp -R "$CORPUS/." "$CHECKOUT_CORPUS/" +"$COMPASS_BIN" update "$CHECKOUT_CORPUS" \ + --out "$CHECKOUT_OUTPUT" --no-cluster --no-viz --no-gitignore \ + >"$QUALIFY_TMP/alternate-checkout.log" +checkout_graph="$(active_graph "$CHECKOUT_OUTPUT")" +cp "$checkout_graph" "$QUALIFY_TMP/checkout.graph.json" +cmp "$QUALIFY_TMP/clean.graph.json" "$QUALIFY_TMP/checkout.graph.json" + +fixture_digest_after="$(fixture_digest)" +[[ "$fixture_digest_before" == "$fixture_digest_after" ]] + +cat >"$QUALIFY_TMP/comparisons.json" <<'JSON' +{"cleanEqualsCheckout":true,"cleanEqualsRebuild":true,"cleanEqualsRestored":true,"cleanEqualsWarm":true,"sourceFixtureUnchanged":true} +JSON + +echo "[code-graph-v1] execute semantic assertions over production graph" +python3 scripts/check_code_graph_v1_coverage.py \ + --manifest "$MANIFEST" \ + --corpus-manifest "$CORPUS_MANIFEST" \ + --fixture-root "$CORPUS" \ + --graph "$QUALIFY_TMP/restored.graph.json" \ + --compass-revision "$(git rev-parse HEAD)" \ + --comparisons "$QUALIFY_TMP/comparisons.json" diff --git a/scripts/test_release_scripts.sh b/scripts/test_release_scripts.sh index 9f90b80f..0350f236 100755 --- a/scripts/test_release_scripts.sh +++ b/scripts/test_release_scripts.sh @@ -6,6 +6,43 @@ test -f "$repo_root/scripts/install.sh" test -f "$repo_root/scripts/install.ps1" test_root=$(mktemp -d) trap 'rm -rf "$test_root"' EXIT HUP INT TERM + +cargo metadata --manifest-path "$repo_root/Cargo.toml" --no-deps --format-version 1 \ + > "$test_root/metadata.json" +python3 "$repo_root/scripts/publishable_crates.py" \ + < "$test_root/metadata.json" > "$test_root/publishable-crates.txt" +python3 - "$test_root/metadata.json" <<'PY' +import json +import pathlib +import sys + +metadata = json.loads(pathlib.Path(sys.argv[1]).read_text()) +packages = {package["name"]: package for package in metadata["packages"]} +internal = {"compass-transcribe", "compass-whisper"} + +for name in internal: + assert packages[name]["publish"] == [], f"{name} must set publish = false" + +reachable = set() +pending = ["compass-cli"] +while pending: + name = pending.pop() + if name in reachable: + continue + reachable.add(name) + pending.extend( + dependency["name"] + for dependency in packages[name]["dependencies"] + if dependency["kind"] != "dev" and dependency["name"] in packages + ) + +assert not reachable & internal, ( + "registry CLI graph reaches internal inference crates: " + + ", ".join(sorted(reachable & internal)) +) +PY +test "$(grep -Ec '^compass-(transcribe|whisper)$' "$test_root/publishable-crates.txt")" -eq 0 + mkdir -p "$test_root/fake-checksum-bin" cat > "$test_root/fake-checksum-bin/shasum" <<'EOF' diff --git a/scripts/tests/test_code_graph_v1_oracle.py b/scripts/tests/test_code_graph_v1_oracle.py new file mode 100644 index 00000000..799b47cd --- /dev/null +++ b/scripts/tests/test_code_graph_v1_oracle.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +"""Post-implementation regression tests for the code-graph v1 oracle.""" + +from __future__ import annotations + +import copy +import json +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "scripts")) + +from code_graph_v1_oracle import ( # noqa: E402 + QualificationError, + assert_coverage, + assert_flows, + assert_negatives, + canonical_bytes, + endpoint_allowed, + load_json, + load_manifest, + qualification_summary, + validate_graph, +) + + +def anchor(file: str = "sample.py", start: int = 0, end: int = 1) -> dict[str, object]: + return { + "file": file, + "startByte": start, + "endByte": end, + "startLine": 1, + "startColumn": start, + "endLine": 1, + "endColumn": end, + } + + +def evidence(file: str = "sample.py") -> list[dict[str, object]]: + return [{ + "extractor": "compass.languages.python", + "origin": "ast", + "confidence": "exact", + "anchors": [anchor(file)], + }] + + +def node(identity: str, kind: str, *, qualified: str | None = None) -> dict[str, object]: + return { + "id": identity, + "kind": kind, + "name": identity, + "qualifiedName": qualified or identity, + "language": "python", + "roles": [], + "source": anchor(), + "evidence": evidence(), + } + + +class OracleTests(unittest.TestCase): + maxDiff = None + + def setUp(self) -> None: + self.manifest = load_json( + ROOT / "tests/qualification/code-graph-v1-semantic.json" + ) + + def graph(self, nodes: list[dict], links: list[dict] | None = None) -> dict: + return { + "directed": True, + "multigraph": True, + "graph": { + "schema": "compass.graph/1", + "files": [{ + "id": "file:sample", + "path": "sample.py", + "byteSize": 8, + "extractionStatus": "extracted", + }], + "coverage": [], + "diagnostics": [], + }, + "nodes": nodes, + "links": links or [], + } + + def test_canonical_bytes_are_order_independent(self) -> None: + self.assertEqual(canonical_bytes({"b": 2, "a": 1}), canonical_bytes({"a": 1, "b": 2})) + + def test_endpoint_matrix_rejects_inheritance_to_variable(self) -> None: + self.assertFalse(endpoint_allowed({"kind": "class"}, {"kind": "extends"}, {"kind": "variable"})) + + def test_validate_graph_rejects_unknown_producer(self) -> None: + item = node("function:a", "function") + item["evidence"][0]["extractor"] = "compass.languages.unknown" + with self.assertRaisesRegex(QualificationError, "unknown_producer"): + validate_graph(self.graph([item]), self.manifest) + + def test_validate_graph_rejects_out_of_bounds_anchor(self) -> None: + item = node("function:a", "function") + item["source"]["endByte"] = 9 + with self.assertRaisesRegex(QualificationError, "invalid_anchor"): + validate_graph(self.graph([item]), self.manifest) + + def test_validate_graph_rejects_non_recursive_self_loop(self) -> None: + item = node("function:a", "function") + edge = { + "id": "edge:1", + "key": "edge:1", + "kind": "references", + "source": item["id"], + "target": item["id"], + "relationshipSite": anchor(), + "evidence": evidence(), + } + with self.assertRaisesRegex(QualificationError, "non_recursive_self_loop"): + validate_graph(self.graph([item], [edge]), self.manifest) + + def test_negative_rejects_exact_route(self) -> None: + route = node("route:1", "route") + route["framework"] = "near-match" + route["source"]["file"] = "negative.py" + route["details"] = {"type": "route", "data": {"resolution": "exact"}} + manifest = {"negatives": [{ + "id": "negative", + "routeFramework": "near-match", + "source": "negative.py", + }]} + with self.assertRaisesRegex(QualificationError, "framework_negative"): + assert_negatives(self.graph([route]), manifest) + + def test_coverage_rejects_false_complete(self) -> None: + graph = self.graph([]) + graph["graph"]["files"][0]["extractionStatus"] = "partial" + graph["graph"]["coverage"] = [{"fileId": "file:sample", "status": "complete"}] + manifest = {"coverage": [{ + "id": "partial", + "source": "sample.py", + "forbidCompleteWhen": ["partial"], + }]} + with self.assertRaisesRegex(QualificationError, "false_coverage"): + assert_coverage(graph, manifest) + + def test_flow_checks_exact_handler_identity_kind_and_language(self) -> None: + route = node("route:1", "route") + route.update({ + "framework": "demo", + "details": { + "type": "route", + "data": { + "operation": "GET", + "path": "/ok", + "resolution": "exact", + "stages": [{"stage": "handler", "position": 0, "candidates": []}], + }, + }, + }) + handler = node("function:handler", "function", qualified="handler()") + edge = { + "id": "edge:route", + "kind": "routes_to", + "source": route["id"], + "target": handler["id"], + "details": {"data": {"stage": "handler", "position": 0}}, + "evidence": [{ + "extractor": "compass.frameworks.demo", + "origin": "ast", + "confidence": "exact", + "rule": "framework-route-stage:handler:0", + "anchors": [anchor()], + }], + } + manifest = {"flows": [{ + "id": "flow", + "framework": "demo", + "routeFramework": "demo", + "operation": "GET", + "path": "/ok", + "routeSource": "sample.py", + "handler": {"qualifiedName": "handler()"}, + "handlerSource": "sample.py", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "function", + "handlerLanguage": "python", + "resolution": "exact", + "origins": ["ast"], + "producer": "compass.frameworks.demo", + "rules": ["framework-route-stage:handler:0"], + "allowHeuristic": False, + "candidates": [], + }]} + graph = self.graph([route, handler], [edge]) + self.assertEqual(assert_flows(graph, manifest, ROOT), { + "flows": 1, + "frameworks": 1, + "resolution_exact": 1, + }) + handler["language"] = "ruby" + with self.assertRaisesRegex(QualificationError, "flow_target_mismatch"): + assert_flows(graph, manifest, ROOT) + + def test_manifest_rejects_unknown_top_level_field(self) -> None: + invalid = copy.deepcopy(self.manifest) + invalid["surprise"] = True + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "manifest.json" + path.write_text(json.dumps(invalid), encoding="utf-8") + with self.assertRaisesRegex(QualificationError, "manifest_unknown_field"): + load_manifest(path, ROOT) + + def test_summary_is_deterministic_and_sorts_comparisons(self) -> None: + graph = self.graph([]) + first = qualification_summary( + compass_revision="abc", + manifest_digest="sha256:manifest", + graph_bytes=b"{}\n", + graph=graph, + assertions={"z": 2, "a": 1}, + comparisons={"warm": True, "clean": True}, + ) + second = qualification_summary( + compass_revision="abc", + manifest_digest="sha256:manifest", + graph_bytes=b"{}\n", + graph=graph, + assertions={"a": 1, "z": 2}, + comparisons={"clean": True, "warm": True}, + ) + self.assertEqual(canonical_bytes(first), canonical_bytes(second)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/qualification/code-graph-v1-corpus.json b/tests/qualification/code-graph-v1-corpus.json new file mode 100644 index 00000000..c356cf71 --- /dev/null +++ b/tests/qualification/code-graph-v1-corpus.json @@ -0,0 +1,62 @@ +{ + "schema": "compass.code-graph-qualification-corpus/1", + "files": [ + {"id":"mcp-config","path":"matrix/mcp.json","language":"mcp-config","contents":"{\"mcpServers\":{\"matrix\":{\"command\":\"node\",\"env\":{\"MATRIX_MODE\":\"semantic\"}}}}\n"}, + {"id":"package-manifest","path":"matrix/pyproject.toml","language":"package-manifest","contents":"[project]\nname = \"matrix\"\n"}, + {"id":"drupal-routing","path":"matrix/example.routing.yml","language":"drupal-routing","contents":"example.route:\n path: /matrix\n defaults:\n _controller: 'Example::view'\n"}, + {"id":"play-routes","path":"matrix/conf/routes","language":"play-routes","contents":"GET /matrix controllers.Example.view()\n"}, + {"id":"drupal-php","path":"matrix/example.module","language":"php","contents":"
;\n"}, + {"id":"go","path":"matrix/sample.go","language":"go","contents":"package matrix\nfunc Example() {}\n"}, + {"id":"rust","path":"matrix/sample.rs","language":"rust","contents":"pub fn matrix() {}\n"}, + {"id":"java","path":"matrix/Sample.java","language":"java","contents":"class Sample {}\n"}, + {"id":"groovy","path":"matrix/sample.groovy","language":"groovy","contents":"class Sample {}\n"}, + {"id":"c","path":"matrix/sample.c","language":"c","contents":"int matrix(void) { return 1; }\n"}, + {"id":"cpp","path":"matrix/sample.cpp","language":"cpp","contents":"class Matrix {};\n"}, + {"id":"ruby","path":"matrix/sample.rb","language":"ruby","contents":"def matrix\nend\n"}, + {"id":"csharp","path":"matrix/Sample.cs","language":"csharp","contents":"class Sample {}\n"}, + {"id":"kotlin","path":"matrix/sample.kt","language":"kotlin","contents":"class Sample\n"}, + {"id":"scala","path":"matrix/Sample.scala","language":"scala","contents":"class Sample\n"}, + {"id":"php","path":"matrix/sample.php","language":"php","contents":"
\n"}, + {"id":"svelte","path":"matrix/Sample.svelte","language":"svelte","contents":"
matrix
\n"}, + {"id":"astro","path":"matrix/sample.astro","language":"astro","contents":"---\nconst value = 1;\n---\n
{value}
\n"}, + {"id":"dart","path":"matrix/sample.dart","language":"dart","contents":"void matrix() {}\n"}, + {"id":"verilog","path":"matrix/sample.sv","language":"verilog","contents":"module matrix; endmodule\n"}, + {"id":"sql","path":"matrix/sample.sql","language":"sql","contents":"CREATE TABLE matrix (id INTEGER);\n"}, + {"id":"r","path":"matrix/sample.r","language":"r","contents":"matrix_fn <- function() 1\n"}, + {"id":"markdown","path":"matrix/sample.md","language":"markdown","contents":"# Matrix\n"}, + {"id":"pascal","path":"matrix/sample.pas","language":"pascal","contents":"program Matrix;\nbegin\nend.\n"}, + {"id":"pascal-form","path":"matrix/sample.dfm","language":"pascal-form","contents":"object Form1: TForm1\nend\n"}, + {"id":"lazarus-package","path":"matrix/sample.lpk","language":"lazarus-package","contents":"\n"}, + {"id":"bash","path":"matrix/sample.sh","language":"bash","contents":"matrix() { :; }\n"}, + {"id":"json","path":"matrix/package.json","language":"json","contents":"{\"name\":\"matrix\",\"dependencies\":{\"example\":\"1.0.0\"}}\n"}, + {"id":"json-schema","path":"matrix/semantic-schema.json","language":"json","contents":"{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"title\":\"SemanticRecord\",\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}}}\n"}, + {"id":"terraform","path":"matrix/sample.tf","language":"terraform","contents":"variable \"matrix\" {}\n"}, + {"id":"dreammaker","path":"matrix/sample.dm","language":"dreammaker","contents":"world\n name = \"matrix\"\n"}, + {"id":"solution","path":"matrix/sample.sln","language":"solution","contents":"Microsoft Visual Studio Solution File, Format Version 12.00\n"}, + {"id":"project-xml","path":"matrix/sample.csproj","language":"project-xml","contents":"\n"}, + {"id":"xaml","path":"matrix/sample.xaml","language":"xaml","contents":"\n"}, + {"id":"razor","path":"matrix/sample.razor","language":"razor","contents":"
matrix
\n"}, + {"id":"apex","path":"matrix/Sample.cls","language":"apex","contents":"class Sample {}\n"} + ] +} diff --git a/tests/qualification/code-graph-v1-repositories.toml b/tests/qualification/code-graph-v1-repositories.toml new file mode 100644 index 00000000..da67d285 --- /dev/null +++ b/tests/qualification/code-graph-v1-repositories.toml @@ -0,0 +1,382 @@ +schema = "compass.code-graph-qualification/1" + +[[repository]] +name = "compass-polyglot-framework-corpus" +url = "https://github.com/crabbuild/compass.git" +commit = "9b9c9f788856417331628119d3e594d6fa563f0d" +size_class = "large" +language_family = "polyglot" +frameworks = [ + "django", "flask", "fastapi", "express", "nestjs", "laravel", "drupal", + "rails", "spring", "play", "gin", "chi", "gorilla-mux", "axum", "actix", + "rocket", "aspnet", "vapor", "react-router", "sveltekit", "vue-router", + "nuxt", "astro" +] + +[[repository.flows]] +framework = "django" +name = "Django function view" +query = "user_detail" +source = "fixtures/code-graph/routes/python/django/project/urls.py" +[[repository.flows]] +framework = "django" +name = "Django regular-expression route" +query = "health" +source = "fixtures/code-graph/routes/python/django/project/urls.py" +[[repository.flows]] +framework = "django" +name = "Django class-based view" +query = "AccountView" +source = "fixtures/code-graph/routes/python/django/project/urls.py" + +[[repository.flows]] +framework = "flask" +name = "Flask application route" +query = "health" +source = "fixtures/code-graph/routes/python/flask_app.py" +[[repository.flows]] +framework = "flask" +name = "Flask blueprint GET route" +query = "user_detail" +source = "fixtures/code-graph/routes/python/flask_app.py" +[[repository.flows]] +framework = "flask" +name = "Flask blueprint PATCH route" +query = "user_detail" +source = "fixtures/code-graph/routes/python/flask_app.py" + +[[repository.flows]] +framework = "fastapi" +name = "FastAPI application GET" +query = "health" +source = "fixtures/code-graph/routes/python/fastapi_app.py" +[[repository.flows]] +framework = "fastapi" +name = "FastAPI router POST" +query = "create_user" +source = "fixtures/code-graph/routes/python/fastapi_app.py" +[[repository.flows]] +framework = "fastapi" +name = "FastAPI dependency middleware" +query = "authenticate" +source = "fixtures/code-graph/routes/python/fastapi_app.py" + +[[repository.flows]] +framework = "express" +name = "Express application health" +query = "health" +source = "fixtures/code-graph/routes/typescript/express.ts" +[[repository.flows]] +framework = "express" +name = "Express router handler" +query = "showUser" +source = "fixtures/code-graph/routes/typescript/express.ts" +[[repository.flows]] +framework = "express" +name = "Express middleware chain" +query = "authenticate" +source = "fixtures/code-graph/routes/typescript/express.ts" + +[[repository.flows]] +framework = "nestjs" +name = "NestJS HTTP query" +query = "showUser" +source = "fixtures/code-graph/routes/typescript/nest.ts" +[[repository.flows]] +framework = "nestjs" +name = "NestJS HTTP mutation" +query = "createUser" +source = "fixtures/code-graph/routes/typescript/nest.ts" +[[repository.flows]] +framework = "nestjs" +name = "NestJS GraphQL resolver" +query = "user" +source = "fixtures/code-graph/routes/typescript/nest.ts" + +[[repository.flows]] +framework = "laravel" +name = "Laravel tuple route" +query = "UserController.index" +source = "fixtures/code-graph/routes/php/laravel.php" +[[repository.flows]] +framework = "laravel" +name = "Laravel controller action" +query = "UserController.store" +source = "fixtures/code-graph/routes/php/laravel.php" +[[repository.flows]] +framework = "laravel" +name = "Laravel resource expansion" +query = "PhotoController.show" +source = "fixtures/code-graph/routes/php/laravel.php" + +[[repository.flows]] +framework = "drupal" +name = "Drupal controller route" +query = "ExampleController.view" +source = "fixtures/code-graph/routes/php/drupal.routing.yml" +[[repository.flows]] +framework = "drupal" +name = "Drupal form route" +query = "ExampleForm" +source = "fixtures/code-graph/routes/php/drupal.routing.yml" +[[repository.flows]] +framework = "drupal" +name = "Drupal hook binding" +query = "hook_entity_type_build" +source = "fixtures/code-graph/routes/php/drupal.module" + +[[repository.flows]] +framework = "rails" +name = "Rails symbol route" +query = "UsersController.show" +source = "fixtures/code-graph/routes/ruby/rails.rb" +[[repository.flows]] +framework = "rails" +name = "Rails hash-rocket route" +query = "UsersController.create" +source = "fixtures/code-graph/routes/ruby/rails.rb" +[[repository.flows]] +framework = "rails" +name = "Rails namespace route" +query = "DashboardController.index" +source = "fixtures/code-graph/routes/ruby/rails.rb" + +[[repository.flows]] +framework = "spring" +name = "Spring GET mapping" +query = "SpringController.show" +source = "fixtures/code-graph/routes/jvm/SpringController.java" +[[repository.flows]] +framework = "spring" +name = "Spring POST mapping" +query = "SpringController.create" +source = "fixtures/code-graph/routes/jvm/SpringController.java" +[[repository.flows]] +framework = "spring" +name = "Spring multi-method mapping" +query = "SpringController.search" +source = "fixtures/code-graph/routes/jvm/SpringController.java" + +[[repository.flows]] +framework = "play" +name = "Play Java GET action" +query = "PlayController.show" +source = "fixtures/code-graph/routes/jvm/play/conf/routes" +[[repository.flows]] +framework = "play" +name = "Play Java POST action" +query = "PlayController.create" +source = "fixtures/code-graph/routes/jvm/play/conf/routes" +[[repository.flows]] +framework = "play" +name = "Play Scala GET action" +query = "ScalaController.show" +source = "fixtures/code-graph/routes/jvm/play/conf/routes" + +[[repository.flows]] +framework = "gin" +name = "Gin GET handler" +query = "listUsers" +source = "fixtures/code-graph/routes/go/gin.go" +[[repository.flows]] +framework = "gin" +name = "Gin POST handler" +query = "createUser" +source = "fixtures/code-graph/routes/go/gin.go" +[[repository.flows]] +framework = "gin" +name = "Gin middleware" +query = "auth" +source = "fixtures/code-graph/routes/go/gin.go" + +[[repository.flows]] +framework = "chi" +name = "Chi path parameter" +query = "showUser" +source = "fixtures/code-graph/routes/go/chi.go" +[[repository.flows]] +framework = "chi" +name = "Chi handler binding" +query = "Routes" +source = "fixtures/code-graph/routes/go/chi.go" +[[repository.flows]] +framework = "chi" +name = "Chi imported router" +query = "chi.Router" +source = "fixtures/code-graph/routes/go/chi.go" + +[[repository.flows]] +framework = "gorilla-mux" +name = "Gorilla PUT method" +query = "updateUser" +source = "fixtures/code-graph/routes/go/gorilla.go" +[[repository.flows]] +framework = "gorilla-mux" +name = "Gorilla PATCH method" +query = "updateUser" +source = "fixtures/code-graph/routes/go/gorilla.go" +[[repository.flows]] +framework = "gorilla-mux" +name = "Gorilla router registration" +query = "Routes" +source = "fixtures/code-graph/routes/go/gorilla.go" + +[[repository.flows]] +framework = "axum" +name = "Axum GET handler" +query = "show_user" +source = "fixtures/code-graph/routes/rust/axum.rs" +[[repository.flows]] +framework = "axum" +name = "Axum POST handler" +query = "create_user" +source = "fixtures/code-graph/routes/rust/axum.rs" +[[repository.flows]] +framework = "axum" +name = "Axum router builder" +query = "router" +source = "fixtures/code-graph/routes/rust/axum.rs" + +[[repository.flows]] +framework = "actix" +name = "Actix GET handler" +query = "list_users" +source = "fixtures/code-graph/routes/rust/actix.rs" +[[repository.flows]] +framework = "actix" +name = "Actix route builder" +query = "app" +source = "fixtures/code-graph/routes/rust/actix.rs" +[[repository.flows]] +framework = "actix" +name = "Actix web registration" +query = "list_users" +source = "fixtures/code-graph/routes/rust/actix.rs" + +[[repository.flows]] +framework = "rocket" +name = "Rocket route attribute" +query = "show_user" +source = "fixtures/code-graph/routes/rust/rocket.rs" +[[repository.flows]] +framework = "rocket" +name = "Rocket parameter route" +query = "show_user" +source = "fixtures/code-graph/routes/rust/rocket.rs" +[[repository.flows]] +framework = "rocket" +name = "Rocket GET handler" +query = "show_user" +source = "fixtures/code-graph/routes/rust/rocket.rs" + +[[repository.flows]] +framework = "aspnet" +name = "ASP.NET GET action" +query = "UsersController.Show" +source = "fixtures/code-graph/routes/csharp/AspNetController.cs" +[[repository.flows]] +framework = "aspnet" +name = "ASP.NET POST action" +query = "UsersController.Create" +source = "fixtures/code-graph/routes/csharp/AspNetController.cs" +[[repository.flows]] +framework = "aspnet" +name = "ASP.NET controller template" +query = "UsersController" +source = "fixtures/code-graph/routes/csharp/AspNetController.cs" + +[[repository.flows]] +framework = "vapor" +name = "Vapor GET handler" +query = "listUsers" +source = "fixtures/code-graph/routes/swift/VaporRoutes.swift" +[[repository.flows]] +framework = "vapor" +name = "Vapor POST handler" +query = "createUser" +source = "fixtures/code-graph/routes/swift/VaporRoutes.swift" +[[repository.flows]] +framework = "vapor" +name = "Vapor grouped health" +query = "routes" +source = "fixtures/code-graph/routes/swift/VaporRoutes.swift" + +[[repository.flows]] +framework = "react-router" +name = "React Router JSX route" +query = "AccountAlias" +source = "fixtures/code-graph/routes/typescript/react-router.tsx" +[[repository.flows]] +framework = "react-router" +name = "React Router data route" +query = "UserPage" +source = "fixtures/code-graph/routes/typescript/react-router.tsx" +[[repository.flows]] +framework = "react-router" +name = "React Router loader" +query = "loadUser" +source = "fixtures/code-graph/routes/typescript/react-router.tsx" + +[[repository.flows]] +framework = "sveltekit" +name = "SvelteKit page route" +query = "+page" +source = "fixtures/code-graph/routes/typescript/sveltekit/src/routes/users/[id]/+page.svelte" +[[repository.flows]] +framework = "sveltekit" +name = "SvelteKit server GET" +query = "GET" +source = "fixtures/code-graph/routes/typescript/sveltekit/src/routes/api/users/[id]/+server.ts" +[[repository.flows]] +framework = "sveltekit" +name = "SvelteKit catch-all page" +query = "rest" +source = "fixtures/code-graph/routes/typescript/sveltekit/src/routes/files/[...rest]/+page.svelte" + +[[repository.flows]] +framework = "vue-router" +name = "Vue Router component" +query = "UserPage" +source = "fixtures/code-graph/routes/typescript/vue-router.ts" +[[repository.flows]] +framework = "vue-router" +name = "Vue Router parameter path" +query = "users" +source = "fixtures/code-graph/routes/typescript/vue-router.ts" +[[repository.flows]] +framework = "vue-router" +name = "Vue Router history" +query = "router" +source = "fixtures/code-graph/routes/typescript/vue-router.ts" + +[[repository.flows]] +framework = "nuxt" +name = "Nuxt page route" +query = "users" +source = "fixtures/code-graph/routes/typescript/nuxt/pages/users/[id].vue" +[[repository.flows]] +framework = "nuxt" +name = "Nuxt GET endpoint" +query = "users.get" +source = "fixtures/code-graph/routes/typescript/nuxt/server/api/users.get.ts" +[[repository.flows]] +framework = "nuxt" +name = "Nuxt POST endpoint" +query = "users" +source = "fixtures/code-graph/routes/typescript/nuxt/server/api/users/[id].post.ts" + +[[repository.flows]] +framework = "astro" +name = "Astro page route" +query = "about" +source = "fixtures/code-graph/routes/typescript/astro/src/pages/about.astro" +[[repository.flows]] +framework = "astro" +name = "Astro GET endpoint" +query = "GET" +source = "fixtures/code-graph/routes/typescript/astro/src/pages/api/items/[id].ts" +[[repository.flows]] +framework = "astro" +name = "Astro dynamic page" +query = "slug" +source = "fixtures/code-graph/routes/typescript/astro/src/pages/blog/[slug].astro" diff --git a/tests/qualification/code-graph-v1-semantic.json b/tests/qualification/code-graph-v1-semantic.json new file mode 100644 index 00000000..3c32c44f --- /dev/null +++ b/tests/qualification/code-graph-v1-semantic.json @@ -0,0 +1,1637 @@ +{ + "schema": "compass.code-graph-qualification/2", + "flows": [ + { + "id": "flow-django", + "framework": "django", + "routeFramework": "django", + "operation": "ANY", + "path": "/qualification-health", + "routeSource": "fixtures/code-graph/routes/python/django/qualification/urls.py", + "handler": { + "qualifiedName": "qualification_health()" + }, + "handlerSource": "fixtures/code-graph/routes/python/django/qualification/urls.py", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "function", + "handlerLanguage": "python", + "resolution": "exact", + "origins": [ + "config" + ], + "producer": "compass.frameworks.django", + "rules": [ + "framework-route-stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-flask", + "framework": "flask", + "routeFramework": "flask", + "operation": "ANY", + "path": "/health", + "routeSource": "fixtures/code-graph/routes/python/flask_app.py", + "handler": { + "qualifiedName": "health()" + }, + "handlerSource": "fixtures/code-graph/routes/python/flask_app.py", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "function", + "handlerLanguage": "python", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.flask", + "rules": [ + "framework-route-stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-fastapi", + "framework": "fastapi", + "routeFramework": "fastapi", + "operation": "POST", + "path": "/v1/users", + "routeSource": "fixtures/code-graph/routes/python/fastapi_app.py", + "handler": { + "qualifiedName": "create_user()" + }, + "handlerSource": "fixtures/code-graph/routes/python/fastapi_app.py", + "relationship": "routes_to", + "stage": "handler", + "position": 1, + "handlerKind": "function", + "handlerLanguage": "python", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.fastapi", + "rules": [ + "framework-route-stage:handler:1" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-express", + "framework": "express", + "routeFramework": "express", + "operation": "GET", + "path": "/health", + "routeSource": "fixtures/code-graph/routes/typescript/express.ts", + "handler": { + "qualifiedName": "health()@324" + }, + "handlerSource": "fixtures/code-graph/routes/typescript/express.ts", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "function", + "handlerLanguage": "typescript", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.express", + "rules": [ + "framework-route-stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-nestjs", + "framework": "nestjs", + "routeFramework": "nestjs", + "operation": "GET", + "path": "/users/{userId}", + "routeSource": "fixtures/code-graph/routes/typescript/nest.ts", + "handler": { + "qualifiedName": "UsersController::showUser()@316" + }, + "handlerSource": "fixtures/code-graph/routes/typescript/nest.ts", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "method", + "handlerLanguage": "typescript", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.nestjs", + "rules": [ + "framework-route-stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-nestjs-graphql", + "framework": "nestjs-graphql", + "routeFramework": "nestjs-graphql", + "operation": "QUERY", + "path": "/graphql/user", + "routeSource": "fixtures/code-graph/routes/typescript/nest.ts", + "handler": { + "qualifiedName": "UsersResolver::user()@582" + }, + "handlerSource": "fixtures/code-graph/routes/typescript/nest.ts", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "method", + "handlerLanguage": "typescript", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.nestjs-graphql", + "rules": [ + "framework-route-stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-laravel", + "framework": "laravel", + "routeFramework": "laravel", + "operation": "GET", + "path": "/users", + "routeSource": "fixtures/code-graph/routes/php/laravel.php", + "handler": { + "qualifiedName": "UserController::index" + }, + "handlerSource": "fixtures/code-graph/routes/php/laravel.php", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "method", + "handlerLanguage": "php", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.laravel", + "rules": [ + "laravel-route-facade|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-drupal", + "framework": "drupal", + "routeFramework": "drupal", + "operation": "GET", + "path": "/examples/{example}", + "routeSource": "fixtures/code-graph/routes/php/drupal.routing.yml", + "handler": { + "qualifiedName": "ExampleController::view" + }, + "handlerSource": "fixtures/code-graph/routes/php/drupal.module", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "method", + "handlerLanguage": "php", + "resolution": "exact", + "origins": [ + "config" + ], + "producer": "compass.frameworks.drupal", + "rules": [ + "drupal-routing-yaml|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-rails", + "framework": "rails", + "routeFramework": "rails", + "operation": "GET", + "path": "/users/:id", + "routeSource": "fixtures/code-graph/routes/ruby/rails.rb", + "handler": { + "qualifiedName": "UsersController::show" + }, + "handlerSource": "fixtures/code-graph/routes/ruby/rails.rb", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "method", + "handlerLanguage": "ruby", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.rails", + "rules": [ + "rails-routes-dsl|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-spring", + "framework": "spring", + "routeFramework": "spring", + "operation": "GET", + "path": "/api/users/{id}", + "routeSource": "fixtures/code-graph/routes/jvm/SpringController.java", + "handler": { + "qualifiedName": "SpringController::show" + }, + "handlerSource": "fixtures/code-graph/routes/jvm/SpringController.java", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "method", + "handlerLanguage": "java", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.spring", + "rules": [ + "spring-request-mapping|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-play", + "framework": "play", + "routeFramework": "play", + "operation": "GET", + "path": "/java/:id", + "routeSource": "fixtures/code-graph/routes/jvm/play/conf/routes", + "handler": { + "qualifiedName": "PlayController::show" + }, + "handlerSource": "fixtures/code-graph/routes/jvm/PlayController.java", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "method", + "handlerLanguage": "java", + "resolution": "exact", + "origins": [ + "config" + ], + "producer": "compass.frameworks.play", + "rules": [ + "play-conf-routes|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-gin", + "framework": "gin", + "routeFramework": "gin", + "operation": "GET", + "path": "/api/users", + "routeSource": "fixtures/code-graph/routes/go/gin.go", + "handler": { + "qualifiedName": "listUsers()" + }, + "handlerSource": "fixtures/code-graph/routes/go/gin.go", + "relationship": "routes_to", + "stage": "handler", + "position": 1, + "handlerKind": "function", + "handlerLanguage": "go", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.gin", + "rules": [ + "gin-router-call|stage:handler:1" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-chi", + "framework": "chi", + "routeFramework": "chi", + "operation": "GET", + "path": "/users/{id}", + "routeSource": "fixtures/code-graph/routes/go/chi.go", + "handler": { + "qualifiedName": "showUser()" + }, + "handlerSource": "fixtures/code-graph/routes/go/chi.go", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "function", + "handlerLanguage": "go", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.chi", + "rules": [ + "chi-router-call|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-gorilla-mux", + "framework": "gorilla-mux", + "routeFramework": "gorilla", + "operation": "PUT", + "path": "/users/{id}", + "routeSource": "fixtures/code-graph/routes/go/gorilla.go", + "handler": { + "qualifiedName": "updateUser()" + }, + "handlerSource": "fixtures/code-graph/routes/go/gorilla.go", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "function", + "handlerLanguage": "go", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.gorilla", + "rules": [ + "gorilla-router-call|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-axum", + "framework": "axum", + "routeFramework": "axum", + "operation": "GET", + "path": "/users/:id", + "routeSource": "fixtures/code-graph/routes/rust/axum.rs", + "handler": { + "qualifiedName": "show_user()@43" + }, + "handlerSource": "fixtures/code-graph/routes/rust/axum.rs", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "function", + "handlerLanguage": "rust", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.axum", + "rules": [ + "rust-router-call|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-actix", + "framework": "actix", + "routeFramework": "actix", + "operation": "GET", + "path": "/users", + "routeSource": "fixtures/code-graph/routes/rust/actix.rs", + "handler": { + "qualifiedName": "list_users()@28" + }, + "handlerSource": "fixtures/code-graph/routes/rust/actix.rs", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "function", + "handlerLanguage": "rust", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.actix", + "rules": [ + "rust-router-call|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-rocket", + "framework": "rocket", + "routeFramework": "rocket", + "operation": "GET", + "path": "/users/", + "routeSource": "fixtures/code-graph/routes/rust/rocket.rs", + "handler": { + "qualifiedName": "show_user(u64)@40" + }, + "handlerSource": "fixtures/code-graph/routes/rust/rocket.rs", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "function", + "handlerLanguage": "rust", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.rocket", + "rules": [ + "rust-route-attribute|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-aspnet", + "framework": "aspnet", + "routeFramework": "aspnet", + "operation": "GET", + "path": "/api/Users/{id}", + "routeSource": "fixtures/code-graph/routes/csharp/AspNetController.cs", + "handler": { + "qualifiedName": "UsersController::Show(string)@129" + }, + "handlerSource": "fixtures/code-graph/routes/csharp/AspNetController.cs", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "method", + "handlerLanguage": "csharp", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.aspnet", + "rules": [ + "aspnet-http-attribute|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-vapor", + "framework": "vapor", + "routeFramework": "vapor", + "operation": "GET", + "path": "/api/users", + "routeSource": "fixtures/code-graph/routes/swift/VaporRoutes.swift", + "handler": { + "qualifiedName": "listUsers()" + }, + "handlerSource": "fixtures/code-graph/routes/swift/VaporRoutes.swift", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "function", + "handlerLanguage": "swift", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.vapor", + "rules": [ + "vapor-route-call|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-react-router", + "framework": "react-router", + "routeFramework": "react-router", + "operation": "PAGE", + "path": "/accounts/{accountId}", + "routeSource": "fixtures/code-graph/routes/typescript/react-router.tsx", + "handler": { + "qualifiedName": "AccountPage()@7" + }, + "handlerSource": "fixtures/code-graph/routes/typescript/AccountPage.tsx", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "function", + "handlerLanguage": "tsx", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.react-router", + "rules": [ + "framework-route-stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-sveltekit", + "framework": "sveltekit", + "routeFramework": "sveltekit", + "operation": "GET", + "path": "/api/users/{id}", + "routeSource": "fixtures/code-graph/routes/typescript/sveltekit/src/routes/api/users/[id]/+server.ts", + "handler": { + "qualifiedName": "sveltekit::route-component::GET::/api/users/{id}" + }, + "handlerSource": "fixtures/code-graph/routes/typescript/sveltekit/src/routes/api/users/[id]/+server.ts", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "component", + "handlerLanguage": "typescript", + "resolution": "exact", + "origins": [ + "convention" + ], + "producer": "compass.frameworks.sveltekit", + "rules": [ + "sveltekit-endpoint-convention|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-vue-router", + "framework": "vue-router", + "routeFramework": "vue-router", + "operation": "PAGE", + "path": "/qualified-users/{userId}", + "routeSource": "fixtures/code-graph/routes/typescript/vue-router-qualified.ts", + "handler": { + "qualifiedName": "QualifiedUserPage()@62" + }, + "handlerSource": "fixtures/code-graph/routes/typescript/vue-router-qualified.ts", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "function", + "handlerLanguage": "typescript", + "resolution": "exact", + "origins": [ + "ast" + ], + "producer": "compass.frameworks.vue-router", + "rules": [ + "framework-route-stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-nuxt", + "framework": "nuxt", + "routeFramework": "nuxt", + "operation": "GET", + "path": "/users", + "routeSource": "fixtures/code-graph/routes/typescript/nuxt/server/api/users.get.ts", + "handler": { + "qualifiedName": "nuxt::route-component::GET::/users" + }, + "handlerSource": "fixtures/code-graph/routes/typescript/nuxt/server/api/users.get.ts", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "component", + "handlerLanguage": "typescript", + "resolution": "exact", + "origins": [ + "convention" + ], + "producer": "compass.frameworks.nuxt", + "rules": [ + "nuxt-server-api-convention|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + }, + { + "id": "flow-astro", + "framework": "astro", + "routeFramework": "astro", + "operation": "GET", + "path": "/api/items/{id}", + "routeSource": "fixtures/code-graph/routes/typescript/astro/src/pages/api/items/[id].ts", + "handler": { + "qualifiedName": "astro::route-component::GET::/api/items/{id}" + }, + "handlerSource": "fixtures/code-graph/routes/typescript/astro/src/pages/api/items/[id].ts", + "relationship": "routes_to", + "stage": "handler", + "position": 0, + "handlerKind": "component", + "handlerLanguage": "typescript", + "resolution": "exact", + "origins": [ + "convention" + ], + "producer": "compass.frameworks.astro", + "rules": [ + "astro-endpoint-convention|stage:handler:0" + ], + "allowHeuristic": false, + "candidates": [] + } + ], + "negatives": [ + { + "id": "negative-django", + "framework": "django", + "source": "fixtures/code-graph/routes/python/near_matches.py", + "routeFramework": "django" + }, + { + "id": "negative-flask", + "framework": "flask", + "source": "fixtures/code-graph/routes/python/near_matches.py", + "routeFramework": "flask" + }, + { + "id": "negative-fastapi", + "framework": "fastapi", + "source": "fixtures/code-graph/routes/python/near_matches.py", + "routeFramework": "fastapi" + }, + { + "id": "negative-express", + "framework": "express", + "source": "fixtures/code-graph/routes/typescript/near-matches.ts", + "routeFramework": "express" + }, + { + "id": "negative-nestjs", + "framework": "nestjs", + "source": "fixtures/code-graph/routes/typescript/near-matches.ts", + "routeFramework": "nestjs" + }, + { + "id": "negative-laravel", + "framework": "laravel", + "source": "fixtures/code-graph/routes/php/near_matches.php", + "routeFramework": "laravel" + }, + { + "id": "negative-drupal", + "framework": "drupal", + "source": "fixtures/code-graph/routes/php/near_matches.php", + "routeFramework": "drupal" + }, + { + "id": "negative-rails", + "framework": "rails", + "source": "fixtures/code-graph/routes/ruby/near_matches.rb", + "routeFramework": "rails" + }, + { + "id": "negative-spring", + "framework": "spring", + "source": "fixtures/code-graph/routes/jvm/NearMatches.java", + "routeFramework": "spring" + }, + { + "id": "negative-play", + "framework": "play", + "source": "fixtures/code-graph/routes/jvm/NearMatches.java", + "routeFramework": "play" + }, + { + "id": "negative-gin", + "framework": "gin", + "source": "fixtures/code-graph/routes/go/near_matches.go", + "routeFramework": "gin" + }, + { + "id": "negative-chi", + "framework": "chi", + "source": "fixtures/code-graph/routes/go/near_matches.go", + "routeFramework": "chi" + }, + { + "id": "negative-gorilla-mux", + "framework": "gorilla-mux", + "source": "fixtures/code-graph/routes/go/near_matches.go", + "routeFramework": "gorilla" + }, + { + "id": "negative-axum", + "framework": "axum", + "source": "fixtures/code-graph/routes/rust/near_matches.rs", + "routeFramework": "axum" + }, + { + "id": "negative-actix", + "framework": "actix", + "source": "fixtures/code-graph/routes/rust/near_matches.rs", + "routeFramework": "actix" + }, + { + "id": "negative-rocket", + "framework": "rocket", + "source": "fixtures/code-graph/routes/rust/near_matches.rs", + "routeFramework": "rocket" + }, + { + "id": "negative-aspnet", + "framework": "aspnet", + "source": "fixtures/code-graph/routes/csharp/NearMatches.cs", + "routeFramework": "aspnet" + }, + { + "id": "negative-vapor", + "framework": "vapor", + "source": "fixtures/code-graph/routes/swift/NearMatches.swift", + "routeFramework": "vapor" + }, + { + "id": "negative-react-router", + "framework": "react-router", + "source": "fixtures/code-graph/routes/typescript/near-matches.ts", + "routeFramework": "react-router" + }, + { + "id": "negative-sveltekit", + "framework": "sveltekit", + "source": "fixtures/code-graph/routes/typescript/near-matches.ts", + "routeFramework": "sveltekit" + }, + { + "id": "negative-vue-router", + "framework": "vue-router", + "source": "fixtures/code-graph/routes/typescript/near-matches.ts", + "routeFramework": "vue-router" + }, + { + "id": "negative-nuxt", + "framework": "nuxt", + "source": "fixtures/code-graph/routes/typescript/near-matches.ts", + "routeFramework": "nuxt" + }, + { + "id": "negative-astro", + "framework": "astro", + "source": "fixtures/code-graph/routes/typescript/near-matches.ts", + "routeFramework": "astro" + } + ], + "nodeProducers": [ + { + "id": "node-file", + "kind": "file", + "source": "fixtures/code-graph/qualification/MissingReference.csproj", + "qualifiedName": "MissingReference.csproj", + "producer": "compass.languages.project-xml", + "origins": [ + "ast" + ], + "detailType": "file" + }, + { + "id": "node-module", + "kind": "module", + "source": "fixtures/code-graph/routes/swift/VaporRoutes.swift", + "qualifiedName": "Vapor", + "producer": "compass.languages.swift", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-package", + "kind": "package", + "source": "matrix/pyproject.toml", + "qualifiedName": "matrix", + "producer": "compass.languages.package-manifest", + "origins": [ + "config" + ], + "detailType": "symbol" + }, + { + "id": "node-namespace", + "kind": "namespace", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "Qualification", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-class", + "kind": "class", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "BaseService@84", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-struct", + "kind": "struct", + "source": "fixtures/code-graph/qualification/rich.rs", + "qualifiedName": "Receipt@101", + "producer": "compass.languages.rust", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-interface", + "kind": "interface", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "IService@26", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-trait", + "kind": "trait", + "source": "fixtures/code-graph/qualification/rich.rs", + "qualifiedName": "Renderable@0", + "producer": "compass.languages.rust", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-protocol", + "kind": "protocol", + "source": "matrix/objc.h", + "qualifiedName": "", + "producer": "compass.languages.objc", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-enum", + "kind": "enum", + "source": "fixtures/code-graph/qualification/rich.rs", + "qualifiedName": "Mode@147", + "producer": "compass.languages.rust", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-enum_member", + "kind": "enum_member", + "source": "fixtures/code-graph/qualification/rich.rs", + "qualifiedName": "Mode@147::Fast@167", + "producer": "compass.languages.rust", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-type_alias", + "kind": "type_alias", + "source": "fixtures/code-graph/qualification/rich.rs", + "qualifiedName": "WidgetId@186", + "producer": "compass.languages.rust", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-function", + "kind": "function", + "source": "fixtures/code-graph/qualification/mcp.json", + "qualifiedName": "node", + "producer": "compass.languages.mcp-config", + "origins": [ + "config" + ], + "detailType": "symbol" + }, + { + "id": "node-method", + "kind": "method", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "BaseService::Run(int)@115", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-constructor", + "kind": "constructor", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "Service::Service(int)@327", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-property", + "kind": "property", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "Service@176::Count@291", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-field", + "kind": "field", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "Service@176::count@280", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-variable", + "kind": "variable", + "source": "fixtures/code-graph/routes/typescript/express.ts", + "qualifiedName": "app", + "producer": "compass.languages.typescript", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-constant", + "kind": "constant", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "Service@176::Limit@244", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-parameter", + "kind": "parameter", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "BaseService::Run(int)@115::value@141", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-import", + "kind": "import", + "source": "fixtures/code-graph/domain/messaging/nest.ts", + "qualifiedName": "@nestjs/common.Controller", + "producer": "compass.frameworks.typescript.imports", + "origins": [ + "ast" + ], + "detailType": "import_export" + }, + { + "id": "node-export", + "kind": "export", + "source": "fixtures/code-graph/qualification/rich.ts", + "qualifiedName": "DefaultStore@271", + "producer": "compass.languages.typescript", + "origins": [ + "ast" + ], + "detailType": "import_export" + }, + { + "id": "node-macro", + "kind": "macro", + "source": "fixtures/code-graph/qualification/rich.rs", + "qualifiedName": "widget@252", + "producer": "compass.languages.rust", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-annotation", + "kind": "annotation", + "source": "fixtures/code-graph/qualification/rich.ts", + "qualifiedName": "sealed@159", + "producer": "compass.languages.typescript", + "origins": [ + "ast" + ], + "detailType": "symbol" + }, + { + "id": "node-route", + "kind": "route", + "source": "fixtures/code-graph/routes/csharp/AspNetController.cs", + "qualifiedName": "aspnet::GET::/api/Users/{id}", + "producer": "compass.frameworks.aspnet", + "origins": [ + "ast" + ], + "detailType": "route" + }, + { + "id": "node-component", + "kind": "component", + "source": "fixtures/code-graph/qualification/mcp.json", + "qualifiedName": "qualification", + "producer": "compass.languages.mcp-config", + "origins": [ + "config" + ], + "detailType": "component" + }, + { + "id": "node-event", + "kind": "event", + "source": "fixtures/code-graph/domain/messaging/nest.ts", + "qualifiedName": "nestjs::event::orders.cancelled", + "producer": "compass.frameworks.nestjs.domain", + "origins": [ + "ast" + ], + "detailType": "messaging" + }, + { + "id": "node-message", + "kind": "message", + "source": "fixtures/code-graph/domain/messaging/nest.ts", + "qualifiedName": "nestjs::message::orders.created", + "producer": "compass.frameworks.nestjs.domain", + "origins": [ + "ast" + ], + "detailType": "messaging" + }, + { + "id": "node-topic", + "kind": "topic", + "source": "fixtures/code-graph/domain/messaging/spring.java", + "qualifiedName": "spring::topic::orders.created", + "producer": "compass.frameworks.spring.domain", + "origins": [ + "ast" + ], + "detailType": "messaging" + }, + { + "id": "node-queue", + "kind": "queue", + "source": "fixtures/code-graph/domain/messaging/spring.java", + "qualifiedName": "spring::queue::orders.queue", + "producer": "compass.frameworks.spring.domain", + "origins": [ + "ast" + ], + "detailType": "messaging" + }, + { + "id": "node-job", + "kind": "job", + "source": "fixtures/code-graph/domain/jobs/aspnet.cs", + "qualifiedName": "aspnet::job::RebuildIndex", + "producer": "compass.frameworks.aspnet.domain", + "origins": [ + "ast" + ], + "detailType": "job" + }, + { + "id": "node-resource", + "kind": "resource", + "source": "fixtures/code-graph/qualification/guide.md", + "qualifiedName": "Qualification guide", + "producer": "compass.languages.markdown", + "origins": [ + "artifact" + ], + "detailType": "resource" + }, + { + "id": "node-schema", + "kind": "schema", + "source": "matrix/semantic-schema.json", + "qualifiedName": "semantic-schema.json", + "producer": "compass.languages.json-config", + "origins": [ + "config" + ], + "detailType": "schema" + }, + { + "id": "node-query", + "kind": "query", + "source": "fixtures/code-graph/qualification/migrations/001_init.sql", + "qualifiedName": "fixtures/code-graph/qualification/migrations/001_init::select@95", + "producer": "compass.languages.sql", + "origins": [ + "artifact" + ], + "detailType": "query" + }, + { + "id": "node-migration", + "kind": "migration", + "source": "fixtures/code-graph/qualification/migrations/001_init.sql", + "qualifiedName": "fixtures/code-graph/qualification/migrations/001_init::001_init.sql", + "producer": "compass.languages.sql", + "origins": [ + "convention" + ], + "detailType": "symbol" + }, + { + "id": "node-config_key", + "kind": "config_key", + "source": "fixtures/code-graph/qualification/mcp.json", + "qualifiedName": "mcpServers.qualification.env.QUALIFICATION_MODE", + "producer": "compass.languages.mcp-config", + "origins": [ + "config" + ], + "detailType": "config" + }, + { + "id": "node-database", + "kind": "database", + "source": "fixtures/code-graph/qualification/migrations/001_init.sql", + "qualifiedName": "fixtures/code-graph/qualification/migrations/001_init", + "producer": "compass.languages.sql", + "origins": [ + "convention" + ], + "detailType": "database" + }, + { + "id": "node-database_schema", + "kind": "database_schema", + "source": "fixtures/code-graph/qualification/migrations/001_init.sql", + "qualifiedName": "CaseSensitive", + "producer": "compass.languages.sql", + "origins": [ + "artifact" + ], + "detailType": "database" + }, + { + "id": "node-database_table", + "kind": "database_table", + "source": "fixtures/code-graph/qualification/migrations/001_init.sql", + "qualifiedName": "CaseSensitive.User", + "producer": "compass.languages.sql", + "origins": [ + "artifact" + ], + "detailType": "database" + }, + { + "id": "node-database_view", + "kind": "database_view", + "source": "fixtures/code-graph/domain/database.sql", + "qualifiedName": "accounting.account_owners", + "producer": "compass.languages.sql", + "origins": [ + "artifact" + ], + "detailType": "database" + }, + { + "id": "node-database_column", + "kind": "database_column", + "source": "fixtures/code-graph/qualification/migrations/001_init.sql", + "qualifiedName": "\"CaseSensitive\".\"User\".\"Id\"", + "producer": "compass.languages.sql", + "origins": [ + "artifact" + ], + "detailType": "database" + }, + { + "id": "node-database_index", + "kind": "database_index", + "source": "fixtures/code-graph/domain/database.sql", + "qualifiedName": "accounts_owner_idx", + "producer": "compass.languages.sql", + "origins": [ + "artifact" + ], + "detailType": "database" + }, + { + "id": "node-database_constraint", + "kind": "database_constraint", + "source": "fixtures/code-graph/domain/database.sql", + "qualifiedName": "\"accounting\".\"accounts\"::\"accounts_owner_fk\"", + "producer": "compass.languages.sql", + "origins": [ + "artifact" + ], + "detailType": "database" + }, + { + "id": "node-database_procedure", + "kind": "database_procedure", + "source": "fixtures/code-graph/domain/database.sql", + "qualifiedName": "accounting.refresh_accounts", + "producer": "compass.languages.sql", + "origins": [ + "artifact" + ], + "detailType": "database" + }, + { + "id": "node-database_trigger", + "kind": "database_trigger", + "source": "fixtures/code-graph/domain/database.sql", + "qualifiedName": "accounts_audit", + "producer": "compass.languages.sql", + "origins": [ + "artifact" + ], + "detailType": "database" + } + ], + "edgeProducers": [ + { + "id": "edge-contains", + "kind": "contains", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "*", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-calls", + "kind": "calls", + "source": "fixtures/code-graph/qualification/rich.rs", + "qualifiedName": "*", + "producer": "compass.languages.rust", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-imports", + "kind": "imports", + "source": "fixtures/code-graph/qualification/MissingReference.csproj", + "qualifiedName": "*", + "producer": "compass.languages.project-xml", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-exports", + "kind": "exports", + "source": "fixtures/code-graph/qualification/export.dart", + "qualifiedName": "*", + "producer": "compass.languages.dart", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-extends", + "kind": "extends", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "*", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-implements", + "kind": "implements", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "*", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-references", + "kind": "references", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "*", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-type_of", + "kind": "type_of", + "source": "fixtures/code-graph/qualification/rich.rs", + "qualifiedName": "*", + "producer": "compass.languages.rust", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-returns", + "kind": "returns", + "source": "fixtures/code-graph/qualification/rich.rs", + "qualifiedName": "*", + "producer": "compass.languages.rust", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-instantiates", + "kind": "instantiates", + "source": "fixtures/code-graph/domain/messaging/dynamic-near-matches.ts", + "qualifiedName": "*", + "producer": "compass.languages.typescript", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-overrides", + "kind": "overrides", + "source": "fixtures/code-graph/qualification/Rich.cs", + "qualifiedName": "*", + "producer": "compass.languages.csharp", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-decorates", + "kind": "decorates", + "source": "fixtures/code-graph/qualification/rich.ts", + "qualifiedName": "*", + "producer": "compass.languages.typescript", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-routes_to", + "kind": "routes_to", + "source": "fixtures/code-graph/routes/csharp/AspNetController.cs", + "qualifiedName": "*", + "producer": "compass.frameworks.aspnet", + "origins": [ + "ast" + ], + "detailType": "route" + }, + { + "id": "edge-reads", + "kind": "reads", + "source": "fixtures/code-graph/qualification/migrations/001_init.sql", + "qualifiedName": "*", + "producer": "compass.languages.sql", + "origins": [ + "artifact" + ], + "detailType": null + }, + { + "id": "edge-writes", + "kind": "writes", + "source": "fixtures/code-graph/domain/database.sql", + "qualifiedName": "*", + "producer": "compass.languages.sql", + "origins": [ + "artifact" + ], + "detailType": null + }, + { + "id": "edge-aliases", + "kind": "aliases", + "source": "fixtures/code-graph/qualification/rich.ts", + "qualifiedName": "*", + "producer": "compass.languages.typescript", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-registers", + "kind": "registers", + "source": "fixtures/code-graph/domain/messaging/nest.ts", + "qualifiedName": "*", + "producer": "compass.frameworks.nestjs.domain", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-handles", + "kind": "handles", + "source": "fixtures/code-graph/domain/messaging/nest.ts", + "qualifiedName": "*", + "producer": "compass.frameworks.nestjs.domain", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-publishes", + "kind": "publishes", + "source": "fixtures/code-graph/domain/messaging/nest.ts", + "qualifiedName": "*", + "producer": "compass.frameworks.nestjs.domain", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-subscribes", + "kind": "subscribes", + "source": "fixtures/code-graph/domain/messaging/nest.ts", + "qualifiedName": "*", + "producer": "compass.frameworks.nestjs.domain", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-produces", + "kind": "produces", + "source": "fixtures/code-graph/domain/messaging/nest.ts", + "qualifiedName": "*", + "producer": "compass.frameworks.nestjs.domain", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-consumes", + "kind": "consumes", + "source": "fixtures/code-graph/domain/messaging/spring.java", + "qualifiedName": "*", + "producer": "compass.frameworks.spring.domain", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-schedules", + "kind": "schedules", + "source": "fixtures/code-graph/domain/jobs/aspnet.cs", + "qualifiedName": "*", + "producer": "compass.frameworks.aspnet.domain", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-triggers", + "kind": "triggers", + "source": "fixtures/code-graph/domain/database.sql", + "qualifiedName": "*", + "producer": "compass.languages.sql", + "origins": [ + "artifact" + ], + "detailType": null + }, + { + "id": "edge-tests", + "kind": "tests", + "source": "fixtures/code-graph/qualification/rich.rs", + "qualifiedName": "*", + "producer": "compass.languages.rust", + "origins": [ + "ast" + ], + "detailType": null + }, + { + "id": "edge-depends_on", + "kind": "depends_on", + "source": "fixtures/code-graph/qualification/mcp.json", + "qualifiedName": "*", + "producer": "compass.languages.mcp-config", + "origins": [ + "config" + ], + "detailType": null + }, + { + "id": "edge-documents", + "kind": "documents", + "source": "fixtures/code-graph/qualification/semantic-doc.md", + "qualifiedName": "*", + "producer": "compass.languages.markdown", + "origins": [ + "artifact" + ], + "detailType": null + }, + { + "id": "edge-maps_to", + "kind": "maps_to", + "source": "fixtures/code-graph/domain/orm/active-record.rb", + "qualifiedName": "*", + "producer": "compass.frameworks.active-record.domain", + "origins": [ + "ast" + ], + "detailType": null + } + ], + "languages": { + "source": "corpus", + "producerVersion": "compass-languages/0.1.10" + }, + "occurrences": [ + { + "id": "repeated-rust-call", + "kind": "calls", + "source": "fixtures/code-graph/qualification/rich.rs", + "sourceQualifiedName": "Widget as Renderable::render(_)@345", + "targetQualifiedName": "target()@441", + "minimum": 2 + } + ], + "coverage": [ + { + "id": "recovery-not-complete", + "source": "fixtures/code-graph/qualification/recovery.py", + "forbidCompleteWhen": [ + "partial", + "parse_failure" + ] + }, + { + "id": "empty-file-supported", + "source": "fixtures/code-graph/qualification/empty.py", + "extractionStatus": "extracted" + }, + { + "id": "missing-reference-diagnostic", + "source": "fixtures/code-graph/qualification/MissingReference.csproj", + "diagnosticCode": "unresolved_external_reference" + } + ], + "limits": { + "maxDiagnostics": 64 + } +} diff --git a/tests/viewer/fixtures/generate.ts b/tests/viewer/fixtures/generate.ts index 33e704a0..a9b5aeb3 100644 --- a/tests/viewer/fixtures/generate.ts +++ b/tests/viewer/fixtures/generate.ts @@ -43,7 +43,21 @@ export default async function generate(): Promise { { id: "helper", label: "helper", kind: "function", community: 0, degree: 2, language: "rust", signature: "fn helper()", size: 24, - source: { file: "src/lib.rs", startLine: 5, endLine: 7 } + source: { file: "src/lib.rs", startLine: 5, endLine: 7 }, + codeEvidence: [{ + layer: "structural_graph", + origin: "ast", + extractor: "rust.functions", + confidence: "exact", + anchor: { + file: "src/lib.rs", startByte: 40, endByte: 64, + startLine: 5, startColumn: 0, endLine: 7, endColumn: 1 + }, + rule: null, + wiringSite: null, + resolution: "exact", + candidates: [] + }] }, { id: "file-only", label: "README", kind: "document", community: 1, degree: 1, @@ -53,7 +67,28 @@ export default async function generate(): Promise { ], edges: [ { id: "e1", source: "run", target: "helper", relation: "calls", confidence: "extracted" }, - { id: "e2", source: "helper", target: "store", relation: "uses", confidence: "inferred" }, + { + id: "e2", source: "helper", target: "store", relation: "uses", + confidence: "inferred", + codeEvidence: [{ + layer: "structural_graph", + origin: "heuristic", + extractor: "rust.dynamic-dispatch", + confidence: "ambiguous", + anchor: null, + rule: "trait-object-call", + wiringSite: { + file: "src/lib.rs", startByte: 52, endByte: 60, + startLine: 6, startColumn: 2, endLine: 6, endColumn: 10 + }, + resolution: "ambiguous", + candidates: [{ + nodeId: "store", + reason: "compatible receiver type", + confidence: "ambiguous" + }] + }] + }, { id: "e3", source: "run", target: "file-only", relation: "documents", confidence: "ambiguous" } ], communities: [ @@ -659,9 +694,18 @@ window.fixtureTimeline=${JSON.stringify(timeline)}; window.historyGraphs=${JSON.stringify(graphs)}; window.historyCommunityGraphs=${JSON.stringify(details)}; window.historyHostMessages=[]; +window.pendingHistoryGraph=null; window.historyBootstrapAttempts=0; window.historyGeneration=0; window.emitHistoryMessage=(message)=>window.postMessage(message,"*"); +window.releaseHistoryGraph=()=>{ + if(!window.pendingHistoryGraph) { + throw new Error("No history graph response is pending"); + } + const response=window.pendingHistoryGraph; + window.pendingHistoryGraph=null; + window.postMessage(response,"*"); +}; window.acquireVsCodeApi=()=>({postMessage(message){ window.historyHostMessages.push(message); if(message.type==="ready" || message.type==="retryTimeline") { @@ -698,7 +742,7 @@ window.acquireVsCodeApi=()=>({postMessage(message){ },120); } else if(message.type==="loadRevision") { const loadScenario=new URLSearchParams(window.location.search).get("load"); - const delay=loadScenario==="slow" ? 500 : message.commit.startsWith("a") ? 180 : 120; + const delay=message.commit.startsWith("a") ? 180 : 120; if(loadScenario==="error") { setTimeout(()=>window.postMessage({ type:"error", @@ -707,13 +751,18 @@ window.acquireVsCodeApi=()=>({postMessage(message){ message:"Fixture graph load failed" },"*"),delay); } else { - setTimeout(()=>window.postMessage({ + const response={ type:"graph", commit:message.commit, realization:"r-"+message.commit.slice(0,1), fingerprint:"f-"+message.commit.slice(0,1), graph:window.historyGraphs[message.commit] - },"*"),delay); + }; + if(loadScenario==="manual") { + window.pendingHistoryGraph=response; + } else { + setTimeout(()=>window.postMessage(response,"*"),delay); + } } } else if(message.type==="changeCounts") { setTimeout(()=>window.postMessage({ diff --git a/tests/viewer/history.spec.ts b/tests/viewer/history.spec.ts index 0cc14594..8b84fdd1 100644 --- a/tests/viewer/history.spec.ts +++ b/tests/viewer/history.spec.ts @@ -1,17 +1,26 @@ import { expect, test } from "@playwright/test"; test("timeline automatically opens an available revision without building", async ({ page }) => { - await page.goto("/history.html?load=slow"); - await expect(page.getByRole("listbox", { name: "Git commit timeline" })).toBeVisible(); - await expect(page.getByLabel("Revision A graph").getByText("graph available")).toBeVisible(); - await expect(page.getByRole("status")).toContainText("Loading Revision A graph"); - await expect(page.getByText(/Viewing graph for aaaaaaaaa/)).toBeVisible(); + await page.goto("/history.html?load=manual"); await expect.poll(() => page.evaluate(() => { const messages = (window as typeof window & { historyHostMessages: Array>; }).historyHostMessages; return messages.filter((message) => message.type === "loadRevision").length; })).toBe(1); + const loadingState = page.locator( + ".history-graph-frame > .workbench-state[data-kind='running']" + ); + await expect( + loadingState.getByRole("heading", { name: "Loading Revision A graph", exact: true }) + ).toBeVisible(); + await expect(loadingState).toContainText("Compass is opening the stored graph"); + await expect(page.getByRole("listbox", { name: "Git commit timeline" })).toBeVisible(); + await expect(page.getByLabel("Revision A graph").getByText("graph available")).toBeVisible(); + await page.evaluate(() => { + (window as typeof window & { releaseHistoryGraph(): void }).releaseHistoryGraph(); + }); + await expect(page.getByText(/Viewing graph for aaaaaaaaa/)).toBeVisible(); }); test("historical graph lazily enters a community and returns to its overview", async ({ page }) => { diff --git a/tests/viewer/query.spec.ts b/tests/viewer/query.spec.ts index 45548ad6..76fdaa79 100644 --- a/tests/viewer/query.spec.ts +++ b/tests/viewer/query.spec.ts @@ -126,3 +126,18 @@ test("traversal answers expose graph and source actions", async ({ page }) => { window as typeof window & { openedQueryGraph?: boolean } ).openedQueryGraph)).toBe(true); }); + +test("typed graph query evidence exposes exact and heuristic provenance", async ({ page }) => { + await page.goto("/graph.html"); + await page.getByRole("combobox", { name: "Search graph nodes" }).fill("helper"); + await page.getByRole("option", { name: /helper/i }).click(); + await expect(page.getByRole("region", { name: "Node evidence" })) + .toContainText("Exact"); + await expect(page.getByRole("region", { name: "Node evidence" })) + .toContainText("rust.functions"); + const relationships = page.getByRole("region", { name: "Relationship evidence" }); + await expect(relationships).toContainText("Ambiguous"); + await expect(relationships).toContainText("trait-object-call"); + await expect(relationships).toContainText("Wired at src/lib.rs:6"); + await expect(relationships).toContainText("compatible receiver type"); +}); diff --git a/vendor/gemm-common/COMPASS_PATCH.md b/vendor/gemm-common/COMPASS_PATCH.md new file mode 100644 index 00000000..b05e9af6 --- /dev/null +++ b/vendor/gemm-common/COMPASS_PATCH.md @@ -0,0 +1,17 @@ +# Compass patch provenance + +This directory contains the crates.io source for `gemm-common` 0.19.0, +published from upstream commit `86102c5b712737978371ac9ef7a11982f686d7bc`. +The unmodified source is MIT licensed; the upstream license is retained as +`LICENSE`. + +Compass carries one platform-correctness patch in `src/simd.rs`: the four +AArch64 vector FP16 helpers are annotated with +`#[target_feature(enable = "neon,fp16")]`. Their callers already select the +FP16 path through runtime `feature_detected!("fp16")` dispatch. The annotations +therefore let LLVM assemble the guarded functions without making FP16 a +process-wide build requirement or allowing an unsupported CPU to execute +those instructions. + +Remove this patch when a released `gemm-common` version contains the same +annotations and Candle can consume it. diff --git a/vendor/gemm-common/Cargo.toml b/vendor/gemm-common/Cargo.toml new file mode 100644 index 00000000..a74fdbcb --- /dev/null +++ b/vendor/gemm-common/Cargo.toml @@ -0,0 +1,110 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "gemm-common" +version = "0.19.0" +authors = ["sarah <>"] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Playground for matrix multiplication algorithms" +readme = "README.md" +keywords = ["linear-algebra"] +license = "MIT" +repository = "https://github.com/sarah-ek/gemm/" + +[features] +default = [ + "std", + "rayon", + "f16", +] +experimental-apple-amx = ["std"] +f16 = ["half"] +rayon = [ + "dep:rayon", + "std", +] +std = [ + "pulp/std", + "dyn-stack/std", + "once_cell/std", + "sysctl", +] +wasm-simd128-enable = [] +x86-v4 = ["pulp/x86-v4"] + +[lib] +name = "gemm_common" +path = "src/lib.rs" + +[dependencies.bytemuck] +version = "1.14" + +[dependencies.dyn-stack] +version = "0.13" +features = ["alloc"] +default-features = false + +[dependencies.half] +version = "2.3" +features = [ + "num-traits", + "bytemuck", +] +optional = true +default-features = false + +[dependencies.libm] +version = "0.2.11" + +[dependencies.num-complex] +version = "0.4" +default-features = false + +[dependencies.num-traits] +version = "0.2" +default-features = false + +[dependencies.once_cell] +version = "1.18" +features = [ + "race", + "alloc", +] +default-features = false + +[dependencies.paste] +version = "1.0" + +[dependencies.pulp] +version = "0.22.0" +default-features = false + +[dependencies.raw-cpuid] +version = "11.2" +default-features = false + +[dependencies.rayon] +version = "1" +optional = true + +[dependencies.seq-macro] +version = "0.3" + +[target.'cfg(target_vendor = "apple")'.dependencies.sysctl] +version = "0.6.0" +optional = true diff --git a/vendor/gemm-common/LICENSE b/vendor/gemm-common/LICENSE new file mode 100644 index 00000000..84e3f443 --- /dev/null +++ b/vendor/gemm-common/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 sarah + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/gemm-common/README.md b/vendor/gemm-common/README.md new file mode 100644 index 00000000..8d97582c --- /dev/null +++ b/vendor/gemm-common/README.md @@ -0,0 +1 @@ +Playground for testing high performance matrix multiplication. diff --git a/vendor/gemm-common/src/cache.rs b/vendor/gemm-common/src/cache.rs new file mode 100644 index 00000000..25337669 --- /dev/null +++ b/vendor/gemm-common/src/cache.rs @@ -0,0 +1,612 @@ +#[derive(Default, Debug, Copy, Clone)] +pub struct CacheInfo { + pub associativity: usize, + pub cache_bytes: usize, + pub cache_line_bytes: usize, +} + +#[derive(Default, Debug, Copy, Clone)] +pub struct KernelParams { + pub kc: usize, + pub mc: usize, + pub nc: usize, +} + +pub trait DivCeil: Sized { + fn msrv_div_ceil(self, rhs: Self) -> Self; + fn msrv_next_multiple_of(self, rhs: Self) -> Self; + fn msrv_checked_next_multiple_of(self, rhs: Self) -> Option; +} + +impl DivCeil for usize { + #[inline] + fn msrv_div_ceil(self, rhs: Self) -> Self { + let d = self / rhs; + let r = self % rhs; + if r > 0 { + d + 1 + } else { + d + } + } + + #[inline] + fn msrv_next_multiple_of(self, rhs: Self) -> Self { + match self % rhs { + 0 => self, + r => self + (rhs - r), + } + } + + #[inline] + fn msrv_checked_next_multiple_of(self, rhs: Self) -> Option { + { + match self.checked_rem(rhs)? { + 0 => Some(self), + r => self.checked_add(rhs - r), + } + } + } +} + +#[cfg(target_vendor = "apple")] +fn has_amx_impl() -> bool { + if !cfg!(miri) { + #[cfg(feature = "std")] + { + use sysctl::Ctl; + use sysctl::Sysctl; + + if let Ok(brand) = + Ctl::new("machdep.cpu.brand_string").and_then(|ctl| ctl.value_string()) + { + let mut words = brand.split_whitespace(); + let apple = words.next(); + let mx = words.next(); + return apple == Some("Apple") && matches!(mx, Some("M1" | "M2" | "M3")); + } + } + } + false +} + +fn cache_info() -> Option<[CacheInfo; 3]> { + if !cfg!(miri) { + #[cfg(feature = "std")] + { + #[cfg(target_os = "linux")] + { + use std::fs; + fn try_cache_info_linux() -> Result<[CacheInfo; 3], std::io::Error> { + let mut all_info = [CacheInfo { + associativity: 8, + cache_bytes: 0, + cache_line_bytes: 64, + }; 3]; + + for cpu_x in fs::read_dir("/sys/devices/system/cpu")? { + let cpu_x = cpu_x?.path(); + let Some(cpu_x_name) = cpu_x.file_name().and_then(|f| f.to_str()) else { + continue; + }; + if !cpu_x_name.starts_with("cpu") { + continue; + } + let cache = cpu_x.join("cache"); + if !cache.is_dir() { + continue; + } + 'index: for index_y in fs::read_dir(cache)? { + let index_y = index_y?.path(); + if !index_y.is_dir() { + continue; + } + let Some(index_y_name) = index_y.file_name().and_then(|f| f.to_str()) + else { + continue; + }; + if !index_y_name.starts_with("index") { + continue; + } + + let mut cache_info = CacheInfo { + associativity: 8, + cache_bytes: 0, + cache_line_bytes: 64, + }; + let mut level: usize = 0; + let mut shared_count: usize = 0; + + for entry in fs::read_dir(index_y)? { + let entry = entry?.path(); + if let Some(name) = entry.file_name() { + let contents = fs::read_to_string(&entry)?; + let contents = contents.trim(); + if name == "type" && !matches!(contents, "Data" | "Unified") { + continue 'index; + } + if name == "shared_cpu_list" { + for item in contents.split(',') { + if item.contains('-') { + let mut item = item.split('-'); + let Some(start) = item.next() else { + continue 'index; + }; + let Some(end) = item.next() else { + continue 'index; + }; + + let Ok(start) = start.parse::() else { + continue 'index; + }; + let Ok(end) = end.parse::() else { + continue 'index; + }; + + shared_count += end + 1 - start; + } else { + shared_count += 1; + } + } + } + + if name == "level" { + let Ok(contents) = contents.parse::() else { + continue 'index; + }; + level = contents; + } + + if name == "coherency_line_size" { + let Ok(contents) = contents.parse::() else { + continue 'index; + }; + cache_info.cache_line_bytes = contents; + } + if name == "ways_of_associativity" { + let Ok(contents) = contents.parse::() else { + continue 'index; + }; + cache_info.associativity = contents; + } + if name == "size" { + if contents.ends_with("G") { + let Ok(contents) = + contents.trim_end_matches('G').parse::() + else { + continue 'index; + }; + cache_info.cache_bytes = contents * 1024 * 1024 * 1024; + } else if contents.ends_with("M") { + let Ok(contents) = + contents.trim_end_matches('M').parse::() + else { + continue 'index; + }; + cache_info.cache_bytes = contents * 1024 * 1024; + } else if contents.ends_with("K") { + let Ok(contents) = + contents.trim_end_matches('K').parse::() + else { + continue 'index; + }; + cache_info.cache_bytes = contents * 1024; + } else { + let Ok(contents) = contents.parse::() else { + continue 'index; + }; + cache_info.cache_bytes = contents; + } + } + } + } + if level == 3 { + shared_count = 1; + } + if level > 0 { + if cache_info.cache_line_bytes + >= all_info[level - 1].cache_line_bytes + { + all_info[level - 1].associativity = cache_info.associativity; + all_info[level - 1].cache_line_bytes = + cache_info.cache_line_bytes; + all_info[level - 1].cache_bytes = + cache_info.cache_bytes / shared_count; + } + } + } + } + + for (info, default) in core::iter::zip(&mut all_info, CACHE_INFO_DEFAULT) { + if info.cache_bytes == 0 { + *info = default; + } + } + + Ok(all_info) + } + if let Ok(info) = try_cache_info_linux() { + return Some(info); + } + + if let Ok(lscpu) = std::process::Command::new("lscpu") + .arg("-B") + .arg("-C=type,level,ways,coherency-size,one-size") + .output() + { + if lscpu.status.success() { + if let Ok(lscpu) = String::from_utf8(lscpu.stdout).as_deref() { + let mut info = CACHE_INFO_DEFAULT; + for line in lscpu.lines().skip(1) { + let mut iter = line.split_whitespace(); + if let [Some(cache_type), Some(level), Some(ways), Some(coherency_size), Some(one_size)] = [ + iter.next(), + iter.next(), + iter.next(), + iter.next(), + iter.next(), + ] { + if let "Data" | "Unified" = cache_type { + let level = level.parse::().unwrap(); + let ways = ways.parse::().unwrap(); + let coherency_size = + coherency_size.parse::().unwrap(); + let one_size = one_size.parse::().unwrap(); + + info[level - 1].associativity = ways; + info[level - 1].cache_line_bytes = coherency_size; + info[level - 1].cache_bytes = one_size; + } + } + } + return Some(info); + } + } + } + } + #[cfg(target_vendor = "apple")] + { + use sysctl::Ctl; + use sysctl::Sysctl; + + let mut all_info = CACHE_INFO_DEFAULT; + if let Ok(l1) = + Ctl::new("hw.perflevel0.l1dcachesize").and_then(|ctl| ctl.value_string()) + { + if let Ok(l1) = l1.parse::() { + all_info[0].cache_bytes = l1; + } + } + if let (Ok(physicalcpu), Ok(cpusperl2), Ok(l2)) = ( + Ctl::new("hw.perflevel0.physicalcpu").and_then(|ctl| ctl.value_string()), + Ctl::new("hw.perflevel0.cpusperl2").and_then(|ctl| ctl.value_string()), + Ctl::new("hw.perflevel0.l2cachesize").and_then(|ctl| ctl.value_string()), + ) { + if let (Ok(_physicalcpu), Ok(cpusperl2), Ok(l2)) = ( + physicalcpu.parse::(), + cpusperl2.parse::(), + l2.parse::(), + ) { + all_info[1].cache_bytes = l2 / cpusperl2; + } + } + all_info[2].cache_bytes = 0; + return Some(all_info); + } + } + + #[cfg(any( + all(target_arch = "x86", not(target_env = "sgx"), target_feature = "sse"), + all(target_arch = "x86_64", not(target_env = "sgx")) + ))] + { + use raw_cpuid::CpuId; + let cpuid = CpuId::new(); + + if let Some(vf) = cpuid.get_vendor_info() { + let vf = vf.as_str(); + if vf == "GenuineIntel" { + if let Some(cparams) = cpuid.get_cache_parameters() { + // not sure why, intel cpus seem to prefer smaller mc + let mut info = [CacheInfo { + cache_bytes: 0, + associativity: 0, + cache_line_bytes: 64, + }; 3]; + + for cache in cparams { + use raw_cpuid::CacheType::*; + match cache.cache_type() { + Null | Instruction | Reserved => continue, + Data | Unified => { + let level = cache.level() as usize; + let associativity = cache.associativity(); + let nsets = cache.sets(); + let cache_line_bytes = cache.coherency_line_size(); + if level > 0 && level < 4 { + let info = &mut info[level - 1]; + info.cache_line_bytes = cache_line_bytes; + info.associativity = associativity; + info.cache_bytes = associativity * nsets * cache_line_bytes; + } + } + } + } + return Some(info); + } + } + + if vf == "AuthenticAMD" { + if let Some(l1) = cpuid.get_l1_cache_and_tlb_info() { + if let Some(l23) = cpuid.get_l2_l3_cache_and_tlb_info() { + let compute_info = |associativity: raw_cpuid::Associativity, + cache_kb: usize, + cache_line_bytes: u8| + -> CacheInfo { + let cache_bytes = cache_kb as usize * 1024; + let cache_line_bytes = cache_line_bytes as usize; + + use raw_cpuid::Associativity::*; + let associativity = match associativity { + Unknown | Disabled => { + return CacheInfo { + associativity: 0, + cache_bytes: 0, + cache_line_bytes: 64, + } + } + FullyAssociative => cache_bytes / cache_line_bytes, + DirectMapped => 1, + NWay(n) => n as usize, + }; + + CacheInfo { + associativity, + cache_bytes, + cache_line_bytes, + } + }; + return Some([ + compute_info( + l1.dcache_associativity(), + l1.dcache_size() as usize, + l1.dcache_line_size(), + ), + compute_info( + l23.l2cache_associativity(), + l23.l2cache_size() as usize, + l23.l2cache_line_size(), + ), + compute_info( + l23.l3cache_associativity(), + l23.l3cache_size() as usize * 512, + l23.l3cache_line_size(), + ), + ]); + } + } + } + } + } + } + None +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +static CACHE_INFO_DEFAULT: [CacheInfo; 3] = [ + CacheInfo { + associativity: 8, + cache_bytes: 32 * 1024, // 32KiB + cache_line_bytes: 64, + }, + CacheInfo { + associativity: 8, + cache_bytes: 256 * 1024, // 256KiB + cache_line_bytes: 64, + }, + CacheInfo { + associativity: 8, + cache_bytes: 2 * 1024 * 1024, // 2MiB + cache_line_bytes: 64, + }, +]; + +#[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))] +static CACHE_INFO_DEFAULT: [CacheInfo; 3] = [ + CacheInfo { + associativity: 8, + cache_bytes: 64 * 1024, // 64KiB + cache_line_bytes: 64, + }, + CacheInfo { + associativity: 8, + cache_bytes: 512 * 1024, // 512KiB + cache_line_bytes: 64, + }, + CacheInfo { + associativity: 8, + cache_bytes: 4 * 1024 * 1024, // 4MiB + cache_line_bytes: 64, + }, +]; + +#[cfg(not(any( + target_arch = "powerpc", + target_arch = "powerpc64", + target_arch = "x86", + target_arch = "x86_64" +)))] +static CACHE_INFO_DEFAULT: [CacheInfo; 3] = [ + CacheInfo { + associativity: 8, + cache_bytes: 16 * 1024, // 16KiB + cache_line_bytes: 64, + }, + CacheInfo { + associativity: 8, + cache_bytes: 512 * 1024, // 512KiB + cache_line_bytes: 64, + }, + CacheInfo { + associativity: 8, + cache_bytes: 1024 * 1024, // 1MiB + cache_line_bytes: 64, + }, +]; + +pub struct CacheInfoDeref; +#[cfg(target_vendor = "apple")] +pub struct HasAmx; + +impl core::ops::Deref for CacheInfoDeref { + type Target = [CacheInfo; 3]; + + #[inline] + fn deref(&self) -> &Self::Target { + #[cfg(not(feature = "std"))] + { + static CACHE_INFO: once_cell::race::OnceBox<[CacheInfo; 3]> = + once_cell::race::OnceBox::new(); + CACHE_INFO + .get_or_init(|| alloc::boxed::Box::new(cache_info().unwrap_or(CACHE_INFO_DEFAULT))) + } + #[cfg(feature = "std")] + { + static CACHE_INFO: once_cell::sync::OnceCell<[CacheInfo; 3]> = + once_cell::sync::OnceCell::new(); + CACHE_INFO.get_or_init(|| cache_info().unwrap_or(CACHE_INFO_DEFAULT)) + } + } +} + +#[cfg(target_vendor = "apple")] +impl HasAmx { + #[inline] + pub fn get() -> bool { + static HAS_AMX: core::sync::atomic::AtomicU8 = core::sync::atomic::AtomicU8::new(u8::MAX); + let mut has_amx = HAS_AMX.load(::core::sync::atomic::Ordering::Relaxed); + if has_amx == u8::MAX { + let b = has_amx_impl() as u8; + HAS_AMX.store(b, core::sync::atomic::Ordering::Relaxed); + has_amx = b; + } + has_amx != 0 + } +} + +pub static CACHE_INFO: CacheInfoDeref = CacheInfoDeref; + +#[inline] +fn gcd(mut a: usize, mut b: usize) -> usize { + while b != 0 { + let rem = a % b; + a = b; + b = rem; + } + a +} + +#[inline] +fn round_down(a: usize, b: usize) -> usize { + a / b * b +} + +pub fn kernel_params( + m: usize, + n: usize, + k: usize, + mr: usize, + nr: usize, + sizeof: usize, +) -> KernelParams { + if m == 0 || n == 0 || k == 0 { + return KernelParams { + kc: k, + mc: m, + nc: n, + }; + } + + let info = *CACHE_INFO; + + let l1_cache_bytes = info[0].cache_bytes.max(32 * 1024); + let l2_cache_bytes = info[1].cache_bytes; + let l3_cache_bytes = info[2].cache_bytes; + + let l1_line_bytes = info[0].cache_line_bytes.max(64); + + let l1_assoc = info[0].associativity.max(2); + let l2_assoc = info[1].associativity.max(2); + let l3_assoc = info[2].associativity.max(2); + + let l1_n_sets = l1_cache_bytes / (l1_line_bytes * l1_assoc); + + // requires + // A micropanels must occupy different cache sets + // so that loading a micropanel evicts the previous one + // => byte stride must be multiple of n_sets×line_bytes + // + // => mr×kc×scalar_bytes == C_A × l1_line_bytes × l1_n_sets + // + // l1 must be able to hold A micropanel, B micropanel + // + // => C_A + C_B <= l1_assoc + + // a×n = b×m + // find lcm of a, b + // n = lcm / a = b/gcd(a,b) + // m = lcm / b = a/gcd(a,b) + + let gcd = gcd(mr * sizeof, l1_line_bytes * l1_n_sets); + let kc_0 = (l1_line_bytes * l1_n_sets) / gcd; + let c_lhs = (mr * sizeof) / gcd; + let c_rhs = (nr * kc_0 * sizeof) / (l1_line_bytes * l1_n_sets); + let kc_multiplier = l1_assoc / (c_lhs + c_rhs); + // let auto_kc = kc_0 * kc_multiplier; + let auto_kc = (kc_0 * kc_multiplier.next_power_of_two()).max(512).min(k); + let k_iter = k.msrv_div_ceil(auto_kc); + let auto_kc = k.msrv_div_ceil(k_iter); + + // l2 cache must hold + // - B micropanel: nr×kc: assume 1 assoc degree + // - A macropanel: mc×kc + // mc×kc×scalar_bytes + let auto_mc = if l2_cache_bytes == 0 { + panic!(); + } else { + let rhs_micropanel_bytes = nr * auto_kc * sizeof; + let rhs_l2_assoc = rhs_micropanel_bytes.msrv_div_ceil(l2_cache_bytes / l2_assoc); + let lhs_l2_assoc = (l2_assoc - 1 - rhs_l2_assoc).max(1); + + let mc_from_lhs_l2_assoc = |lhs_l2_assoc: usize| -> usize { + (lhs_l2_assoc * l2_cache_bytes) / (l2_assoc * sizeof * auto_kc) + }; + + let auto_mc = round_down(mc_from_lhs_l2_assoc(lhs_l2_assoc), mr); + let m_iter = m.msrv_div_ceil(auto_mc); + m.msrv_div_ceil(m_iter * mr) * mr + }; + let auto_mc = Ord::min(auto_mc, 8 * mr); + + // l3 cache must hold + // - A macropanel: mc×kc: assume 1 assoc degree + // - B macropanel: nc×kc + let auto_nc = if l3_cache_bytes == 0 { + 0 + } else { + // let lhs_macropanel_bytes = auto_mc * auto_kc * sizeof; + // let lhs_l3_assoc = msrv_div_ceil(lhs_macropanel_bytes, l3_cache_bytes / l3_assoc); + let rhs_l3_assoc = l3_assoc - 1; + let rhs_macropanel_max_bytes = (rhs_l3_assoc * l3_cache_bytes) / l3_assoc; + + let auto_nc = round_down(rhs_macropanel_max_bytes / (sizeof * auto_kc), nr); + let n_iter = n.msrv_div_ceil(auto_nc); + n.msrv_div_ceil(n_iter * nr) * nr + }; + + KernelParams { + kc: auto_kc, + mc: auto_mc, + nc: auto_nc, + } +} diff --git a/vendor/gemm-common/src/gemm.rs b/vendor/gemm-common/src/gemm.rs new file mode 100644 index 00000000..07626e22 --- /dev/null +++ b/vendor/gemm-common/src/gemm.rs @@ -0,0 +1,1172 @@ +use crate::{ + cache::{kernel_params, DivCeil, KernelParams, CACHE_INFO}, + gemv, gevv, + microkernel::{HMicroKernelFn, MicroKernelFn}, + pack_operands::{pack_lhs, pack_rhs}, + simd::MixedSimd, + Parallelism, Ptr, +}; +use core::sync::atomic::{AtomicUsize, Ordering}; +use dyn_stack::{DynStack, StackReq}; +#[cfg(feature = "f16")] +use half::f16; +use num_traits::{One, Zero}; + +#[allow(non_camel_case_types)] +pub type c32 = num_complex::Complex32; +#[allow(non_camel_case_types)] +pub type c64 = num_complex::Complex64; + +// https://rust-lang.github.io/hashbrown/src/crossbeam_utils/cache_padded.rs.html#128-130 +pub const CACHELINE_ALIGN: usize = { + #[cfg(any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "powerpc64", + ))] + { + 128 + } + #[cfg(any( + target_arch = "arm", + target_arch = "mips", + target_arch = "mips64", + target_arch = "riscv64", + ))] + { + 32 + } + #[cfg(target_arch = "s390x")] + { + 256 + } + #[cfg(not(any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "powerpc64", + target_arch = "arm", + target_arch = "mips", + target_arch = "mips64", + target_arch = "riscv64", + target_arch = "s390x", + )))] + { + 64 + } +}; + +#[cfg(feature = "std")] +thread_local! { + pub static L2_SLAB: core::cell::RefCell = core::cell::RefCell::new(dyn_stack::MemBuffer::new( + StackReq::new_aligned::(CACHE_INFO[1].cache_bytes, CACHELINE_ALIGN) + )); +} + +pub trait Conj: Copy { + fn conj(self) -> Self; +} + +#[cfg(feature = "f16")] +impl Conj for f16 { + #[inline(always)] + fn conj(self) -> Self { + self + } +} + +impl Conj for f32 { + #[inline(always)] + fn conj(self) -> Self { + self + } +} +impl Conj for f64 { + #[inline(always)] + fn conj(self) -> Self { + self + } +} + +impl Conj for c32 { + #[inline(always)] + fn conj(self) -> Self { + c32 { + re: self.re, + im: -self.im, + } + } +} +impl Conj for c64 { + #[inline(always)] + fn conj(self) -> Self { + c64 { + re: self.re, + im: -self.im, + } + } +} + +pub const DEFAULT_THREADING_THRESHOLD: usize = 48 * 48 * 256; + +// we REALLY want to pack the rhs on aarch64 since we can use mul_add_lane +#[cfg(target_arch = "aarch64")] +pub const DEFAULT_RHS_PACKING_THRESHOLD: usize = 2; +#[cfg(not(target_arch = "aarch64"))] +pub const DEFAULT_RHS_PACKING_THRESHOLD: usize = 128; + +pub const DEFAULT_LHS_PACKING_THRESHOLD_SINGLE_THREAD: usize = 8; +pub const DEFAULT_LHS_PACKING_THRESHOLD_MULTI_THREAD: usize = 16; + +static THREADING_THRESHOLD: AtomicUsize = AtomicUsize::new(DEFAULT_THREADING_THRESHOLD); +static RHS_PACKING_THRESHOLD: AtomicUsize = AtomicUsize::new(DEFAULT_RHS_PACKING_THRESHOLD); +static LHS_PACKING_THRESHOLD_SINGLE_THREAD: AtomicUsize = + AtomicUsize::new(DEFAULT_LHS_PACKING_THRESHOLD_SINGLE_THREAD); +static LHS_PACKING_THRESHOLD_MULTI_THREAD: AtomicUsize = + AtomicUsize::new(DEFAULT_LHS_PACKING_THRESHOLD_MULTI_THREAD); + +#[inline] +pub fn get_threading_threshold() -> usize { + THREADING_THRESHOLD.load(Ordering::Relaxed) +} +#[inline] +pub fn set_threading_threshold(value: usize) { + THREADING_THRESHOLD.store(value, Ordering::Relaxed); +} + +#[inline] +pub fn get_rhs_packing_threshold() -> usize { + RHS_PACKING_THRESHOLD.load(Ordering::Relaxed) +} +#[inline] +pub fn set_rhs_packing_threshold(value: usize) { + RHS_PACKING_THRESHOLD.store(value.min(256), Ordering::Relaxed); +} + +#[inline] +pub fn get_lhs_packing_threshold_single_thread() -> usize { + LHS_PACKING_THRESHOLD_SINGLE_THREAD.load(Ordering::Relaxed) +} +#[inline] +pub fn set_lhs_packing_threshold_single_thread(value: usize) { + LHS_PACKING_THRESHOLD_SINGLE_THREAD.store(value.min(256), Ordering::Relaxed); +} + +#[inline] +pub fn get_lhs_packing_threshold_multi_thread() -> usize { + LHS_PACKING_THRESHOLD_MULTI_THREAD.load(Ordering::Relaxed) +} +#[inline] +pub fn set_lhs_packing_threshold_multi_thread(value: usize) { + LHS_PACKING_THRESHOLD_MULTI_THREAD.store(value.min(256), Ordering::Relaxed); +} + +#[cfg(feature = "rayon")] +pub fn par_for_each(n_threads: usize, func: impl Fn(usize) + Send + Sync) { + fn inner(n_threads: usize, func: &(dyn Fn(usize) + Send + Sync)) { + use rayon::prelude::*; + (0..n_threads).into_par_iter().for_each(func); + } + + inner(n_threads, &func) +} + +#[inline(always)] +pub unsafe fn gemm_basic_generic< + S: MixedSimd, + T: Copy + + Zero + + One + + Conj + + Send + + Sync + + core::fmt::Debug + + core::ops::Add + + core::ops::Mul + + core::cmp::PartialEq + + 'static, + const N: usize, + const MR: usize, + const NR: usize, + const MR_DIV_N: usize, + const H_M: usize, + const H_N: usize, +>( + simd: S, + m: usize, + n: usize, + k: usize, + dst: *mut T, + dst_cs: isize, + dst_rs: isize, + read_dst: bool, + lhs: *const T, + lhs_cs: isize, + lhs_rs: isize, + rhs: *const T, + rhs_cs: isize, + rhs_rs: isize, + mut alpha: T, + beta: T, + conj_dst: bool, + conj_lhs: bool, + conj_rhs: bool, + mul_add: impl Copy + Fn(T, T, T) -> T, + dispatcher: &[[MicroKernelFn; NR]; MR_DIV_N], + horizontal_dispatcher: &[[HMicroKernelFn; H_N]; H_M], + _requires_row_major_rhs: bool, + parallelism: Parallelism, +) { + if m == 0 || n == 0 { + return; + } + if !read_dst { + alpha.set_zero(); + } + + if k == 0 { + // dst = alpha * conj?(dst) + + if alpha.is_zero() { + for j in 0..n { + for i in 0..m { + *dst.offset(i as isize * dst_rs + j as isize * dst_cs) = T::zero(); + } + } + return; + } + + if alpha.is_one() && !conj_dst { + return; + } + + if conj_dst { + for j in 0..n { + for i in 0..m { + let dst = dst.offset(i as isize * dst_rs + j as isize * dst_cs); + *dst = alpha * (*dst).conj(); + } + } + } else { + for j in 0..n { + for i in 0..m { + let dst = dst.offset(i as isize * dst_rs + j as isize * dst_cs); + *dst = alpha * *dst; + } + } + } + return; + } + + if (H_M > 0 && H_N > 0) && (!conj_dst && lhs_cs == 1 && rhs_rs == 1 && (m * n) <= 16 * 16) { + let kc = 1024; + let mut depth = 0; + let mut conj_dst = conj_dst; + while depth < k { + let kb = Ord::min(kc, k - depth); + let alpha_status = if alpha.is_zero() { + 0 + } else if alpha.is_one() { + 1 + } else { + 2 + }; + + let mut col = 0; + while col < n { + let nb = Ord::min(H_N, n - col); + + let mut row = 0; + while row < m { + let mb = Ord::min(H_M, m - row); + + horizontal_dispatcher[mb - 1][nb - 1]( + kb, + dst.wrapping_offset(dst_rs * row as isize + dst_cs * col as isize), + lhs.wrapping_offset(lhs_rs * row as isize + depth as isize), + rhs.wrapping_offset(rhs_cs * col as isize + depth as isize), + dst_cs, + dst_rs, + lhs_rs, + rhs_cs, + alpha, + beta, + alpha_status, + conj_dst, + conj_lhs, + conj_rhs, + ); + + row += mb; + } + + col += nb; + } + + alpha = T::one(); + conj_dst = false; + depth += kb; + } + + return; + } + + if !conj_dst && !conj_lhs && !conj_rhs { + if k <= 2 { + gevv::gevv( + simd, m, n, k, dst, dst_cs, dst_rs, lhs, lhs_cs, lhs_rs, rhs, rhs_cs, rhs_rs, + alpha, beta, mul_add, + ); + return; + } + + if n <= 1 && lhs_rs == 1 && dst_rs == 1 { + gemv::mixed_gemv_colmajor( + simd, m, n, k, dst, dst_cs, dst_rs, lhs, lhs_cs, lhs_rs, rhs, rhs_cs, rhs_rs, + alpha, beta, + ); + return; + } + if n <= 1 && lhs_cs == 1 && rhs_rs == 1 { + gemv::mixed_gemv_rowmajor( + simd, m, n, k, dst, dst_cs, dst_rs, lhs, lhs_cs, lhs_rs, rhs, rhs_cs, rhs_rs, + alpha, beta, + ); + return; + } + if m <= 1 && rhs_cs == 1 && dst_cs == 1 { + gemv::mixed_gemv_colmajor( + simd, n, m, k, dst, dst_rs, dst_cs, rhs, rhs_rs, rhs_cs, lhs, lhs_rs, lhs_cs, + alpha, beta, + ); + return; + } + if m <= 1 && rhs_rs == 1 && lhs_cs == 1 { + gemv::mixed_gemv_rowmajor( + simd, n, m, k, dst, dst_rs, dst_cs, rhs, rhs_rs, rhs_cs, lhs, lhs_rs, lhs_cs, + alpha, beta, + ); + return; + } + } + + let KernelParams { kc, mc, nc } = if m <= 64 && n <= 64 { + // skip expensive kernel_params call for small sizes + let kc = k.min(512); + let alloc = CACHE_INFO[1].cache_bytes / core::mem::size_of::(); + let mc = (alloc / kc) / MR * MR; + + KernelParams { + kc, + mc, + nc: n.msrv_next_multiple_of(NR), + } + } else { + kernel_params(m, n, k, MR, NR, core::mem::size_of::()) + }; + let nc = if nc > 0 { + nc + } else { + match parallelism { + Parallelism::None => 128 * NR, + #[cfg(feature = "rayon")] + Parallelism::Rayon(_) => n.msrv_next_multiple_of(NR), + } + }; + + let simd_align = CACHELINE_ALIGN; + + let packed_rhs_stride = kc * NR; + let packed_lhs_stride = kc * MR; + + let dst = Ptr(dst); + let lhs = Ptr(lhs as *mut T); + let rhs = Ptr(rhs as *mut T); + + #[cfg(feature = "rayon")] + let max_threads = match parallelism { + Parallelism::None => 1, + Parallelism::Rayon(n_threads) => { + if n_threads == 0 { + rayon::current_num_threads() + } else { + n_threads + } + } + }; + + #[cfg(feature = "rayon")] + let threading_threshold = { + use core::any::TypeId; + let is_c32 = TypeId::of::() == TypeId::of::(); + let is_c64 = TypeId::of::() == TypeId::of::(); + if is_c32 { + get_threading_threshold() / 4 + } else if is_c64 { + get_threading_threshold() / 16 + } else { + get_threading_threshold() + } + }; + + #[cfg(target_arch = "aarch64")] + let do_pack_rhs = _requires_row_major_rhs || m > get_rhs_packing_threshold() * MR; + + // no need to pack if the lhs is already contiguous-ish + #[cfg(not(target_arch = "aarch64"))] + let do_pack_rhs = (rhs_rs.unsigned_abs() != 1 && m > 2 * MR) + || (rhs_rs.unsigned_abs() == 1 && m > get_rhs_packing_threshold() * MR); + let do_prepack_lhs = m <= 2 * mc && ((m % N != 0) || lhs_rs != 1); + + let mut mem = if do_pack_rhs || do_prepack_lhs { + let rhs_req = StackReq::new_aligned::( + if do_pack_rhs { + packed_rhs_stride * (nc / NR) + } else { + 0 + }, + simd_align, + ); + let lhs_req = StackReq::new_aligned::( + if do_prepack_lhs { + packed_lhs_stride * (m.msrv_next_multiple_of(MR) / MR) + } else { + 0 + }, + simd_align, + ); + Some(dyn_stack::MemBuffer::new(rhs_req.and(lhs_req))) + } else { + None + }; + + #[cfg(not(feature = "std"))] + let mut l2_slab = dyn_stack::MemBuffer::new(StackReq::new_aligned::( + packed_lhs_stride * (mc / MR), + simd_align, + )); + + let mut packed_storage = mem.as_mut().map(|mem| { + let stack = DynStack::new(mem); + let (rhs, stack) = stack.make_aligned_uninit::( + if do_pack_rhs { + packed_rhs_stride * (nc / NR) + } else { + 0 + }, + simd_align, + ); + + ( + rhs, + stack + .make_aligned_uninit::( + if do_prepack_lhs { + packed_lhs_stride * (m.msrv_next_multiple_of(MR) / MR) + } else { + 0 + }, + simd_align, + ) + .0, + ) + }); + + let (packed_rhs, prepacked_lhs) = packed_storage + .as_mut() + .map(|storage| { + ( + storage.0.as_mut_ptr() as *mut T, + storage.1.as_mut_ptr() as *mut T, + ) + }) + .unwrap_or((core::ptr::null_mut(), core::ptr::null_mut())); + + let packed_rhs = Ptr(packed_rhs); + let prepacked_lhs = Ptr(prepacked_lhs); + + let packed_rhs_rs = if do_pack_rhs { NR as isize } else { rhs_rs }; + let packed_rhs_cs = if do_pack_rhs { 1 } else { rhs_cs }; + + let mut did_pack_lhs = alloc::vec![false; mc / MR]; + let did_pack_lhs = Ptr((&mut *did_pack_lhs) as *mut _); + + let mut col_outer = 0; + while col_outer != n { + let n_chunk = nc.min(n - col_outer); + + let mut alpha = alpha; + let mut conj_dst = conj_dst; + + let mut depth_outer = 0; + while depth_outer != k { + let k_chunk = kc.min(k - depth_outer); + let alpha_status = if alpha.is_zero() { + 0 + } else if alpha.is_one() { + 1 + } else { + 2 + }; + + let n_threads = match parallelism { + Parallelism::None => 1, + #[cfg(feature = "rayon")] + Parallelism::Rayon(_) => { + let total_work = (m * n_chunk).saturating_mul(k_chunk); + if total_work < threading_threshold { + 1 + } else { + max_threads + } + } + }; + + let packing_threshold = if n_threads == 1 { + get_lhs_packing_threshold_single_thread() + } else { + get_lhs_packing_threshold_multi_thread() + }; + + if do_pack_rhs { + if n_threads <= 1 { + // on aarch64 we want the registers to be fully initialized + // for use with neon/amx + #[cfg(target_arch = "aarch64")] + pack_rhs::( + simd, + n_chunk, + k_chunk, + packed_rhs, + rhs.wrapping_offset( + depth_outer as isize * rhs_rs + col_outer as isize * rhs_cs, + ), + rhs_cs, + rhs_rs, + packed_rhs_stride, + ); + #[cfg(not(target_arch = "aarch64"))] + pack_rhs::( + simd, + n_chunk, + k_chunk, + packed_rhs, + rhs.wrapping_offset( + depth_outer as isize * rhs_rs + col_outer as isize * rhs_cs, + ), + rhs_cs, + rhs_rs, + packed_rhs_stride, + ); + } else { + #[cfg(feature = "rayon")] + { + let n_tasks = n_chunk.msrv_div_ceil(NR); + let base = n_tasks / n_threads; + let rem = n_tasks % n_threads; + + let tid_to_col_inner = |tid: usize| { + if tid == n_threads { + return n_chunk; + } + + let col = if tid < rem { + NR * tid * (base + 1) + } else { + NR * (rem + tid * base) + }; + col.min(n_chunk) + }; + + let func = |tid: usize| { + let col_inner = tid_to_col_inner(tid); + let ncols = tid_to_col_inner(tid + 1) - col_inner; + let j = col_inner / NR; + + if ncols > 0 { + #[cfg(target_arch = "aarch64")] + pack_rhs::( + simd, + ncols, + k_chunk, + packed_rhs.wrapping_add(j * packed_rhs_stride), + rhs.wrapping_offset( + depth_outer as isize * rhs_rs + + (col_outer + col_inner) as isize * rhs_cs, + ), + rhs_cs, + rhs_rs, + packed_rhs_stride, + ); + #[cfg(not(target_arch = "aarch64"))] + pack_rhs::( + simd, + ncols, + k_chunk, + packed_rhs.wrapping_add(j * packed_rhs_stride), + rhs.wrapping_offset( + depth_outer as isize * rhs_rs + + (col_outer + col_inner) as isize * rhs_cs, + ), + rhs_cs, + rhs_rs, + packed_rhs_stride, + ); + } + }; + par_for_each(n_threads, func); + } + + #[cfg(not(feature = "rayon"))] + { + unreachable!(); + } + } + } + if do_prepack_lhs { + pack_lhs::( + simd, + m, + k_chunk, + prepacked_lhs, + lhs.wrapping_offset(depth_outer as isize * lhs_cs), + lhs_cs, + lhs_rs, + packed_lhs_stride, + ); + } + + let n_col_mini_chunks = (n_chunk + (NR - 1)) / NR; + + let mut n_jobs = 0; + let mut row_outer = 0; + while row_outer != m { + let mut m_chunk = mc.min(m - row_outer); + if m_chunk > N && !do_prepack_lhs { + m_chunk = m_chunk / N * N; + } + let n_row_mini_chunks = (m_chunk + (MR - 1)) / MR; + n_jobs += n_col_mini_chunks * n_row_mini_chunks; + row_outer += m_chunk; + } + + let func = move |tid, packed_lhs: Ptr| { + let mut did_pack_lhs_storage = + alloc::vec![false; if tid > 0 { mc / MR } else { 0 }]; + let did_pack_lhs = if tid > 0 { + &mut *did_pack_lhs_storage + } else { + &mut *({ did_pack_lhs }.0) + }; + + let min_jobs_per_thread = n_jobs / n_threads; + let rem = n_jobs - n_threads * min_jobs_per_thread; + + // thread `tid` takes min_jobs_per_thread or min_jobs_per_thread + 1 + let (job_start, job_end) = if tid < rem { + let start = tid * (min_jobs_per_thread + 1); + (start, start + min_jobs_per_thread + 1) + } else { + // start = rem * (min_jobs_per_thread + 1) + (tid - rem) * min_jobs_per_thread; + let start = tid * min_jobs_per_thread + rem; + (start, start + min_jobs_per_thread) + }; + + let mut row_outer = 0; + let mut job_id = 0; + while row_outer != m { + let mut m_chunk = mc.min(m - row_outer); + if m_chunk > N && !do_prepack_lhs { + m_chunk = m_chunk / N * N; + } + let n_row_mini_chunks = (m_chunk + (MR - 1)) / MR; + + let n_mini_jobs = n_col_mini_chunks * n_row_mini_chunks; + + if job_id >= job_end { + return; + } + if job_id + n_mini_jobs < job_start { + row_outer += m_chunk; + job_id += n_mini_jobs; + continue; + } + + let do_pack_lhs = !do_prepack_lhs + && ((m_chunk % N != 0) || lhs_rs != 1 || n_chunk > packing_threshold * NR); + let packed_lhs_cs = if do_prepack_lhs || do_pack_lhs { + MR as isize + } else { + lhs_cs + }; + + let mut j = 0; + did_pack_lhs.fill(false); + while j < n_col_mini_chunks { + let mut i = 0; + while i < n_row_mini_chunks { + let col_inner = NR * j; + let n_chunk_inner = NR.min(n_chunk - col_inner); + + let row_inner = MR * i; + let m_chunk_inner = MR.min(m_chunk - row_inner); + + if job_id < job_start || job_id >= job_end { + job_id += 1; + i += 1; + continue; + } + job_id += 1; + + let dst = dst.wrapping_offset( + (row_outer + row_inner) as isize * dst_rs + + (col_outer + col_inner) as isize * dst_cs, + ); + + let func = + dispatcher[(m_chunk_inner + (N - 1)) / N - 1][n_chunk_inner - 1]; + + if do_pack_lhs && !did_pack_lhs[i] { + pack_lhs::( + simd, + m_chunk_inner, + k_chunk, + packed_lhs.wrapping_add(i * packed_lhs_stride), + lhs.wrapping_offset( + (row_outer + row_inner) as isize * lhs_rs + + depth_outer as isize * lhs_cs, + ), + lhs_cs, + lhs_rs, + packed_lhs_stride, + ); + did_pack_lhs[i] = true; + } + + func( + m_chunk_inner, + n_chunk_inner, + k_chunk, + dst.0, + if do_pack_lhs { + packed_lhs.wrapping_add(i * packed_lhs_stride).0 + } else if do_prepack_lhs { + packed_lhs + .wrapping_add((i + row_outer / MR) * packed_lhs_stride) + .0 + } else { + lhs.wrapping_offset( + (row_outer + row_inner) as isize * lhs_rs + + depth_outer as isize * lhs_cs, + ) + .0 + }, + if do_pack_rhs { + packed_rhs.wrapping_add(j * packed_rhs_stride).0 + } else { + rhs.wrapping_offset( + depth_outer as isize * rhs_rs + + (col_outer + col_inner) as isize * rhs_cs, + ) + .0 + }, + dst_cs, + dst_rs, + packed_lhs_cs, + packed_rhs_rs, + packed_rhs_cs, + alpha, + beta, + alpha_status, + conj_dst, + conj_lhs, + conj_rhs, + core::ptr::null(), + ); + i += 1; + } + j += 1; + } + + row_outer += m_chunk; + } + }; + + if do_prepack_lhs { + match parallelism { + Parallelism::None => func(0, prepacked_lhs), + #[cfg(feature = "rayon")] + Parallelism::Rayon(_) => { + if n_threads == 1 { + func(0, prepacked_lhs); + } else { + par_for_each(n_threads, |tid| func(tid, prepacked_lhs)); + } + } + } + } else { + #[cfg(feature = "std")] + let func = |tid: usize| { + L2_SLAB.with(|mem| { + let mut mem = mem.borrow_mut(); + let stack = DynStack::new(&mut mem); + let (packed_lhs_storage, _) = stack + .make_aligned_uninit::(packed_lhs_stride * (mc / MR), simd_align); + let packed_lhs = Ptr(packed_lhs_storage.as_mut_ptr() as *mut T); + func(tid, packed_lhs); + }); + }; + + #[cfg(not(feature = "std"))] + let mut func = |tid: usize| { + let stack = DynStack::new(&mut l2_slab); + let (packed_lhs_storage, _) = + stack.make_aligned_uninit::(packed_lhs_stride * (mc / MR), simd_align); + let packed_lhs = Ptr(packed_lhs_storage.as_mut_ptr() as *mut T); + func(tid, packed_lhs); + }; + + match parallelism { + Parallelism::None => func(0), + #[cfg(feature = "rayon")] + Parallelism::Rayon(_) => { + if n_threads == 1 { + func(0); + } else { + par_for_each(n_threads, func); + } + } + } + } + + conj_dst = false; + alpha.set_one(); + + depth_outer += k_chunk; + } + col_outer += n_chunk; + } +} + +#[macro_export] +macro_rules! __inject_mod { + ($module: ident, $ty: ident, $N: expr, $simd: ident, $requires_packed_rhs: expr) => { + mod $module { + use super::*; + use crate::gemm_common::simd::MixedSimd; + use crate::microkernel::$module::$ty::*; + const N: usize = $N; + + #[inline(never)] + pub unsafe fn gemm_basic( + m: usize, + n: usize, + k: usize, + dst: *mut $ty, + dst_cs: isize, + dst_rs: isize, + read_dst: bool, + lhs: *const $ty, + lhs_cs: isize, + lhs_rs: isize, + rhs: *const $ty, + rhs_cs: isize, + rhs_rs: isize, + alpha: $ty, + beta: $ty, + conj_dst: bool, + conj_lhs: bool, + conj_rhs: bool, + parallelism: $crate::Parallelism, + ) { + $crate::gemm::gemm_basic_generic::< + _, + $ty, + N, + { MR_DIV_N * N }, + NR, + MR_DIV_N, + H_M, + H_N, + >( + <$crate::simd::$simd as MixedSimd<$ty, $ty, $ty, $ty>>::try_new().unwrap(), + m, + n, + k, + dst, + dst_cs, + dst_rs, + read_dst, + lhs, + lhs_cs, + lhs_rs, + rhs, + rhs_cs, + rhs_rs, + alpha, + beta, + conj_dst, + conj_lhs, + conj_rhs, + |a, b, c| a * b + c, + &UKR, + &H_UKR, + $requires_packed_rhs, + parallelism, + ); + } + } + }; +} + +#[macro_export] +macro_rules! __inject_mod_cplx { + ($module: ident, $ty: ident, $N: expr, $simd: ident) => { + paste::paste! { + mod [<$module _cplx>] { + use super::*; + use crate::microkernel::$module::$ty::*; + use crate::gemm_common::simd::MixedSimd; + const N: usize = $N; + + #[inline(never)] + pub unsafe fn gemm_basic_cplx( + m: usize, + n: usize, + k: usize, + dst: *mut num_complex::Complex, + dst_cs: isize, + dst_rs: isize, + read_dst: bool, + lhs: *const num_complex::Complex, + lhs_cs: isize, + lhs_rs: isize, + rhs: *const num_complex::Complex, + rhs_cs: isize, + rhs_rs: isize, + alpha: num_complex::Complex, + beta: num_complex::Complex, + conj_dst: bool, + conj_lhs: bool, + conj_rhs: bool, + parallelism: $crate::Parallelism, + ) { + $crate::gemm::gemm_basic_generic::<_, _, N, { CPLX_MR_DIV_N * N }, CPLX_NR, CPLX_MR_DIV_N, H_CPLX_M, H_CPLX_N>( + <$crate::simd::$simd as MixedSimd>::try_new().unwrap(), + m, + n, + k, + dst, + dst_cs, + dst_rs, + read_dst, + lhs, + lhs_cs, + lhs_rs, + rhs, + rhs_cs, + rhs_rs, + alpha, + beta, + conj_dst, + conj_lhs, + conj_rhs, + |a, b, c| a * b + c, + &CPLX_UKR, + &H_CPLX_UKR, + false, + parallelism, + ); + } + } + } + }; +} + +#[macro_export] +macro_rules! gemm_def { + ($ty: tt, $multiplier: expr) => { + type GemmTy = unsafe fn( + usize, + usize, + usize, + *mut T, + isize, + isize, + bool, + *const T, + isize, + isize, + *const T, + isize, + isize, + T, + T, + bool, + bool, + bool, + $crate::Parallelism, + ); + + #[inline] + fn init_gemm_fn() -> GemmTy { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + #[cfg(feature = "x86-v4")] + if $crate::feature_detected!("avx512f") { + return avx512f::gemm_basic; + } + if $crate::feature_detected!("fma") { + fma::gemm_basic + } else { + scalar::gemm_basic + } + } + + #[cfg(target_arch = "aarch64")] + { + if $crate::feature_detected!("neon") { + #[cfg(feature = "experimental-apple-amx")] + if $crate::cache::HasAmx::get() { + return amx::gemm_basic; + } + neon::gemm_basic + } else { + scalar::gemm_basic + } + } + + #[cfg(target_arch = "wasm32")] + { + if $crate::feature_detected!("simd128") { + simd128::gemm_basic + } else { + scalar::gemm_basic + } + } + + #[cfg(not(any( + target_arch = "x86", + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "wasm32", + )))] + { + scalar::gemm_basic + } + } + + static GEMM_PTR: ::core::sync::atomic::AtomicPtr<()> = + ::core::sync::atomic::AtomicPtr::new(::core::ptr::null_mut()); + + #[inline(never)] + fn init_gemm_ptr() -> GemmTy { + let gemm_fn = init_gemm_fn(); + GEMM_PTR.store(gemm_fn as *mut (), ::core::sync::atomic::Ordering::Relaxed); + gemm_fn + } + + #[inline(always)] + pub fn get_gemm_fn() -> GemmTy { + let mut gemm_fn = GEMM_PTR.load(::core::sync::atomic::Ordering::Relaxed); + if gemm_fn.is_null() { + gemm_fn = init_gemm_ptr() as *mut (); + } + unsafe { ::core::mem::transmute(gemm_fn) } + } + + $crate::__inject_mod!(scalar, $ty, 1, Scalar, false); + + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + $crate::__inject_mod!(fma, $ty, 4 * $multiplier, V3, false); + #[cfg(all(feature = "x86-v4", any(target_arch = "x86", target_arch = "x86_64")))] + $crate::__inject_mod!(avx512f, $ty, 8 * $multiplier, V4, false); + + #[cfg(target_arch = "aarch64")] + $crate::__inject_mod!(neon, $ty, 2 * $multiplier, Scalar, false); + #[cfg(target_arch = "aarch64")] + #[cfg(feature = "experimental-apple-amx")] + $crate::__inject_mod!(amx, $ty, 8 * $multiplier, Scalar, true); + + #[cfg(target_arch = "wasm32")] + $crate::__inject_mod!(simd128, $ty, 2 * $multiplier, Scalar, false); + }; +} + +#[macro_export] +macro_rules! gemm_cplx_def { + ($ty: tt, $cplx_ty: tt, $multiplier: expr) => { + type GemmCplxTy = unsafe fn( + usize, + usize, + usize, + *mut num_complex::Complex, + isize, + isize, + bool, + *const num_complex::Complex, + isize, + isize, + *const num_complex::Complex, + isize, + isize, + num_complex::Complex, + num_complex::Complex, + bool, + bool, + bool, + $crate::Parallelism, + ); + + fn init_gemm_cplx_fn() -> GemmCplxTy { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + #[cfg(feature = "x86-v4")] + if $crate::feature_detected!("avx512f") { + return avx512f_cplx::gemm_basic_cplx; + } + if $crate::feature_detected!("fma") { + return fma_cplx::gemm_basic_cplx; + } + } + + #[cfg(target_arch = "aarch64")] + { + #[cfg(target_arch = "aarch64")] + if $crate::feature_detected!("neon") && $crate::feature_detected!("fcma") { + return neonfcma::gemm_basic; + } + } + + scalar_cplx::gemm_basic_cplx + } + + static GEMM_PTR: ::core::sync::atomic::AtomicPtr<()> = + ::core::sync::atomic::AtomicPtr::new(::core::ptr::null_mut()); + + #[inline(never)] + fn init_gemm_ptr() -> GemmCplxTy { + let gemm_fn = init_gemm_cplx_fn(); + GEMM_PTR.store(gemm_fn as *mut (), ::core::sync::atomic::Ordering::Relaxed); + gemm_fn + } + + #[inline(always)] + pub fn get_gemm_fn() -> GemmCplxTy { + let mut gemm_fn = GEMM_PTR.load(::core::sync::atomic::Ordering::Relaxed); + if gemm_fn.is_null() { + gemm_fn = init_gemm_ptr() as *mut (); + } + unsafe { ::core::mem::transmute(gemm_fn) } + } + + $crate::__inject_mod_cplx!(scalar, $ty, 1, Scalar); + + #[cfg(target_arch = "aarch64")] + $crate::__inject_mod!(neonfcma, $cplx_ty, 1 * $multiplier, Scalar, false); + + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + $crate::__inject_mod_cplx!(fma, $ty, 2 * $multiplier, V3); + #[cfg(all(feature = "x86-v4", any(target_arch = "x86", target_arch = "x86_64")))] + $crate::__inject_mod_cplx!(avx512f, $ty, 4 * $multiplier, V4); + }; +} diff --git a/vendor/gemm-common/src/gemv.rs b/vendor/gemm-common/src/gemv.rs new file mode 100644 index 00000000..ac770b67 --- /dev/null +++ b/vendor/gemm-common/src/gemv.rs @@ -0,0 +1,587 @@ +use core::slice::from_raw_parts_mut; + +use num_traits::{One, Zero}; +use seq_macro::seq; + +use crate::simd::{Boilerplate, MixedSimd, Simd}; + +#[inline(always)] +pub unsafe fn gemv< + T: Copy + + Zero + + One + + Send + + Sync + + core::ops::Add + + core::ops::Mul + + core::cmp::PartialEq, + S: Simd, +>( + _simd: S, + m: usize, + n: usize, + k: usize, + dst: *mut T, + dst_cs: isize, + dst_rs: isize, + lhs: *const T, + lhs_cs: isize, + lhs_rs: isize, + rhs: *const T, + rhs_cs: isize, + rhs_rs: isize, + alpha: T, + beta: T, + mul_add: impl Fn(T, T, T) -> T, +) { + if !alpha.is_zero() { + for col in 0..n { + for row in 0..m { + let dst = dst + .wrapping_offset(row as isize * dst_rs) + .wrapping_offset(col as isize * dst_cs); + + *dst = alpha * *dst; + } + } + } else { + for col in 0..n { + for row in 0..m { + let dst = dst + .wrapping_offset(row as isize * dst_rs) + .wrapping_offset(col as isize * dst_cs); + + *dst = T::zero(); + } + } + } + + macro_rules! do_work { + ($n: tt) => { + for depth in 0..k { + seq!(COL in 0..$n { + let rhs~COL = beta * *rhs + .wrapping_offset(COL as isize * rhs_cs) + .wrapping_offset(depth as isize * rhs_rs); + }); + for row in 0..m { + let lhs = *lhs + .wrapping_offset(depth as isize * lhs_cs) + .wrapping_offset(row as isize * lhs_rs); + + seq!(COL in 0..$n { + { + let dst = dst + .wrapping_offset(COL as isize * dst_cs) + .wrapping_offset(row as isize * dst_rs); + *dst = mul_add(rhs~COL, lhs, *dst); + } + }); + } + } + } + } + match n { + 1 => do_work!(1), + _ => unreachable!(), + } +} + +// dst, lhs are colmajor +// n is small +#[inline(always)] +pub unsafe fn mixed_gemv_colmajor< + Lhs: Boilerplate + One + Zero, + Rhs: Boilerplate + One + Zero, + Dst: Boilerplate + One + Zero, + Acc: Boilerplate + One + Zero, + S: MixedSimd, +>( + simd: S, + + m: usize, + n: usize, + k: usize, + + dst: *mut Dst, + dst_cs: isize, + dst_rs: isize, + + lhs: *const Lhs, + lhs_cs: isize, + lhs_rs: isize, + + rhs: *const Rhs, + rhs_cs: isize, + rhs_rs: isize, + + alpha: Acc, + beta: Acc, +) { + #[inline(always)] + unsafe fn implementation< + 'a, + Lhs: Boilerplate + One + Zero, + Rhs: Boilerplate + One + Zero, + Dst: Boilerplate + One + Zero, + Acc: Boilerplate + One + Zero, + S: MixedSimd, + >( + noalias_dst: (&'a mut [Dst],), + simd: S, + m: usize, + k: usize, + lhs: *const Lhs, + lhs_cs: isize, + rhs: *const Rhs, + rhs_cs: isize, + rhs_rs: isize, + alpha: Acc, + beta: Acc, + ) { + #[allow(dead_code)] + struct Impl<'a, Lhs, Rhs, Dst, Acc, S> { + simd: S, + m: usize, + k: usize, + noalias_dst: (&'a mut [Dst],), + lhs: *const Lhs, + lhs_cs: isize, + rhs: *const Rhs, + rhs_cs: isize, + rhs_rs: isize, + alpha: Acc, + beta: Acc, + } + impl< + Lhs: Boilerplate + One + Zero, + Rhs: Boilerplate + One + Zero, + Dst: Boilerplate + One + Zero, + Acc: Boilerplate + One + Zero, + S: MixedSimd, + > pulp::NullaryFnOnce for Impl<'_, Lhs, Rhs, Dst, Acc, S> + { + type Output = (); + + #[inline(always)] + fn call(self) -> Self::Output { + unsafe { + let Self { + simd, + m, + k, + noalias_dst, + lhs, + lhs_cs, + rhs, + rhs_cs: _, + rhs_rs, + mut alpha, + beta, + } = self; + + let lane = S::SIMD_WIDTH; + let dst = noalias_dst.0.as_mut_ptr(); + let m_lane = m / lane * lane; + for col in 0..k { + let lhs = lhs.wrapping_offset(col as isize * lhs_cs); + let rhs = simd.from_rhs(*rhs.wrapping_offset(col as isize * rhs_rs)); + + let alpha_s = alpha; + let alpha_v = simd.simd_splat(alpha_s); + + let rhs_scalar = simd.mult(beta, rhs); + let rhs = simd.simd_splat(rhs_scalar); + + if alpha_s.is_zero() { + let mut row = 0usize; + while row < m_lane { + let dst_ptr = dst.wrapping_add(row) as *mut S::DstN; + let lhs = + simd.simd_from_lhs(*(lhs.wrapping_add(row) as *const S::LhsN)); + *dst_ptr = simd.simd_into_dst(simd.simd_mul(lhs, rhs)); + row += lane; + } + while row < m { + let dst_ptr = dst.wrapping_add(row); + let lhs = simd.from_lhs(*lhs.wrapping_add(row)); + *dst_ptr = simd.into_dst(simd.mult(lhs, rhs_scalar)); + row += 1; + } + } else if alpha_s.is_one() { + let mut row = 0usize; + while row < m_lane { + let dst_ptr = dst.wrapping_add(row) as *mut S::DstN; + let dst = *dst_ptr; + let lhs = + simd.simd_from_lhs(*(lhs.wrapping_add(row) as *const S::LhsN)); + *dst_ptr = simd.simd_into_dst(simd.simd_mult_add( + lhs, + rhs, + simd.simd_from_dst(dst), + )); + row += lane; + } + while row < m { + let dst_ptr = dst.wrapping_add(row); + let dst = *dst_ptr; + let lhs = simd.from_lhs(*lhs.wrapping_add(row)); + *dst_ptr = simd.into_dst(simd.mult_add( + lhs, + rhs_scalar, + simd.from_dst(dst), + )); + row += 1; + } + } else { + let mut row = 0usize; + while row < m_lane { + let dst_ptr = dst.wrapping_add(row) as *mut S::DstN; + let dst = *dst_ptr; + let lhs = + simd.simd_from_lhs(*(lhs.wrapping_add(row) as *const S::LhsN)); + *dst_ptr = simd.simd_into_dst(simd.simd_add( + simd.simd_mul(lhs, rhs), + simd.simd_mul(alpha_v, simd.simd_from_dst(dst)), + )); + row += lane; + } + while row < m { + let dst_ptr = dst.wrapping_add(row); + let dst = *dst_ptr; + let lhs = simd.from_lhs(*lhs.wrapping_add(row)); + *dst_ptr = simd.into_dst(simd.add( + simd.mult(lhs, rhs_scalar), + simd.mult(alpha_s, simd.from_dst(dst)), + )); + row += 1; + } + } + alpha = Acc::one(); + } + } + } + } + + simd.vectorize(Impl { + simd, + m, + k, + noalias_dst, + lhs, + lhs_cs, + rhs, + rhs_cs, + rhs_rs, + alpha, + beta, + }) + } + + assert_eq!(lhs_rs, 1); + assert_eq!(dst_rs, 1); + + if k == 0 { + if alpha.is_one() { + return; + } + if alpha.is_zero() { + for j in 0..n { + core::ptr::write_bytes(dst.wrapping_offset(j as isize * dst_cs), 0u8, m); + } + return; + } + + for j in 0..n { + let dst = dst.wrapping_offset(j as isize * dst_cs); + for i in 0..m { + let dst = dst.add(i); + *dst = simd.into_dst(simd.mult(simd.from_dst(*dst), alpha)); + } + } + } + + for x in 0..n { + implementation( + (from_raw_parts_mut( + dst.wrapping_offset(x as isize * dst_cs) as _, + m, + ),), + simd, + m, + k, + lhs, + lhs_cs, + rhs.wrapping_offset(rhs_cs * x as isize), + rhs_cs, + rhs_rs, + alpha, + beta, + ); + } +} + +// lhs is rowmajor +// rhs is colmajor +// n is small +#[inline(always)] +pub unsafe fn mixed_gemv_rowmajor< + Lhs: Boilerplate + One + Zero, + Rhs: Boilerplate + One + Zero, + Dst: Boilerplate + One + Zero, + Acc: Boilerplate + One + Zero, + S: MixedSimd, +>( + simd: S, + + m: usize, + n: usize, + k: usize, + + dst: *mut Dst, + dst_cs: isize, + dst_rs: isize, + + lhs: *const Lhs, + lhs_cs: isize, + lhs_rs: isize, + + rhs: *const Rhs, + rhs_cs: isize, + rhs_rs: isize, + + alpha: Acc, + beta: Acc, +) { + #[inline(always)] + unsafe fn implementation< + 'a, + Lhs: Boilerplate + One + Zero, + Rhs: Boilerplate + One + Zero, + Dst: Boilerplate + One + Zero, + Acc: Boilerplate + One + Zero, + S: MixedSimd, + >( + simd: S, + dst: *mut Dst, + dst_rs: isize, + m: usize, + k: usize, + lhs: *const Lhs, + lhs_rs: isize, + rhs: *const Rhs, + alpha: Acc, + beta: Acc, + ) { + #[allow(dead_code)] + struct Impl { + simd: S, + dst: *mut Dst, + dst_rs: isize, + m: usize, + k: usize, + lhs: *const Lhs, + lhs_rs: isize, + rhs: *const Rhs, + alpha: Acc, + beta: Acc, + } + impl< + Lhs: Boilerplate + One + Zero, + Rhs: Boilerplate + One + Zero, + Dst: Boilerplate + One + Zero, + Acc: Boilerplate + One + Zero, + S: MixedSimd, + > pulp::NullaryFnOnce for Impl + { + type Output = (); + + #[inline(always)] + fn call(self) -> Self::Output { + unsafe { + let Self { + simd, + dst, + dst_rs, + m, + k, + lhs, + lhs_rs, + rhs, + alpha, + beta, + } = self; + + let lane = S::SIMD_WIDTH; + let lane8 = 8 * S::SIMD_WIDTH; + + let k_lane = k / lane * lane; + let k_lane8 = k / lane8 * lane8; + + for row in 0..m { + let lhs = lhs.wrapping_offset(row as isize * lhs_rs); + + let mut depth = 0; + + let mut acc0 = simd.simd_splat(Acc::zero()); + let mut acc1 = simd.simd_splat(Acc::zero()); + let mut acc2 = simd.simd_splat(Acc::zero()); + let mut acc3 = simd.simd_splat(Acc::zero()); + let mut acc4 = simd.simd_splat(Acc::zero()); + let mut acc5 = simd.simd_splat(Acc::zero()); + let mut acc6 = simd.simd_splat(Acc::zero()); + let mut acc7 = simd.simd_splat(Acc::zero()); + + while depth < k_lane8 { + let lhs0 = *(lhs.wrapping_add(depth + lane * 0) as *const S::LhsN); + let rhs0 = *(rhs.wrapping_add(depth + lane * 0) as *const S::RhsN); + acc0 = simd.simd_mult_add( + simd.simd_from_lhs(lhs0), + simd.simd_from_rhs(rhs0), + acc0, + ); + + let lhs1 = *(lhs.wrapping_add(depth + lane * 1) as *const S::LhsN); + let rhs1 = *(rhs.wrapping_add(depth + lane * 1) as *const S::RhsN); + acc1 = simd.simd_mult_add( + simd.simd_from_lhs(lhs1), + simd.simd_from_rhs(rhs1), + acc1, + ); + + let lhs2 = *(lhs.wrapping_add(depth + lane * 2) as *const S::LhsN); + let rhs2 = *(rhs.wrapping_add(depth + lane * 2) as *const S::RhsN); + acc2 = simd.simd_mult_add( + simd.simd_from_lhs(lhs2), + simd.simd_from_rhs(rhs2), + acc2, + ); + + let lhs3 = *(lhs.wrapping_add(depth + lane * 3) as *const S::LhsN); + let rhs3 = *(rhs.wrapping_add(depth + lane * 3) as *const S::RhsN); + acc3 = simd.simd_mult_add( + simd.simd_from_lhs(lhs3), + simd.simd_from_rhs(rhs3), + acc3, + ); + + let lhs4 = *(lhs.wrapping_add(depth + lane * 4) as *const S::LhsN); + let rhs4 = *(rhs.wrapping_add(depth + lane * 4) as *const S::RhsN); + acc4 = simd.simd_mult_add( + simd.simd_from_lhs(lhs4), + simd.simd_from_rhs(rhs4), + acc4, + ); + + let lhs5 = *(lhs.wrapping_add(depth + lane * 5) as *const S::LhsN); + let rhs5 = *(rhs.wrapping_add(depth + lane * 5) as *const S::RhsN); + acc5 = simd.simd_mult_add( + simd.simd_from_lhs(lhs5), + simd.simd_from_rhs(rhs5), + acc5, + ); + + let lhs6 = *(lhs.wrapping_add(depth + lane * 6) as *const S::LhsN); + let rhs6 = *(rhs.wrapping_add(depth + lane * 6) as *const S::RhsN); + acc6 = simd.simd_mult_add( + simd.simd_from_lhs(lhs6), + simd.simd_from_rhs(rhs6), + acc6, + ); + + let lhs7 = *(lhs.wrapping_add(depth + lane * 7) as *const S::LhsN); + let rhs7 = *(rhs.wrapping_add(depth + lane * 7) as *const S::RhsN); + acc7 = simd.simd_mult_add( + simd.simd_from_lhs(lhs7), + simd.simd_from_rhs(rhs7), + acc7, + ); + + depth += lane8; + } + + let acc0 = simd.simd_add(acc0, acc1); + let acc2 = simd.simd_add(acc2, acc3); + let acc4 = simd.simd_add(acc4, acc5); + let acc6 = simd.simd_add(acc6, acc7); + + let acc0 = simd.simd_add(acc0, acc2); + let acc4 = simd.simd_add(acc4, acc6); + + let mut acc0 = simd.simd_add(acc0, acc4); + + while depth < k_lane { + let lhs0 = *(lhs.wrapping_add(depth) as *const S::LhsN); + let rhs0 = *(rhs.wrapping_add(depth) as *const S::RhsN); + acc0 = simd.simd_mult_add( + simd.simd_from_lhs(lhs0), + simd.simd_from_rhs(rhs0), + acc0, + ); + + depth += lane; + } + + let acc_ptr = &acc0 as *const _ as *const Acc; + let mut acc0 = *acc_ptr; + for x in 1..S::SIMD_WIDTH { + acc0 = simd.add(acc0, *acc_ptr.add(x)); + } + + while depth < k { + let lhs0 = *(lhs.wrapping_add(depth + 0)); + let rhs0 = *(rhs.wrapping_add(depth + 0)); + + acc0 = simd.mult_add(simd.from_lhs(lhs0), simd.from_rhs(rhs0), acc0); + + depth += 1; + } + + if alpha.is_zero() { + let dst = dst.wrapping_offset(dst_rs * row as isize); + *dst = simd.into_dst(simd.mult(acc0, beta)); + } else { + let dst = dst.wrapping_offset(dst_rs * row as isize); + *dst = + simd.into_dst(simd.add( + simd.mult(acc0, beta), + simd.mult(simd.from_dst(*dst), alpha), + )); + } + } + } + } + } + + simd.vectorize(Impl { + simd, + dst, + dst_rs, + m, + k, + lhs, + lhs_rs, + rhs, + alpha, + beta, + }) + } + + assert_eq!(lhs_cs, 1); + assert_eq!(rhs_rs, 1); + + for x in 0..n { + implementation( + simd, + dst.wrapping_offset(x as isize * dst_cs), + dst_rs, + m, + k, + lhs, + lhs_rs, + rhs.wrapping_offset(rhs_cs * x as isize), + alpha, + beta, + ); + } +} diff --git a/vendor/gemm-common/src/gevv.rs b/vendor/gemm-common/src/gevv.rs new file mode 100644 index 00000000..1269ea8b --- /dev/null +++ b/vendor/gemm-common/src/gevv.rs @@ -0,0 +1,166 @@ +use crate::simd::Simd; +use num_traits::{One, Zero}; + +#[inline(always)] +pub unsafe fn gevv< + T: Copy + + Zero + + One + + Send + + Sync + + core::fmt::Debug + + core::ops::Add + + core::ops::Mul + + core::cmp::PartialEq, + S: Simd, +>( + _simd: S, + m: usize, + n: usize, + k: usize, + dst: *mut T, + dst_cs: isize, + dst_rs: isize, + lhs: *const T, + lhs_cs: isize, + lhs_rs: isize, + rhs: *const T, + rhs_cs: isize, + rhs_rs: isize, + alpha: T, + beta: T, + mul_add: impl Fn(T, T, T) -> T, +) { + macro_rules! do_work { + () => { + match k { + 0 => { + if !alpha.is_zero() { + for col in 0..n { + for row in 0..m { + let dst = dst + .wrapping_offset(row as isize * dst_rs) + .wrapping_offset(col as isize * dst_cs); + + *dst = alpha * *dst; + } + } + } else { + for col in 0..n { + for row in 0..m { + let dst = dst + .wrapping_offset(row as isize * dst_rs) + .wrapping_offset(col as isize * dst_cs); + + *dst = T::zero(); + } + } + } + return; + } + 1 => { + if !alpha.is_zero() { + if alpha.is_one() { + for col in 0..n { + let rhs = beta * *rhs.wrapping_offset(col as isize * rhs_cs); + for row in 0..m { + let lhs = *lhs.wrapping_offset(row as isize * lhs_rs); + let dst = dst + .wrapping_offset(row as isize * dst_rs) + .wrapping_offset(col as isize * dst_cs); + + *dst = mul_add(lhs, rhs, *dst); + } + } + } else { + for col in 0..n { + let rhs = beta * *rhs.wrapping_offset(col as isize * rhs_cs); + for row in 0..m { + let lhs = *lhs.wrapping_offset(row as isize * lhs_rs); + let dst = dst + .wrapping_offset(row as isize * dst_rs) + .wrapping_offset(col as isize * dst_cs); + + *dst = mul_add(lhs, rhs, alpha * *dst); + } + } + } + } else { + for col in 0..n { + let rhs = beta * *rhs.wrapping_offset(col as isize * rhs_cs); + for row in 0..m { + let lhs = *lhs.wrapping_offset(row as isize * lhs_rs); + let dst = dst + .wrapping_offset(row as isize * dst_rs) + .wrapping_offset(col as isize * dst_cs); + + *dst = lhs * rhs; + } + } + } + return; + } + 2 => { + if !alpha.is_zero() { + if alpha.is_one() { + for col in 0..n { + let rhs0 = + beta * *rhs.wrapping_offset(col as isize * rhs_cs + 0 * rhs_rs); + let rhs1 = + beta * *rhs.wrapping_offset(col as isize * rhs_cs + 1 * rhs_rs); + for row in 0..m { + let lhs0 = + *lhs.wrapping_offset(row as isize * lhs_rs + 0 * lhs_cs); + let lhs1 = + *lhs.wrapping_offset(row as isize * lhs_rs + 1 * lhs_cs); + let dst = dst + .wrapping_offset(row as isize * dst_rs) + .wrapping_offset(col as isize * dst_cs); + + *dst = mul_add(lhs1, rhs1, mul_add(lhs0, rhs0, *dst)); + } + } + } else { + for col in 0..n { + let rhs0 = + beta * *rhs.wrapping_offset(col as isize * rhs_cs + 0 * rhs_rs); + let rhs1 = + beta * *rhs.wrapping_offset(col as isize * rhs_cs + 1 * rhs_rs); + for row in 0..m { + let lhs0 = + *lhs.wrapping_offset(row as isize * lhs_rs + 0 * lhs_cs); + let lhs1 = + *lhs.wrapping_offset(row as isize * lhs_rs + 1 * lhs_cs); + let dst = dst + .wrapping_offset(row as isize * dst_rs) + .wrapping_offset(col as isize * dst_cs); + + *dst = mul_add(lhs1, rhs1, mul_add(lhs0, rhs0, alpha * *dst)); + } + } + } + } else { + for col in 0..n { + let rhs0 = + beta * *rhs.wrapping_offset(col as isize * rhs_cs + 0 * rhs_rs); + let rhs1 = + beta * *rhs.wrapping_offset(col as isize * rhs_cs + 1 * rhs_rs); + for row in 0..m { + let lhs0 = *lhs.wrapping_offset(row as isize * lhs_rs + 0 * lhs_cs); + let lhs1 = *lhs.wrapping_offset(row as isize * lhs_rs + 1 * lhs_cs); + let dst = dst + .wrapping_offset(row as isize * dst_rs) + .wrapping_offset(col as isize * dst_cs); + + *dst = mul_add(lhs1, rhs1, lhs0 * rhs0); + } + } + } + return; + } + _ => unreachable!(), + } + }; + } + do_work!() +} diff --git a/vendor/gemm-common/src/horizontal_microkernel.rs b/vendor/gemm-common/src/horizontal_microkernel.rs new file mode 100644 index 00000000..eb84b55c --- /dev/null +++ b/vendor/gemm-common/src/horizontal_microkernel.rs @@ -0,0 +1,244 @@ +#[macro_export] +macro_rules! horizontal_kernel { + ($([$target: tt])?, $name: ident, $m: tt, $n: tt $(,)?) => { + $(#[target_feature(enable = $target)])? + pub unsafe fn $name( + k: usize, + dst: *mut T, + lhs: *const T, + rhs: *const T, + dst_cs: isize, + dst_rs: isize, + lhs_rs: isize, + rhs_cs: isize, + alpha: T, + beta: T, + alpha_status: u8, + _conj_dst: bool, + _conj_lhs: bool, + _conj_rhs: bool, + ) { + let mut accum = [[splat(::core::mem::zeroed()); $m]; $n]; + seq_macro::seq!(M_ITER in 0..$m { + let lhs~M_ITER = lhs.wrapping_offset(M_ITER * lhs_rs); + }); + seq_macro::seq!(N_ITER in 0..$n { + let rhs~N_ITER = rhs.wrapping_offset(N_ITER * rhs_cs); + }); + + let mut depth = 0; + while depth < k / N * N { + seq_macro::seq!(M_ITER in 0..$m { + let lhs~M_ITER = *(lhs~M_ITER.add(depth) as *const [T; N]); + }); + seq_macro::seq!(N_ITER in 0..$n { + let rhs~N_ITER = *(rhs~N_ITER.add(depth) as *const [T; N]); + }); + + seq_macro::seq!(M_ITER in 0..$m { + seq_macro::seq!(N_ITER in 0..$n { + accum[N_ITER][M_ITER] = mul_add(lhs~M_ITER, rhs~N_ITER, accum[N_ITER][M_ITER]); + }); + }); + + depth += N; + } + + if depth < k { + seq_macro::seq!(M_ITER in 0..$m { + let lhs~M_ITER = partial_load(lhs~M_ITER.add(depth), k - depth); + }); + seq_macro::seq!(N_ITER in 0..$n { + let rhs~N_ITER = partial_load(rhs~N_ITER.add(depth), k - depth); + }); + + seq_macro::seq!(M_ITER in 0..$m { + seq_macro::seq!(N_ITER in 0..$n { + accum[N_ITER][M_ITER] = mul_add(lhs~M_ITER, rhs~N_ITER, accum[N_ITER][M_ITER]); + }); + }); + } + + let mut accum_reduced: [[T; $m]; $n] = core::mem::zeroed(); + seq_macro::seq!(M_ITER in 0..$m { + seq_macro::seq!(N_ITER in 0..$n { + accum_reduced[N_ITER][M_ITER] = reduce_sum(accum[N_ITER][M_ITER]); + }); + }); + + if alpha_status == 0 { + seq_macro::seq!(M_ITER in 0..$m {{ + seq_macro::seq!(N_ITER in 0..$n {{ + let dst = dst.offset(dst_cs * N_ITER + dst_rs * M_ITER); + *dst = scalar_mul(beta, accum_reduced[N_ITER][M_ITER]); + }}); + }}); + } else if alpha_status == 1 { + seq_macro::seq!(M_ITER in 0..$m {{ + seq_macro::seq!(N_ITER in 0..$n {{ + let dst = dst.offset(dst_cs * N_ITER + dst_rs * M_ITER); + *dst = scalar_mul_add( + beta, + accum_reduced[N_ITER][M_ITER], + *dst, + ); + }}); + }}); + } else { + seq_macro::seq!(M_ITER in 0..$m {{ + seq_macro::seq!(N_ITER in 0..$n {{ + let dst = dst.offset(dst_cs * N_ITER + dst_rs * M_ITER); + *dst = scalar_add( + scalar_mul(beta, accum_reduced[N_ITER][M_ITER]), + scalar_mul(alpha, *dst), + ); + }}); + }}); + } + } + }; +} + +#[macro_export] +macro_rules! horizontal_cplx_kernel { + ($([$target: tt])?, $name: ident, $m: tt, $n: tt $(,)?) => { + $(#[target_feature(enable = $target)])? + pub unsafe fn $name( + k: usize, + dst: *mut num_complex::Complex, + lhs: *const num_complex::Complex, + rhs: *const num_complex::Complex, + dst_cs: isize, + dst_rs: isize, + lhs_rs: isize, + rhs_cs: isize, + alpha: num_complex::Complex, + beta: num_complex::Complex, + alpha_status: u8, + _conj_dst: bool, + conj_lhs: bool, + conj_rhs: bool, + ) { + let mut accum = [[splat(::core::mem::zeroed()); $m]; $n]; + seq_macro::seq!(M_ITER in 0..$m { + let lhs~M_ITER = lhs.wrapping_offset(M_ITER * lhs_rs); + }); + seq_macro::seq!(N_ITER in 0..$n { + let rhs~N_ITER = rhs.wrapping_offset(N_ITER * rhs_cs); + }); + + let (conj_lhs, conj_all) = match (conj_lhs, conj_rhs) { + (true, true) => (false, true), + (false, true) => (true, true), + (true, false) => (true, false), + (false, false) => (false, false), + }; + + if conj_lhs { + let mut depth = 0; + while depth < k / CPLX_N * CPLX_N { + seq_macro::seq!(M_ITER in 0..$m { + let lhs~M_ITER = *(lhs~M_ITER.add(depth) as *const Pack); + }); + seq_macro::seq!(N_ITER in 0..$n { + let rhs~N_ITER = *(rhs~N_ITER.add(depth) as *const Pack); + }); + + seq_macro::seq!(M_ITER in 0..$m { + seq_macro::seq!(N_ITER in 0..$n { + accum[N_ITER][M_ITER] = conj_mul_add(lhs~M_ITER, rhs~N_ITER, accum[N_ITER][M_ITER]); + }); + }); + + depth += CPLX_N; + } + + if depth < k { + seq_macro::seq!(M_ITER in 0..$m { + let lhs~M_ITER = partial_load(lhs~M_ITER.add(depth), k - depth); + }); + seq_macro::seq!(N_ITER in 0..$n { + let rhs~N_ITER = partial_load(rhs~N_ITER.add(depth), k - depth); + }); + + seq_macro::seq!(M_ITER in 0..$m { + seq_macro::seq!(N_ITER in 0..$n { + accum[N_ITER][M_ITER] = conj_mul_add(lhs~M_ITER, rhs~N_ITER, accum[N_ITER][M_ITER]); + }); + }); + } + } else { + let mut depth = 0; + while depth < k / CPLX_N * CPLX_N { + seq_macro::seq!(M_ITER in 0..$m { + let lhs~M_ITER = *(lhs~M_ITER.add(depth) as *const Pack); + }); + seq_macro::seq!(N_ITER in 0..$n { + let rhs~N_ITER = *(rhs~N_ITER.add(depth) as *const Pack); + }); + + seq_macro::seq!(M_ITER in 0..$m { + seq_macro::seq!(N_ITER in 0..$n { + accum[N_ITER][M_ITER] = mul_add(lhs~M_ITER, rhs~N_ITER, accum[N_ITER][M_ITER]); + }); + }); + + depth += CPLX_N; + } + + if depth < k { + seq_macro::seq!(M_ITER in 0..$m { + let lhs~M_ITER = partial_load(lhs~M_ITER.add(depth), k - depth); + }); + seq_macro::seq!(N_ITER in 0..$n { + let rhs~N_ITER = partial_load(rhs~N_ITER.add(depth), k - depth); + }); + + seq_macro::seq!(M_ITER in 0..$m { + seq_macro::seq!(N_ITER in 0..$n { + accum[N_ITER][M_ITER] = mul_add(lhs~M_ITER, rhs~N_ITER, accum[N_ITER][M_ITER]); + }); + }); + } + } + + if conj_all { + seq_macro::seq!(M_ITER in 0..$m {{ + seq_macro::seq!(N_ITER in 0..$n {{ + accum[N_ITER][M_ITER] = conj(accum[N_ITER][M_ITER]); + }}); + }}); + } + + let mut accum_reduced: [[num_complex::Complex; $m]; $n] = core::mem::zeroed(); + seq_macro::seq!(M_ITER in 0..$m { + seq_macro::seq!(N_ITER in 0..$n { + accum_reduced[N_ITER][M_ITER] = reduce_sum(accum[N_ITER][M_ITER]); + }); + }); + + if alpha_status == 0 { + seq_macro::seq!(M_ITER in 0..$m {{ + seq_macro::seq!(N_ITER in 0..$n {{ + let dst = dst.offset(dst_cs * N_ITER + dst_rs * M_ITER); + *dst = beta * accum_reduced[N_ITER][M_ITER]; + }}); + }}); + } else if alpha_status == 1 { + seq_macro::seq!(M_ITER in 0..$m {{ + seq_macro::seq!(N_ITER in 0..$n {{ + let dst = dst.offset(dst_cs * N_ITER + dst_rs * M_ITER); + *dst = (beta * accum_reduced[N_ITER][M_ITER]) + *dst; + }}); + }}); + } else { + seq_macro::seq!(M_ITER in 0..$m {{ + seq_macro::seq!(N_ITER in 0..$n {{ + let dst = dst.offset(dst_cs * N_ITER + dst_rs * M_ITER); + *dst = (beta * accum_reduced[N_ITER][M_ITER]) + (alpha * *dst); + }}); + }}); + } + } + }; +} diff --git a/vendor/gemm-common/src/lib.rs b/vendor/gemm-common/src/lib.rs new file mode 100644 index 00000000..94210884 --- /dev/null +++ b/vendor/gemm-common/src/lib.rs @@ -0,0 +1,97 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use core::sync::atomic::{AtomicBool, Ordering::Relaxed}; + +#[cfg(feature = "wasm-simd128-enable")] +pub const DEFAULT_WASM_SIMD128: bool = true; + +#[cfg(not(feature = "wasm-simd128-enable"))] +pub const DEFAULT_WASM_SIMD128: bool = false; + +static WASM_SIMD128: AtomicBool = AtomicBool::new(DEFAULT_WASM_SIMD128); + +#[inline] +pub fn get_wasm_simd128() -> bool { + WASM_SIMD128.load(Relaxed) +} +#[inline] +pub fn set_wasm_simd128(enable: bool) { + WASM_SIMD128.store(enable, Relaxed) +} + +extern crate alloc; + +pub mod cache; + +pub mod gemm; +pub mod gemv; +pub mod gevv; + +pub mod horizontal_microkernel; +pub mod microkernel; + +pub mod pack_operands; +pub mod simd; + +pub use pulp; + +#[derive(Copy, Clone, Debug)] +pub enum Parallelism { + None, + #[cfg(feature = "rayon")] + Rayon(usize), +} + +pub struct Ptr(pub *mut T); + +impl Clone for Ptr { + #[inline] + fn clone(&self) -> Self { + *self + } +} +impl Copy for Ptr {} + +unsafe impl Send for Ptr {} +unsafe impl Sync for Ptr {} + +impl Ptr { + #[inline(always)] + pub fn wrapping_offset(self, offset: isize) -> Self { + Ptr::(self.0.wrapping_offset(offset)) + } + #[inline(always)] + pub fn wrapping_add(self, offset: usize) -> Self { + Ptr::(self.0.wrapping_add(offset)) + } +} + +#[cfg(not(feature = "std"))] +#[macro_export] +macro_rules! feature_detected { + ($tt: tt) => { + cfg!(feature = $tt) + }; +} + +#[cfg(all(feature = "std", any(target_arch = "x86", target_arch = "x86_64")))] +#[macro_export] +macro_rules! feature_detected { + ($tt: tt) => { + ::std::arch::is_x86_feature_detected!($tt) + }; +} +#[cfg(all(feature = "std", target_arch = "aarch64"))] +#[macro_export] +macro_rules! feature_detected { + ($tt: tt) => { + ::std::arch::is_aarch64_feature_detected!($tt) + }; +} +#[cfg(all(feature = "std", target_family = "wasm"))] +#[macro_export] +macro_rules! feature_detected { + ("simd128") => { + $crate::get_wasm_simd128() + }; +} diff --git a/vendor/gemm-common/src/microkernel.rs b/vendor/gemm-common/src/microkernel.rs new file mode 100644 index 00000000..5aa98b33 --- /dev/null +++ b/vendor/gemm-common/src/microkernel.rs @@ -0,0 +1,1879 @@ +pub type MicroKernelFn = unsafe fn( + usize, + usize, + usize, + *mut T, + *const T, + *const T, + isize, + isize, + isize, + isize, + isize, + T, + T, + u8, + bool, + bool, + bool, + *const T, +); + +pub type HMicroKernelFn = unsafe fn( + k: usize, + dst: *mut T, + lhs: *const T, + rhs: *const T, + dst_cs: isize, + dst_rs: isize, + lhs_rs: isize, + rhs_cs: isize, + alpha: T, + beta: T, + alpha_status: u8, + _conj_dst: bool, + _conj_lhs: bool, + _conj_rhs: bool, +); + +// microkernel_fn_array!{ +// [ a, b, c, ], +// [ d, e, f, ], +// } +// +// expands to +// pub const UKR: [[[MicroKernelFn; 3]; 2]; 3] = [ +// [ +// [ a::<0>, b::<0>, c::<0>, ], +// [ d::<0>, e::<0>, f::<0>, ], +// ], +// [ +// [ a::<1>, b::<1>, c::<1>, ], +// [ d::<1>, e::<1>, f::<1>, ], +// ], +// [ +// [ a::<2>, b::<2>, c::<2>, ], +// [ d::<2>, e::<2>, f::<2>, ], +// ], +// ] +#[macro_export] +macro_rules! __one { + ( + $tt: tt + ) => { + 1 + }; +} + +#[macro_export] +macro_rules! __microkernel_fn_array_helper { + ( + [ $($tt: tt,)* ] + ) => { + { + let mut count = 0_usize; + $(count += $crate::__one!($tt);)* + count + } + } +} + +#[macro_export] +macro_rules! __microkernel_fn_array_helper_nr { + ($([ + $($ukr: ident,)* + ],)*) => { + { + let counts = [$({ + let mut count = 0_usize; + $(count += $crate::__one!($ukr);)* + count + },)*]; + + counts[0] + } + } +} + +#[macro_export] +macro_rules! microkernel_fn_array { + ($([ + $($ukr: ident,)* + ],)*) => { + pub const MR_DIV_N: usize = + $crate::__microkernel_fn_array_helper!([$([$($ukr,)*],)*]); + pub const NR: usize = + $crate::__microkernel_fn_array_helper_nr!($([$($ukr,)*],)*); + + pub const UKR: [[$crate::microkernel::MicroKernelFn; NR]; MR_DIV_N] = + [ $([$($ukr,)*]),* ]; + }; +} + +#[macro_export] +macro_rules! hmicrokernel_fn_array { + ($([ + $($ukr: ident,)* + ],)*) => { + pub const H_M: usize = + $crate::__microkernel_fn_array_helper!([$([$($ukr,)*],)*]); + pub const H_N: usize = + $crate::__microkernel_fn_array_helper_nr!($([$($ukr,)*],)*); + + pub const H_UKR: [[$crate::microkernel::HMicroKernelFn; H_N]; H_M] = + [ $([$($ukr,)*]),* ]; + }; +} + +#[macro_export] +macro_rules! microkernel_cplx_fn_array { + ($([ + $($ukr: ident,)* + ],)*) => { + pub const CPLX_MR_DIV_N: usize = + $crate::__microkernel_fn_array_helper!([$([$($ukr,)*],)*]); + pub const CPLX_NR: usize = + $crate::__microkernel_fn_array_helper_nr!($([$($ukr,)*],)*); + + pub const CPLX_UKR: [[$crate::microkernel::MicroKernelFn>; CPLX_NR]; CPLX_MR_DIV_N] = + [ $([$($ukr,)*]),* ]; + }; +} + +#[macro_export] +macro_rules! hmicrokernel_cplx_fn_array { + ($([ + $($ukr: ident,)* + ],)*) => { + pub const H_CPLX_M: usize = + $crate::__microkernel_fn_array_helper!([$([$($ukr,)*],)*]); + pub const H_CPLX_N: usize = + $crate::__microkernel_fn_array_helper_nr!($([$($ukr,)*],)*); + + pub const H_CPLX_UKR: [[$crate::microkernel::HMicroKernelFn>; H_CPLX_N]; H_CPLX_M] = + [ $([$($ukr,)*]),* ]; + }; +} + +#[macro_export] +macro_rules! amx { + ($op: tt, $gpr: expr) => { + ::core::arch::asm!(::core::concat!(".word (0x201000 + (", ::core::stringify!($op) ," << 5))") , in("x0")$gpr) + }; +} + +// Credit to RuQing Xu (https://github.com/xrq-phys/blis_apple) for the reference implementation. +#[macro_export] +macro_rules! microkernel_amx { + ($ty: tt, $([$target: tt])?, $unroll: tt, $name: ident, $mr_div_n: tt, $nr: tt , $nr_div_n: tt, $n: tt) => { + $(#[target_feature(enable = $target)])? + // 0, 1, or 2 for generic alpha + pub unsafe fn $name( + m: usize, + n: usize, + k: usize, + dst: *mut T, + mut packed_lhs: *const T, + mut packed_rhs: *const T, + dst_cs: isize, + dst_rs: isize, + lhs_cs: isize, + rhs_rs: isize, + rhs_cs: isize, + alpha: T, + beta: T, + alpha_status: u8, + _conj_dst: bool, + _conj_lhs: bool, + _conj_rhs: bool, + mut next_lhs: *const T, + ) { + assert_eq!(rhs_cs, 1); + + macro_rules! amx_nop { + ($imm5: tt) => { + ::core::arch::asm!( + ::core::concat!( + "nop\nnop\nnop\n.word (0x201000 + (17 << 5) + ", + ::core::stringify!($imm5), + ")" + ), + options(nostack) + ); + }; + } + macro_rules! amx_set { () => { amx_nop!(0) }; } + macro_rules! amx_clr { () => { amx_nop!(1) }; } + + macro_rules! __ldx { ($gpr: expr) => { $crate::amx!(0, $gpr) } } + macro_rules! __ldy { ($gpr: expr) => { $crate::amx!(1, $gpr) } } + macro_rules! __stz { ($gpr: expr) => { $crate::amx!(5, $gpr) } } + macro_rules! __extrx { ($gpr: expr) => { $crate::amx!(8, $gpr) } } + macro_rules! __fma { + (f64, $gpr: expr) => { $crate::amx!(10, $gpr) }; + (f32, $gpr: expr) => { $crate::amx!(12, $gpr) }; + (f16, $gpr: expr) => { $crate::amx!(15, $gpr) }; + } + + macro_rules! ldx { ($addr: expr, $idx: expr) => { __ldx!( ($idx << 56) | (($addr as usize) & ((1 << 56) - 1)) ) } } + macro_rules! ldy { ($addr: expr, $idx: expr) => { __ldy!( ($idx << 56) | (($addr as usize) & ((1 << 56) - 1)) ) } } + macro_rules! stz { ($addr: expr, $idx: expr) => { __stz!( ($idx << 56) | (($addr as usize) & ((1 << 56) - 1)) ) } } + macro_rules! extrx { ($z: expr, $x: expr) => { __extrx!( ($x << 16) | ($z << 20) ) } } + macro_rules! fma { + ($x: expr, $y: expr, $z: expr) => { __fma!($ty, ($y << 6) | ($x << 16) | ($z << 20)) }; + } + macro_rules! mul_select { + ($col: expr, $x: expr, $y: expr, $z: expr) => { + __fma!( + $ty, + (1usize << 27) + | (1usize << 37) + | ($col << 32) + | ($y << 6) + | ($x << 16) + | ($z << 20) + ) + }; + } + macro_rules! fma_select { + ($col: expr, $x: expr, $y: expr, $z: expr) => { + __fma!( + $ty, + (1usize << 37) + | ($col << 32) + | ($y << 6) + | ($x << 16) + | ($z << 20) + ) + }; + } + + amx_set!(); + + ldx!(packed_lhs, 0); + + let k_unroll = k / $unroll; + let k_leftover = k % $unroll ; + + let mut depth = k_unroll; + if depth != 0 { + loop { + seq_macro::seq!(UNROLL_ITER in 0..$unroll {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + ldx!(packed_lhs.offset(lhs_cs * UNROLL_ITER + M_ITER * $n), UNROLL_ITER + $unroll * M_ITER); + }}); + seq_macro::seq!(N_ITER in 0..$nr_div_n {{ + ldy!(packed_rhs.offset(rhs_rs * UNROLL_ITER + N_ITER * $n), UNROLL_ITER + $unroll * N_ITER); + }}); + }}); + seq_macro::seq!(UNROLL_ITER in 0..$unroll {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + seq_macro::seq!(N_ITER in 0..$nr_div_n {{ + fma!(UNROLL_ITER + $unroll * M_ITER, UNROLL_ITER + $unroll * N_ITER, M_ITER + $mr_div_n * N_ITER); + }}); + }}); + }}); + + packed_lhs = packed_lhs.wrapping_offset($unroll * lhs_cs); + packed_rhs = packed_rhs.wrapping_offset($unroll * rhs_rs); + next_lhs = next_lhs.wrapping_offset($unroll * lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + depth = k_leftover; + if depth != 0 { + loop { + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + ldx!(packed_lhs.offset(lhs_cs * 0 + M_ITER * $n), 0 + $unroll * M_ITER); + }}); + seq_macro::seq!(N_ITER in 0..$nr_div_n {{ + ldy!(packed_rhs.offset(rhs_rs * 0 + N_ITER * $n), 0 + $unroll * N_ITER); + }}); + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + seq_macro::seq!(N_ITER in 0..$nr_div_n {{ + fma!($unroll * M_ITER, $unroll * N_ITER, M_ITER + $mr_div_n * N_ITER); + }}); + }}); + + packed_lhs = packed_lhs.wrapping_offset(lhs_cs); + packed_rhs = packed_rhs.wrapping_offset(rhs_rs); + next_lhs = next_lhs.wrapping_offset(lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + + let alpha_c = [alpha; $n]; + let beta_c = [beta; $n]; + ldy!(alpha_c.as_ptr(), 0); + ldy!(beta_c.as_ptr(), 1); + + let stride = 64 / N; + + for i in 0..N { + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + seq_macro::seq!(N_ITER in 0..$nr_div_n {{ + // extrx!((i * stride + M_ITER + $mr_div_n * N_ITER), M_ITER + $mr_div_n * N_ITER); + }}); + }}); + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + seq_macro::seq!(N_ITER in 0..$nr_div_n {{ + extrx!((i * stride + M_ITER + $mr_div_n * N_ITER), M_ITER + $mr_div_n * N_ITER); + mul_select!(i, M_ITER + $mr_div_n * N_ITER, 1, i * stride + M_ITER + $mr_div_n * N_ITER); + }}); + }}); + } + + let mut tmp_dst = [::core::mem::MaybeUninit::::uninit(); { $mr_div_n * $n * $nr }]; + let mut tmp_dst = tmp_dst.as_mut_ptr() as *mut T; + let mut tmp_dst_cs = $mr_div_n * $n; + + if dst_rs == 1 && m == $mr_div_n * N && n == $nr { + tmp_dst = dst; + tmp_dst_cs = dst_cs; + } else { + for j in 0..n { + for i in 0..m { + *tmp_dst.offset(i as isize + j as isize * tmp_dst_cs) = + *dst .offset(i as isize * dst_rs + j as isize * dst_cs); + } + for i in m..$mr_div_n * N { + *tmp_dst.offset(i as isize + j as isize * tmp_dst_cs) = ::core::mem::zeroed(); + } + } + } + + if alpha_status == 0 { + for i in 0..N { + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + seq_macro::seq!(N_ITER in 0..$nr_div_n {{ + stz!( + tmp_dst.offset((i as isize + N_ITER * $n) * tmp_dst_cs + M_ITER * $n), + (i * stride + M_ITER + $mr_div_n * N_ITER) + ); + }}); + }}); + } + } else { + for i in 0..N { + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + seq_macro::seq!(N_ITER in 0..$nr_div_n {{ + ldx!( + tmp_dst.offset((i as isize + N_ITER * $n) * tmp_dst_cs + M_ITER * $n), + M_ITER + $mr_div_n * N_ITER + ); + // ldx!(tmp_dst.offset(i as isize * tmp_dst_cs), M_ITER + $mr_div_n * N_ITER); + fma_select!(i, M_ITER + $mr_div_n * N_ITER, 0, i * stride + M_ITER + $mr_div_n * N_ITER); + stz!( + tmp_dst.offset((i as isize + N_ITER * $n) * tmp_dst_cs + M_ITER * $n), + i * stride + M_ITER + $mr_div_n * N_ITER + ); + }}); + }}); + } + } + + if !(dst_rs == 1 && m == $mr_div_n * N && n == $nr) { + for j in 0..n { + for i in 0..m { + *dst .offset(i as isize * dst_rs + j as isize * dst_cs) = + *tmp_dst.offset(i as isize + j as isize * tmp_dst_cs); + } + } + } + + amx_clr!(); + } + }; +} + +#[macro_export] +macro_rules! microkernel { + ($([$target: tt])?, $unroll: tt, $name: ident, $mr_div_n: tt, $nr: tt $(, $nr_div_n: tt, $n: tt)?) => { + $(#[target_feature(enable = $target)])? + // 0, 1, or 2 for generic alpha + pub unsafe fn $name( + m: usize, + n: usize, + k: usize, + dst: *mut T, + mut packed_lhs: *const T, + mut packed_rhs: *const T, + dst_cs: isize, + dst_rs: isize, + lhs_cs: isize, + rhs_rs: isize, + rhs_cs: isize, + alpha: T, + beta: T, + alpha_status: u8, + _conj_dst: bool, + _conj_lhs: bool, + _conj_rhs: bool, + mut next_lhs: *const T, + ) { + let mut accum_storage = [[splat(::core::mem::zeroed()); $mr_div_n]; $nr]; + let accum = accum_storage.as_mut_ptr() as *mut Pack; + + let mut lhs = [::core::mem::MaybeUninit::::uninit(); $mr_div_n]; + let mut rhs = ::core::mem::MaybeUninit::::uninit(); + + #[derive(Copy, Clone)] + struct KernelIter { + packed_lhs: *const T, + packed_rhs: *const T, + next_lhs: *const T, + lhs_cs: isize, + rhs_rs: isize, + rhs_cs: isize, + accum: *mut Pack, + lhs: *mut Pack, + rhs: *mut Pack, + } + + impl KernelIter { + #[inline(always)] + unsafe fn execute(self, iter: usize) { + let packed_lhs = self.packed_lhs.wrapping_offset(iter as isize * self.lhs_cs); + let packed_rhs = self.packed_rhs.wrapping_offset(iter as isize * self.rhs_rs); + let next_lhs = self.next_lhs.wrapping_offset(iter as isize * self.lhs_cs); + + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + *self.lhs.add(M_ITER) = *(packed_lhs.add(M_ITER * N) as *const Pack); + }}); + + seq_macro::seq!(N_ITER in 0..$nr {{ + *self.rhs = splat(*packed_rhs.wrapping_offset(N_ITER * self.rhs_cs)); + let accum = self.accum.add(N_ITER * $mr_div_n); + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let accum = &mut *accum.add(M_ITER); + *accum = mul_add( + *self.lhs.add(M_ITER), + *self.rhs, + *accum, + ); + }}); + }}); + + let _ = next_lhs; + } + + $( + #[inline(always)] + unsafe fn execute_neon(self, iter: usize) { + debug_assert_eq!(self.rhs_cs, 1); + let packed_lhs = self.packed_lhs.wrapping_offset(iter as isize * self.lhs_cs); + let packed_rhs = self.packed_rhs.wrapping_offset(iter as isize * self.rhs_rs); + + load::<$mr_div_n>(self.lhs, packed_lhs); + + seq_macro::seq!(N_ITER0 in 0..$nr_div_n {{ + *self.rhs = *(packed_rhs.wrapping_offset(N_ITER0 * $n) as *const Pack); + + seq_macro::seq!(N_ITER1 in 0..$n {{ + const N_ITER: usize = N_ITER0 * $n + N_ITER1; + let accum = self.accum.add(N_ITER * $mr_div_n); + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let accum = &mut *accum.add(M_ITER); + *accum = mul_add_lane::( + *self.lhs.add(M_ITER), + *self.rhs, + *accum, + ); + }}); + }}); + }}); + } + )? + } + + let k_unroll = k / $unroll; + let k_leftover = k % $unroll; + + let mut main_loop = { + #[inline(always)] + || { + loop { + $( + let _ = $nr_div_n; + if rhs_cs == 1 { + let mut depth = k_unroll; + if depth != 0 { + loop { + let iter = KernelIter { + packed_lhs, + next_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs: lhs.as_mut_ptr() as _, + rhs: &mut rhs as *mut _ as _, + }; + + seq_macro::seq!(UNROLL_ITER in 0..$unroll {{ + iter.execute_neon(UNROLL_ITER); + }}); + + packed_lhs = packed_lhs.wrapping_offset($unroll * lhs_cs); + packed_rhs = packed_rhs.wrapping_offset($unroll * rhs_rs); + next_lhs = next_lhs.wrapping_offset($unroll * lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + depth = k_leftover; + if depth != 0 { + loop { + KernelIter { + packed_lhs, + next_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs: lhs.as_mut_ptr() as _, + rhs: &mut rhs as *mut _ as _, + } + .execute_neon(0); + + packed_lhs = packed_lhs.wrapping_offset(lhs_cs); + packed_rhs = packed_rhs.wrapping_offset(rhs_rs); + next_lhs = next_lhs.wrapping_offset(lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + break; + } + )? + + let mut depth = k_unroll; + if depth != 0 { + loop { + let iter = KernelIter { + packed_lhs, + next_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs: lhs.as_mut_ptr() as _, + rhs: &mut rhs as *mut _ as _, + }; + + seq_macro::seq!(UNROLL_ITER in 0..$unroll {{ + iter.execute(UNROLL_ITER); + }}); + + packed_lhs = packed_lhs.wrapping_offset($unroll * lhs_cs); + packed_rhs = packed_rhs.wrapping_offset($unroll * rhs_rs); + next_lhs = next_lhs.wrapping_offset($unroll * lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + depth = k_leftover; + if depth != 0 { + loop { + KernelIter { + packed_lhs, + next_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs: lhs.as_mut_ptr() as _, + rhs: &mut rhs as *mut _ as _, + } + .execute(0); + + packed_lhs = packed_lhs.wrapping_offset(lhs_cs); + packed_rhs = packed_rhs.wrapping_offset(rhs_rs); + next_lhs = next_lhs.wrapping_offset(lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + break; + } + } + }; + + if rhs_rs == 1 { + main_loop(); + } else { + main_loop(); + } + + if m == $mr_div_n * N && n == $nr && dst_rs == 1 { + let alpha = splat(alpha); + let beta = splat(beta); + if alpha_status == 2 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * N as isize + N_ITER * dst_cs) as *mut Pack; + dst.write_unaligned(add( + mul(alpha, *dst), + mul(beta, *accum.offset(M_ITER + $mr_div_n * N_ITER)), + )); + }}); + }}); + } else if alpha_status == 1 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * N as isize + N_ITER * dst_cs) as *mut Pack; + dst.write_unaligned(mul_add( + beta, + *accum.offset(M_ITER + $mr_div_n * N_ITER), + *dst, + )); + }}); + }}); + } else { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * N as isize + N_ITER * dst_cs) as *mut Pack; + dst.write_unaligned(mul(beta, *accum.offset(M_ITER + $mr_div_n * N_ITER))); + }}); + }}); + } + } else { + let src = accum_storage; // write to stack + let src = src.as_ptr() as *const T; + + if alpha_status == 2 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = scalar_add(scalar_mul(alpha, *dst_ij), scalar_mul(beta, *src_ij)); + } + } + } else if alpha_status == 1 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = scalar_mul_add(beta, *src_ij, *dst_ij); + } + } + } else { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = scalar_mul(beta, *src_ij); + } + } + } + } + + } + }; +} + +#[macro_export] +macro_rules! microkernel_cplx_2step { + ($([$target: tt])?, $unroll: tt, $name: ident, $mr_div_n: tt, $nr: tt) => { + $(#[target_feature(enable = $target)])? + // 0, 1, or 2 for generic alpha + pub unsafe fn $name( + m: usize, + n: usize, + k: usize, + dst: *mut num_complex::Complex, + mut packed_lhs: *const num_complex::Complex, + mut packed_rhs: *const num_complex::Complex, + dst_cs: isize, + dst_rs: isize, + lhs_cs: isize, + rhs_rs: isize, + rhs_cs: isize, + alpha: num_complex::Complex, + beta: num_complex::Complex, + alpha_status: u8, + conj_dst: bool, + conj_lhs: bool, + conj_rhs: bool, + mut next_lhs: *const num_complex::Complex, + ) { + let mut accum_storage = [[splat(0.0); $mr_div_n]; $nr]; + let accum = accum_storage.as_mut_ptr() as *mut Pack; + + let (neg_conj_rhs, conj_all, neg_all) = match (conj_lhs, conj_rhs) { + (true, true) => (true, false, true), + (false, true) => (false, true, false), + (true, false) => (false, false, false), + (false, false) => (true, true, true), + }; + + let mut lhs_re_im = [::core::mem::MaybeUninit::::uninit(); $mr_div_n]; + let mut lhs_im_re = [::core::mem::MaybeUninit::::uninit(); $mr_div_n]; + let mut rhs_re = ::core::mem::MaybeUninit::::uninit(); + let mut rhs_im = ::core::mem::MaybeUninit::::uninit(); + + #[derive(Copy, Clone)] + struct KernelIter { + packed_lhs: *const num_complex::Complex, + next_lhs: *const num_complex::Complex, + packed_rhs: *const num_complex::Complex, + lhs_cs: isize, + rhs_rs: isize, + rhs_cs: isize, + accum: *mut Pack, + lhs_re_im: *mut Pack, + lhs_im_re: *mut Pack, + rhs_re: *mut Pack, + rhs_im: *mut Pack, + } + + impl KernelIter { + #[inline(always)] + unsafe fn execute(self, iter: usize, neg_conj_rhs: bool) { + let packed_lhs = self.packed_lhs.wrapping_offset(iter as isize * self.lhs_cs); + let packed_rhs = self.packed_rhs.wrapping_offset(iter as isize * self.rhs_rs); + let next_lhs = self.next_lhs.wrapping_offset(iter as isize * self.lhs_cs); + + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let tmp = *(packed_lhs.add(M_ITER * CPLX_N) as *const Pack); + *self.lhs_re_im.add(M_ITER) = tmp; + *self.lhs_im_re.add(M_ITER) = swap_re_im(tmp); + }}); + + seq_macro::seq!(N_ITER in 0..$nr {{ + *self.rhs_re = splat((*packed_rhs.wrapping_offset(N_ITER * self.rhs_cs)).re); + + let accum = self.accum.add(N_ITER * $mr_div_n); + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let accum = &mut *accum.add(M_ITER); + *accum = mul_add_cplx_step0( + *self.lhs_re_im.add(M_ITER), + *self.rhs_re, + *accum, + neg_conj_rhs, + ); + }}); + }}); + + seq_macro::seq!(N_ITER in 0..$nr {{ + *self.rhs_im = splat((*packed_rhs.wrapping_offset(N_ITER * self.rhs_cs)).im); + + let accum = self.accum.add(N_ITER * $mr_div_n); + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let accum = &mut *accum.add(M_ITER); + *accum = mul_add_cplx_step1( + *self.lhs_im_re.add(M_ITER), + *self.rhs_im, + *accum, + neg_conj_rhs, + ); + }}); + }}); + + let _ = next_lhs; + } + } + + let k_unroll = k / $unroll; + let k_leftover = k % $unroll; + + let mut main_loop = { + #[inline(always)] + || { + loop { + if neg_conj_rhs { + let mut depth = k_unroll; + if depth != 0 { + loop { + let iter = KernelIter { + packed_lhs, + next_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs_re_im: lhs_re_im.as_mut_ptr() as _, + lhs_im_re: lhs_im_re.as_mut_ptr() as _, + rhs_re: &mut rhs_re as *mut _ as _, + rhs_im: &mut rhs_im as *mut _ as _, + }; + + seq_macro::seq!(UNROLL_ITER in 0..$unroll {{ + iter.execute(UNROLL_ITER, true); + }}); + + packed_lhs = packed_lhs.wrapping_offset($unroll * lhs_cs); + packed_rhs = packed_rhs.wrapping_offset($unroll * rhs_rs); + next_lhs = next_lhs.wrapping_offset($unroll * lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + depth = k_leftover; + if depth != 0 { + loop { + KernelIter { + packed_lhs, + next_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs_re_im: lhs_re_im.as_mut_ptr() as _, + lhs_im_re: lhs_im_re.as_mut_ptr() as _, + rhs_re: &mut rhs_re as *mut _ as _, + rhs_im: &mut rhs_im as *mut _ as _, + } + .execute(0, true); + + packed_lhs = packed_lhs.wrapping_offset(lhs_cs); + packed_rhs = packed_rhs.wrapping_offset(rhs_rs); + next_lhs = next_lhs.wrapping_offset(lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + break; + } else { + let mut depth = k_unroll; + if depth != 0 { + loop { + let iter = KernelIter { + next_lhs, + packed_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs_re_im: lhs_re_im.as_mut_ptr() as _, + lhs_im_re: lhs_im_re.as_mut_ptr() as _, + rhs_re: &mut rhs_re as *mut _ as _, + rhs_im: &mut rhs_im as *mut _ as _, + }; + + seq_macro::seq!(UNROLL_ITER in 0..$unroll {{ + iter.execute(UNROLL_ITER, false); + }}); + + packed_lhs = packed_lhs.wrapping_offset($unroll * lhs_cs); + packed_rhs = packed_rhs.wrapping_offset($unroll * rhs_rs); + next_lhs = next_lhs.wrapping_offset($unroll * lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + depth = k_leftover; + if depth != 0 { + loop { + KernelIter { + next_lhs, + packed_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs_re_im: lhs_re_im.as_mut_ptr() as _, + lhs_im_re: lhs_im_re.as_mut_ptr() as _, + rhs_re: &mut rhs_re as *mut _ as _, + rhs_im: &mut rhs_im as *mut _ as _, + } + .execute(0, false); + + packed_lhs = packed_lhs.wrapping_offset(lhs_cs); + packed_rhs = packed_rhs.wrapping_offset(rhs_rs); + next_lhs = next_lhs.wrapping_offset(lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + break; + } + } + } + }; + + if rhs_rs == 1 { + main_loop(); + } else { + main_loop(); + } + + if conj_all && neg_all { + seq_macro::seq!(N_ITER in 0..$nr {{ + let accum = accum.add(N_ITER * $mr_div_n); + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let accum = &mut *accum.add(M_ITER); + *accum = neg_conj(*accum); + }}); + }}); + } else if !conj_all && neg_all { + seq_macro::seq!(N_ITER in 0..$nr {{ + let accum = accum.add(N_ITER * $mr_div_n); + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let accum = &mut *accum.add(M_ITER); + *accum = neg(*accum); + }}); + }}); + } else if conj_all && !neg_all { + seq_macro::seq!(N_ITER in 0..$nr {{ + let accum = accum.add(N_ITER * $mr_div_n); + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let accum = &mut *accum.add(M_ITER); + *accum = conj(*accum); + }}); + }}); + } + + if m == $mr_div_n * CPLX_N && n == $nr && dst_rs == 1 { + let alpha_re = splat(alpha.re); + let alpha_im = splat(alpha.im); + let beta_re = splat(beta.re); + let beta_im = splat(beta.im); + + if conj_dst { + if alpha_status == 2 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * CPLX_N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = add( + mul_cplx(conj(*dst), swap_re_im(conj(*dst)), alpha_re, alpha_im), + mul_cplx(accum, swap_re_im(accum), beta_re, beta_im), + ); + }}); + }}); + } else if alpha_status == 1 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * CPLX_N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = add( + conj(*dst), + mul_cplx(accum, swap_re_im(accum), beta_re, beta_im), + ); + }}); + }}); + } else { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * CPLX_N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = mul_cplx(accum, swap_re_im(accum), beta_re, beta_im); + }}); + }}); + } + } else { + if alpha_status == 2 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * CPLX_N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = add( + mul_cplx(*dst, swap_re_im(*dst), alpha_re, alpha_im), + mul_cplx(accum, swap_re_im(accum), beta_re, beta_im), + ); + }}); + }}); + } else if alpha_status == 1 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * CPLX_N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = add( + *dst, + mul_cplx(accum, swap_re_im(accum), beta_re, beta_im), + ); + }}); + }}); + } else { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * CPLX_N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = mul_cplx(accum, swap_re_im(accum), beta_re, beta_im); + }}); + }}); + } + } + } else { + let src = accum_storage; // write to stack + let src = src.as_ptr() as *const num_complex::Complex; + + if conj_dst { + if alpha_status == 2 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * CPLX_N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = alpha * (*dst_ij).conj() + beta * *src_ij; + } + } + } else if alpha_status == 1 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * CPLX_N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = (*dst_ij).conj() + beta * *src_ij; + } + } + } else { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * CPLX_N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = beta * *src_ij; + } + } + } + } else { + if alpha_status == 2 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * CPLX_N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = alpha * *dst_ij + beta * *src_ij; + } + } + } else if alpha_status == 1 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * CPLX_N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = *dst_ij + beta * *src_ij; + } + } + } else { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * CPLX_N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = beta * *src_ij; + } + } + } + } + } + } + }; +} + +#[macro_export] +macro_rules! microkernel_cplx { + ($([$target: tt])?, $unroll: tt, $name: ident, $mr_div_n: tt, $nr: tt) => { + $(#[target_feature(enable = $target)])? + // 0, 1, or 2 for generic alpha + pub unsafe fn $name( + m: usize, + n: usize, + k: usize, + dst: *mut num_complex::Complex, + mut packed_lhs: *const num_complex::Complex, + mut packed_rhs: *const num_complex::Complex, + dst_cs: isize, + dst_rs: isize, + lhs_cs: isize, + rhs_rs: isize, + rhs_cs: isize, + alpha: num_complex::Complex, + beta: num_complex::Complex, + alpha_status: u8, + conj_dst: bool, + conj_lhs: bool, + conj_rhs: bool, + mut next_lhs: *const num_complex::Complex, + ) { + let mut accum_storage = [[splat(0.0); $mr_div_n]; $nr]; + let accum = accum_storage.as_mut_ptr() as *mut Pack; + + let conj_both_lhs_rhs = conj_lhs; + let conj_rhs = conj_lhs != conj_rhs; + + let mut lhs_re_im = [::core::mem::MaybeUninit::::uninit(); $mr_div_n]; + let mut lhs_im_re = [::core::mem::MaybeUninit::::uninit(); $mr_div_n]; + let mut rhs_re = ::core::mem::MaybeUninit::::uninit(); + let mut rhs_im = ::core::mem::MaybeUninit::::uninit(); + + #[derive(Copy, Clone)] + struct KernelIter { + packed_lhs: *const num_complex::Complex, + next_lhs: *const num_complex::Complex, + packed_rhs: *const num_complex::Complex, + lhs_cs: isize, + rhs_rs: isize, + rhs_cs: isize, + accum: *mut Pack, + lhs_re_im: *mut Pack, + lhs_im_re: *mut Pack, + rhs_re: *mut Pack, + rhs_im: *mut Pack, + } + + impl KernelIter { + #[inline(always)] + unsafe fn execute(self, iter: usize, conj_rhs: bool) { + let packed_lhs = self.packed_lhs.wrapping_offset(iter as isize * self.lhs_cs); + let packed_rhs = self.packed_rhs.wrapping_offset(iter as isize * self.rhs_rs); + let next_lhs = self.next_lhs.wrapping_offset(iter as isize * self.lhs_cs); + + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let tmp = *(packed_lhs.add(M_ITER * CPLX_N) as *const Pack); + *self.lhs_re_im.add(M_ITER) = tmp; + *self.lhs_im_re.add(M_ITER) = swap_re_im(tmp); + }}); + + seq_macro::seq!(N_ITER in 0..$nr {{ + *self.rhs_re = splat((*packed_rhs.wrapping_offset(N_ITER * self.rhs_cs)).re); + *self.rhs_im = splat((*packed_rhs.wrapping_offset(N_ITER * self.rhs_cs)).im); + + let accum = self.accum.add(N_ITER * $mr_div_n); + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let accum = &mut *accum.add(M_ITER); + *accum = mul_add_cplx( + *self.lhs_re_im.add(M_ITER), + *self.lhs_im_re.add(M_ITER), + *self.rhs_re, + *self.rhs_im, + *accum, + conj_rhs, + ); + }}); + }}); + + let _ = next_lhs; + } + } + + let k_unroll = k / $unroll; + let k_leftover = k % $unroll; + + loop { + if conj_rhs { + let mut depth = k_unroll; + if depth != 0 { + loop { + let iter = KernelIter { + packed_lhs, + next_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs_re_im: lhs_re_im.as_mut_ptr() as _, + lhs_im_re: lhs_im_re.as_mut_ptr() as _, + rhs_re: &mut rhs_re as *mut _ as _, + rhs_im: &mut rhs_im as *mut _ as _, + }; + + seq_macro::seq!(UNROLL_ITER in 0..$unroll {{ + iter.execute(UNROLL_ITER, true); + }}); + + packed_lhs = packed_lhs.wrapping_offset($unroll * lhs_cs); + packed_rhs = packed_rhs.wrapping_offset($unroll * rhs_rs); + next_lhs = next_lhs.wrapping_offset($unroll * lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + depth = k_leftover; + if depth != 0 { + loop { + KernelIter { + packed_lhs, + next_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs_re_im: lhs_re_im.as_mut_ptr() as _, + lhs_im_re: lhs_im_re.as_mut_ptr() as _, + rhs_re: &mut rhs_re as *mut _ as _, + rhs_im: &mut rhs_im as *mut _ as _, + } + .execute(0, true); + + packed_lhs = packed_lhs.wrapping_offset(lhs_cs); + packed_rhs = packed_rhs.wrapping_offset(rhs_rs); + next_lhs = next_lhs.wrapping_offset(lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + break; + } else { + let mut depth = k_unroll; + if depth != 0 { + loop { + let iter = KernelIter { + next_lhs, + packed_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs_re_im: lhs_re_im.as_mut_ptr() as _, + lhs_im_re: lhs_im_re.as_mut_ptr() as _, + rhs_re: &mut rhs_re as *mut _ as _, + rhs_im: &mut rhs_im as *mut _ as _, + }; + + seq_macro::seq!(UNROLL_ITER in 0..$unroll {{ + iter.execute(UNROLL_ITER, false); + }}); + + packed_lhs = packed_lhs.wrapping_offset($unroll * lhs_cs); + packed_rhs = packed_rhs.wrapping_offset($unroll * rhs_rs); + next_lhs = next_lhs.wrapping_offset($unroll * lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + depth = k_leftover; + if depth != 0 { + loop { + KernelIter { + next_lhs, + packed_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs_re_im: lhs_re_im.as_mut_ptr() as _, + lhs_im_re: lhs_im_re.as_mut_ptr() as _, + rhs_re: &mut rhs_re as *mut _ as _, + rhs_im: &mut rhs_im as *mut _ as _, + } + .execute(0, false); + + packed_lhs = packed_lhs.wrapping_offset(lhs_cs); + packed_rhs = packed_rhs.wrapping_offset(rhs_rs); + next_lhs = next_lhs.wrapping_offset(lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + break; + } + } + + if conj_both_lhs_rhs { + seq_macro::seq!(N_ITER in 0..$nr {{ + let accum = accum.add(N_ITER * $mr_div_n); + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let accum = &mut *accum.add(M_ITER); + *accum = conj(*accum); + }}); + }}); + } + + if m == $mr_div_n * CPLX_N && n == $nr && dst_rs == 1 { + let alpha_re = splat(alpha.re); + let alpha_im = splat(alpha.im); + let beta_re = splat(beta.re); + let beta_im = splat(beta.im); + + if conj_dst { + if alpha_status == 2 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * CPLX_N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = add( + mul_cplx(conj(*dst), swap_re_im(conj(*dst)), alpha_re, alpha_im), + mul_cplx(accum, swap_re_im(accum), beta_re, beta_im), + ); + }}); + }}); + } else if alpha_status == 1 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * CPLX_N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = add( + conj(*dst), + mul_cplx(accum, swap_re_im(accum), beta_re, beta_im), + ); + }}); + }}); + } else { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * CPLX_N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = mul_cplx(accum, swap_re_im(accum), beta_re, beta_im); + }}); + }}); + } + } else { + if alpha_status == 2 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * CPLX_N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = add( + mul_cplx(*dst, swap_re_im(*dst), alpha_re, alpha_im), + mul_cplx(accum, swap_re_im(accum), beta_re, beta_im), + ); + }}); + }}); + } else if alpha_status == 1 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * CPLX_N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = add( + *dst, + mul_cplx(accum, swap_re_im(accum), beta_re, beta_im), + ); + }}); + }}); + } else { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * CPLX_N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = mul_cplx(accum, swap_re_im(accum), beta_re, beta_im); + }}); + }}); + } + } + } else { + let src = accum_storage; // write to stack + let src = src.as_ptr() as *const num_complex::Complex; + + if conj_dst { + if alpha_status == 2 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * CPLX_N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = alpha * (*dst_ij).conj() + beta * *src_ij; + } + } + } else if alpha_status == 1 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * CPLX_N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = (*dst_ij).conj() + beta * *src_ij; + } + } + } else { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * CPLX_N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = beta * *src_ij; + } + } + } + } else { + if alpha_status == 2 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * CPLX_N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = alpha * *dst_ij + beta * *src_ij; + } + } + } else if alpha_status == 1 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * CPLX_N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = *dst_ij + beta * *src_ij; + } + } + } else { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * CPLX_N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = beta * *src_ij; + } + } + } + } + } + } + }; +} + +#[macro_export] +macro_rules! microkernel_cplx_packed { + ($([$target: tt])?, $unroll: tt, $name: ident, $mr_div_n: tt, $nr: tt) => { + $(#[target_feature(enable = $target)])? + // 0, 1, or 2 for generic alpha + pub unsafe fn $name( + m: usize, + n: usize, + k: usize, + dst: *mut T, + mut packed_lhs: *const T, + mut packed_rhs: *const T, + dst_cs: isize, + dst_rs: isize, + lhs_cs: isize, + rhs_rs: isize, + rhs_cs: isize, + alpha: T, + beta: T, + alpha_status: u8, + conj_dst: bool, + conj_lhs: bool, + conj_rhs: bool, + mut next_lhs: *const T, + ) { + let mut accum_storage = [[core::mem::zeroed::(); $mr_div_n]; $nr]; + let accum = accum_storage.as_mut_ptr() as *mut Pack; + + let conj_both_lhs_rhs = conj_lhs; + let conj_rhs = conj_lhs != conj_rhs; + + let mut lhs = [::core::mem::MaybeUninit::::uninit(); $mr_div_n]; + let mut rhs = ::core::mem::MaybeUninit::::uninit(); + + #[derive(Copy, Clone)] + struct KernelIter { + packed_lhs: *const T, + next_lhs: *const T, + packed_rhs: *const T, + lhs_cs: isize, + rhs_rs: isize, + rhs_cs: isize, + accum: *mut Pack, + lhs: *mut Pack, + rhs: *mut Pack, + } + + impl KernelIter { + #[inline(always)] + unsafe fn execute(self, iter: usize, conj_rhs: bool) { + let packed_lhs = self.packed_lhs.wrapping_offset(iter as isize * self.lhs_cs); + let packed_rhs = self.packed_rhs.wrapping_offset(iter as isize * self.rhs_rs); + let next_lhs = self.next_lhs.wrapping_offset(iter as isize * self.lhs_cs); + + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + *self.lhs.add(M_ITER) = *(packed_lhs.add(M_ITER * N) as *const Pack); + }}); + + seq_macro::seq!(N_ITER in 0..$nr {{ + *self.rhs = splat(*packed_rhs.wrapping_offset(N_ITER * self.rhs_cs)); + + let accum = self.accum.add(N_ITER * $mr_div_n); + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let accum = &mut *accum.add(M_ITER); + *accum = mul_add_cplx( + *self.lhs.add(M_ITER), + *self.rhs, + *accum, + conj_rhs, + ); + }}); + }}); + + let _ = next_lhs; + } + } + + let k_unroll = k / $unroll; + let k_leftover = k % $unroll; + + loop { + if conj_rhs { + let mut depth = k_unroll; + if depth != 0 { + loop { + let iter = KernelIter { + packed_lhs, + next_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs: lhs.as_mut_ptr() as _, + rhs: &mut rhs as *mut _ as _, + }; + + seq_macro::seq!(UNROLL_ITER in 0..$unroll {{ + iter.execute(UNROLL_ITER, true); + }}); + + packed_lhs = packed_lhs.wrapping_offset($unroll * lhs_cs); + packed_rhs = packed_rhs.wrapping_offset($unroll * rhs_rs); + next_lhs = next_lhs.wrapping_offset($unroll * lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + depth = k_leftover; + if depth != 0 { + loop { + KernelIter { + packed_lhs, + next_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs: lhs.as_mut_ptr() as _, + rhs: &mut rhs as *mut _ as _, + } + .execute(0, true); + + packed_lhs = packed_lhs.wrapping_offset(lhs_cs); + packed_rhs = packed_rhs.wrapping_offset(rhs_rs); + next_lhs = next_lhs.wrapping_offset(lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + break; + } else { + let mut depth = k_unroll; + if depth != 0 { + loop { + let iter = KernelIter { + next_lhs, + packed_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs: lhs.as_mut_ptr() as _, + rhs: &mut rhs as *mut _ as _, + }; + + seq_macro::seq!(UNROLL_ITER in 0..$unroll {{ + iter.execute(UNROLL_ITER, false); + }}); + + packed_lhs = packed_lhs.wrapping_offset($unroll * lhs_cs); + packed_rhs = packed_rhs.wrapping_offset($unroll * rhs_rs); + next_lhs = next_lhs.wrapping_offset($unroll * lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + depth = k_leftover; + if depth != 0 { + loop { + KernelIter { + next_lhs, + packed_lhs, + packed_rhs, + lhs_cs, + rhs_rs, + rhs_cs, + accum, + lhs: lhs.as_mut_ptr() as _, + rhs: &mut rhs as *mut _ as _, + } + .execute(0, false); + + packed_lhs = packed_lhs.wrapping_offset(lhs_cs); + packed_rhs = packed_rhs.wrapping_offset(rhs_rs); + next_lhs = next_lhs.wrapping_offset(lhs_cs); + + depth -= 1; + if depth == 0 { + break; + } + } + } + break; + } + } + + if conj_both_lhs_rhs { + seq_macro::seq!(N_ITER in 0..$nr {{ + let accum = accum.add(N_ITER * $mr_div_n); + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let accum = &mut *accum.add(M_ITER); + *accum = conj(*accum); + }}); + }}); + } + + if m == $mr_div_n * N && n == $nr && dst_rs == 1 { + let alpha = splat(alpha); + let beta = splat(beta); + + if conj_dst { + if alpha_status == 2 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = add( + mul_cplx(conj(*dst), alpha), + mul_cplx(accum, beta), + ); + }}); + }}); + } else if alpha_status == 1 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = add( + conj(*dst), + mul_cplx(accum, beta), + ); + }}); + }}); + } else { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = mul_cplx(accum, beta); + }}); + }}); + } + } else { + if alpha_status == 2 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = add( + mul_cplx(*dst, alpha), + mul_cplx(accum, beta), + ); + }}); + }}); + } else if alpha_status == 1 { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = add( + *dst, + mul_cplx(accum, beta), + ); + }}); + }}); + } else { + seq_macro::seq!(N_ITER in 0..$nr {{ + seq_macro::seq!(M_ITER in 0..$mr_div_n {{ + let dst = dst.offset(M_ITER * N as isize + N_ITER * dst_cs) as *mut Pack; + let accum = *accum.offset(M_ITER + $mr_div_n * N_ITER); + *dst = mul_cplx(accum, beta); + }}); + }}); + } + } + } else { + let src = accum_storage; // write to stack + let src = src.as_ptr() as *const T; + + if conj_dst { + if alpha_status == 2 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = alpha * (*dst_ij).conj() + beta * *src_ij; + } + } + } else if alpha_status == 1 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = (*dst_ij).conj() + beta * *src_ij; + } + } + } else { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = beta * *src_ij; + } + } + } + } else { + if alpha_status == 2 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = alpha * *dst_ij + beta * *src_ij; + } + } + } else if alpha_status == 1 { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = *dst_ij + beta * *src_ij; + } + } + } else { + for j in 0..n { + let dst_j = dst.offset(dst_cs * j as isize); + let src_j = src.add(j * $mr_div_n * N); + + for i in 0..m { + let dst_ij = dst_j.offset(dst_rs * i as isize); + let src_ij = src_j.add(i); + + *dst_ij = beta * *src_ij; + } + } + } + } + } + } + }; +} diff --git a/vendor/gemm-common/src/pack_operands.rs b/vendor/gemm-common/src/pack_operands.rs new file mode 100644 index 00000000..4b791972 --- /dev/null +++ b/vendor/gemm-common/src/pack_operands.rs @@ -0,0 +1,303 @@ +use crate::simd::Simd; + +#[inline(always)] +pub fn quick_zero(slice: &mut [core::mem::MaybeUninit]) { + let n = slice.len(); + match n { + 1 => unsafe { *(slice.as_mut_ptr() as *mut [T; 1]) = core::mem::zeroed() }, + 2 => unsafe { *(slice.as_mut_ptr() as *mut [T; 2]) = core::mem::zeroed() }, + 3 => unsafe { *(slice.as_mut_ptr() as *mut [T; 3]) = core::mem::zeroed() }, + 4 => unsafe { *(slice.as_mut_ptr() as *mut [T; 4]) = core::mem::zeroed() }, + 5 => unsafe { *(slice.as_mut_ptr() as *mut [T; 5]) = core::mem::zeroed() }, + 6 => unsafe { *(slice.as_mut_ptr() as *mut [T; 6]) = core::mem::zeroed() }, + 7 => unsafe { *(slice.as_mut_ptr() as *mut [T; 7]) = core::mem::zeroed() }, + 8 => unsafe { *(slice.as_mut_ptr() as *mut [T; 8]) = core::mem::zeroed() }, + 9 => unsafe { *(slice.as_mut_ptr() as *mut [T; 9]) = core::mem::zeroed() }, + 10 => unsafe { *(slice.as_mut_ptr() as *mut [T; 10]) = core::mem::zeroed() }, + 11 => unsafe { *(slice.as_mut_ptr() as *mut [T; 11]) = core::mem::zeroed() }, + 12 => unsafe { *(slice.as_mut_ptr() as *mut [T; 12]) = core::mem::zeroed() }, + 13 => unsafe { *(slice.as_mut_ptr() as *mut [T; 13]) = core::mem::zeroed() }, + 14 => unsafe { *(slice.as_mut_ptr() as *mut [T; 14]) = core::mem::zeroed() }, + 15 => unsafe { *(slice.as_mut_ptr() as *mut [T; 15]) = core::mem::zeroed() }, + 16 => unsafe { *(slice.as_mut_ptr() as *mut [T; 16]) = core::mem::zeroed() }, + 17 => unsafe { *(slice.as_mut_ptr() as *mut [T; 17]) = core::mem::zeroed() }, + 18 => unsafe { *(slice.as_mut_ptr() as *mut [T; 18]) = core::mem::zeroed() }, + 19 => unsafe { *(slice.as_mut_ptr() as *mut [T; 19]) = core::mem::zeroed() }, + 20 => unsafe { *(slice.as_mut_ptr() as *mut [T; 20]) = core::mem::zeroed() }, + 21 => unsafe { *(slice.as_mut_ptr() as *mut [T; 21]) = core::mem::zeroed() }, + 22 => unsafe { *(slice.as_mut_ptr() as *mut [T; 22]) = core::mem::zeroed() }, + 23 => unsafe { *(slice.as_mut_ptr() as *mut [T; 23]) = core::mem::zeroed() }, + 24 => unsafe { *(slice.as_mut_ptr() as *mut [T; 24]) = core::mem::zeroed() }, + 25 => unsafe { *(slice.as_mut_ptr() as *mut [T; 25]) = core::mem::zeroed() }, + 26 => unsafe { *(slice.as_mut_ptr() as *mut [T; 26]) = core::mem::zeroed() }, + 27 => unsafe { *(slice.as_mut_ptr() as *mut [T; 27]) = core::mem::zeroed() }, + 28 => unsafe { *(slice.as_mut_ptr() as *mut [T; 28]) = core::mem::zeroed() }, + 29 => unsafe { *(slice.as_mut_ptr() as *mut [T; 29]) = core::mem::zeroed() }, + 30 => unsafe { *(slice.as_mut_ptr() as *mut [T; 30]) = core::mem::zeroed() }, + 31 => unsafe { *(slice.as_mut_ptr() as *mut [T; 31]) = core::mem::zeroed() }, + 32 => unsafe { *(slice.as_mut_ptr() as *mut [T; 32]) = core::mem::zeroed() }, + 33 => unsafe { *(slice.as_mut_ptr() as *mut [T; 33]) = core::mem::zeroed() }, + 34 => unsafe { *(slice.as_mut_ptr() as *mut [T; 34]) = core::mem::zeroed() }, + 35 => unsafe { *(slice.as_mut_ptr() as *mut [T; 35]) = core::mem::zeroed() }, + 36 => unsafe { *(slice.as_mut_ptr() as *mut [T; 36]) = core::mem::zeroed() }, + 37 => unsafe { *(slice.as_mut_ptr() as *mut [T; 37]) = core::mem::zeroed() }, + 38 => unsafe { *(slice.as_mut_ptr() as *mut [T; 38]) = core::mem::zeroed() }, + 39 => unsafe { *(slice.as_mut_ptr() as *mut [T; 39]) = core::mem::zeroed() }, + 40 => unsafe { *(slice.as_mut_ptr() as *mut [T; 40]) = core::mem::zeroed() }, + 41 => unsafe { *(slice.as_mut_ptr() as *mut [T; 41]) = core::mem::zeroed() }, + 42 => unsafe { *(slice.as_mut_ptr() as *mut [T; 42]) = core::mem::zeroed() }, + 43 => unsafe { *(slice.as_mut_ptr() as *mut [T; 43]) = core::mem::zeroed() }, + 44 => unsafe { *(slice.as_mut_ptr() as *mut [T; 44]) = core::mem::zeroed() }, + 45 => unsafe { *(slice.as_mut_ptr() as *mut [T; 45]) = core::mem::zeroed() }, + 46 => unsafe { *(slice.as_mut_ptr() as *mut [T; 46]) = core::mem::zeroed() }, + 47 => unsafe { *(slice.as_mut_ptr() as *mut [T; 47]) = core::mem::zeroed() }, + 48 => unsafe { *(slice.as_mut_ptr() as *mut [T; 48]) = core::mem::zeroed() }, + 49 => unsafe { *(slice.as_mut_ptr() as *mut [T; 49]) = core::mem::zeroed() }, + 50 => unsafe { *(slice.as_mut_ptr() as *mut [T; 50]) = core::mem::zeroed() }, + 51 => unsafe { *(slice.as_mut_ptr() as *mut [T; 51]) = core::mem::zeroed() }, + 52 => unsafe { *(slice.as_mut_ptr() as *mut [T; 52]) = core::mem::zeroed() }, + 53 => unsafe { *(slice.as_mut_ptr() as *mut [T; 53]) = core::mem::zeroed() }, + 54 => unsafe { *(slice.as_mut_ptr() as *mut [T; 54]) = core::mem::zeroed() }, + 55 => unsafe { *(slice.as_mut_ptr() as *mut [T; 55]) = core::mem::zeroed() }, + 56 => unsafe { *(slice.as_mut_ptr() as *mut [T; 56]) = core::mem::zeroed() }, + 57 => unsafe { *(slice.as_mut_ptr() as *mut [T; 57]) = core::mem::zeroed() }, + 58 => unsafe { *(slice.as_mut_ptr() as *mut [T; 58]) = core::mem::zeroed() }, + 59 => unsafe { *(slice.as_mut_ptr() as *mut [T; 59]) = core::mem::zeroed() }, + 60 => unsafe { *(slice.as_mut_ptr() as *mut [T; 60]) = core::mem::zeroed() }, + 61 => unsafe { *(slice.as_mut_ptr() as *mut [T; 61]) = core::mem::zeroed() }, + 62 => unsafe { *(slice.as_mut_ptr() as *mut [T; 62]) = core::mem::zeroed() }, + 63 => unsafe { *(slice.as_mut_ptr() as *mut [T; 63]) = core::mem::zeroed() }, + 64 => unsafe { *(slice.as_mut_ptr() as *mut [T; 64]) = core::mem::zeroed() }, + _ => { + for value in slice { + *value = unsafe { core::mem::zeroed() }; + } + } + } +} + +#[inline(always)] +unsafe fn quick_copy(dst: *mut T, src: *const T, n: usize) { + match n { + 1 => unsafe { *(dst as *mut [T; 1]) = *(src as *const [T; 1]) }, + 2 => unsafe { *(dst as *mut [T; 2]) = *(src as *const [T; 2]) }, + 3 => unsafe { *(dst as *mut [T; 3]) = *(src as *const [T; 3]) }, + 4 => unsafe { *(dst as *mut [T; 4]) = *(src as *const [T; 4]) }, + 5 => unsafe { *(dst as *mut [T; 5]) = *(src as *const [T; 5]) }, + 6 => unsafe { *(dst as *mut [T; 6]) = *(src as *const [T; 6]) }, + 7 => unsafe { *(dst as *mut [T; 7]) = *(src as *const [T; 7]) }, + 8 => unsafe { *(dst as *mut [T; 8]) = *(src as *const [T; 8]) }, + 9 => unsafe { *(dst as *mut [T; 9]) = *(src as *const [T; 9]) }, + 10 => unsafe { *(dst as *mut [T; 10]) = *(src as *const [T; 10]) }, + 11 => unsafe { *(dst as *mut [T; 11]) = *(src as *const [T; 11]) }, + 12 => unsafe { *(dst as *mut [T; 12]) = *(src as *const [T; 12]) }, + 13 => unsafe { *(dst as *mut [T; 13]) = *(src as *const [T; 13]) }, + 14 => unsafe { *(dst as *mut [T; 14]) = *(src as *const [T; 14]) }, + 15 => unsafe { *(dst as *mut [T; 15]) = *(src as *const [T; 15]) }, + 16 => unsafe { *(dst as *mut [T; 16]) = *(src as *const [T; 16]) }, + 17 => unsafe { *(dst as *mut [T; 17]) = *(src as *const [T; 17]) }, + 18 => unsafe { *(dst as *mut [T; 18]) = *(src as *const [T; 18]) }, + 19 => unsafe { *(dst as *mut [T; 19]) = *(src as *const [T; 19]) }, + 20 => unsafe { *(dst as *mut [T; 20]) = *(src as *const [T; 20]) }, + 21 => unsafe { *(dst as *mut [T; 21]) = *(src as *const [T; 21]) }, + 22 => unsafe { *(dst as *mut [T; 22]) = *(src as *const [T; 22]) }, + 23 => unsafe { *(dst as *mut [T; 23]) = *(src as *const [T; 23]) }, + 24 => unsafe { *(dst as *mut [T; 24]) = *(src as *const [T; 24]) }, + 25 => unsafe { *(dst as *mut [T; 25]) = *(src as *const [T; 25]) }, + 26 => unsafe { *(dst as *mut [T; 26]) = *(src as *const [T; 26]) }, + 27 => unsafe { *(dst as *mut [T; 27]) = *(src as *const [T; 27]) }, + 28 => unsafe { *(dst as *mut [T; 28]) = *(src as *const [T; 28]) }, + 29 => unsafe { *(dst as *mut [T; 29]) = *(src as *const [T; 29]) }, + 30 => unsafe { *(dst as *mut [T; 30]) = *(src as *const [T; 30]) }, + 31 => unsafe { *(dst as *mut [T; 31]) = *(src as *const [T; 31]) }, + 32 => unsafe { *(dst as *mut [T; 32]) = *(src as *const [T; 32]) }, + 33 => unsafe { *(dst as *mut [T; 33]) = *(src as *const [T; 33]) }, + 34 => unsafe { *(dst as *mut [T; 34]) = *(src as *const [T; 34]) }, + 35 => unsafe { *(dst as *mut [T; 35]) = *(src as *const [T; 35]) }, + 36 => unsafe { *(dst as *mut [T; 36]) = *(src as *const [T; 36]) }, + 37 => unsafe { *(dst as *mut [T; 37]) = *(src as *const [T; 37]) }, + 38 => unsafe { *(dst as *mut [T; 38]) = *(src as *const [T; 38]) }, + 39 => unsafe { *(dst as *mut [T; 39]) = *(src as *const [T; 39]) }, + 40 => unsafe { *(dst as *mut [T; 40]) = *(src as *const [T; 40]) }, + 41 => unsafe { *(dst as *mut [T; 41]) = *(src as *const [T; 41]) }, + 42 => unsafe { *(dst as *mut [T; 42]) = *(src as *const [T; 42]) }, + 43 => unsafe { *(dst as *mut [T; 43]) = *(src as *const [T; 43]) }, + 44 => unsafe { *(dst as *mut [T; 44]) = *(src as *const [T; 44]) }, + 45 => unsafe { *(dst as *mut [T; 45]) = *(src as *const [T; 45]) }, + 46 => unsafe { *(dst as *mut [T; 46]) = *(src as *const [T; 46]) }, + 47 => unsafe { *(dst as *mut [T; 47]) = *(src as *const [T; 47]) }, + 48 => unsafe { *(dst as *mut [T; 48]) = *(src as *const [T; 48]) }, + 49 => unsafe { *(dst as *mut [T; 49]) = *(src as *const [T; 49]) }, + 50 => unsafe { *(dst as *mut [T; 50]) = *(src as *const [T; 50]) }, + 51 => unsafe { *(dst as *mut [T; 51]) = *(src as *const [T; 51]) }, + 52 => unsafe { *(dst as *mut [T; 52]) = *(src as *const [T; 52]) }, + 53 => unsafe { *(dst as *mut [T; 53]) = *(src as *const [T; 53]) }, + 54 => unsafe { *(dst as *mut [T; 54]) = *(src as *const [T; 54]) }, + 55 => unsafe { *(dst as *mut [T; 55]) = *(src as *const [T; 55]) }, + 56 => unsafe { *(dst as *mut [T; 56]) = *(src as *const [T; 56]) }, + 57 => unsafe { *(dst as *mut [T; 57]) = *(src as *const [T; 57]) }, + 58 => unsafe { *(dst as *mut [T; 58]) = *(src as *const [T; 58]) }, + 59 => unsafe { *(dst as *mut [T; 59]) = *(src as *const [T; 59]) }, + 60 => unsafe { *(dst as *mut [T; 60]) = *(src as *const [T; 60]) }, + 61 => unsafe { *(dst as *mut [T; 61]) = *(src as *const [T; 61]) }, + 62 => unsafe { *(dst as *mut [T; 62]) = *(src as *const [T; 62]) }, + 63 => unsafe { *(dst as *mut [T; 63]) = *(src as *const [T; 63]) }, + 64 => unsafe { *(dst as *mut [T; 64]) = *(src as *const [T; 64]) }, + _ => core::ptr::copy_nonoverlapping(src, dst, n), + } +} + +#[inline(always)] +unsafe fn pack_generic_inner_loop( + mut dst: *mut T, + mut src: *const T, + src_rs: isize, + src_cs: isize, + src_width: usize, + k: usize, +) { + if src_width == DST_WIDTH { + if src_rs == 1 { + for _ in 0..k { + let val = (src as *const [T; DST_WIDTH]).read(); + (dst as *mut [T; DST_WIDTH]).write(val); + + src = src.wrapping_offset(src_cs); + dst = dst.add(DST_WIDTH); + } + } else { + for _ in 0..k { + for j in 0..DST_WIDTH { + *dst.add(j) = *src.offset(j as isize * src_rs); + } + src = src.wrapping_offset(src_cs); + dst = dst.add(DST_WIDTH); + } + } + } else if src_width == N { + if src_rs == 1 { + for _ in 0..k { + let val = (src as *const [T; N]).read(); + (dst as *mut [T; N]).write(val); + + src = src.wrapping_offset(src_cs); + dst = dst.add(DST_WIDTH); + } + } else { + for _ in 0..k { + for j in 0..N { + *dst.add(j) = *src.offset(j as isize * src_rs); + } + src = src.wrapping_offset(src_cs); + dst = dst.add(DST_WIDTH); + } + } + } else if src_width == 2 * N { + if src_rs == 1 { + for _ in 0..k { + let val0 = (src as *const [T; N]).read(); + let val1 = (src.add(N) as *const [T; N]).read(); + (dst as *mut [T; N]).write(val0); + (dst.add(N) as *mut [T; N]).write(val1); + + src = src.wrapping_offset(src_cs); + dst = dst.add(DST_WIDTH); + } + } else { + for _ in 0..k { + for j in 0..2 * N { + *dst.add(j) = *src.offset(j as isize * src_rs); + } + src = src.wrapping_offset(src_cs); + dst = dst.add(DST_WIDTH); + } + } + } else { + if src_rs == 1 { + for _ in 0..k { + quick_copy(dst, src, src_width); + quick_zero::(core::slice::from_raw_parts_mut( + dst.add(src_width) as _, + DST_WIDTH - src_width, + )); + src = src.wrapping_offset(src_cs); + dst = dst.add(DST_WIDTH); + } + } else { + for _ in 0..k { + for j in 0..src_width { + *dst.add(j) = *src.offset(j as isize * src_rs); + } + quick_zero::(core::slice::from_raw_parts_mut( + dst.add(src_width) as _, + DST_WIDTH - src_width, + )); + src = src.wrapping_offset(src_cs); + dst = dst.add(DST_WIDTH); + } + } + } +} + +#[inline(always)] +unsafe fn pack_generic( + m: usize, + k: usize, + mut dst: *mut T, + mut src: *const T, + src_cs: isize, + src_rs: isize, + dst_stride: usize, +) { + let m_width = m / DST_WIDTH * DST_WIDTH; + + let mut i = 0; + while i < m_width { + pack_generic_inner_loop::<_, N, DST_WIDTH>(dst, src, src_rs, src_cs, DST_WIDTH, k); + src = src.wrapping_offset(src_rs * DST_WIDTH as isize); + dst = dst.add(dst_stride); + + i += DST_WIDTH; + } + if i < m { + pack_generic_inner_loop::<_, N, DST_WIDTH>(dst, src, src_rs, src_cs, m - i, k); + } +} + +#[inline(never)] +pub unsafe fn pack_lhs( + _: S, + m: usize, + k: usize, + dst: crate::Ptr, + src: crate::Ptr, + src_cs: isize, + src_rs: isize, + dst_stride: usize, +) { + let dst = dst.0; + let src = src.0; + S::vectorize( + #[inline(always)] + || pack_generic::(m, k, dst, src, src_cs, src_rs, dst_stride), + ); +} + +#[inline(never)] +pub unsafe fn pack_rhs( + _: S, + n: usize, + k: usize, + dst: crate::Ptr, + src: crate::Ptr, + src_cs: isize, + src_rs: isize, + dst_stride: usize, +) { + let dst = dst.0; + let src = src.0; + S::vectorize( + #[inline(always)] + || pack_generic::(n, k, dst, src, src_rs, src_cs, dst_stride), + ); +} diff --git a/vendor/gemm-common/src/simd.rs b/vendor/gemm-common/src/simd.rs new file mode 100644 index 00000000..cc318701 --- /dev/null +++ b/vendor/gemm-common/src/simd.rs @@ -0,0 +1,2434 @@ +pub use bytemuck::Pod; +#[cfg(feature = "f16")] +use half::f16; +pub use pulp::{cast, NullaryFnOnce}; + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use x86::*; + +#[cfg(target_arch = "aarch64")] +pub use aarch64::*; + +use crate::gemm::{c32, c64}; + +pub trait Simd: Copy + Send + Sync + 'static { + unsafe fn vectorize(f: F) -> F::Output; +} + +#[derive(Copy, Clone, Debug)] +pub struct Scalar; + +impl Simd for Scalar { + #[inline(always)] + unsafe fn vectorize(f: F) -> F::Output { + f.call() + } +} + +#[cfg(feature = "f16")] +unsafe impl MixedSimd for Scalar { + const SIMD_WIDTH: usize = 1; + + type LhsN = f16; + type RhsN = f16; + type DstN = f16; + type AccN = f32; + + #[inline] + fn try_new() -> Option { + Some(Self) + } + + #[inline(always)] + fn add(self, lhs: f32, rhs: f32) -> f32 { + lhs + rhs + } + + #[inline(always)] + fn mult(self, lhs: f32, rhs: f32) -> f32 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: f32, rhs: f32, acc: f32) -> f32 { + lhs * rhs + acc + } + + #[inline(always)] + fn from_lhs(self, lhs: f16) -> f32 { + lhs.into() + } + + #[inline(always)] + fn from_rhs(self, rhs: f16) -> f32 { + rhs.into() + } + + #[inline(always)] + fn from_dst(self, dst: f16) -> f32 { + dst.into() + } + + #[inline(always)] + fn into_dst(self, acc: f32) -> f16 { + f16::from_f32(acc) + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + lhs * rhs + acc + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs.into() + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs.into() + } + + #[inline(always)] + fn simd_splat(self, lhs: f32) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst.into() + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + f16::from_f32(acc) + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + f.call() + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + lhs * rhs + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + lhs + rhs + } +} + +unsafe impl MixedSimd for Scalar { + const SIMD_WIDTH: usize = 1; + + type LhsN = f32; + type RhsN = f32; + type DstN = f32; + type AccN = f32; + + #[inline] + fn try_new() -> Option { + Some(Self) + } + + #[inline(always)] + fn mult(self, lhs: f32, rhs: f32) -> f32 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: f32, rhs: f32, acc: f32) -> f32 { + lhs * rhs + acc + } + + #[inline(always)] + fn from_lhs(self, lhs: f32) -> f32 { + lhs + } + + #[inline(always)] + fn from_rhs(self, rhs: f32) -> f32 { + rhs + } + + #[inline(always)] + fn from_dst(self, dst: f32) -> f32 { + dst + } + + #[inline(always)] + fn into_dst(self, acc: f32) -> f32 { + acc + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + lhs * rhs + acc + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs + } + + #[inline(always)] + fn simd_splat(self, lhs: f32) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + acc + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + f.call() + } + + #[inline(always)] + fn add(self, lhs: f32, rhs: f32) -> f32 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + lhs * rhs + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + lhs + rhs + } +} + +unsafe impl MixedSimd for Scalar { + const SIMD_WIDTH: usize = 1; + + type LhsN = f64; + type RhsN = f64; + type DstN = f64; + type AccN = f64; + + #[inline] + fn try_new() -> Option { + Some(Self) + } + + #[inline(always)] + fn mult(self, lhs: f64, rhs: f64) -> f64 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: f64, rhs: f64, acc: f64) -> f64 { + lhs * rhs + acc + } + + #[inline(always)] + fn from_lhs(self, lhs: f64) -> f64 { + lhs + } + + #[inline(always)] + fn from_rhs(self, rhs: f64) -> f64 { + rhs + } + + #[inline(always)] + fn from_dst(self, dst: f64) -> f64 { + dst + } + + #[inline(always)] + fn into_dst(self, acc: f64) -> f64 { + acc + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + lhs * rhs + acc + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs + } + + #[inline(always)] + fn simd_splat(self, lhs: f64) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + acc + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + f.call() + } + + #[inline(always)] + fn add(self, lhs: f64, rhs: f64) -> f64 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + lhs * rhs + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + lhs + rhs + } +} + +unsafe impl MixedSimd for Scalar { + const SIMD_WIDTH: usize = 1; + + type LhsN = c32; + type RhsN = c32; + type DstN = c32; + type AccN = c32; + + #[inline] + fn try_new() -> Option { + Some(Self) + } + + #[inline(always)] + fn mult(self, lhs: c32, rhs: c32) -> c32 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: c32, rhs: c32, acc: c32) -> c32 { + lhs * rhs + acc + } + + #[inline(always)] + fn from_lhs(self, lhs: c32) -> c32 { + lhs + } + + #[inline(always)] + fn from_rhs(self, rhs: c32) -> c32 { + rhs + } + + #[inline(always)] + fn from_dst(self, dst: c32) -> c32 { + dst + } + + #[inline(always)] + fn into_dst(self, acc: c32) -> c32 { + acc + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + lhs * rhs + acc + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs + } + + #[inline(always)] + fn simd_splat(self, lhs: c32) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + acc + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + f.call() + } + + #[inline(always)] + fn add(self, lhs: c32, rhs: c32) -> c32 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + lhs * rhs + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + lhs + rhs + } +} + +unsafe impl MixedSimd for Scalar { + const SIMD_WIDTH: usize = 1; + + type LhsN = c64; + type RhsN = c64; + type DstN = c64; + type AccN = c64; + + #[inline] + fn try_new() -> Option { + Some(Self) + } + + #[inline(always)] + fn mult(self, lhs: c64, rhs: c64) -> c64 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: c64, rhs: c64, acc: c64) -> c64 { + lhs * rhs + acc + } + + #[inline(always)] + fn from_lhs(self, lhs: c64) -> c64 { + lhs + } + + #[inline(always)] + fn from_rhs(self, rhs: c64) -> c64 { + rhs + } + + #[inline(always)] + fn from_dst(self, dst: c64) -> c64 { + dst + } + + #[inline(always)] + fn into_dst(self, acc: c64) -> c64 { + acc + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + lhs * rhs + acc + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs + } + + #[inline(always)] + fn simd_splat(self, lhs: c64) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + acc + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + f.call() + } + + #[inline(always)] + fn add(self, lhs: c64, rhs: c64) -> c64 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + lhs * rhs + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + lhs + rhs + } +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod x86 { + use super::*; + #[cfg(target_arch = "x86")] + use core::arch::x86::*; + #[cfg(target_arch = "x86_64")] + use core::arch::x86_64::*; + + #[inline(always)] + pub unsafe fn v3_fmaf(a: f32, b: f32, c: f32) -> f32 { + #[cfg(feature = "std")] + { + f32::mul_add(a, b, c) + } + #[cfg(not(feature = "std"))] + { + libm::fmaf(a, b, c) + } + } + + #[inline(always)] + pub unsafe fn v3_fma(a: f64, b: f64, c: f64) -> f64 { + #[cfg(feature = "std")] + { + f64::mul_add(a, b, c) + } + #[cfg(not(feature = "std"))] + { + libm::fma(a, b, c) + } + } + + #[derive(Copy, Clone)] + pub struct Sse; + #[derive(Copy, Clone)] + pub struct Avx; + #[derive(Copy, Clone)] + pub struct Fma; + + #[cfg(feature = "x86-v4")] + #[derive(Copy, Clone)] + pub struct Avx512f; + + impl Simd for Sse { + #[inline] + #[target_feature(enable = "sse,sse2")] + unsafe fn vectorize(f: F) -> F::Output { + f.call() + } + } + + impl Simd for Avx { + #[inline] + #[target_feature(enable = "avx")] + unsafe fn vectorize(f: F) -> F::Output { + f.call() + } + } + + impl Simd for Fma { + #[inline] + #[target_feature(enable = "fma")] + unsafe fn vectorize(f: F) -> F::Output { + f.call() + } + } + + #[cfg(feature = "x86-v4")] + impl Simd for Avx512f { + #[inline] + #[target_feature(enable = "avx512f")] + unsafe fn vectorize(f: F) -> F::Output { + f.call() + } + } + + #[derive(Debug, Copy, Clone)] + pub struct V3Half { + __private: (), + } + + #[cfg(feature = "f16")] + pulp::simd_type! { + pub struct V3 { + pub sse: "sse", + pub sse2: "sse2", + pub fxsr: "fxsr", + pub sse3: "sse3", + pub ssse3: "ssse3", + pub sse4_1: "sse4.1", + pub sse4_2: "sse4.2", + pub avx: "avx", + pub avx2: "avx2", + pub fma: "fma", + pub f16c: "f16c", + } + } + + #[cfg(feature = "x86-v4")] + #[cfg(feature = "f16")] + pulp::simd_type! { + pub struct V4 { + pub sse: "sse", + pub sse2: "sse2", + pub fxsr: "fxsr", + pub sse3: "sse3", + pub ssse3: "ssse3", + pub sse4_1: "sse4.1", + pub sse4_2: "sse4.2", + pub avx: "avx", + pub avx2: "avx2", + pub fma: "fma", + pub f16c: "f16c", + pub avx512f: "avx512f", + } + } + + #[cfg(not(feature = "f16"))] + pulp::simd_type! { + pub struct V3 { + pub sse: "sse", + pub sse2: "sse2", + pub fxsr: "fxsr", + pub sse3: "sse3", + pub ssse3: "ssse3", + pub sse4_1: "sse4.1", + pub sse4_2: "sse4.2", + pub avx: "avx", + pub avx2: "avx2", + pub fma: "fma", + } + } + + #[cfg(feature = "x86-v4")] + #[cfg(not(feature = "f16"))] + pulp::simd_type! { + pub struct V4 { + pub sse: "sse", + pub sse2: "sse2", + pub fxsr: "fxsr", + pub sse3: "sse3", + pub ssse3: "ssse3", + pub sse4_1: "sse4.1", + pub sse4_2: "sse4.2", + pub avx: "avx", + pub avx2: "avx2", + pub fma: "fma", + pub avx512f: "avx512f", + } + } + + impl Simd for V3Half { + unsafe fn vectorize(f: F) -> F::Output { + f.call() + } + } + + #[cfg(feature = "f16")] + unsafe impl MixedSimd for V3Half { + const SIMD_WIDTH: usize = 4; + + type LhsN = [f16; 4]; + type RhsN = [f16; 4]; + type DstN = [f16; 4]; + type AccN = [f32; 4]; + + #[inline] + fn try_new() -> Option { + Some(Self { __private: () }) + } + + #[inline(always)] + fn mult(self, lhs: f32, rhs: f32) -> f32 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: f32, rhs: f32, acc: f32) -> f32 { + unsafe { v3_fmaf(lhs, rhs, acc) } + } + + #[inline(always)] + fn from_lhs(self, _lhs: f16) -> f32 { + todo!() + } + + #[inline(always)] + fn from_rhs(self, _rhs: f16) -> f32 { + todo!() + } + + #[inline(always)] + fn from_dst(self, _dst: f16) -> f32 { + todo!() + } + + #[inline(always)] + fn into_dst(self, _acc: f32) -> f16 { + todo!() + } + + #[inline(always)] + fn simd_mult_add(self, _lhs: Self::AccN, _rhs: Self::AccN, _acc: Self::AccN) -> Self::AccN { + todo!() + } + + #[inline(always)] + fn simd_from_lhs(self, _lhs: Self::LhsN) -> Self::AccN { + todo!() + } + + #[inline(always)] + fn simd_from_rhs(self, _rhs: Self::RhsN) -> Self::AccN { + todo!() + } + + #[inline(always)] + fn simd_splat(self, _lhs: f32) -> Self::AccN { + todo!() + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + unsafe { cast(_mm_cvtph_ps(cast([dst, [f16::ZERO; 4]]))) } + } + + #[inline(always)] + fn simd_into_dst(self, _acc: Self::AccN) -> Self::DstN { + todo!() + } + + #[inline(always)] + fn vectorize(self, _f: F) -> F::Output { + todo!() + } + + #[inline(always)] + fn add(self, _lhs: f32, _rhs: f32) -> f32 { + todo!() + } + + #[inline(always)] + fn simd_mul(self, _lhs: Self::AccN, _rhs: Self::AccN) -> Self::AccN { + todo!() + } + + #[inline(always)] + fn simd_add(self, _lhs: Self::AccN, _rhs: Self::AccN) -> Self::AccN { + todo!() + } + } + + #[cfg(feature = "f16")] + unsafe impl MixedSimd for V3 { + const SIMD_WIDTH: usize = 8; + + type LhsN = [f16; 8]; + type RhsN = [f16; 8]; + type DstN = [f16; 8]; + type AccN = [f32; 8]; + + #[inline] + fn try_new() -> Option { + Self::try_new() + } + + #[inline(always)] + fn mult(self, lhs: f32, rhs: f32) -> f32 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: f32, rhs: f32, acc: f32) -> f32 { + unsafe { v3_fmaf(lhs, rhs, acc) } + } + + #[inline(always)] + fn from_lhs(self, lhs: f16) -> f32 { + unsafe { pulp::cast_lossy(_mm_cvtph_ps(self.sse2._mm_set1_epi16(cast(lhs)))) } + } + + #[inline(always)] + fn from_rhs(self, rhs: f16) -> f32 { + unsafe { pulp::cast_lossy(_mm_cvtph_ps(self.sse2._mm_set1_epi16(cast(rhs)))) } + } + + #[inline(always)] + fn from_dst(self, dst: f16) -> f32 { + unsafe { pulp::cast_lossy(_mm_cvtph_ps(self.sse2._mm_set1_epi16(cast(dst)))) } + } + + #[inline(always)] + fn into_dst(self, acc: f32) -> f16 { + unsafe { + pulp::cast_lossy(_mm_cvtps_ph::<_MM_FROUND_CUR_DIRECTION>( + self.sse._mm_load_ss(&acc), + )) + } + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + cast(self.fma._mm256_fmadd_ps(cast(lhs), cast(rhs), cast(acc))) + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + unsafe { cast(_mm256_cvtph_ps(cast(lhs))) } + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + unsafe { cast(_mm256_cvtph_ps(cast(rhs))) } + } + + #[inline(always)] + fn simd_splat(self, lhs: f32) -> Self::AccN { + cast(self.avx._mm256_set1_ps(lhs)) + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + unsafe { cast(_mm256_cvtph_ps(cast(dst))) } + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + unsafe { cast(_mm256_cvtps_ph::<_MM_FROUND_CUR_DIRECTION>(cast(acc))) } + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + self.vectorize(f) + } + + #[inline(always)] + fn add(self, lhs: f32, rhs: f32) -> f32 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx._mm256_mul_ps(cast(lhs), cast(rhs))) + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx._mm256_add_ps(cast(lhs), cast(rhs))) + } + } + + unsafe impl MixedSimd for V3 { + const SIMD_WIDTH: usize = 4; + + type LhsN = [c32; 4]; + type RhsN = [c32; 4]; + type DstN = [c32; 4]; + type AccN = [c32; 4]; + + #[inline] + fn try_new() -> Option { + Self::try_new() + } + + #[inline(always)] + fn mult(self, lhs: c32, rhs: c32) -> c32 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: c32, rhs: c32, acc: c32) -> c32 { + lhs * rhs + acc + } + + #[inline(always)] + fn from_lhs(self, lhs: c32) -> c32 { + lhs + } + + #[inline(always)] + fn from_rhs(self, rhs: c32) -> c32 { + rhs + } + + #[inline(always)] + fn from_dst(self, dst: c32) -> c32 { + dst + } + + #[inline(always)] + fn into_dst(self, acc: c32) -> c32 { + acc + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + unsafe { + let ab = cast(lhs); + let xy = cast(rhs); + + let yx = _mm256_permute_ps::<0b10_11_00_01>(xy); + let aa = _mm256_moveldup_ps(ab); + let bb = _mm256_movehdup_ps(ab); + + cast(_mm256_fmaddsub_ps( + aa, + xy, + _mm256_fmaddsub_ps(bb, yx, cast(acc)), + )) + } + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs + } + + #[inline(always)] + fn simd_splat(self, lhs: c32) -> Self::AccN { + cast(self.avx._mm256_set1_pd(cast(lhs))) + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + acc + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + self.vectorize(f) + } + + #[inline(always)] + fn add(self, lhs: c32, rhs: c32) -> c32 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + unsafe { + let ab = cast(lhs); + let xy = cast(rhs); + + let yx = _mm256_permute_ps::<0b10_11_00_01>(xy); + let aa = _mm256_moveldup_ps(ab); + let bb = _mm256_movehdup_ps(ab); + + cast(_mm256_fmaddsub_ps(aa, xy, _mm256_mul_ps(bb, yx))) + } + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx._mm256_add_ps(cast(lhs), cast(rhs))) + } + } + + unsafe impl MixedSimd for V3 { + const SIMD_WIDTH: usize = 2; + + type LhsN = [c64; 2]; + type RhsN = [c64; 2]; + type DstN = [c64; 2]; + type AccN = [c64; 2]; + + #[inline] + fn try_new() -> Option { + Self::try_new() + } + + #[inline(always)] + fn mult(self, lhs: c64, rhs: c64) -> c64 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: c64, rhs: c64, acc: c64) -> c64 { + lhs * rhs + acc + } + + #[inline(always)] + fn from_lhs(self, lhs: c64) -> c64 { + lhs + } + + #[inline(always)] + fn from_rhs(self, rhs: c64) -> c64 { + rhs + } + + #[inline(always)] + fn from_dst(self, dst: c64) -> c64 { + dst + } + + #[inline(always)] + fn into_dst(self, acc: c64) -> c64 { + acc + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + unsafe { + let ab = cast(lhs); + let xy = cast(rhs); + + let yx = _mm256_permute_pd::<0b0101>(xy); + let aa = _mm256_unpacklo_pd(ab, ab); + let bb = _mm256_unpackhi_pd(ab, ab); + + cast(_mm256_fmaddsub_pd( + aa, + xy, + _mm256_fmaddsub_pd(bb, yx, cast(acc)), + )) + } + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs + } + + #[inline(always)] + fn simd_splat(self, lhs: c64) -> Self::AccN { + cast([lhs; 2]) + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + acc + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + self.vectorize(f) + } + + #[inline(always)] + fn add(self, lhs: c64, rhs: c64) -> c64 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + unsafe { + let ab = cast(lhs); + let xy = cast(rhs); + + let yx = _mm256_permute_pd::<0b0101>(xy); + let aa = _mm256_unpacklo_pd(ab, ab); + let bb = _mm256_unpackhi_pd(ab, ab); + + cast(_mm256_fmaddsub_pd(aa, xy, _mm256_mul_pd(bb, yx))) + } + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx._mm256_add_pd(cast(lhs), cast(rhs))) + } + } + + #[cfg(feature = "x86-v4")] + unsafe impl MixedSimd for V4 { + const SIMD_WIDTH: usize = 8; + + type LhsN = [c32; 8]; + type RhsN = [c32; 8]; + type DstN = [c32; 8]; + type AccN = [c32; 8]; + + #[inline] + fn try_new() -> Option { + Self::try_new() + } + + #[inline(always)] + fn mult(self, lhs: c32, rhs: c32) -> c32 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: c32, rhs: c32, acc: c32) -> c32 { + lhs * rhs + acc + } + + #[inline(always)] + fn from_lhs(self, lhs: c32) -> c32 { + lhs + } + + #[inline(always)] + fn from_rhs(self, rhs: c32) -> c32 { + rhs + } + + #[inline(always)] + fn from_dst(self, dst: c32) -> c32 { + dst + } + + #[inline(always)] + fn into_dst(self, acc: c32) -> c32 { + acc + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + unsafe { + let ab = cast(lhs); + let xy = cast(rhs); + + let yx = _mm512_permute_ps::<0b10_11_00_01>(xy); + let aa = _mm512_moveldup_ps(ab); + let bb = _mm512_movehdup_ps(ab); + + cast(_mm512_fmaddsub_ps( + aa, + xy, + _mm512_fmaddsub_ps(bb, yx, cast(acc)), + )) + } + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs + } + + #[inline(always)] + fn simd_splat(self, lhs: c32) -> Self::AccN { + cast(self.avx512f._mm512_set1_pd(cast(lhs))) + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + acc + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + self.vectorize(f) + } + + #[inline(always)] + fn add(self, lhs: c32, rhs: c32) -> c32 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + unsafe { + let ab = cast(lhs); + let xy = cast(rhs); + + let yx = _mm512_permute_ps::<0b10_11_00_01>(xy); + let aa = _mm512_moveldup_ps(ab); + let bb = _mm512_movehdup_ps(ab); + + cast(_mm512_fmaddsub_ps(aa, xy, _mm512_mul_ps(bb, yx))) + } + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx512f._mm512_add_ps(cast(lhs), cast(rhs))) + } + } + + #[cfg(feature = "x86-v4")] + unsafe impl MixedSimd for V4 { + const SIMD_WIDTH: usize = 4; + + type LhsN = [c64; 4]; + type RhsN = [c64; 4]; + type DstN = [c64; 4]; + type AccN = [c64; 4]; + + #[inline] + fn try_new() -> Option { + Self::try_new() + } + + #[inline(always)] + fn mult(self, lhs: c64, rhs: c64) -> c64 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: c64, rhs: c64, acc: c64) -> c64 { + lhs * rhs + acc + } + + #[inline(always)] + fn from_lhs(self, lhs: c64) -> c64 { + lhs + } + + #[inline(always)] + fn from_rhs(self, rhs: c64) -> c64 { + rhs + } + + #[inline(always)] + fn from_dst(self, dst: c64) -> c64 { + dst + } + + #[inline(always)] + fn into_dst(self, acc: c64) -> c64 { + acc + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + unsafe { + let ab = cast(lhs); + let xy = cast(rhs); + + let yx = _mm512_permute_pd::<0b01010101>(xy); + let aa = _mm512_unpacklo_pd(ab, ab); + let bb = _mm512_unpackhi_pd(ab, ab); + + cast(_mm512_fmaddsub_pd( + aa, + xy, + _mm512_fmaddsub_pd(bb, yx, cast(acc)), + )) + } + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs + } + + #[inline(always)] + fn simd_splat(self, lhs: c64) -> Self::AccN { + cast([lhs; 4]) + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + acc + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + self.vectorize(f) + } + + #[inline(always)] + fn add(self, lhs: c64, rhs: c64) -> c64 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + unsafe { + let ab = cast(lhs); + let xy = cast(rhs); + + let yx = _mm512_permute_pd::<0b01010101>(xy); + let aa = _mm512_unpacklo_pd(ab, ab); + let bb = _mm512_unpackhi_pd(ab, ab); + + cast(_mm512_fmaddsub_pd(aa, xy, _mm512_mul_pd(bb, yx))) + } + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx512f._mm512_add_pd(cast(lhs), cast(rhs))) + } + } + + unsafe impl MixedSimd for V3 { + const SIMD_WIDTH: usize = 8; + + type LhsN = [f32; 8]; + type RhsN = [f32; 8]; + type DstN = [f32; 8]; + type AccN = [f32; 8]; + + #[inline] + fn try_new() -> Option { + Self::try_new() + } + + #[inline(always)] + fn mult(self, lhs: f32, rhs: f32) -> f32 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: f32, rhs: f32, acc: f32) -> f32 { + unsafe { v3_fmaf(lhs, rhs, acc) } + } + + #[inline(always)] + fn from_lhs(self, lhs: f32) -> f32 { + lhs + } + + #[inline(always)] + fn from_rhs(self, rhs: f32) -> f32 { + rhs + } + + #[inline(always)] + fn from_dst(self, dst: f32) -> f32 { + dst + } + + #[inline(always)] + fn into_dst(self, acc: f32) -> f32 { + acc + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + cast(self.fma._mm256_fmadd_ps(cast(lhs), cast(rhs), cast(acc))) + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs + } + + #[inline(always)] + fn simd_splat(self, lhs: f32) -> Self::AccN { + cast(self.avx._mm256_set1_ps(lhs)) + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + acc + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + self.vectorize(f) + } + + #[inline(always)] + fn add(self, lhs: f32, rhs: f32) -> f32 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx._mm256_mul_ps(cast(lhs), cast(rhs))) + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx._mm256_add_ps(cast(lhs), cast(rhs))) + } + } + + unsafe impl MixedSimd for V3 { + const SIMD_WIDTH: usize = 4; + + type LhsN = [f64; 4]; + type RhsN = [f64; 4]; + type DstN = [f64; 4]; + type AccN = [f64; 4]; + + #[inline] + fn try_new() -> Option { + Self::try_new() + } + + #[inline(always)] + fn mult(self, lhs: f64, rhs: f64) -> f64 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: f64, rhs: f64, acc: f64) -> f64 { + unsafe { v3_fma(lhs, rhs, acc) } + } + + #[inline(always)] + fn from_lhs(self, lhs: f64) -> f64 { + lhs + } + + #[inline(always)] + fn from_rhs(self, rhs: f64) -> f64 { + rhs + } + + #[inline(always)] + fn from_dst(self, dst: f64) -> f64 { + dst + } + + #[inline(always)] + fn into_dst(self, acc: f64) -> f64 { + acc + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + cast(self.fma._mm256_fmadd_pd(cast(lhs), cast(rhs), cast(acc))) + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs + } + + #[inline(always)] + fn simd_splat(self, lhs: f64) -> Self::AccN { + cast(self.avx._mm256_set1_pd(lhs)) + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + acc + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + self.vectorize(f) + } + + #[inline(always)] + fn add(self, lhs: f64, rhs: f64) -> f64 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx._mm256_mul_pd(cast(lhs), cast(rhs))) + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx._mm256_add_pd(cast(lhs), cast(rhs))) + } + } + + impl Simd for V3 { + #[inline(always)] + unsafe fn vectorize(f: F) -> F::Output { + Self::new_unchecked().vectorize(f) + } + } + + #[cfg(feature = "x86-v4")] + impl Simd for V4 { + #[inline(always)] + unsafe fn vectorize(f: F) -> F::Output { + Self::new_unchecked().vectorize(f) + } + } + + #[cfg(feature = "x86-v4")] + #[cfg(feature = "f16")] + unsafe impl MixedSimd for V4 { + const SIMD_WIDTH: usize = 16; + + type LhsN = [f16; 16]; + type RhsN = [f16; 16]; + type DstN = [f16; 16]; + type AccN = [f32; 16]; + + #[inline] + fn try_new() -> Option { + Self::try_new() + } + + #[inline(always)] + fn mult(self, lhs: f32, rhs: f32) -> f32 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: f32, rhs: f32, acc: f32) -> f32 { + unsafe { v3_fmaf(lhs, rhs, acc) } + } + + #[inline(always)] + fn from_lhs(self, lhs: f16) -> f32 { + unsafe { pulp::cast_lossy(_mm_cvtph_ps(self.sse2._mm_set1_epi16(cast(lhs)))) } + } + + #[inline(always)] + fn from_rhs(self, rhs: f16) -> f32 { + unsafe { pulp::cast_lossy(_mm_cvtph_ps(self.sse2._mm_set1_epi16(cast(rhs)))) } + } + + #[inline(always)] + fn from_dst(self, dst: f16) -> f32 { + unsafe { pulp::cast_lossy(_mm_cvtph_ps(self.sse2._mm_set1_epi16(cast(dst)))) } + } + + #[inline(always)] + fn into_dst(self, acc: f32) -> f16 { + unsafe { + pulp::cast_lossy(_mm_cvtps_ph::<_MM_FROUND_CUR_DIRECTION>( + self.sse._mm_load_ss(&acc), + )) + } + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + cast( + self.avx512f + ._mm512_fmadd_ps(cast(lhs), cast(rhs), cast(acc)), + ) + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + unsafe { cast(_mm512_cvtph_ps(cast(lhs))) } + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + unsafe { cast(_mm512_cvtph_ps(cast(rhs))) } + } + + #[inline(always)] + fn simd_splat(self, lhs: f32) -> Self::AccN { + cast(self.avx512f._mm512_set1_ps(lhs)) + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + unsafe { cast(_mm512_cvtph_ps(cast(dst))) } + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + unsafe { cast(_mm512_cvtps_ph::<_MM_FROUND_CUR_DIRECTION>(cast(acc))) } + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + self.vectorize(f) + } + + #[inline(always)] + fn add(self, lhs: f32, rhs: f32) -> f32 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx512f._mm512_mul_ps(cast(lhs), cast(rhs))) + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx512f._mm512_add_ps(cast(lhs), cast(rhs))) + } + } + + #[cfg(feature = "x86-v4")] + unsafe impl MixedSimd for V4 { + const SIMD_WIDTH: usize = 16; + + type LhsN = [f32; 16]; + type RhsN = [f32; 16]; + type DstN = [f32; 16]; + type AccN = [f32; 16]; + + #[inline] + fn try_new() -> Option { + Self::try_new() + } + + #[inline(always)] + fn mult(self, lhs: f32, rhs: f32) -> f32 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: f32, rhs: f32, acc: f32) -> f32 { + unsafe { v3_fmaf(lhs, rhs, acc) } + } + + #[inline(always)] + fn from_lhs(self, lhs: f32) -> f32 { + lhs + } + + #[inline(always)] + fn from_rhs(self, rhs: f32) -> f32 { + rhs + } + + #[inline(always)] + fn from_dst(self, dst: f32) -> f32 { + dst + } + + #[inline(always)] + fn into_dst(self, acc: f32) -> f32 { + acc + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + cast( + self.avx512f + ._mm512_fmadd_ps(cast(lhs), cast(rhs), cast(acc)), + ) + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs + } + + #[inline(always)] + fn simd_splat(self, lhs: f32) -> Self::AccN { + cast(self.avx512f._mm512_set1_ps(lhs)) + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + acc + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + self.vectorize(f) + } + + #[inline(always)] + fn add(self, lhs: f32, rhs: f32) -> f32 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx512f._mm512_mul_ps(cast(lhs), cast(rhs))) + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx512f._mm512_add_ps(cast(lhs), cast(rhs))) + } + } + + #[cfg(feature = "x86-v4")] + unsafe impl MixedSimd for V4 { + const SIMD_WIDTH: usize = 8; + + type LhsN = [f64; 8]; + type RhsN = [f64; 8]; + type DstN = [f64; 8]; + type AccN = [f64; 8]; + + #[inline] + fn try_new() -> Option { + Self::try_new() + } + + #[inline(always)] + fn mult(self, lhs: f64, rhs: f64) -> f64 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: f64, rhs: f64, acc: f64) -> f64 { + unsafe { v3_fma(lhs, rhs, acc) } + } + + #[inline(always)] + fn from_lhs(self, lhs: f64) -> f64 { + lhs + } + + #[inline(always)] + fn from_rhs(self, rhs: f64) -> f64 { + rhs + } + + #[inline(always)] + fn from_dst(self, dst: f64) -> f64 { + dst + } + + #[inline(always)] + fn into_dst(self, acc: f64) -> f64 { + acc + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + cast( + self.avx512f + ._mm512_fmadd_pd(cast(lhs), cast(rhs), cast(acc)), + ) + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs + } + + #[inline(always)] + fn simd_splat(self, lhs: f64) -> Self::AccN { + cast(self.avx512f._mm512_set1_pd(lhs)) + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + acc + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + self.vectorize(f) + } + + #[inline(always)] + fn add(self, lhs: f64, rhs: f64) -> f64 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx512f._mm512_mul_pd(cast(lhs), cast(rhs))) + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + cast(self.avx512f._mm512_add_pd(cast(lhs), cast(rhs))) + } + } +} + +#[cfg(target_arch = "aarch64")] +pub mod aarch64 { + use super::*; + use core::arch::aarch64::*; + use core::arch::asm; + #[allow(unused_imports)] + use core::mem::transmute; + use core::mem::MaybeUninit; + use core::ptr; + + #[inline(always)] + pub unsafe fn neon_fmaf(a: f32, b: f32, c: f32) -> f32 { + #[cfg(feature = "std")] + { + f32::mul_add(a, b, c) + } + #[cfg(not(feature = "std"))] + { + a * b + c + } + } + + #[inline(always)] + pub unsafe fn neon_fma(a: f64, b: f64, c: f64) -> f64 { + #[cfg(feature = "std")] + { + f64::mul_add(a, b, c) + } + #[cfg(not(feature = "std"))] + { + a * b + c + } + } + + #[target_feature(enable = "fp16,neon")] + #[inline] + pub unsafe fn f16_to_f32_fp16(i: u16) -> f32 { + let result: f32; + asm!( + "fcvt {0:s}, {1:h}", + out(vreg) result, + in(vreg) i, + options(pure, nomem, nostack)); + result + } + + #[target_feature(enable = "fp16,neon")] + #[inline] + pub unsafe fn f32_to_f16_fp16(f: f32) -> u16 { + let result: u16; + asm!( + "fcvt {0:h}, {1:s}", + out(vreg) result, + in(vreg) f, + options(pure, nomem, nostack)); + result + } + + #[target_feature(enable = "fp16,neon")] + #[inline] + pub unsafe fn f16x4_to_f32x4_fp16(v: &[u16; 4]) -> [f32; 4] { + let mut vec = MaybeUninit::::uninit(); + ptr::copy_nonoverlapping(v.as_ptr(), vec.as_mut_ptr().cast(), 4); + let result: float32x4_t; + asm!( + "fcvtl {0:v}.4s, {1:v}.4h", + out(vreg) result, + in(vreg) vec.assume_init(), + options(pure, nomem, nostack)); + *(&result as *const float32x4_t).cast() + } + + #[target_feature(enable = "fp16,neon")] + #[inline] + pub unsafe fn f32x4_to_f16x4_fp16(v: &[f32; 4]) -> [u16; 4] { + let mut vec = MaybeUninit::::uninit(); + ptr::copy_nonoverlapping(v.as_ptr(), vec.as_mut_ptr().cast(), 4); + let result: uint16x4_t; + asm!( + "fcvtn {0:v}.4h, {1:v}.4s", + out(vreg) result, + in(vreg) vec.assume_init(), + options(pure, nomem, nostack)); + *(&result as *const uint16x4_t).cast() + } + + #[target_feature(enable = "fp16")] + #[inline] + pub unsafe fn add_f16_fp16(a: u16, b: u16) -> u16 { + let result: u16; + asm!( + "fadd {0:h}, {1:h}, {2:h}", + out(vreg) result, + in(vreg) a, + in(vreg) b, + options(pure, nomem, nostack)); + result + } + + #[target_feature(enable = "fp16")] + #[inline] + pub unsafe fn fmaq_f16(mut a: u16, b: u16, c: u16) -> u16 { + asm!( + "fmadd {0:h}, {1:h}, {2:h}, {0:h}", + inout(vreg) a, + in(vreg) b, + in(vreg) c, + options(pure, nomem, nostack)); + a + } + + #[target_feature(enable = "fp16")] + #[inline] + pub unsafe fn multiply_f16_fp16(a: u16, b: u16) -> u16 { + let result: u16; + asm!( + "fmul {0:h}, {1:h}, {2:h}", + out(vreg) result, + in(vreg) a, + in(vreg) b, + options(pure, nomem, nostack)); + result + } + + #[allow(non_camel_case_types)] + type float16x8_t = uint16x8_t; + + /// Floating point multiplication + /// [doc](https://developer.arm.com/documentation/dui0801/g/A64-SIMD-Vector-Instructions/FMUL--vector-) + #[target_feature(enable = "neon,fp16")] + #[inline] + pub unsafe fn vmulq_f16(a: float16x8_t, b: float16x8_t) -> float16x8_t { + let result: float16x8_t; + asm!( + "fmul {0:v}.8h, {1:v}.8h, {2:v}.8h", + out(vreg) result, + in(vreg) a, + in(vreg) b, + options(pure, nomem, nostack)); + result + } + + /// Floating point addition + /// [doc](https://developer.arm.com/documentation/dui0801/g/A64-SIMD-Vector-Instructions/FADD--vector-) + #[target_feature(enable = "neon,fp16")] + #[inline] + pub unsafe fn vaddq_f16(a: float16x8_t, b: float16x8_t) -> float16x8_t { + let result: float16x8_t; + asm!( + "fadd {0:v}.8h, {1:v}.8h, {2:v}.8h", + out(vreg) result, + in(vreg) a, + in(vreg) b, + options(pure, nomem, nostack)); + result + } + + /// Fused multiply add [doc](https://developer.arm.com/documentation/dui0801/g/A64-SIMD-Vector-Instructions/FMLA--vector-) + #[target_feature(enable = "neon,fp16")] + #[inline] + pub unsafe fn vfmaq_f16(mut a: float16x8_t, b: float16x8_t, c: float16x8_t) -> float16x8_t { + asm!( + "fmla {0:v}.8h, {1:v}.8h, {2:v}.8h", + inout(vreg) a, + in(vreg) b, + in(vreg) c, + options(pure, nomem, nostack)); + a + } + + #[target_feature(enable = "neon,fp16")] + #[inline] + pub unsafe fn vfmaq_laneq_f16( + mut a: float16x8_t, + b: float16x8_t, + c: float16x8_t, + ) -> float16x8_t { + match LANE { + 0 => asm!( + "fmla {0:v}.8h, {1:v}.8h, {2:v}.h[0]", + inout(vreg) a, + in(vreg) b, + in(vreg_low16) c, + options(pure, nomem, nostack)), + 1 => asm!( + "fmla {0:v}.8h, {1:v}.8h, {2:v}.h[1]", + inout(vreg) a, + in(vreg) b, + in(vreg_low16) c, + options(pure, nomem, nostack)), + 2 => asm!( + "fmla {0:v}.8h, {1:v}.8h, {2:v}.h[2]", + inout(vreg) a, + in(vreg) b, + in(vreg_low16) c, + options(pure, nomem, nostack)), + 3 => asm!( + "fmla {0:v}.8h, {1:v}.8h, {2:v}.h[3]", + inout(vreg) a, + in(vreg) b, + in(vreg_low16) c, + options(pure, nomem, nostack)), + 4 => asm!( + "fmla {0:v}.8h, {1:v}.8h, {2:v}.h[4]", + inout(vreg) a, + in(vreg) b, + in(vreg_low16) c, + options(pure, nomem, nostack)), + 5 => asm!( + "fmla {0:v}.8h, {1:v}.8h, {2:v}.h[5]", + inout(vreg) a, + in(vreg) b, + in(vreg_low16) c, + options(pure, nomem, nostack)), + 6 => asm!( + "fmla {0:v}.8h, {1:v}.8h, {2:v}.h[6]", + inout(vreg) a, + in(vreg) b, + in(vreg_low16) c, + options(pure, nomem, nostack)), + 7 => asm!( + "fmla {0:v}.8h, {1:v}.8h, {2:v}.h[7]", + inout(vreg) a, + in(vreg) b, + in(vreg_low16) c, + options(pure, nomem, nostack)), + _ => unreachable!(), + } + a + } + + #[derive(Copy, Clone, Debug)] + pub struct Neon { + __private: (), + } + + #[derive(Copy, Clone, Debug)] + pub struct NeonFp16 { + __private: (), + } + + #[derive(Copy, Clone, Debug)] + pub struct NeonFcma { + __private: (), + } + + impl Simd for Neon { + #[inline] + #[target_feature(enable = "neon")] + unsafe fn vectorize(f: F) -> F::Output { + f.call() + } + } + + impl Simd for NeonFp16 { + #[inline] + #[target_feature(enable = "neon,fp16")] + unsafe fn vectorize(f: F) -> F::Output { + f.call() + } + } + + #[cfg(feature = "f16")] + unsafe impl MixedSimd for NeonFp16 { + const SIMD_WIDTH: usize = 4; + + type LhsN = [f16; 4]; + type RhsN = [f16; 4]; + type DstN = [f16; 4]; + type AccN = [f32; 4]; + + #[inline] + fn try_new() -> Option { + if crate::feature_detected!("neon") && crate::feature_detected!("fp16") { + Some(Self { __private: () }) + } else { + None + } + } + + #[inline(always)] + fn mult(self, lhs: f32, rhs: f32) -> f32 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: f32, rhs: f32, acc: f32) -> f32 { + unsafe { neon_fmaf(lhs, rhs, acc) } + } + + #[inline(always)] + fn from_lhs(self, lhs: f16) -> f32 { + unsafe { f16_to_f32_fp16(cast(lhs)) } + } + + #[inline(always)] + fn from_rhs(self, rhs: f16) -> f32 { + unsafe { f16_to_f32_fp16(cast(rhs)) } + } + + #[inline(always)] + fn from_dst(self, dst: f16) -> f32 { + unsafe { f16_to_f32_fp16(cast(dst)) } + } + + #[inline(always)] + fn into_dst(self, acc: f32) -> f16 { + unsafe { cast(f32_to_f16_fp16(acc)) } + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + unsafe { transmute(vfmaq_f32(transmute(acc), transmute(lhs), transmute(rhs))) } + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + unsafe { f16x4_to_f32x4_fp16(&cast(lhs)) } + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + unsafe { f16x4_to_f32x4_fp16(&cast(rhs)) } + } + + #[inline(always)] + fn simd_splat(self, lhs: f32) -> Self::AccN { + [lhs, lhs, lhs, lhs] + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + unsafe { f16x4_to_f32x4_fp16(&cast(dst)) } + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + unsafe { cast(f32x4_to_f16x4_fp16(&acc)) } + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + #[inline] + #[target_feature(enable = "neon,fp16")] + unsafe fn implementation(f: F) -> F::Output { + f.call() + } + + unsafe { implementation(f) } + } + + #[inline(always)] + fn add(self, lhs: f32, rhs: f32) -> f32 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + unsafe { transmute(vmulq_f32(transmute(lhs), transmute(rhs))) } + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + unsafe { transmute(vaddq_f32(transmute(lhs), transmute(rhs))) } + } + } + + #[cfg(feature = "f16")] + unsafe impl MixedSimd for NeonFp16 { + const SIMD_WIDTH: usize = 8; + + type LhsN = [f16; 8]; + type RhsN = [f16; 8]; + type DstN = [f16; 8]; + type AccN = [f16; 8]; + + #[inline] + fn try_new() -> Option { + if crate::feature_detected!("neon") && crate::feature_detected!("fp16") { + Some(Self { __private: () }) + } else { + None + } + } + + #[inline(always)] + fn mult(self, lhs: f16, rhs: f16) -> f16 { + unsafe { cast(multiply_f16_fp16(cast(lhs), cast(rhs))) } + } + + #[inline(always)] + fn mult_add(self, lhs: f16, rhs: f16, acc: f16) -> f16 { + unsafe { cast(fmaq_f16(cast(acc), cast(lhs), cast(rhs))) } + } + + #[inline(always)] + fn from_lhs(self, lhs: f16) -> f16 { + lhs + } + + #[inline(always)] + fn from_rhs(self, rhs: f16) -> f16 { + rhs + } + + #[inline(always)] + fn from_dst(self, dst: f16) -> f16 { + dst + } + + #[inline(always)] + fn into_dst(self, acc: f16) -> f16 { + acc + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + unsafe { transmute(vfmaq_f16(transmute(acc), transmute(lhs), transmute(rhs))) } + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + lhs + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + rhs + } + + #[inline(always)] + fn simd_splat(self, lhs: f16) -> Self::AccN { + [lhs, lhs, lhs, lhs, lhs, lhs, lhs, lhs] + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + dst + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + acc + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + #[inline] + #[target_feature(enable = "neon,fp16")] + unsafe fn implementation(f: F) -> F::Output { + f.call() + } + + unsafe { implementation(f) } + } + + #[inline(always)] + fn add(self, lhs: f16, rhs: f16) -> f16 { + unsafe { cast(add_f16_fp16(cast(lhs), cast(rhs))) } + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + unsafe { transmute(vmulq_f16(transmute(lhs), transmute(rhs))) } + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + unsafe { transmute(vaddq_f16(transmute(lhs), transmute(rhs))) } + } + } + + #[cfg(feature = "f16")] + unsafe impl MixedSimd for Neon { + const SIMD_WIDTH: usize = 4; + + type LhsN = [f16; 4]; + type RhsN = [f16; 4]; + type DstN = [f16; 4]; + type AccN = [f32; 4]; + + #[inline] + fn try_new() -> Option { + if crate::feature_detected!("neon") { + Some(Self { __private: () }) + } else { + None + } + } + + #[inline(always)] + fn mult(self, lhs: f32, rhs: f32) -> f32 { + lhs * rhs + } + + #[inline(always)] + fn mult_add(self, lhs: f32, rhs: f32, acc: f32) -> f32 { + unsafe { neon_fmaf(lhs, rhs, acc) } + } + + #[inline(always)] + fn from_lhs(self, lhs: f16) -> f32 { + lhs.into() + } + + #[inline(always)] + fn from_rhs(self, rhs: f16) -> f32 { + rhs.into() + } + + #[inline(always)] + fn from_dst(self, dst: f16) -> f32 { + dst.into() + } + + #[inline(always)] + fn into_dst(self, acc: f32) -> f16 { + f16::from_f32(acc) + } + + #[inline(always)] + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN { + unsafe { transmute(vfmaq_f32(transmute(acc), transmute(lhs), transmute(rhs))) } + } + + #[inline(always)] + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN { + [lhs[0].into(), lhs[1].into(), lhs[2].into(), lhs[3].into()] + } + + #[inline(always)] + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN { + [rhs[0].into(), rhs[1].into(), rhs[2].into(), rhs[3].into()] + } + + #[inline(always)] + fn simd_splat(self, lhs: f32) -> Self::AccN { + [lhs, lhs, lhs, lhs] + } + + #[inline(always)] + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN { + [dst[0].into(), dst[1].into(), dst[2].into(), dst[3].into()] + } + + #[inline(always)] + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN { + [ + f16::from_f32(acc[0]), + f16::from_f32(acc[1]), + f16::from_f32(acc[2]), + f16::from_f32(acc[3]), + ] + } + + #[inline(always)] + fn vectorize(self, f: F) -> F::Output { + #[inline] + #[target_feature(enable = "neon")] + unsafe fn implementation(f: F) -> F::Output { + f.call() + } + + unsafe { implementation(f) } + } + + #[inline(always)] + fn add(self, lhs: f32, rhs: f32) -> f32 { + lhs + rhs + } + + #[inline(always)] + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + unsafe { transmute(vmulq_f32(transmute(lhs), transmute(rhs))) } + } + + #[inline(always)] + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN { + unsafe { transmute(vaddq_f32(transmute(lhs), transmute(rhs))) } + } + } +} + +pub trait Boilerplate: Copy + Send + Sync + core::fmt::Debug + 'static + PartialEq {} +impl Boilerplate for T {} + +pub unsafe trait MixedSimd: Simd { + const SIMD_WIDTH: usize; + + type LhsN: Boilerplate; + type RhsN: Boilerplate; + type DstN: Boilerplate; + type AccN: Boilerplate; + + fn try_new() -> Option; + + fn vectorize(self, f: F) -> F::Output; + + fn add(self, lhs: Acc, rhs: Acc) -> Acc; + fn mult(self, lhs: Acc, rhs: Acc) -> Acc; + fn mult_add(self, lhs: Acc, rhs: Acc, acc: Acc) -> Acc; + fn from_lhs(self, lhs: Lhs) -> Acc; + fn from_rhs(self, rhs: Rhs) -> Acc; + fn from_dst(self, dst: Dst) -> Acc; + fn into_dst(self, acc: Acc) -> Dst; + + fn simd_mul(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN; + fn simd_add(self, lhs: Self::AccN, rhs: Self::AccN) -> Self::AccN; + fn simd_mult_add(self, lhs: Self::AccN, rhs: Self::AccN, acc: Self::AccN) -> Self::AccN; + fn simd_from_lhs(self, lhs: Self::LhsN) -> Self::AccN; + fn simd_from_rhs(self, rhs: Self::RhsN) -> Self::AccN; + fn simd_splat(self, lhs: Acc) -> Self::AccN; + + fn simd_from_dst(self, dst: Self::DstN) -> Self::AccN; + fn simd_into_dst(self, acc: Self::AccN) -> Self::DstN; +}