Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,21 @@ jobs:
curl --proto '=https' --tlsv1.2 -LsSf \
"https://github.com/astral-sh/hawk/releases/download/${HAWK_VERSION}/cargo-hawk-installer.sh" | sh
- name: "Hawk"
run: cargo +1.99.0 hawk check --target-dir target/hawk --color=always -D warnings
# The fork SPO/transcode libraries are an external API: OGAR and the
# transcode consumers call them from outside this workspace, which a
# closed-world analysis cannot see. Hawk compiles them but does not
# report their public declarations.
run: >-
cargo +1.99.0 hawk check --target-dir target/hawk --color=always -D warnings
--exclude-crate ruff_cpp_codegen
--exclude-crate ruff_cpp_spo
--exclude-crate ruff_csharp_spo
--exclude-crate ruff_python_dto_check
--exclude-crate ruff_python_spo
--exclude-crate ruff_ruby_spo
--exclude-crate ruff_spo_address
--exclude-crate ruff_spo_triplet
--exclude-crate ruff_sqlalchemy_spo

cargo-publish-dry-run:
name: "cargo publish dry-run"
Expand Down Expand Up @@ -656,6 +670,54 @@ jobs:
MSRV: ${{ steps.msrv.outputs.value }}
run: cargo "+${MSRV}" test --no-run --all-features

cargo-r2il:
# `ruff_r2il` (fork-only) is excluded from the workspace because it path-depends
# on the `r2sleigh` and `a2ui-rs` sibling checkouts, so the workspace-wide fmt,
# clippy and test jobs never reach it. This job checks out both siblings next to
# this repository, matching the `../../../<sibling>` paths in its Cargo.toml.
name: "cargo (ruff_r2il)"
runs-on: ubuntu-latest
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 30
env:
CARGO_PROFILE_DEV_DEBUG: line-tables-only
R2IL_MANIFEST: crates/ruff_r2il/Cargo.toml
defaults:
run:
working-directory: ruff
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: ruff
persist-credentials: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: AdaWorldAPI/r2sleigh
path: r2sleigh
persist-credentials: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: AdaWorldAPI/a2ui-rs
path: a2ui-rs
persist-credentials: false
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: ruff/crates/ruff_r2il
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain"
run: rustup component add rustfmt clippy
- name: "Install mold"
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1
- name: "cargo fmt"
run: cargo fmt --manifest-path "${R2IL_MANIFEST}" --check
- name: "cargo clippy"
run: cargo clippy --manifest-path "${R2IL_MANIFEST}" --all-targets --locked -- -D warnings
- name: "cargo clippy (lift)"
run: cargo clippy --manifest-path "${R2IL_MANIFEST}" --features lift --all-targets --locked -- -D warnings
- name: "cargo test"
run: cargo test --manifest-path "${R2IL_MANIFEST}" --locked

