ship one record per function: merge the release set, gen reads it, descriptions as doc comments, gates for what was only claimed; v3.0
This commit is contained in:
parent
71ce34edd2
commit
3410a79b6a
28 changed files with 30596 additions and 955 deletions
|
|
@ -17,13 +17,14 @@ use serde::Deserialize;
|
|||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::Path;
|
||||
|
||||
/// The provenance the deriver stamps on a name it read out of Valve's entity-IO datadesc. Kept in step
|
||||
/// with `pipeline::VALVE_DATADESC` — the two halves of one fact: which names the datadesc named, and
|
||||
/// what the engine's dispatch contract therefore says about them.
|
||||
const VALVE_DATADESC: &str = "valve-datadesc";
|
||||
// The provenance ids this module READS are the ones the pipeline STAMPS, imported rather than re-spelled:
|
||||
// they are one fact — which evidence named the function — and two copies of it "kept in step" by a
|
||||
// comment is an invariant nothing enforces. A drift there would silently stop matching, and a prototype
|
||||
// that stops matching does not fail; it simply stops being claimed.
|
||||
use crate::pipeline::{VALVE_CONCOMMAND, VALVE_DATADESC, VALVE_VSCRIPT};
|
||||
|
||||
/// What the manifest calls a prototype that came from how the ENGINE invokes the function rather than
|
||||
/// from anyone's declaration of it.
|
||||
/// from anyone's declaration of it. Declared here because only this module states it.
|
||||
const ENGINE_CONTRACT: &str = "engine-contract";
|
||||
|
||||
/// The prototype the engine invokes EVERY entity-IO handler through. Kept in step with
|
||||
|
|
@ -31,10 +32,6 @@ const ENGINE_CONTRACT: &str = "engine-contract";
|
|||
/// derive as a standing oracle — two halves of one fact, one asserting it and one checking it.
|
||||
const ENGINE_CONTRACT_PARAMS: [&str; 2] = ["CEntityInstance*", "InputData_t&"];
|
||||
|
||||
/// The provenance prefix a console-command handler ships under, `:<form>`-suffixed. Kept in step with
|
||||
/// `pipeline::VALVE_CONCOMMAND`.
|
||||
const VALVE_CONCOMMAND: &str = "valve-concommand";
|
||||
|
||||
/// What the engine passes EVERY console-command callback, whatever form it takes.
|
||||
const CONCOMMAND_PARAMS: [&str; 2] = ["CCommandContext*", "CCommand*"];
|
||||
|
||||
|
|
@ -72,6 +69,62 @@ fn concommand_contract(source: &str) -> Option<Vec<String>> {
|
|||
)
|
||||
}
|
||||
|
||||
/// Read the DIRECTION of a footprint disagreement, and decide what it means.
|
||||
///
|
||||
/// Split out because it is the whole content of the `mismatch` verdict, and it has to be callable: the
|
||||
/// tests used to re-implement this rule rather than call it, so the regression guard could only fail if
|
||||
/// someone edited both copies the same wrong way. One definition, two callers.
|
||||
///
|
||||
/// Only an over-READ refutes a declaration. `declared_over` alone is the documented LOWER-BOUND case —
|
||||
/// calling through it loads a register nobody reads, which is safe — while `measured_over` means the
|
||||
/// callee reads a register the declaration never mentions, which is not. `both` stays a mismatch: a
|
||||
/// class where the callee reads more is unsafe regardless of another class where it reads fewer. 81 of
|
||||
/// CS2's 140 former mismatches were the safe direction, reported as "does not describe this build".
|
||||
fn adjudicate_mismatch(
|
||||
chosen: &Candidate,
|
||||
s: &model::AbiShape,
|
||||
types: Option<&BTreeMap<String, model::TypeLayout>>,
|
||||
) -> (model::AbiStatus, &'static str) {
|
||||
let (i, f) = footprint(chosen.params, types);
|
||||
// The direction has to be read through the SAME allowance the verdict was, or the invisible `this`
|
||||
// reads as an over-count on its own: `CGameEvent::GetFloat` is declared `(char const*, float)` and
|
||||
// measures `int=2 float=0`, where the extra integer register is the receiver and the only real
|
||||
// disagreement is the float.
|
||||
let i = if chosen.complete {
|
||||
i
|
||||
} else {
|
||||
(i..=i + 1)
|
||||
.min_by_key(|d| d.abs_diff(s.int as usize))
|
||||
.expect("the range always has two elements")
|
||||
};
|
||||
let (i, f) = (i.min(6), f.min(8));
|
||||
let measured_over = s.int as usize > i || s.float as usize > f;
|
||||
let declared_over = i > s.int as usize || f > s.float as usize;
|
||||
let status = if declared_over && !measured_over {
|
||||
model::AbiStatus::LowerBound
|
||||
} else {
|
||||
model::AbiStatus::Mismatch
|
||||
};
|
||||
let note = match (measured_over, declared_over) {
|
||||
(true, true) => {
|
||||
"measured and declared footprints disagree in BOTH directions, in different register \
|
||||
classes: the callee reads a register the declaration does not mention AND the declaration \
|
||||
passes one the callee never reads"
|
||||
}
|
||||
(false, true) => {
|
||||
"the declaration passes registers the callee never reads, and contradicts it in no register \
|
||||
class — the measured footprint is a documented LOWER bound, so this is expected rather than \
|
||||
evidence against the declaration"
|
||||
}
|
||||
(true, false) => {
|
||||
"measured footprint EXCEEDS declared: the callee reads a register the declaration does not \
|
||||
mention, so this declaration does not describe this build"
|
||||
}
|
||||
_ => "the footprints disagree in neither direction, which a mismatch cannot be",
|
||||
};
|
||||
(status, note)
|
||||
}
|
||||
|
||||
/// One declared prototype as the frozen input records it.
|
||||
#[derive(Deserialize)]
|
||||
struct Decl {
|
||||
|
|
@ -288,6 +341,7 @@ pub fn build_manifest(
|
|||
prototypes: &Path,
|
||||
mono: &model::Monolith,
|
||||
types: Option<&BTreeMap<String, model::TypeLayout>>,
|
||||
vscript_ret: Option<&BTreeMap<String, String>>,
|
||||
) -> Result<model::AbiManifest> {
|
||||
let doc: PrototypeDoc = serde_json::from_str(
|
||||
&std::fs::read_to_string(prototypes)
|
||||
|
|
@ -397,7 +451,22 @@ pub fn build_manifest(
|
|||
.and_then(|e| e.locator.offset);
|
||||
|
||||
let decls: &[Decl] = exact.or(by_bare_hit).map_or(&[][..], |v| v.as_slice());
|
||||
if decls.is_empty() && !is_contract {
|
||||
|
||||
// The SCRIPT VM'S OWN declared return, for a name the registry states. It ranks above the
|
||||
// measured register class for the reason spelled out below: a callee cannot tell whether its
|
||||
// caller reads RAX, so measurement is wrong about known-void functions roughly seven times
|
||||
// in eight — and `void` is what the registry declares for 849 of Dota's bindings, which is
|
||||
// exactly the population measurement gets wrong. It ranks BELOW a real declaration only to
|
||||
// keep "a source wrote this down" ahead of anything derived; in practice the two never
|
||||
// compete, because no VScript name is also a declared name (measured: zero overlap).
|
||||
//
|
||||
// Read BEFORE the gate below, not after: a registry-declared return is on its own enough to
|
||||
// have something to say about a function, so a name carrying one must not be skipped for
|
||||
// having no parameter list. That is precisely the `return-only` case.
|
||||
let vs_ret = vscript_ret.and_then(|m| m.get(name)).cloned();
|
||||
let has_vs_ret = vs_ret.is_some();
|
||||
|
||||
if decls.is_empty() && !is_contract && !has_vs_ret {
|
||||
bump(&format!("{tier}:none"));
|
||||
continue;
|
||||
}
|
||||
|
|
@ -413,6 +482,7 @@ pub fn build_manifest(
|
|||
any_ret
|
||||
.clone()
|
||||
.or(contract)
|
||||
.or(vs_ret)
|
||||
.or_else(|| sh.map(|s| s.ret.clone()))
|
||||
};
|
||||
let mut provenance: Vec<String> = decls
|
||||
|
|
@ -424,6 +494,9 @@ pub fn build_manifest(
|
|||
if is_contract {
|
||||
provenance.push(ENGINE_CONTRACT.to_string());
|
||||
}
|
||||
if has_vs_ret {
|
||||
provenance.push(VALVE_VSCRIPT.to_string());
|
||||
}
|
||||
|
||||
// The contract goes in FIRST, so that where it and a declaration both fit the measurement,
|
||||
// `most_specific` reports the one that names its receiver — which the contract always does
|
||||
|
|
@ -440,8 +513,14 @@ pub fn build_manifest(
|
|||
});
|
||||
}
|
||||
collect_candidates(decls, &mut cands);
|
||||
// `bare-name` is a CLAIM — "one declaration bears this method name and the measurement could
|
||||
// adjudicate" — so it must not be the fallback for an entry that was never name-matched at
|
||||
// all. A registry-declared return with no declaration behind it is neither exact nor
|
||||
// bare-name; it is the script VM stating its own contract, and it says so.
|
||||
let matched_by = if exact.is_some() {
|
||||
"exact"
|
||||
} else if decls.is_empty() && has_vs_ret {
|
||||
VALVE_VSCRIPT
|
||||
} else {
|
||||
"bare-name"
|
||||
};
|
||||
|
|
@ -567,46 +646,9 @@ pub fn build_manifest(
|
|||
// former mismatches were the former, reported as "does not describe this build".
|
||||
if status == model::AbiStatus::Mismatch {
|
||||
let s = sh.expect("a mismatch is only reachable with a measurement");
|
||||
let (i, f) = footprint(chosen.params, types);
|
||||
// The direction has to be read through the SAME allowance the verdict was, or the
|
||||
// invisible `this` reads as an over-count on its own: `CGameEvent::GetFloat` is
|
||||
// declared `(char const*, float)` and measures `int=2 float=0`, where the extra
|
||||
// integer register is the receiver and the only real disagreement is the float.
|
||||
let i = if chosen.complete {
|
||||
i
|
||||
} else {
|
||||
(i..=i + 1)
|
||||
.min_by_key(|d| d.abs_diff(s.int as usize))
|
||||
.expect("the range always has two elements")
|
||||
};
|
||||
let (i, f) = (i.min(6), f.min(8));
|
||||
let measured_over = s.int as usize > i || s.float as usize > f;
|
||||
let declared_over = i > s.int as usize || f > s.float as usize;
|
||||
// Only an over-read refutes the declaration. `both` stays a mismatch: a class where the
|
||||
// callee reads more is unsafe regardless of another class where it reads fewer.
|
||||
if declared_over && !measured_over {
|
||||
status = model::AbiStatus::LowerBound;
|
||||
}
|
||||
note = Some(
|
||||
match (measured_over, declared_over) {
|
||||
(true, true) => {
|
||||
"measured and declared footprints disagree in BOTH directions, in different \
|
||||
register classes: the callee reads a register the declaration does not \
|
||||
mention AND the declaration passes one the callee never reads"
|
||||
}
|
||||
(false, true) => {
|
||||
"the declaration passes registers the callee never reads, and contradicts it \
|
||||
in no register class — the measured footprint is a documented LOWER bound, \
|
||||
so this is expected rather than evidence against the declaration"
|
||||
}
|
||||
(true, false) => {
|
||||
"measured footprint EXCEEDS declared: the callee reads a register the \
|
||||
declaration does not mention, so this declaration does not describe this build"
|
||||
}
|
||||
_ => "the footprints disagree in neither direction, which a mismatch cannot be",
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
let (verdict, why) = adjudicate_mismatch(&chosen, s, types);
|
||||
status = verdict;
|
||||
note = Some(why.to_string());
|
||||
}
|
||||
// A BARE-NAME claim that the measurement CONTRADICTS is withdrawn, not reported. The gate
|
||||
// admits a bare name only when a measurement exists to adjudicate it — and adjudicating
|
||||
|
|
@ -645,6 +687,9 @@ pub fn build_manifest(
|
|||
note,
|
||||
overloads: (cands.len() > 1).then_some(all_sigs),
|
||||
vtable,
|
||||
// Prose belongs to the function record, not to a prototype; the merge attaches it
|
||||
// there and `Rosetta::abi_manifest` joins it back on for the emitters.
|
||||
doc: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -834,36 +879,28 @@ mod tests {
|
|||
}
|
||||
|
||||
/// The verdict AND the note, for one declaration against one measurement.
|
||||
/// CALLS the shipped rule rather than restating it. It used to re-implement `build_manifest`'s
|
||||
/// direction logic, which made the assertions below unfalsifiable: only an edit that changed both
|
||||
/// copies the same wrong way could fail them, and that is the one edit nobody makes by accident.
|
||||
/// The direction is read back out of the shipped note text, so the mapping from direction to prose
|
||||
/// is under test too.
|
||||
fn judge(params: &[&str], sh: &model::AbiShape, complete: bool) -> (model::AbiStatus, String) {
|
||||
let ps = p(params);
|
||||
let c = cand(&ps, complete);
|
||||
if agrees(&c, sh, None) {
|
||||
return (model::AbiStatus::Verified, String::new());
|
||||
}
|
||||
let (i, f) = footprint(c.params, None);
|
||||
let i = if complete {
|
||||
i
|
||||
let (status, note) = adjudicate_mismatch(&c, sh, None);
|
||||
let direction = if note.starts_with("measured and declared") {
|
||||
"both"
|
||||
} else if note.starts_with("measured footprint EXCEEDS") {
|
||||
"measured-exceeds"
|
||||
} else if note.starts_with("the declaration passes") {
|
||||
"declared-exceeds"
|
||||
} else {
|
||||
(i..=i + 1)
|
||||
.min_by_key(|d| d.abs_diff(sh.int as usize))
|
||||
.unwrap()
|
||||
"neither"
|
||||
};
|
||||
let (i, f) = (i.min(6), f.min(8));
|
||||
let over = sh.int as usize > i || sh.float as usize > f;
|
||||
let under = i > sh.int as usize || f > sh.float as usize;
|
||||
(
|
||||
if under && !over {
|
||||
model::AbiStatus::LowerBound
|
||||
} else {
|
||||
model::AbiStatus::Mismatch
|
||||
},
|
||||
match (over, under) {
|
||||
(true, true) => "both",
|
||||
(true, false) => "measured-exceeds",
|
||||
_ => "declared-exceeds",
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
(status, direction.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue