diff --git a/CHANGELOG.md b/CHANGELOG.md index f1616f6..9543a06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. +## [2.5.0] - 2026-09-17 + +### Added +- Extension implication and composition: extensions that are architecturally implied by others actually present, but that `/proc/cpuinfo` didn't enumerate directly (e.g. `Zba`+`Zbb`+`Zbs` present but `B` missing, `M` present but `Zmmul` missing), are now computed and shown in parentheses, e.g. `Ext: I M A F D C V (B)` / `Z-Multiply: (Zmmul)`. Sourced from LLVM mainline's `RISCVFeatures.td` `Implies` tables and the RISC-V ISA manual's shorthand-extension definitions (#10) +- `ExtensionInfo.derived` / `ExtensionEntry.derived` fields (additive) marking whether an extension was reported directly (`false`) or inferred (`true`); `--json` output includes the same field + +### Changed +- `--help` now includes a line explaining the parentheses notation for derived extensions +- `riscfetch-core` bumped to **3.0.0**: adding the required `derived: bool` field to the + public, non-`#[non_exhaustive]` `ExtensionEntry`/`ExtensionInfo` structs is additive for + JSON consumers but breaks any Rust caller constructing these structs by literal, so + semver requires a major bump for the library even though the CLI itself is 2.5.0 + ## [2.4.0] - 2026-09-17 ### Added diff --git a/Cargo.lock b/Cargo.lock index c725b66..a929fa6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -277,7 +277,7 @@ dependencies = [ [[package]] name = "riscfetch" -version = "2.4.0" +version = "2.5.0" dependencies = [ "cargo-husky", "clap", @@ -292,7 +292,7 @@ dependencies = [ [[package]] name = "riscfetch-core" -version = "2.3.0" +version = "3.0.0" dependencies = [ "serde", "sysinfo", diff --git a/Cargo.toml b/Cargo.toml index 4de6d25..4c29b4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/riscfetch-core", "crates/riscfetch-cli"] resolver = "2" [workspace.package] -version = "2.4.0" +version = "2.5.0" edition = "2021" authors = ["kako-jun"] license = "MIT" diff --git a/README.ja.md b/README.ja.md index d22ab6c..7cc3c99 100644 --- a/README.ja.md +++ b/README.ja.md @@ -68,9 +68,10 @@ riscfetch -l pine64 # Pine64 ロゴを使用 ``` ISA: rv64imafdcv_zicsr_zifencei_zba_zbb_zbs_sstc... -Ext: I M A F D C V +Ext: I M A F D C V (B) Z-Base: Zicsr Zifencei Zicntr Zihpm Z-Bit: Zba Zbb Zbc Zbs +Z-Multiply: (Zmmul) Z-Vector: Zvl128b Zvl256b S-Sup: Sstc Vector: Enabled, VLEN>=256 @@ -88,6 +89,8 @@ Uptime: 3h 42m User: user@visionfive2 ``` +**括弧付き**で表示される拡張(`(B)` や `(Zmmul)` など)は、`/proc/cpuinfo` に直接は載っていないものの、他に存在する拡張から導かれる(含意される)拡張です。例えば `Zba`+`Zbb`+`Zbs` が揃っていれば `B` を、`M` があれば `Zmmul` を意味します。それ以外はカーネルが実際に列挙した拡張そのままです。 + ## オプション | フラグ | 説明 | diff --git a/README.md b/README.md index b7be5cb..f8a4500 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,10 @@ Extensions are grouped by category: ``` ISA: rv64imafdcv_zicsr_zifencei_zba_zbb_zbs_sstc... -Ext: I M A F D C V +Ext: I M A F D C V (B) Z-Base: Zicsr Zifencei Zicntr Zihpm Z-Bit: Zba Zbb Zbc Zbs +Z-Multiply: (Zmmul) Z-Vector: Zvl128b Zvl256b S-Sup: Sstc Vector: Enabled, VLEN>=256 @@ -88,6 +89,8 @@ Uptime: 3h 42m User: user@visionfive2 ``` +Extensions shown in **parentheses**, like `(B)` or `(Zmmul)`, were not reported directly by `/proc/cpuinfo` but are implied by other extensions that are present — e.g. `Zba`+`Zbb`+`Zbs` implies `B`, and `M` implies `Zmmul`. Everything else is exactly what the kernel enumerated. + ## Options | Flag | Description | diff --git a/README.zh-CN.md b/README.zh-CN.md index e47eaff..0d771e8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -68,9 +68,10 @@ riscfetch -l pine64 # 使用 Pine64 logo ``` ISA: rv64imafdcv_zicsr_zifencei_zba_zbb_zbs_sstc... -Ext: I M A F D C V +Ext: I M A F D C V (B) Z-Base: Zicsr Zifencei Zicntr Zihpm Z-Bit: Zba Zbb Zbc Zbs +Z-Multiply: (Zmmul) Z-Vector: Zvl128b Zvl256b S-Sup: Sstc Vector: Enabled, VLEN>=256 @@ -88,6 +89,8 @@ Uptime: 3h 42m User: user@visionfive2 ``` +带**括号**显示的扩展(例如 `(B)` 或 `(Zmmul)`)并未直接出现在 `/proc/cpuinfo` 中,而是根据当前已有的其他扩展推导(隐含)得出的。例如 `Zba`+`Zbb`+`Zbs` 齐全即隐含 `B`,有 `M` 即隐含 `Zmmul`。其余扩展均为内核实际列出的原始内容。 + ## 选项 | 参数 | 说明 | diff --git a/crates/riscfetch-cli/Cargo.toml b/crates/riscfetch-cli/Cargo.toml index 363e00f..b16edd6 100644 --- a/crates/riscfetch-cli/Cargo.toml +++ b/crates/riscfetch-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "riscfetch" -version = "2.4.0" +version = "2.5.0" edition = "2021" authors = ["kako-jun"] description = "RISC-V architecture information display tool - Show off your RISC-V setup!" @@ -11,7 +11,7 @@ categories = ["command-line-utilities"] readme = "../../README.md" [dependencies] -riscfetch-core = { path = "../riscfetch-core", version = "2.3.0" } +riscfetch-core = { path = "../riscfetch-core", version = "3.0.0" } clap = { version = "4.5", features = ["derive"] } colored = "2.1" serde_json = "1.0" diff --git a/crates/riscfetch-cli/SPEC.md b/crates/riscfetch-cli/SPEC.md index 0f68d98..53bae06 100644 --- a/crates/riscfetch-cli/SPEC.md +++ b/crates/riscfetch-cli/SPEC.md @@ -67,10 +67,11 @@ Extensions are grouped by category for better readability: ``` ISA: rv64imafdcv_zicbom_zicboz_zicntr_zicsr_zifencei_... -Ext: I M A F D C V +Ext: I M A F D C V (B) Z-Base: Zicsr Zifencei Zicntr Zihpm Z-Bit: Zba Zbb Zbc Zbs Z-Cache: Zicbom Zicboz +Z-Multiply: (Zmmul) Z-Vector: Zvl128b Zvl256b S-Sup: Sstc Vector: Enabled, VLEN>=256 @@ -88,14 +89,19 @@ Uptime: 3h 42m User: user@spacemit ``` +Extensions shown in **parentheses** (e.g. `(B)`, `(Zmmul)`) were not reported directly by +`/proc/cpuinfo` but are implied by other extensions that are present — see "Derived +Extensions" in `riscfetch-core/SPEC.md` for the full implication/composition rules and +sources. Everything not in parentheses is exactly what the kernel enumerated. + ### Field Definitions | Field | Description | Example | |-------|-------------|---------| | ISA | Full ISA string from /proc/cpuinfo | `rv64imafdc_zicsr_zifencei` | -| Ext | Standard extensions (space-separated) | `I M A F D C V` | -| Z-{Category}: | Z-extensions grouped by category | `Z-Bit: Zba Zbb Zbc Zbs` | -| S-{Category}: | S-extensions (privileged) by category | `S-Sup: Sstc` | +| Ext | Standard extensions (space-separated); derived ones in parentheses | `I M A F D C V (B)` | +| Z-{Category}: | Z-extensions grouped by category; derived ones in parentheses | `Z-Bit: Zba Zbb Zbc Zbs` | +| S-{Category}: | S-extensions (privileged) by category; derived ones in parentheses | `S-Sup: Sstc` | | Vector | Vector extension status and VLEN | `Enabled, VLEN>=256` or empty | | Harts | Number of hardware threads | `4 harts` | | HW IDs | Hardware identifiers | `vendor:0x489 arch:0x... impl:0x...` | @@ -153,6 +159,7 @@ Extensions: D Double-Precision Float C Compressed (16-bit) V Vector (SIMD) + (B) Bit Manipulation Z-Extensions (Base): Zicsr CSR Instructions @@ -169,6 +176,9 @@ Z-Extensions (Bit Manipulation): Z-Extensions (Cryptography): Zkt Data-Indep Timing +Z-Extensions (Multiply): + (Zmmul) Multiply Only (no Div) + Z-Extensions (Vector): Zvl128b VLEN >= 128 bits Zvl256b VLEN >= 256 bits @@ -178,6 +188,9 @@ S-Extensions (Supervisor): Sstc Supervisor Timer ``` +As in compact mode, extensions in parentheses (`(B)`, `(Zmmul)`) are derived via +implication/composition rather than reported directly. + --- ## Output Format (--all Mode) @@ -215,6 +228,10 @@ Z-Extensions (Base): The `--all` flag can be combined with other options (`-e`, `-l`, `-j`, `-r`, etc.). +Note: `--all` mode's `✓`/`✗` reflects only what `/proc/cpuinfo` reports directly; it does +not (yet) mark derived extensions differently. Derived (parenthesized) display is a +default-mode / `--explain` feature only. + --- ## Output Format (--json Mode) @@ -223,24 +240,27 @@ The `--all` flag can be combined with other options (`-e`, `-l`, `-j`, `-r`, etc ```json { - "isa": "rv64imafdcv_zicsr_zifencei_zba_zbb_sstc", + "isa": "rv64imafdcv_zicsr_zifencei_zba_zbb_zbs_sstc", "extensions": [ - {"name": "I", "description": "Base Integer Instructions"}, - {"name": "M", "description": "Integer Multiply/Divide"}, - {"name": "A", "description": "Atomic Instructions"}, - {"name": "F", "description": "Single-Precision Float"}, - {"name": "D", "description": "Double-Precision Float"}, - {"name": "C", "description": "Compressed (16-bit)"}, - {"name": "V", "description": "Vector (SIMD)"} + {"name": "I", "description": "Base Integer Instructions", "derived": false}, + {"name": "M", "description": "Integer Multiply/Divide", "derived": false}, + {"name": "A", "description": "Atomic Instructions", "derived": false}, + {"name": "F", "description": "Single-Precision Float", "derived": false}, + {"name": "D", "description": "Double-Precision Float", "derived": false}, + {"name": "C", "description": "Compressed (16-bit)", "derived": false}, + {"name": "V", "description": "Vector (SIMD)", "derived": false}, + {"name": "B", "description": "Bit Manipulation", "derived": true} ], "z_extensions": [ - {"name": "Zicsr", "description": "CSR Instructions"}, - {"name": "Zifencei", "description": "Instruction-Fetch Fence"}, - {"name": "Zba", "description": "Address Generation"}, - {"name": "Zbb", "description": "Basic Bit Manipulation"} + {"name": "Zicsr", "description": "CSR Instructions", "derived": false}, + {"name": "Zifencei", "description": "Instruction-Fetch Fence", "derived": false}, + {"name": "Zba", "description": "Address Generation", "derived": false}, + {"name": "Zbb", "description": "Basic Bit Manipulation", "derived": false}, + {"name": "Zbs", "description": "Single-bit Operations", "derived": false}, + {"name": "Zmmul", "description": "Multiply Only (no Div)", "derived": true} ], "s_extensions": [ - {"name": "Sstc", "description": "Supervisor Timer"} + {"name": "Sstc", "description": "Supervisor Timer", "derived": false} ], "vector": { "enabled": true, @@ -268,6 +288,11 @@ The `--all` flag can be combined with other options (`-e`, `-l`, `-j`, `-r`, etc } ``` +`extensions`/`z_extensions`/`s_extensions` entries carry `"derived": true` for +extensions inferred via implication/composition (issue #10) that `/proc/cpuinfo` did +not report directly; reported entries have `"derived": false`. This is an additive +field — existing consumers that only read `name`/`description` are unaffected. + ### Error (on non-RISC-V) ```json @@ -375,12 +400,15 @@ Total score: 1234 Must include: - Program description: "RISC-V architecture information display tool" +- A note explaining the parentheses notation for derived extensions (issue #10) - All options with short and long forms - Brief description of each option ``` RISC-V architecture information display tool +Extensions shown in parentheses, e.g. (B) or (Zmmul), were not reported directly by /proc/cpuinfo but are implied by other extensions that are present (e.g. Zba+Zbb+Zbs implies B). + Usage: riscfetch [OPTIONS] Options: @@ -444,7 +472,7 @@ Example: `riscfetch 0.2.0` ## Version -- Spec version: 2.1 -- Last updated: 2026-09 +- Spec version: 2.2 +- Last updated: 2026-09-17 - Based on RISC-V ISA spec version: 2026-09 - Supports 103 Z-extensions and 48 S-extensions (151 total) diff --git a/crates/riscfetch-cli/src/cli.rs b/crates/riscfetch-cli/src/cli.rs index ef3f954..3418bb9 100644 --- a/crates/riscfetch-cli/src/cli.rs +++ b/crates/riscfetch-cli/src/cli.rs @@ -2,7 +2,12 @@ use clap::Parser; #[derive(Parser, Debug)] #[command(name = "riscfetch")] -#[command(author, version, about = "RISC-V architecture information display tool", long_about = None)] +#[command( + author, + version, + about = "RISC-V architecture information display tool", + long_about = "RISC-V architecture information display tool\n\nExtensions shown in parentheses, e.g. (B) or (Zmmul), were not reported directly by /proc/cpuinfo but are implied by other extensions that are present (e.g. Zba+Zbb+Zbs implies B)." +)] #[allow(clippy::struct_excessive_bools)] pub struct Args { /// Vendor logo (default, sifive, starfive, thead, milkv, sipeed, pine64, eswin, ultrarisc, kendryte, allwinner, espressif, spacemit, sophgo, wch) diff --git a/crates/riscfetch-cli/src/main.rs b/crates/riscfetch-cli/src/main.rs index 726d49e..40af2e1 100644 --- a/crates/riscfetch-cli/src/main.rs +++ b/crates/riscfetch-cli/src/main.rs @@ -170,15 +170,15 @@ fn display_riscv_info(vendor: &str, style: &str, explain: bool, riscv_only: bool display_all_extensions_compact(&all_std, &all_z, &all_s); } } else { - // Show only detected extensions - let extensions_compact = info::get_extensions_compact(); - let z_exts_with_cat = info::get_z_extensions_with_category(); - let s_exts_with_cat = info::get_s_extensions_with_category(); + // Show detected extensions, plus ones implied/composed from them (issue #10) + let std_exts = info::get_extensions_with_derived(&isa_string); + let z_exts_with_cat = info::parse_z_extensions_with_category_and_derived(&isa_string); + let s_exts_with_cat = info::parse_s_extensions_with_category_and_derived(&isa_string); if explain { - display_extensions_explained(&extensions_compact, &z_exts_with_cat, &s_exts_with_cat); + display_extensions_explained(&std_exts, &z_exts_with_cat, &s_exts_with_cat); } else { - display_extensions_compact(&extensions_compact, &z_exts_with_cat, &s_exts_with_cat); + display_extensions_compact(&std_exts, &z_exts_with_cat, &s_exts_with_cat); } } @@ -266,26 +266,45 @@ fn display_riscv_info(vendor: &str, style: &str, explain: bool, riscv_only: bool println!(); } +/// Format a single extension name for compact display. Extensions the ISA string +/// names directly are shown plain; ones inferred via implication/composition (issue +/// #10) are wrapped in parentheses and dimmed, since the parentheses (not the color) +/// are the primary signal. +fn format_ext_token(name: &str, derived: bool) -> String { + if derived { + format!("({name})").bright_black().to_string() + } else { + name.white().to_string() + } +} + /// Display extensions in compact mode (category-grouped multiple lines) fn display_extensions_compact( - std_exts: &str, + std_exts: &[info::ExtensionInfo], z_exts: &[info::ExtensionInfo], s_exts: &[info::ExtensionInfo], ) { // Standard extensions if !std_exts.is_empty() { - println!("{} {}", "Ext:".bright_yellow().bold(), std_exts.white()); + let parts: Vec = std_exts + .iter() + .map(|e| format_ext_token(&e.name, e.derived)) + .collect(); + println!("{} {}", "Ext:".bright_yellow().bold(), parts.join(" ")); } // Z-extensions grouped by category let z_groups = info::group_by_category(z_exts); for (category, exts) in &z_groups { let cat_name = info::get_z_category_name(category); - let ext_names: Vec<&str> = exts.iter().map(|e| e.name.as_str()).collect(); + let ext_parts: Vec = exts + .iter() + .map(|e| format_ext_token(&e.name, e.derived)) + .collect(); println!( "{} {}", format!("Z-{cat_name}:").bright_yellow().bold(), - ext_names.join(" ").white() + ext_parts.join(" ") ); } @@ -293,25 +312,38 @@ fn display_extensions_compact( let s_groups = info::group_by_category(s_exts); for (category, exts) in &s_groups { let cat_name = info::get_s_category_name(category); - let ext_names: Vec<&str> = exts.iter().map(|e| e.name.as_str()).collect(); + let ext_parts: Vec = exts + .iter() + .map(|e| format_ext_token(&e.name, e.derived)) + .collect(); println!( "{} {}", format!("S-{cat_name}:").bright_magenta().bold(), - ext_names.join(" ").white() + ext_parts.join(" ") ); } } /// Display extensions in explained mode (category-grouped with aligned columns) fn display_extensions_explained( - _std_exts: &str, + std_exts: &[info::ExtensionInfo], z_exts: &[info::ExtensionInfo], s_exts: &[info::ExtensionInfo], ) { // Standard extensions println!("{}", "Extensions:".bright_yellow().bold()); - for (ext, desc) in info::get_extensions_explained() { - println!(" {:<10} {}", ext.bright_green(), desc); + for ext in std_exts { + let label = if ext.derived { + format!("({})", ext.name) + } else { + ext.name.clone() + }; + let label_colored = if ext.derived { + label.bright_black() + } else { + label.bright_green() + }; + println!(" {label_colored:<12} {}", ext.description); } // Z-extensions grouped by category @@ -324,7 +356,17 @@ fn display_extensions_explained( format!("Z-Extensions ({cat_name}):").bright_yellow().bold() ); for ext in exts { - println!(" {:<10} {}", ext.name.bright_green(), ext.description); + let label = if ext.derived { + format!("({})", ext.name) + } else { + ext.name.clone() + }; + let label_colored = if ext.derived { + label.bright_black() + } else { + label.bright_green() + }; + println!(" {label_colored:<12} {}", ext.description); } } @@ -340,7 +382,17 @@ fn display_extensions_explained( .bold() ); for ext in exts { - println!(" {:<10} {}", ext.name.bright_green(), ext.description); + let label = if ext.derived { + format!("({})", ext.name) + } else { + ext.name.clone() + }; + let label_colored = if ext.derived { + label.bright_black() + } else { + label.bright_green() + }; + println!(" {label_colored:<12} {}", ext.description); } } } diff --git a/crates/riscfetch-core/Cargo.toml b/crates/riscfetch-core/Cargo.toml index 1621ea9..f58b8b8 100644 --- a/crates/riscfetch-core/Cargo.toml +++ b/crates/riscfetch-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "riscfetch-core" -version = "2.3.0" +version = "3.0.0" edition = "2021" authors = ["kako-jun"] description = "RISC-V system information library - ISA extensions, hart count, hardware IDs" diff --git a/crates/riscfetch-core/SPEC.md b/crates/riscfetch-core/SPEC.md index d413fe8..b7e32d1 100644 --- a/crates/riscfetch-core/SPEC.md +++ b/crates/riscfetch-core/SPEC.md @@ -276,6 +276,73 @@ Unknown extensions are still parsed but may not have descriptions. --- +## Derived Extensions (Implication & Composition) + +`/proc/cpuinfo` only lists the extensions the kernel bothered to enumerate. It does not +compute the extensions that are architecturally implied by those, so riscfetch computes +them itself (issue #10). + +Two relations are combined: + +- **Implication** (forward): if extension `X` is present, its dependencies are present + too, even if the ISA string never spells them out. Examples: `M` implies `Zmmul`, `D` + implies `F`, `F` implies `Zicsr`, `A` implies `Zalrsc`+`Zaamo`, `C` implies `Zca`, `V` + implies a chain down to `Zve32x` (and, per the ISA manual's minimum vector + requirements, also `Zve64d`/`Zve64f`/`Zve32f`/`Zve64x`/`D`/`F`/`Zicsr`), `Zfh` implies + `Zfhmin`, `Zvfh` implies `Zvfhmin`/`Zfhmin`, `Zdinx` implies `Zfinx`, and the `Zvl*` + family chains from the largest declared minimum VLEN down to `Zvl32b`. +- **Composition** (converse): some extension names are pure ISA-manual shorthand for the + union of a fixed set of other extensions (no behavior of their own). If every member + of the set is present, the shorthand name is considered present too. Examples: + `Zba`+`Zbb`+`Zbs` composes into `B`; `Zaamo`+`Zalrsc` composes into `A`; + `Zkn`+`Zkr`+`Zkt` composes into `Zk`; similarly for `Zkn`, `Zks`, `Zvkn`, `Zvks`, `Zce`. + +Both relations are resolved to their full transitive closure with a worklist +(`while changed`), so implication chains and composition chains are fully followed and +the computation terminates even in the presence of a cycle. + +### Source of truth + +The implication table (`riscfetch_core::implications::IMPLICATIONS`) is sourced from the +`Implies` field of each `RISCVExtension` definition in LLVM mainline +(`llvm/lib/Target/RISCV/RISCVFeatures.td`, fetched 2026-09-17). The composition table +(`riscfetch_core::implications::COMPOSITIONS`) mirrors the same member sets, applied in +the converse direction per the RISC-V ISA manual's definition of those names as +shorthand. Entries that could not be confirmed against the LLVM table, or whose +implication depends on rv32 vs rv64 in a way the table can't express (e.g. `Zcf` is only +legal on rv32), are intentionally left out — missing coverage is preferred over a false +positive. + +### `ExtensionInfo.derived` + +`ExtensionInfo` (returned by `get_extensions_with_derived`, +`parse_z_extensions_with_category_and_derived`, +`parse_s_extensions_with_category_and_derived`, and the corresponding `--all` / +plain functions) has a `derived: bool` field: + +| `derived` | Meaning | +|-----------|---------| +| `false` | The ISA string names this extension directly (including via the `G` shorthand) | +| `true` | Not named directly; inferred via implication or composition | + +`supported` keeps its existing meaning (the extension is present) regardless of +`derived` — a derived extension is still `supported: true`. + +`ExtensionEntry` (the JSON-serializable type used by `collect_riscv_info` / +`collect_all_info`) carries the same `derived: bool` field. + +Whether this is a breaking change depends on which surface you consume: + +- **JSON output**: additive. `derived` is a new field alongside `name`/`description`; + existing consumers that only read the fields they already knew about are unaffected. +- **Rust API**: breaking. Both `ExtensionEntry` and `ExtensionInfo` are public structs + with all-public fields and no `#[non_exhaustive]`, so any caller that constructs one by + struct literal (`ExtensionEntry { name, description }`) no longer compiles once + `derived` is a required field. This is why `riscfetch-core` is versioned 3.0.0 even + though the JSON schema change alone would only warrant a minor bump. + +--- + ## Edge Cases ### Case Sensitivity @@ -316,7 +383,7 @@ parse_extensions_compact("rv64gc") → "I M A F D C" (not "G C") ## Version -- Spec version: 2.0 -- Last updated: 2026-09 +- Spec version: 2.1 +- Last updated: 2026-09-17 - Based on RISC-V ISA spec version: 2026-09 (Unprivileged/Privileged) - Reference: LLVM mainline RISC-V extension support diff --git a/crates/riscfetch-core/src/implications.rs b/crates/riscfetch-core/src/implications.rs new file mode 100644 index 0000000..3b7d7a6 --- /dev/null +++ b/crates/riscfetch-core/src/implications.rs @@ -0,0 +1,212 @@ +//! Extension implication and composition tables +//! +//! `/proc/cpuinfo` only lists the extensions the kernel bothered to enumerate. It does +//! not compute the extensions that are *implied* by those (e.g. `M` implies `Zmmul`), +//! nor does it collapse a set of extensions into the shorthand name the ISA manual +//! defines for their union (e.g. `Zba`+`Zbb`+`Zbs` is, by definition, `B`). This module +//! computes both directions so riscfetch can show extensions that are genuinely present +//! even when the kernel didn't spell them out (issue #10). +//! +//! ## Sources +//! +//! - **Implication** entries are taken from the `Implies` field of each +//! `RISCVExtension`/`RISCVExperimentalExtension` definition in LLVM mainline +//! (`llvm/lib/Target/RISCV/RISCVFeatures.td`, fetched 2026-09-17). Each entry below is +//! commented with the corresponding `FeatureStdExt*` definition it was read from. +//! - **Composition** entries mark extension names that the RISC-V ISA manual defines as +//! pure shorthand/union notation for a fixed set of other extensions (no additional +//! behavior of their own), so the presence of every member of the set means the +//! shorthand name is present too, even if the ISA string never spells it out. The +//! member sets are the same sets LLVM uses for the forward `Implies` direction of +//! those shorthand extensions. +//! - Entries that could not be confirmed against the LLVM table, or whose implication +//! is conditional on rv32 vs rv64 in a way this table can't express (e.g. `Zcf` is +//! only legal on rv32), are intentionally left out. Missing coverage is preferred over +//! a false positive. + +/// Forward implication: if the extension named by `.0` is present, every extension +/// named in `.1` is implied to be present too, regardless of whether the ISA string +/// mentions it directly. +#[allow(clippy::type_complexity)] +pub const IMPLICATIONS: &[(&str, &[&str])] = &[ + // --- Integer / CSR --- + ("M", &["Zmmul"]), // FeatureStdExtM -> Zmmul + ("Zicntr", &["Zicsr"]), // FeatureStdExtZicntr -> Zicsr + ("Zihpm", &["Zicsr"]), // FeatureStdExtZihpm -> Zicsr + ("Ziccid", &["Ziccif"]), // FeatureStdExtZiccid -> Ziccif + // --- Floating point --- + ("F", &["Zicsr"]), // FeatureStdExtF -> Zicsr + ("D", &["F"]), // FeatureStdExtD -> F + ("Q", &["D"]), // FeatureStdExtQ -> D + ("Zfh", &["Zfhmin"]), // FeatureStdExtZfh -> Zfhmin + ("Zfhmin", &["F"]), // FeatureStdExtZfhmin -> F + ("Zvfh", &["Zvfhmin", "Zfhmin"]), // FeatureStdExtZvfh -> Zvfhmin, Zfhmin + ("Zvfhmin", &["Zve32f"]), // FeatureStdExtZvfhmin -> Zve32f + ("Zfinx", &["Zicsr"]), // FeatureStdExtZfinx -> Zicsr + ("Zdinx", &["Zfinx"]), // FeatureStdExtZdinx -> Zfinx + ("Zhinx", &["Zhinxmin"]), // FeatureStdExtZhinx -> Zhinxmin + // --- Atomics --- + ("A", &["Zaamo", "Zalrsc"]), // FeatureStdExtA -> Zaamo, Zalrsc + ("Zabha", &["Zaamo"]), // FeatureStdExtZabha -> Zaamo + ("Zacas", &["Zaamo"]), // FeatureStdExtZacas -> Zaamo + // --- Compressed --- + ("C", &["Zca"]), // FeatureStdExtC -> Zca + ("Zcb", &["Zca"]), // FeatureStdExtZcb -> Zca + ("Zcf", &["F", "Zca"]), // FeatureStdExtZcf -> F, Zca + ("Zcd", &["D", "Zca"]), // FeatureStdExtZcd -> D, Zca + ("Zce", &["Zca", "Zcb", "Zcmp", "Zcmt"]), // FeatureStdExtZce -> Zca, Zcb, Zcmp, Zcmt + // --- Bit manipulation / scalar crypto --- + ("B", &["Zba", "Zbb", "Zbs"]), // FeatureStdExtB -> Zba, Zbb, Zbs + ("Zbc", &["Zbkc"]), // FeatureStdExtZbc -> Zbkc + ("Zk", &["Zkn", "Zkr", "Zkt"]), // FeatureStdExtZk -> Zkn, Zkr, Zkt + ("Zkn", &["Zbkb", "Zbkc", "Zbkx", "Zkne", "Zknd", "Zknh"]), // FeatureStdExtZkn -> Zbkb, Zbkc, Zbkx, Zkne, Zknd, Zknh + ("Zks", &["Zbkb", "Zbkc", "Zbkx", "Zksed", "Zksh"]), // FeatureStdExtZks -> Zbkb, Zbkc, Zbkx, Zksed, Zksh + // --- Vector --- + ("V", &["Zve64d", "Zvl128b"]), // FeatureStdExtV -> Zve64d, Zvl128b + ("Zve64d", &["Zve64f", "D"]), // FeatureStdExtZve64d -> Zve64f, D + ("Zve64f", &["Zve32f", "Zve64x"]), // FeatureStdExtZve64f -> Zve32f, Zve64x + ("Zve64x", &["Zve32x", "Zvl64b"]), // FeatureStdExtZve64x -> Zve32x, Zvl64b + ("Zve32f", &["Zve32x", "F"]), // FeatureStdExtZve32f -> Zve32x, F + ("Zvbb", &["Zvkb"]), // FeatureStdExtZvbb -> Zvkb + ("Zvbc", &["Zve64x"]), // FeatureStdExtZvbc -> Zve64x + ("Zvkb", &["Zve32x"]), // FeatureStdExtZvkb -> Zve32x + ("Zvkned", &["Zve32x"]), // FeatureStdExtZvkned -> Zve32x + ("Zvknha", &["Zve32x"]), // FeatureStdExtZvknha -> Zve32x + ("Zvknhb", &["Zve64x", "Zvknha"]), // FeatureStdExtZvknhb -> Zve64x, Zvknha + ("Zvkn", &["Zvkned", "Zvknhb", "Zvkb", "Zvkt"]), // FeatureStdExtZvkn -> Zvkned, Zvknhb, Zvkb, Zvkt + ("Zvks", &["Zvksed", "Zvksh", "Zvkb", "Zvkt"]), // FeatureStdExtZvks -> Zvksed, Zvksh, Zvkb, Zvkt + // Zvl chain: a larger guaranteed minimum VLEN implies every smaller minimum too. + ("Zvl65536b", &["Zvl32768b"]), + ("Zvl32768b", &["Zvl16384b"]), + ("Zvl16384b", &["Zvl8192b"]), + ("Zvl8192b", &["Zvl4096b"]), + ("Zvl4096b", &["Zvl2048b"]), + ("Zvl2048b", &["Zvl1024b"]), + ("Zvl1024b", &["Zvl512b"]), + ("Zvl512b", &["Zvl256b"]), + ("Zvl256b", &["Zvl128b"]), + ("Zvl128b", &["Zvl64b"]), + ("Zvl64b", &["Zvl32b"]), +]; + +/// Composition (converse of a shorthand extension): if every extension in `.1` is +/// present, the ISA manual defines `.0` as pure shorthand for that exact set, so it is +/// considered present too. +pub const COMPOSITIONS: &[(&str, &[&str])] = &[ + // A is defined by the ISA manual as exactly Zaamo + Zalrsc; LLVM's separate bitmask + // is a hwprobe identifier, not extra behavior. + ("A", &["Zaamo", "Zalrsc"]), + ("B", &["Zba", "Zbb", "Zbs"]), + ("Zce", &["Zca", "Zcb", "Zcmp", "Zcmt"]), + ("Zk", &["Zkn", "Zkr", "Zkt"]), + ("Zkn", &["Zbkb", "Zbkc", "Zbkx", "Zkne", "Zknd", "Zknh"]), + ("Zks", &["Zbkb", "Zbkc", "Zbkx", "Zksed", "Zksh"]), + ("Zvkn", &["Zvkned", "Zvknhb", "Zvkb", "Zvkt"]), + ("Zvks", &["Zvksed", "Zvksh", "Zvkb", "Zvkt"]), +]; + +use std::collections::BTreeSet; + +/// Given the set of extension names already known to be present (canonical casing, +/// e.g. `"M"`, `"Zba"`, `"Sstc"`), compute the set of additional extension names that +/// become present through implication or composition. +/// +/// This resolves the full transitive closure with a worklist (`while changed`), so +/// implication chains (`V` -> `Zve64d` -> ... -> `Zve32x`) and composition chains are +/// fully followed. The worklist only ever inserts names into a set, so it terminates +/// even if the tables ever contained a cycle. +#[must_use] +pub fn compute_derived(explicit: &BTreeSet) -> BTreeSet { + let mut known: BTreeSet = explicit.clone(); + let mut derived: BTreeSet = BTreeSet::new(); + + loop { + let mut changed = false; + + for &(name, implies) in IMPLICATIONS { + if known.contains(name) { + for &imp in implies { + if known.insert(imp.to_string()) { + derived.insert(imp.to_string()); + changed = true; + } + } + } + } + + for &(name, requires) in COMPOSITIONS { + if !known.contains(name) && requires.iter().all(|r| known.contains(*r)) { + known.insert(name.to_string()); + derived.insert(name.to_string()); + changed = true; + } + } + + if !changed { + break; + } + } + + derived +} + +#[cfg(test)] +mod tests { + use super::*; + + fn set(names: &[&str]) -> BTreeSet { + names.iter().map(|s| s.to_string()).collect() + } + + #[test] + fn m_implies_zmmul() { + let derived = compute_derived(&set(&["M"])); + assert!(derived.contains("Zmmul")); + } + + #[test] + fn zihpm_implies_zicsr() { + let derived = compute_derived(&set(&["Zihpm"])); + assert!(derived.contains("Zicsr")); + } + + #[test] + fn zba_zbb_zbs_compose_into_b() { + let derived = compute_derived(&set(&["Zba", "Zbb", "Zbs"])); + assert!(derived.contains("B")); + } + + #[test] + fn zba_alone_does_not_compose_into_b() { + let derived = compute_derived(&set(&["Zba"])); + assert!(!derived.contains("B")); + } + + #[test] + fn v_transitively_implies_zve32x() { + let derived = compute_derived(&set(&["V"])); + assert!(derived.contains("Zve64d")); + assert!(derived.contains("Zve64f")); + assert!(derived.contains("Zve32f")); + assert!(derived.contains("Zve32x")); + assert!(derived.contains("Zve64x")); + assert!(derived.contains("D")); + assert!(derived.contains("F")); + assert!(derived.contains("Zicsr")); + } + + #[test] + fn explicit_extensions_are_not_marked_derived() { + // If B is already explicit, it must not appear in the derived set even though + // its components are also present. + let derived = compute_derived(&set(&["B", "Zba", "Zbb", "Zbs"])); + assert!(!derived.contains("B")); + assert!(!derived.contains("Zba")); + } + + #[test] + fn no_infinite_loop_on_empty_input() { + let derived = compute_derived(&BTreeSet::new()); + assert!(derived.is_empty()); + } +} diff --git a/crates/riscfetch-core/src/lib.rs b/crates/riscfetch-core/src/lib.rs index 6a0ce2d..ca89d70 100644 --- a/crates/riscfetch-core/src/lib.rs +++ b/crates/riscfetch-core/src/lib.rs @@ -16,6 +16,7 @@ mod extensions; mod hardware; +mod implications; mod parsing; mod system; mod types; @@ -30,12 +31,14 @@ pub use extensions::{ // Re-export parsing functions and types pub use parsing::{ - get_all_s_extensions_with_status, get_all_standard_extensions_with_status, - get_all_z_extensions_with_status, get_s_category_name, get_z_category_name, group_by_category, + compute_derived_extension_names, get_all_s_extensions_with_status, + get_all_standard_extensions_with_status, get_all_z_extensions_with_status, + get_extensions_with_derived, get_s_category_name, get_z_category_name, group_by_category, parse_extensions_compact, parse_extensions_explained, parse_s_extensions, - parse_s_extensions_explained, parse_s_extensions_with_category, parse_vector_from_isa, - parse_z_extensions, parse_z_extensions_explained, parse_z_extensions_with_category, - ExtensionInfo, + parse_s_extensions_explained, parse_s_extensions_with_category, + parse_s_extensions_with_category_and_derived, parse_vector_from_isa, parse_z_extensions, + parse_z_extensions_explained, parse_z_extensions_with_category, + parse_z_extensions_with_category_and_derived, ExtensionInfo, }; // Re-export hardware functions @@ -120,6 +123,27 @@ pub fn get_s_extensions_with_category() -> Vec { parse_s_extensions_with_category(&get_isa_string()) } +/// Get standard extensions, including ones inferred via implication/composition +/// (issue #10). Inferred entries have `derived: true`. +#[must_use] +pub fn get_extensions_with_derived_for_system() -> Vec { + get_extensions_with_derived(&get_isa_string()) +} + +/// Get Z-extensions with category info, including ones inferred via +/// implication/composition (issue #10). Inferred entries have `derived: true`. +#[must_use] +pub fn get_z_extensions_with_category_and_derived() -> Vec { + parse_z_extensions_with_category_and_derived(&get_isa_string()) +} + +/// Get S-extensions with category info, including ones inferred via +/// implication/composition (issue #10). Inferred entries have `derived: true`. +#[must_use] +pub fn get_s_extensions_with_category_and_derived() -> Vec { + parse_s_extensions_with_category_and_derived(&get_isa_string()) +} + /// Collect RISC-V specific information only (excludes generic system info) #[must_use] pub fn collect_riscv_info() -> RiscvInfo { @@ -129,13 +153,21 @@ pub fn collect_riscv_info() -> RiscvInfo { sys.refresh_cpu_all(); let isa = get_isa_string(); - let exts: Vec = get_extensions_explained() + let exts: Vec = get_extensions_with_derived(&isa) .into_iter() - .map(|(name, description)| ExtensionEntry { name, description }) + .map(|e| ExtensionEntry { + name: e.name, + description: e.description, + derived: e.derived, + }) .collect(); - let z_exts: Vec = get_z_extensions_explained() + let z_exts: Vec = parse_z_extensions_with_category_and_derived(&isa) .into_iter() - .map(|(name, description)| ExtensionEntry { name, description }) + .map(|e| ExtensionEntry { + name: e.name, + description: e.description, + derived: e.derived, + }) .collect(); let hw_ids = get_hardware_ids(); @@ -167,17 +199,29 @@ pub fn collect_all_info() -> SystemInfo { sys.refresh_cpu_all(); let isa = get_isa_string(); - let exts: Vec = get_extensions_explained() + let exts: Vec = get_extensions_with_derived(&isa) .into_iter() - .map(|(name, description)| ExtensionEntry { name, description }) + .map(|e| ExtensionEntry { + name: e.name, + description: e.description, + derived: e.derived, + }) .collect(); - let z_exts: Vec = get_z_extensions_explained() + let z_exts: Vec = parse_z_extensions_with_category_and_derived(&isa) .into_iter() - .map(|(name, description)| ExtensionEntry { name, description }) + .map(|e| ExtensionEntry { + name: e.name, + description: e.description, + derived: e.derived, + }) .collect(); - let s_exts: Vec = get_s_extensions_explained() + let s_exts: Vec = parse_s_extensions_with_category_and_derived(&isa) .into_iter() - .map(|(name, description)| ExtensionEntry { name, description }) + .map(|e| ExtensionEntry { + name: e.name, + description: e.description, + derived: e.derived, + }) .collect(); let hw_ids = get_hardware_ids(); diff --git a/crates/riscfetch-core/src/parsing.rs b/crates/riscfetch-core/src/parsing.rs index 901b235..6f5fa59 100644 --- a/crates/riscfetch-core/src/parsing.rs +++ b/crates/riscfetch-core/src/parsing.rs @@ -3,6 +3,8 @@ use crate::extensions::{ STANDARD_EXTENSIONS, S_CATEGORY_NAMES, S_EXTENSIONS, Z_CATEGORY_NAMES, Z_EXTENSIONS, }; +use crate::implications::compute_derived; +use std::collections::BTreeSet; /// Extension info with category and support status #[derive(Debug, Clone)] @@ -11,6 +13,11 @@ pub struct ExtensionInfo { pub description: String, pub category: String, pub supported: bool, + /// `true` if this extension was not reported directly in the ISA string but was + /// inferred through implication or composition from other extensions that are + /// present (see the `implications` module). `supported` keeps its existing meaning + /// regardless of this flag: a derived extension is still "supported". + pub derived: bool, } /// Strip rv32/rv64 prefix from ISA base part to get extension letters only @@ -179,12 +186,14 @@ pub fn parse_z_extensions_with_category(isa: &str) -> Vec { description: "CSR Instructions".to_string(), category: "base".to_string(), supported: true, + derived: false, }); z_exts.push(ExtensionInfo { name: "Zifencei".to_string(), description: "Instruction-Fetch Fence".to_string(), category: "base".to_string(), supported: true, + derived: false, }); } @@ -197,6 +206,7 @@ pub fn parse_z_extensions_with_category(isa: &str) -> Vec { description: desc.to_string(), category: category.to_string(), supported: true, + derived: false, }); } } @@ -218,6 +228,126 @@ pub fn parse_s_extensions_with_category(isa: &str) -> Vec { description: desc.to_string(), category: category.to_string(), supported: true, + derived: false, + }); + } + } + + s_exts +} + +/// Collect the canonical names of every standard/Z/S extension the ISA string reports +/// directly, including the ones implied by the `G` shorthand (I, M, A, F, D, Zicsr, +/// Zifencei). Names match the `name` field of `STANDARD_EXTENSIONS`/`Z_EXTENSIONS`/ +/// `S_EXTENSIONS` exactly, so they can be fed straight into the implication tables. +fn explicit_extension_names(isa: &str) -> BTreeSet { + let isa_lower = isa.to_lowercase(); + let base = isa_lower.split('_').next().unwrap_or(&isa_lower); + let ext_part = strip_rv_prefix(base); + let has_g = ext_part.contains('g'); + + let mut names: BTreeSet = BTreeSet::new(); + + for name in parse_extensions_compact(isa).split_whitespace() { + names.insert(name.to_string()); + } + + for &(pattern, name, _desc, _category) in Z_EXTENSIONS { + if isa_has_extension(&isa_lower, pattern) + || (has_g && (pattern == "zicsr" || pattern == "zifencei")) + { + names.insert(name.to_string()); + } + } + + for &(pattern, name, _desc, _category) in S_EXTENSIONS { + if isa_has_extension(&isa_lower, pattern) { + names.insert(name.to_string()); + } + } + + names +} + +/// Compute every extension name that is implied or composed from the extensions the +/// ISA string reports directly, but that the ISA string itself does not mention (see +/// the `implications` module). These are the names that should be shown in parentheses. +#[must_use] +pub fn compute_derived_extension_names(isa: &str) -> BTreeSet { + compute_derived(&explicit_extension_names(isa)) +} + +/// Get standard (single-letter, including `G` expansion) extensions as `ExtensionInfo`, +/// including ones inferred via implication/composition (e.g. `B` from `Zba`+`Zbb`+`Zbs`, +/// per issue #10). Extensions the ISA string names directly have `derived: false`; +/// inferred ones have `derived: true`. +#[must_use] +pub fn get_extensions_with_derived(isa: &str) -> Vec { + let explicit: BTreeSet = parse_extensions_compact(isa) + .split_whitespace() + .map(str::to_string) + .collect(); + let derived_names = compute_derived_extension_names(isa); + + STANDARD_EXTENSIONS + .iter() + .filter_map(|&(_ch, name, desc)| { + let is_explicit = explicit.contains(name); + let is_derived = !is_explicit && derived_names.contains(name); + if is_explicit || is_derived { + Some(ExtensionInfo { + name: name.to_string(), + description: desc.to_string(), + category: "std".to_string(), + supported: true, + derived: is_derived, + }) + } else { + None + } + }) + .collect() +} + +/// Same as [`parse_z_extensions_with_category`], plus Z-extensions inferred via +/// implication/composition (see issue #10). Inferred entries have `derived: true`. +#[must_use] +pub fn parse_z_extensions_with_category_and_derived(isa: &str) -> Vec { + let mut z_exts = parse_z_extensions_with_category(isa); + let derived_names = compute_derived_extension_names(isa); + + for &(_pattern, name, desc, category) in Z_EXTENSIONS { + if derived_names.contains(name) && !z_exts.iter().any(|e| e.name.eq_ignore_ascii_case(name)) + { + z_exts.push(ExtensionInfo { + name: name.to_string(), + description: desc.to_string(), + category: category.to_string(), + supported: true, + derived: true, + }); + } + } + + z_exts +} + +/// Same as [`parse_s_extensions_with_category`], plus S-extensions inferred via +/// implication/composition (see issue #10). Inferred entries have `derived: true`. +#[must_use] +pub fn parse_s_extensions_with_category_and_derived(isa: &str) -> Vec { + let mut s_exts = parse_s_extensions_with_category(isa); + let derived_names = compute_derived_extension_names(isa); + + for &(_pattern, name, desc, category) in S_EXTENSIONS { + if derived_names.contains(name) && !s_exts.iter().any(|e| e.name.eq_ignore_ascii_case(name)) + { + s_exts.push(ExtensionInfo { + name: name.to_string(), + description: desc.to_string(), + category: category.to_string(), + supported: true, + derived: true, }); } } @@ -274,6 +404,7 @@ pub fn get_all_z_extensions_with_status(isa: &str) -> Vec { description: desc.to_string(), category: category.to_string(), supported, + derived: false, } }) .collect() @@ -293,6 +424,7 @@ pub fn get_all_s_extensions_with_status(isa: &str) -> Vec { description: desc.to_string(), category: category.to_string(), supported, + derived: false, } }) .collect() @@ -744,4 +876,110 @@ mod tests { assert!(detail.contains("Enabled")); assert!(!detail.contains("VLEN")); } + + // === Derived extensions (issue #10) === + // + // Real ISA strings from actual RISC-V boards, taken from external bug reports. + const ISA_ORANGEPI_RV2: &str = "rv64imafdcv_zicbom_zicboz_zicntr_zicond_zicsr_zifencei_zihintpause_zihpm_zfh_zfhmin_zca_zcd_zba_zbb_zbc_zbs_zkt_zve32f_zve32x_zve64d_zve64f_zve64x_zvfh_zvfhmin_zvkt_sscofpmf_sstc_svinval_svnapot_svpbmt"; + const ISA_MANGOPI_MQ_PRO: &str = "rv64imafdc"; + + #[test] + fn derived_m_implies_zmmul() { + let z_exts = parse_z_extensions_with_category_and_derived("rv64ima"); + let zmmul = z_exts.iter().find(|e| e.name == "Zmmul").unwrap(); + assert!(zmmul.derived); + } + + #[test] + fn derived_b_from_zba_zbb_zbs_composition() { + let std_exts = get_extensions_with_derived("rv64i_zba_zbb_zbs"); + let b = std_exts.iter().find(|e| e.name == "B").unwrap(); + assert!(b.derived); + + // Zba alone must not compose into B. + let std_exts_partial = get_extensions_with_derived("rv64i_zba"); + assert!(!std_exts_partial.iter().any(|e| e.name == "B")); + } + + #[test] + fn derived_v_transitively_reaches_zve32x() { + let z_exts = parse_z_extensions_with_category_and_derived("rv64imafdv"); + let zve32x = z_exts.iter().find(|e| e.name == "Zve32x").unwrap(); + assert!(zve32x.derived); + } + + #[test] + fn explicit_extension_stays_not_derived() { + // "zba" is spelled out directly in the ISA string, so it must never be marked + // derived even though it also participates in composing B. + let isa = "rv64i_zba_zbb_zbs"; + let z_exts = parse_z_extensions_with_category_and_derived(isa); + let zba = z_exts.iter().find(|e| e.name == "Zba").unwrap(); + assert!(!zba.derived); + } + + #[test] + fn composition_with_explicit_shorthand_does_not_mark_shorthand_derived() { + // When "b" is already spelled out in the ISA string, B itself must stay + // derived: false (it's explicit), while its components (not spelled out here) + // become derived: true. + let isa = "rv64ib"; + let std_exts = get_extensions_with_derived(isa); + let b = std_exts.iter().find(|e| e.name == "B").unwrap(); + assert!(!b.derived); + + let z_exts = parse_z_extensions_with_category_and_derived(isa); + for name in ["Zba", "Zbb", "Zbs"] { + let ext = z_exts.iter().find(|e| e.name == name).unwrap(); + assert!(ext.derived, "{name} should be derived from explicit B"); + } + } + + #[test] + fn regression_orangepi_rv2_spacemit_k1_shows_derived_b() { + // Reported in issue #10: Zba/Zbb/Zbs are present in cpuinfo but B was not shown. + let std_exts = get_extensions_with_derived(ISA_ORANGEPI_RV2); + let b = std_exts + .iter() + .find(|e| e.name == "B") + .expect("B should be present (derived) for Orange Pi RV2 / SpacemiT K1"); + assert!(b.derived); + } + + #[test] + fn regression_mangopi_mq_pro_d1_shows_derived_extensions() { + // Reported in issue #10: M is present in cpuinfo but Zmmul was not shown. + let z_exts = parse_z_extensions_with_category_and_derived(ISA_MANGOPI_MQ_PRO); + for name in ["Zmmul", "Zca", "Zicsr"] { + let ext = z_exts + .iter() + .find(|e| e.name == name) + .unwrap_or_else(|| panic!("{name} should be derived for MangoPi MQ-Pro / D1")); + assert!(ext.derived, "{name} should be marked derived"); + } + } + + #[test] + fn zihpm_implies_zicsr() { + // FeatureStdExtZihpm -> FeatureStdExtZicsr in LLVM's RISCVFeatures.td. + let z_exts = parse_z_extensions_with_category_and_derived("rv64i_zihpm"); + let zicsr = z_exts + .iter() + .find(|e| e.name == "Zicsr") + .expect("Zicsr should be derived from Zihpm alone"); + assert!(zicsr.derived); + } + + #[test] + fn orangepi_rv2_zihpm_does_not_spuriously_derive_anything_new() { + // Orange Pi RV2 already spells out zicsr directly, so adding the + // Zihpm -> Zicsr implication must not change its derived set: Zicsr was + // already derived via Zicntr -> Zicsr (and is also explicit here). + let z_exts = parse_z_extensions_with_category_and_derived(ISA_ORANGEPI_RV2); + let zicsr = z_exts.iter().find(|e| e.name == "Zicsr").unwrap(); + assert!( + !zicsr.derived, + "zicsr is spelled out explicitly in this ISA string" + ); + } } diff --git a/crates/riscfetch-core/src/types.rs b/crates/riscfetch-core/src/types.rs index 05af530..20b4a35 100644 --- a/crates/riscfetch-core/src/types.rs +++ b/crates/riscfetch-core/src/types.rs @@ -7,6 +7,10 @@ use serde::Serialize; pub struct ExtensionEntry { pub name: String, pub description: String, + /// `true` if this extension was not reported directly in the ISA string but was + /// inferred through implication or composition from other extensions that are + /// present (see issue #10 / `riscfetch_core::implications`). + pub derived: bool, } /// Hardware IDs from RISC-V CSRs