cargo-fuzz-build:
name: "cargo fuzz build"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'github-ubuntu-24.04-x86_64-4' || 'ubuntu-latest' }}
Expand Down Expand Up @@ -1537,6 +1599,7 @@ jobs:
- cargo-test-linux
- cargo-test-wasm
- cargo-build-msrv
- cargo-r2il
- shellcheck
- scripts
- prek
Expand Down
28 changes: 26 additions & 2 deletions crates/ruff_r2il/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions crates/ruff_r2il/examples/r2il_corpus_profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,7 @@ struct Pass2Stats {
vocab_interned_id_bytes: Vec<usize>,
facet_ok: usize,
facet_unknown_custom_space: usize,
facet_unresolved_space: usize,
facet_ordinal_exhausted: usize,
smelt_harvested: usize,
smelt_classified: usize,
Expand Down Expand Up @@ -723,12 +724,15 @@ fn print_pass2(stats: &Pass2Stats) {
print_distribution("vocab.ssa_name_bytes", &stats.vocab_ssa_name_bytes);
print_distribution("vocab.interned_id_bytes", &stats.vocab_interned_id_bytes);

let facet_total =
stats.facet_ok + stats.facet_unknown_custom_space + stats.facet_ordinal_exhausted;
let facet_total = stats.facet_ok
+ stats.facet_unknown_custom_space
+ stats.facet_unresolved_space
+ stats.facet_ordinal_exhausted;
println!(
" facet::project sweep: ok={} FacetOverflow(UnknownCustomSpace)={} FacetOverflow(CustomOrdinalExhausted)={} total={}",
" facet::project sweep: ok={} FacetOverflow(UnknownCustomSpace)={} FacetOverflow(UnresolvedSpace)={} FacetOverflow(CustomOrdinalExhausted)={} total={}",
stats.facet_ok,
stats.facet_unknown_custom_space,
stats.facet_unresolved_space,
stats.facet_ordinal_exhausted,
facet_total
);
Expand Down Expand Up @@ -932,6 +936,9 @@ fn run_pass2_over(
Err(facet::FacetOverflow::UnknownCustomSpace { .. }) => {
stats.facet_unknown_custom_space += 1;
}
Err(facet::FacetOverflow::UnresolvedSpace) => {
stats.facet_unresolved_space += 1;
}
Err(facet::FacetOverflow::CustomOrdinalExhausted { .. }) => {
stats.facet_ordinal_exhausted += 1;
}
Expand Down
27 changes: 27 additions & 0 deletions crates/ruff_r2il/src/facet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ pub enum FacetOverflow {
/// back to `raw as u16` (`65541` and `5` both truncate to `5`, silently colliding two
/// distinct architecture-defined spaces).
UnknownCustomSpace { raw: u32 },
/// The varnode's space is `SpaceId::Unresolved`: the lifter could not name it, so it has
/// no stable discriminant. Projection REFUSES rather than assign one, because every
/// unresolved space compares equal and a shared discriminant would merge spaces that may
/// be distinct.
UnresolvedSpace,
/// The set of distinct custom-space raw ids exceeds [`MAX_CUSTOM_ORDINAL`]. Overflow is
/// EVIDENCE a route needs factoring, never silent truncation (the `mint_factored`
/// principle).
Expand All @@ -137,6 +142,12 @@ impl core::fmt::Display for FacetOverflow {
FacetOverflow::UnknownCustomSpace { raw } => {
write!(f, "custom space {raw} is not in the interned table")
}
FacetOverflow::UnresolvedSpace => {
write!(
f,
"the varnode's address space was not resolved by the lifter"
)
}
FacetOverflow::CustomOrdinalExhausted { count } => {
write!(
f,
Expand Down Expand Up @@ -228,6 +239,7 @@ pub fn project(vn: &Varnode, spaces: &CustomSpaceTable) -> Result<VarnodeFacet,
SpaceId::Custom(raw) => spaces
.ordinal_of(raw)
.ok_or(FacetOverflow::UnknownCustomSpace { raw })?,
SpaceId::Unresolved => return Err(FacetOverflow::UnresolvedSpace),
};
let classid = ((PROVISIONAL_R2IL_VARNODE as u32) << 16) | discriminant as u32;
let offset_lo = (vn.offset & 0xFFFF_FFFF) as u32;
Expand Down Expand Up @@ -305,6 +317,21 @@ mod tests {
assert_eq!(back, vn);
}

#[test]
fn unresolved_space_is_refused_rather_than_given_a_discriminant() {
// Every unresolved space compares equal, so no table, however populated, can
// give one a discriminant without risking a merge of two different spaces.
let table = CustomSpaceTable::from_ids([0, 5]).expect("2 ids is within budget");
let vn = Varnode::new(SpaceId::Unresolved, 0x10, 4);
assert_eq!(project(&vn, &table), Err(FacetOverflow::UnresolvedSpace));
assert_eq!(
project(&vn, &CustomSpaceTable::default()),
Err(FacetOverflow::UnresolvedSpace)
);
// The same offset and size in a named space still projects.
assert!(project(&Varnode::new(SpaceId::Ram, 0x10, 4), &table).is_ok());
}

#[test]
fn custom_space_outside_the_table_errors_and_never_truncates() {
let empty = CustomSpaceTable::default();
Expand Down
12 changes: 12 additions & 0 deletions crates/ruff_r2il/src/furnace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,18 @@ pub fn smelt(
prov,
);
}
Err(FacetOverflow::UnresolvedSpace) => {
// Same fallback as an unknown custom space: the residual is anchored
// at the enclosing op's block so it still carries an address.
let at = any_anchor_facet(behavior, prov, conv);
push_named_residual(
&mut ledger,
ResidualReason::UnresolvedSpace,
at,
conv,
prov,
);
}
Err(FacetOverflow::CustomOrdinalExhausted { count }) => {
// Structurally unreachable for a validly-constructed `CustomSpaceTable`
// (the budget is enforced at `from_ids`/`from_arch` time) — handled
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_r2il/src/oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ pub fn permissive_convention(blocks: &[R2ILBlock]) -> Result<R2ilConvention, Fac
valid_ranges: Vec::new(),
bank_id: None,
segment_id: None,
aliases: None,
});
}

Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_r2il/src/sink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ fn reason_payload_cols(r: &ResidualReason) -> (String, String) {
(expected.as_str().to_string(), found.as_str().to_string())
}
ResidualReason::NoConventionRowAtAddress
| ResidualReason::UnresolvedSpace
| ResidualReason::MemoryObjectEscaped
| ResidualReason::IndirectTarget
| ResidualReason::NoFacetCoordinate => (String::new(), String::new()),
Expand Down Expand Up @@ -526,6 +527,7 @@ fn parse_reason(name: &str, p1: &str, p2: &str) -> io::Result<ResidualReason> {
"custom_space_not_in_convention" => ResidualReason::CustomSpaceNotInConvention {
raw: parse_u32(p1)?,
},
"unresolved_space" => ResidualReason::UnresolvedSpace,
"facet_overflow_at_key" => ResidualReason::FacetOverflowAtKey {
raw: parse_u32(p1)?,
},
Expand Down
13 changes: 11 additions & 2 deletions crates/ruff_r2il/src/slag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ pub enum ResidualReason {
/// The varnode's `SpaceId::Custom(raw)` is not in the convention's
/// [`crate::facet::CustomSpaceTable`].
CustomSpaceNotInConvention { raw: u32 },
/// The varnode's space is `SpaceId::Unresolved`: the lifter could not name it,
/// so no stable facet coordinate exists for it. Every such varnode compares
/// equal, so there is no payload to distinguish them.
UnresolvedSpace,
/// The `Custom(raw)` space exists in the arch data but exceeds the
/// interned-ordinal budget at CONFIG-KEY construction time (§4's
/// promoted falsifier) — a typed overflow, never a silent truncation.
Expand Down Expand Up @@ -126,6 +130,7 @@ impl ResidualReason {
"no_convention_row_at_address",
"userop_not_in_convention",
"custom_space_not_in_convention",
"unresolved_space",
"facet_overflow_at_key",
"variadic_arity",
"phi_fan_in_exceeds_predecessors",
Expand All @@ -143,6 +148,7 @@ impl ResidualReason {
ResidualReason::NoConventionRowAtAddress => "no_convention_row_at_address",
ResidualReason::UserOpNotInConvention { .. } => "userop_not_in_convention",
ResidualReason::CustomSpaceNotInConvention { .. } => "custom_space_not_in_convention",
ResidualReason::UnresolvedSpace => "unresolved_space",
ResidualReason::FacetOverflowAtKey { .. } => "facet_overflow_at_key",
ResidualReason::VariadicArity { .. } => "variadic_arity",
ResidualReason::PhiFanInExceedsPredecessors { .. } => "phi_fan_in_exceeds_predecessors",
Expand Down Expand Up @@ -172,6 +178,7 @@ impl ResidualReason {
ResidualReason::CustomSpaceNotInConvention { raw } => {
bytes.extend_from_slice(&raw.to_le_bytes());
}
ResidualReason::UnresolvedSpace => {}
ResidualReason::FacetOverflowAtKey { raw } => {
bytes.extend_from_slice(&raw.to_le_bytes());
}
Expand Down Expand Up @@ -463,13 +470,14 @@ mod tests {
// `ResidualReason`, this fails to COMPILE until this test (and
// `ResidualReason::ALL`) are updated to match. That non-compiling
// failure mode is the falsifier for "no catch-all reason".
let sample: [ResidualReason; 11] = [
let sample: [ResidualReason; 12] = [
ResidualReason::OpcodeNotInConvention {
opcode: OpTag::Copy,
},
ResidualReason::NoConventionRowAtAddress,
ResidualReason::UserOpNotInConvention { userop: 0 },
ResidualReason::CustomSpaceNotInConvention { raw: 0 },
ResidualReason::UnresolvedSpace,
ResidualReason::FacetOverflowAtKey { raw: 0 },
ResidualReason::VariadicArity { arity: 0 },
ResidualReason::PhiFanInExceedsPredecessors {
Expand All @@ -492,6 +500,7 @@ mod tests {
ResidualReason::NoConventionRowAtAddress => 1,
ResidualReason::UserOpNotInConvention { .. } => 1,
ResidualReason::CustomSpaceNotInConvention { .. } => 1,
ResidualReason::UnresolvedSpace => 1,
ResidualReason::FacetOverflowAtKey { .. } => 1,
ResidualReason::VariadicArity { .. } => 1,
ResidualReason::PhiFanInExceedsPredecessors { .. } => 1,
Expand All @@ -502,7 +511,7 @@ mod tests {
// Deliberately NO `_ =>` arm.
};
}
assert_eq!(variant_count, 11);
assert_eq!(variant_count, 12);
assert_eq!(ResidualReason::ALL.len(), variant_count);

let mut sorted = ResidualReason::ALL.to_vec();
Expand Down
Loading