diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index d5464ebab2..bff64c115d 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -209,7 +209,8 @@ jobs: - name: Probe every declared LLVM-NVPTX target run: | # Stock runners do not carry the recorded CUDA 13.3 ptxas binary. - # This still checks the pinned llc identity and exact emitted PTX. + # Identity is llc version plus the rustc commit from upstream.lock; + # binary bytes are provenance only. cargo run -p cuda-intrinsics-gen -- probe --all --skip-terminal --per-target - name: Check append-only intrinsic ABI ledger diff --git a/Justfile b/Justfile index 8daa16a9a6..8d01da6177 100644 --- a/Justfile +++ b/Justfile @@ -117,8 +117,8 @@ test-cuda: # against 7 that are hand-written. # # Needs no CUDA toolkit: `--skip-terminal` is CI's own flag for runners without -# the recorded CUDA 13.3 ptxas, leaving the pinned llc identity and the exact -# emitted PTX as what gets checked, and llc comes from the pinned toolchain. +# the recorded CUDA 13.3 ptxas. Identity is llc version plus the rustc commit +# from upstream.lock; binary bytes are provenance only. # The whole recipe is ~13s. # # `base_ref` is what the append-only ABI ledger is compared against. CI uses the diff --git a/crates/cuda-intrinsics-gen/src/extract.rs b/crates/cuda-intrinsics-gen/src/extract.rs index 2ae9648430..432373cb02 100644 --- a/crates/cuda-intrinsics-gen/src/extract.rs +++ b/crates/cuda-intrinsics-gen/src/extract.rs @@ -742,6 +742,59 @@ fn llvm_name(source_record: &str, record: &Value) -> Result { mod tests { use super::*; use serde_json::json; + use std::sync::atomic::{AtomicU64, Ordering}; + + struct TestRepo(PathBuf); + + impl Drop for TestRepo { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } + } + + fn repo_with_lock(contents: &str) -> TestRepo { + static NEXT: AtomicU64 = AtomicU64::new(0); + let root = std::env::temp_dir().join(format!( + "cuda-intrinsics-upstream-lock-test-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(root.join("intrinsics")).unwrap(); + fs::write(root.join("intrinsics/upstream.lock"), contents).unwrap(); + TestRepo(root) + } + + fn committed_lock() -> (&'static str, UpstreamLock) { + let text = include_str!("../../../intrinsics/upstream.lock"); + let lock = toml::from_str(text).unwrap(); + (text, lock) + } + + #[test] + fn upstream_lock_requires_rust_toolchain_table() { + let (original, parsed) = committed_lock(); + let commit = format!("commit_hash = \"{}\"", parsed.rust_toolchain.commit_hash); + let mutated = original + .replace("[rust_toolchain]", "") + .replace(&commit, ""); + assert_ne!(mutated, original); + let repo = repo_with_lock(&mutated); + let error = read_upstream_lock(&repo.0).unwrap_err(); + assert!(format!("{error:#}").contains("rust_toolchain")); + } + + #[test] + fn upstream_lock_rejects_malformed_rust_toolchain_commit_hash() { + let (original, parsed) = committed_lock(); + let commit = format!("commit_hash = \"{}\"", parsed.rust_toolchain.commit_hash); + let mutated = original.replace(&commit, "commit_hash = \"not-a-commit\""); + assert_ne!(mutated, original); + let repo = repo_with_lock(&mutated); + let error = read_upstream_lock(&repo.0).unwrap_err(); + let message = format!("{error:#}"); + assert!(message.contains("commit_hash"), "{message}"); + assert!(message.contains("40 lowercase hexadecimal"), "{message}"); + } #[test] fn joins_selection_by_def_reference_not_printable_text() { diff --git a/crates/cuda-intrinsics-gen/src/model/imported.rs b/crates/cuda-intrinsics-gen/src/model/imported.rs index 7b5f796120..6ee32470f3 100644 --- a/crates/cuda-intrinsics-gen/src/model/imported.rs +++ b/crates/cuda-intrinsics-gen/src/model/imported.rs @@ -4,11 +4,14 @@ */ use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct UpstreamLock { pub schema: u32, + pub rust_toolchain: LockedRustToolchain, pub llvm: LockedLlvm, pub llvm_tblgen: LockedTool, #[serde(default)] @@ -16,6 +19,48 @@ pub struct UpstreamLock { pub dumps: LockedDumps, } +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LockedRustToolchain { + pub commit_hash: RustcCommit, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(try_from = "String")] +pub struct RustcCommit(String); + +impl FromStr for RustcCommit { + type Err = String; + + fn from_str(value: &str) -> Result { + if value.len() == 40 + && value + .bytes() + .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f')) + { + Ok(Self(value.to_owned())) + } else { + Err(format!( + "expected 40 lowercase hexadecimal characters, found {value:?}" + )) + } + } +} + +impl TryFrom for RustcCommit { + type Error = String; + + fn try_from(value: String) -> Result { + value.parse() + } +} + +impl fmt::Display for RustcCommit { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(formatter) + } +} + #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct LockedLlvm { diff --git a/crates/cuda-intrinsics-gen/src/probe.rs b/crates/cuda-intrinsics-gen/src/probe.rs index 16dec789ce..a4e5520c43 100644 --- a/crates/cuda-intrinsics-gen/src/probe.rs +++ b/crates/cuda-intrinsics-gen/src/probe.rs @@ -3,10 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ +use crate::extract::read_upstream_lock; use crate::model::{ BackendLoweringMechanism, CatalogFile, CatalogInputs, CatalogIntrinsic, CatalogLlvm, CatalogTargetRequirement, CpAsyncSourceSize, EvidenceStageKind, IntrinsicBackend, - IntrinsicSource, SparseMmaSelector, WarpShuffleAdapter, + IntrinsicSource, RustcCommit, SparseMmaSelector, WarpShuffleAdapter, }; use crate::ptx::{ InstructionPattern, OperandPattern, instructions_with_matching_head, matching_instructions, @@ -23,18 +24,37 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::str::FromStr; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ProbeMode { - SelectedEvidence, - Comparison, -} - #[derive(Debug, Clone, PartialEq, Eq)] -struct LlcIdentity { +struct ToolIdentity { version: String, sha256: String, } +#[derive(Debug, Clone, PartialEq, Eq)] +struct SelectedBackendIdentity { + tool: ToolIdentity, + rustc_commit: RustcCommit, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum ProbeBackendIdentity { + SelectedEvidence(SelectedBackendIdentity), + Comparison(ToolIdentity), +} + +impl ProbeBackendIdentity { + fn tool(&self) -> &ToolIdentity { + match self { + Self::SelectedEvidence(identity) => &identity.tool, + Self::Comparison(identity) => identity, + } + } + + fn is_selected_evidence(&self) -> bool { + matches!(self, Self::SelectedEvidence(_)) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct CandidateProbeOptions { pub intrinsic_id: String, @@ -137,15 +157,12 @@ pub fn run_all( let runner = ProbeRunner::new(repo_root, &catalog, llc, skip_terminal)?; validate_backend_identities( - runner.mode, - catalog.intrinsics.iter().map(|record| { - ( - record.id.as_str(), - record.backend.version.as_str(), - record.backend.sha256.as_str(), - ) - }), &runner.identity, + &runner.expected_commit, + catalog.intrinsics.iter().map(|record| BackendExpectation { + intrinsic_id: &record.id, + version: &record.backend.version, + }), )?; let total = catalog.intrinsics.len(); @@ -165,8 +182,8 @@ pub fn run_all( struct ProbeRunner<'a> { catalog: &'a CatalogFile, llc: PathBuf, - mode: ProbeMode, - identity: LlcIdentity, + identity: ProbeBackendIdentity, + expected_commit: RustcCommit, output_dir: PathBuf, catalog_hash: String, skip_terminal: bool, @@ -179,11 +196,22 @@ impl<'a> ProbeRunner<'a> { llc: Option, skip_terminal: bool, ) -> Result { - let (llc, mode) = match llc { - Some(path) => (path, ProbeMode::Comparison), - None => (rust_toolchain_llc()?, ProbeMode::SelectedEvidence), + let expected_commit = read_upstream_lock(repo_root)?.rust_toolchain.commit_hash; + let (llc, identity) = match llc { + Some(path) => { + let identity = ProbeBackendIdentity::Comparison(llc_identity(&path)?); + (path, identity) + } + None => { + let (path, rustc_commit) = rust_toolchain_llc()?; + let tool = llc_identity(&path)?; + let identity = ProbeBackendIdentity::SelectedEvidence(SelectedBackendIdentity { + tool, + rustc_commit, + }); + (path, identity) + } }; - let identity = llc_identity(&llc)?; let output_dir = repo_root.join("target/intrinsics/probes"); fs::create_dir_all(&output_dir)?; let catalog_json = pretty_json(catalog)?; @@ -191,8 +219,8 @@ impl<'a> ProbeRunner<'a> { Ok(Self { catalog, llc, - mode, identity, + expected_commit, output_dir, catalog_hash, skip_terminal, @@ -201,9 +229,8 @@ impl<'a> ProbeRunner<'a> { fn validate_backend(&self, record: &CatalogIntrinsic) -> Result<()> { validate_backend_identity( - self.mode, &record.backend.version, - &record.backend.sha256, + &self.expected_commit, &self.identity, ) } @@ -218,7 +245,7 @@ impl<'a> ProbeRunner<'a> { .with_context(|| format!("write in-memory probe {}", input.display()))?; if record.llvm.is_some() - && self.mode == ProbeMode::SelectedEvidence + && self.identity.is_selected_evidence() && uses_typed_llvm_nvptx_lowering(record) { assert_intrinsic_declaration_canonicalizes( @@ -289,7 +316,7 @@ impl<'a> ProbeRunner<'a> { .iter() .any(|stage| stage.stage == EvidenceStageKind::PtxAssembly) }); - if self.mode == ProbeMode::SelectedEvidence && has_terminal_stage { + if self.identity.is_selected_evidence() && has_terminal_stage { if !selected_evidence_target { println!( "derived target {gpu_target} is LLVM-selection-only; terminal assembly evidence exists only for selected target {}", @@ -310,20 +337,25 @@ impl<'a> ProbeRunner<'a> { effective_floor % 10 ) }); - match self.mode { - ProbeMode::SelectedEvidence if selected_evidence_target => println!( - "selected evidence backend {} (SHA-256 {}) lowered {} to `{}` for selected evidence target {} {}", - self.identity.version, - self.identity.sha256, - intrinsic_id, - record.expected_ptx, - gpu_target, - ptx_configuration, - ), - ProbeMode::SelectedEvidence => println!( - "selected evidence backend {} (SHA-256 {}) lowered {} to `{}` for derived target {} {}; record evidence selects {} {}", - self.identity.version, - self.identity.sha256, + let tool = self.identity.tool(); + match &self.identity { + ProbeBackendIdentity::SelectedEvidence(identity) if selected_evidence_target => { + println!( + "selected evidence backend {} (rustc commit {}; llc SHA-256 {}) lowered {} to `{}` for selected evidence target {} {}", + tool.version, + identity.rustc_commit, + tool.sha256, + intrinsic_id, + record.expected_ptx, + gpu_target, + ptx_configuration, + ) + } + ProbeBackendIdentity::SelectedEvidence(identity) => println!( + "selected evidence backend {} (rustc commit {}; llc SHA-256 {}) lowered {} to `{}` for derived target {} {}; record evidence selects {} {}", + tool.version, + identity.rustc_commit, + tool.sha256, intrinsic_id, record.expected_ptx, gpu_target, @@ -331,10 +363,10 @@ impl<'a> ProbeRunner<'a> { record.backend.gpu_target, record.backend.ptx_feature, ), - ProbeMode::Comparison => println!( + ProbeBackendIdentity::Comparison(_) => println!( "comparison backend {} (SHA-256 {}) lowered {} to `{}` for {} target {} {}; record evidence selects {} {}; this does not validate selected evidence {} (SHA-256 {})", - self.identity.version, - self.identity.sha256, + tool.version, + tool.sha256, intrinsic_id, record.expected_ptx, if selected_evidence_target { @@ -378,14 +410,19 @@ fn derived_ptx_feature(gpu_target: &str, instruction_floor: u16) -> Result<(Opti } } +struct BackendExpectation<'a> { + intrinsic_id: &'a str, + version: &'a str, +} + fn validate_backend_identities<'a>( - mode: ProbeMode, - identities: impl IntoIterator, - actual: &LlcIdentity, + actual: &ProbeBackendIdentity, + expected_commit: &RustcCommit, + identities: impl IntoIterator>, ) -> Result<()> { - for (intrinsic_id, expected_version, expected_sha256) in identities { - validate_backend_identity(mode, expected_version, expected_sha256, actual) - .with_context(|| format!("validate probe backend for {intrinsic_id}"))?; + for expectation in identities { + validate_backend_identity(expectation.version, expected_commit, actual) + .with_context(|| format!("validate probe backend for {}", expectation.intrinsic_id))?; } Ok(()) } @@ -749,7 +786,7 @@ fn validate_candidate_cubin(cubin: &Path) -> Result<()> { Ok(()) } -fn candidate_tool(role: &str, path: &Path, identity: &LlcIdentity) -> CandidateToolDraft { +fn candidate_tool(role: &str, path: &Path, identity: &ToolIdentity) -> CandidateToolDraft { CandidateToolDraft { role: role.into(), path: path.display().to_string(), @@ -1702,7 +1739,7 @@ fn resolve_evidence_tool_with_path( }) } -fn llc_identity(llc: &Path) -> Result { +fn llc_identity(llc: &Path) -> Result { let version = Command::new(llc) .arg("--version") .output() @@ -1718,13 +1755,13 @@ fn llc_identity(llc: &Path) -> Result { .context("llc --version did not report an LLVM version")? .trim() .to_owned(); - Ok(LlcIdentity { + Ok(ToolIdentity { version, sha256: sha256_file(llc)?, }) } -fn ptxas_identity(ptxas: &Path) -> Result { +fn ptxas_identity(ptxas: &Path) -> Result { let output = Command::new(ptxas) .arg("--version") .output() @@ -1737,7 +1774,7 @@ fn ptxas_identity(ptxas: &Path) -> Result { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); let version = parse_ptxas_version(&stdout, &stderr)?; - Ok(LlcIdentity { + Ok(ToolIdentity { version, sha256: sha256_file(ptxas)?, }) @@ -1763,28 +1800,27 @@ fn parse_ptxas_version(stdout: &str, stderr: &str) -> Result { } fn validate_backend_identity( - mode: ProbeMode, expected_version: &str, - expected_sha256: &str, - actual: &LlcIdentity, + expected_commit: &RustcCommit, + actual: &ProbeBackendIdentity, ) -> Result<()> { - if mode == ProbeMode::Comparison { + let ProbeBackendIdentity::SelectedEvidence(actual) = actual else { return Ok(()); - } + }; ensure!( - actual.version == expected_version, + actual.tool.version == expected_version, "rust-toolchain llc version mismatch: selected evidence records {expected_version:?}, found {:?}; use an explicit `--llc` only for a comparison probe", - actual.version + actual.tool.version ); ensure!( - actual.sha256 == expected_sha256, - "rust-toolchain llc SHA-256 mismatch: selected evidence records {expected_sha256}, found {}; use an explicit `--llc` only for a comparison probe", - actual.sha256 + actual.rustc_commit == *expected_commit, + "rust-toolchain commit mismatch: upstream.lock records {expected_commit}, found {}; use an explicit `--llc` only for a comparison probe", + actual.rustc_commit ); Ok(()) } -fn rust_toolchain_llc() -> Result { +fn rust_toolchain_llc() -> Result<(PathBuf, RustcCommit)> { let sysroot = Command::new("rustc") .args(["--print", "sysroot"]) .output() @@ -1795,11 +1831,8 @@ fn rust_toolchain_llc() -> Result { .output() .context("query rustc host")?; ensure!(verbose.status.success(), "rustc -vV failed"); - let host = String::from_utf8_lossy(&verbose.stdout) - .lines() - .find_map(|line| line.strip_prefix("host: ")) - .context("rustc -vV did not report a host")? - .to_owned(); + let verbose = String::from_utf8_lossy(&verbose.stdout); + let (host, commit) = parse_rustc_verbose(&verbose)?; let path = PathBuf::from(String::from_utf8_lossy(&sysroot.stdout).trim()) .join("lib/rustlib") .join(host) @@ -1809,7 +1842,23 @@ fn rust_toolchain_llc() -> Result { "rust toolchain has no llc at {}", path.display() ); - Ok(path) + Ok((path, commit)) +} + +fn parse_rustc_verbose(stdout: &str) -> Result<(String, RustcCommit)> { + let host = stdout + .lines() + .find_map(|line| line.strip_prefix("host: ")) + .context("rustc -vV did not report a host")? + .to_owned(); + let commit = stdout + .lines() + .find_map(|line| line.strip_prefix("commit-hash: ")) + .context("rustc -vV did not report a commit-hash")? + .parse() + .map_err(anyhow::Error::msg) + .context("parse rustc -vV commit-hash")?; + Ok((host, commit)) } #[cfg(test)] @@ -2186,62 +2235,92 @@ mod tests { assert!(error.to_string().contains("expected PTX must be")); } - fn identity() -> LlcIdentity { - LlcIdentity { + fn commit(value: &str) -> RustcCommit { + value.parse().unwrap() + } + + fn tool_identity() -> ToolIdentity { + ToolIdentity { version: "LLVM version 22.1.2-test".into(), sha256: "abc123".into(), } } + fn selected_identity(commit_hash: &str) -> ProbeBackendIdentity { + ProbeBackendIdentity::SelectedEvidence(SelectedBackendIdentity { + tool: tool_identity(), + rustc_commit: commit(commit_hash), + }) + } + #[test] - fn selected_probe_requires_exact_recorded_backend() { + fn selected_probe_accepts_same_version_and_commit_with_different_llc_sha256() { validate_backend_identity( - ProbeMode::SelectedEvidence, "LLVM version 22.1.2-test", - "abc123", - &identity(), + &commit("1111111111111111111111111111111111111111"), + &ProbeBackendIdentity::SelectedEvidence(SelectedBackendIdentity { + tool: ToolIdentity { + sha256: "different".into(), + ..tool_identity() + }, + rustc_commit: commit("1111111111111111111111111111111111111111"), + }), ) .unwrap(); + } - let version_error = validate_backend_identity( - ProbeMode::SelectedEvidence, - "LLVM version 21", - "abc123", - &identity(), + #[test] + fn selected_probe_rejects_different_commit() { + let commit_error = validate_backend_identity( + "LLVM version 22.1.2-test", + &commit("2222222222222222222222222222222222222222"), + &selected_identity("1111111111111111111111111111111111111111"), ) .unwrap_err(); - assert!(version_error.to_string().contains("version mismatch")); + assert!(commit_error.to_string().contains("commit")); + } - let hash_error = validate_backend_identity( - ProbeMode::SelectedEvidence, - "LLVM version 22.1.2-test", - "different", - &identity(), + #[test] + fn selected_probe_rejects_different_version() { + let version_error = validate_backend_identity( + "LLVM version 21", + &commit("1111111111111111111111111111111111111111"), + &selected_identity("1111111111111111111111111111111111111111"), ) .unwrap_err(); - assert!(hash_error.to_string().contains("SHA-256 mismatch")); + assert!(version_error.to_string().contains("version mismatch")); } #[test] fn all_probe_preflight_checks_every_backend_identity() { let error = validate_backend_identities( - ProbeMode::SelectedEvidence, + &selected_identity("1111111111111111111111111111111111111111"), + &commit("1111111111111111111111111111111111111111"), [ - ("first", "LLVM version 22.1.2-test", "abc123"), - ("last", "LLVM version 22.1.2-test", "different"), + BackendExpectation { + intrinsic_id: "first", + version: "LLVM version 22.1.2-test", + }, + BackendExpectation { + intrinsic_id: "last", + version: "LLVM version 21", + }, ], - &identity(), ) .unwrap_err(); let message = format!("{error:#}"); assert!(message.contains("validate probe backend for last")); - assert!(message.contains("SHA-256 mismatch")); + assert!(message.contains("version mismatch")); } #[test] fn explicit_probe_is_always_comparison_only() { - validate_backend_identity(ProbeMode::Comparison, "different", "different", &identity()) - .unwrap(); + validate_backend_identity( + "different", + &commit("2222222222222222222222222222222222222222"), + &ProbeBackendIdentity::Comparison(tool_identity()), + ) + .unwrap(); } fn llvm_facts( @@ -2734,6 +2813,30 @@ attributes #0 = { speculatable memory(none) } } } + #[test] + fn rustc_verbose_requires_a_concrete_commit_hash() { + let valid = "rustc 1.100.0-nightly (e457a7b0d 2026-08-27)\n\ + binary: rustc\n\ + commit-hash: e457a7b0d326d67b4322ef0d11bd715cfaeda48f\n\ + commit-date: 2026-08-27\n\ + host: x86_64-unknown-linux-gnu\n\ + release: 1.100.0-nightly\n\ + LLVM version: 23.1.0\n"; + let (host, commit) = parse_rustc_verbose(valid).unwrap(); + assert_eq!(host, "x86_64-unknown-linux-gnu"); + assert_eq!( + commit, + "e457a7b0d326d67b4322ef0d11bd715cfaeda48f".parse().unwrap() + ); + + let missing = parse_rustc_verbose("host: x86_64-unknown-linux-gnu\n").unwrap_err(); + assert!(missing.to_string().contains("commit-hash")); + + let unknown = parse_rustc_verbose("commit-hash: unknown\nhost: x86_64-unknown-linux-gnu\n") + .unwrap_err(); + assert!(format!("{unknown:#}").contains("commit-hash")); + } + #[cfg(unix)] #[test] fn successful_fake_ptxas_without_a_real_cubin_is_rejected() { diff --git a/cuda-oxide-book/compiler/catalog-generated-intrinsics.md b/cuda-oxide-book/compiler/catalog-generated-intrinsics.md index 73ab1bb72e..575b1672f8 100644 --- a/cuda-oxide-book/compiler/catalog-generated-intrinsics.md +++ b/cuda-oxide-book/compiler/catalog-generated-intrinsics.md @@ -56,7 +56,7 @@ Their roles are different: | File / artifact | Role | | :-------------- | :--- | -| `intrinsics/upstream.lock` | Pins the LLVM source/extraction identity used by the imported facts. | +| `intrinsics/upstream.lock` | Pins the LLVM source/extraction identity used by the imported facts, and the rustc commit whose `llc` the evidence probe must run under. | | `intrinsics/imported.json` | Records what the pinned LLVM/NVPTX source declares. Normal intrinsic additions consume this committed file; they do not rerun extraction. | | `intrinsics/overlay.toml` | Explicitly indexes the admitted overlay shards. | | `intrinsics/overlay/*.toml` | Records cuda-oxide policy for reviewed intrinsic families. | diff --git a/intrinsics/upstream.lock b/intrinsics/upstream.lock index 1ebcafba11..8de13fd6ff 100644 --- a/intrinsics/upstream.lock +++ b/intrinsics/upstream.lock @@ -6,6 +6,9 @@ revision = "16696adcd119e6ba9cc175207d984d7021211acb" provenance = "Public rust-lang/llvm-project source revision used by Rust commit e457a7b0d" public_output_allowed = true +[rust_toolchain] +commit_hash = "e457a7b0d326d67b4322ef0d11bd715cfaeda48f" + [llvm_tblgen] name = "llvm-tblgen" version_line = "LLVM version 23.1.0"