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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ target/
# built on demand rather than committed. The small packs under `infact-packs/`
# are the ones this repository's own tests and documentation depend on.
/generated-packs

# The standard library's catalog. Generated the same way, but 26 MB rather than
# the few hundred kilobytes a library takes, and reproducible in two seconds
# from a rustup component. `infact-packs/rust-std/README.md` is the command.
/infact-packs/rust-std/api/*.json
88 changes: 88 additions & 0 deletions crates/infact-catalog/src/rustdoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,67 @@ pub fn build_catalog(request: CatalogRequest<'_>) -> Result<ExternalCatalog> {
}
}

// Methods written on a type rather than in a trait. They are associated
// items, so the free-function pass below skips them, and they belong to no
// trait, so the pass above never saw them: `Vec::push` and `<[T]>::sorted`
// fell between the two and the catalog held only what a library declared in
// traits. For core that was five thousand methods, and for the standard
// library it is most of what anyone calls.
for item in index.values() {
let item = object(item, "item")?;
let Some(implementation) = item
.get("inner")
.and_then(Value::as_object)
.and_then(|inner| inner.get("impl"))
.and_then(Value::as_object)
else {
continue;
};
// A trait implementation's methods belong to the trait, which the pass
// above already catalogued from its declaration.
if implementation
.get("trait")
.is_some_and(|value| !value.is_null())
{
continue;
}
let Some(container) = implemented_type(implementation.get("for"), paths) else {
continue;
};
for method_id in implementation
.get("items")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
let Some(method) = method_id
.as_u64()
.and_then(|id| index.get(&id.to_string()))
.and_then(Value::as_object)
else {
continue;
};
let Some(function) = method
.get("inner")
.and_then(Value::as_object)
.and_then(|inner| inner.get("function"))
.and_then(Value::as_object)
else {
continue;
};
let Some(name) = string(method.get("name")) else {
continue;
};
callables.push(ExternalCallable {
path: format!("{container}::{name}"),
container: CallableContainer::Type {
path: container.clone(),
},
signature: Some(signature(function)?),
});
}
}

for (id, item) in index {
if associated_items.contains(id) {
continue;
Expand Down Expand Up @@ -318,6 +379,33 @@ fn type_arguments(value: Option<&Value>) -> Result<Vec<ExternalType>> {
.collect()
}

/// The name of the type an inherent implementation is written on.
///
/// A nominal type is named by the path the crate publishes it under. A built-in
/// one has no path to look up and is named the way the language names it, which
/// is also how its documentation is addressed: `slice`, `str`, `u32`. Anything
/// else — a raw pointer, a function type — has no name a caller would write a
/// method path with, and is left out rather than given an invented one.
fn implemented_type(value: Option<&Value>, paths: &Map<String, Value>) -> Option<String> {
let target = value?.as_object()?;
if let Some(resolved) = target.get("resolved_path").and_then(Value::as_object) {
let id = resolved.get("id")?.as_u64()?.to_string();
// The published path is what a caller writes. Falling back to the bare
// name would put two types with one name under a single heading.
return public_path(paths, &id);
}
if let Some(primitive) = target.get("primitive").and_then(Value::as_str) {
return Some(primitive.to_owned());
}
if target.contains_key("slice") {
return Some("slice".to_owned());
}
if target.contains_key("array") {
return Some("array".to_owned());
}
None
}

fn public_path(paths: &Map<String, Value>, id: &str) -> Option<String> {
paths
.get(id)?
Expand Down
28 changes: 26 additions & 2 deletions crates/infact-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,21 @@ pub struct ExternalCallable {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CallableContainer {
Trait { path: String },
Module { path: String },
Trait {
path: String,
},
Module {
path: String,
},
/// The type a method is written on, outside any trait.
///
/// `Vec::push` and `<[T]>::is_sorted` belong to no trait and no module, and
/// without this they belonged to nothing and were dropped. In core alone
/// that is 5,621 methods — most of what a caller of the standard library
/// actually calls.
Type {
path: String,
},
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
Expand Down Expand Up @@ -439,6 +452,14 @@ pub enum Condition {
/// A quadratic scan of four elements beats allocating a hash set. Which one
/// this is depends on the caller, and nothing in the callee says.
SmallInputsFavourTheCode,
/// The API and the code disagree where two elements are incomparable.
///
/// A hand-written sortedness check refuses only on a strict `a > b`; the
/// API accepts only on `a <= b`. Under a total order those are the same
/// question. Under a partial one they are not — two `f64` NaNs are neither
/// greater nor less than each other, so the loop calls the sequence sorted
/// and the API does not.
IncomparableElements,
}

impl std::fmt::Display for Condition {
Expand All @@ -458,6 +479,9 @@ impl std::fmt::Display for Condition {
Self::SmallInputsFavourTheCode => {
formatter.write_str("the code is faster at small sizes")
}
Self::IncomparableElements => formatter.write_str(
"two elements that cannot be compared are sorted to the code and not to the API",
),
}
}
}
Expand Down
82 changes: 82 additions & 0 deletions crates/infact-normalize/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,25 @@ pub enum Coverage {
/// [`Coverage::Once`] about anything order- or count-sensitive and exactly
/// as much about anything that is neither.
BothWays,
/// Each element with the one after it: the `windows(2)` walk.
///
/// A single loop, not a nested one, and the only coverage where `left` and
/// `right` are neighbours rather than an arbitrary pair. That makes it the
/// one that says something about ORDER: a test over adjacent pairs decides
/// whether the sequence is sorted, and the same test over every pair
/// decides something much stronger.
Adjacent,
}

/// Whether a position is one past a named one.
///
/// Canonicalized arithmetic puts the name first, but both orders are accepted
/// so that a rewrite does not depend on which way the ordering happened to fall.
fn is_next_position(form: &Form, index: u32) -> bool {
matches!(form, Form::Binary { operator, left, right }
if operator == "+"
&& ((**left == Form::Local(index) && **right == Form::Number("1".to_owned()))
|| (**right == Form::Local(index) && **left == Form::Number("1".to_owned()))))
}

/// Which way a walk runs.
Expand Down Expand Up @@ -1127,6 +1146,68 @@ impl Form {
Some(())
}

/// The largest binding number anything here introduces or mentions.
///
/// A rewrite that turns one name into two needs a number for the second,
/// and reusing one already in play would silently identify two different
/// values. Renaming makes the numbers canonical afterwards, so any unused
/// one will do.
fn highest_binding(&self) -> Option<u32> {
let here = match self {
Self::Local(index) => Some(*index),
_ => None,
};
self.children()
.into_iter()
.filter_map(Self::highest_binding)
.chain(here)
.max()
}

/// Whether a body reads a sequence only by indexing it at a position and
/// its successor.
///
/// The adjacent-pairs licence, and narrower than it looks: `v[i]` and
/// `v[i + 1]` are the only two readings allowed, so `v[i + 2]`, a bare `i`,
/// or any other use of `v` declines. Without that, a loop that happens to
/// read a neighbour among other things would be reported as a walk over
/// neighbours, which is not what it does.
fn adjacent_only(&self, sequence: &Self, index: u32) -> bool {
if let Self::Index {
sequence: indexed,
position,
} = self
&& indexed.as_ref() == sequence
&& (**position == Self::Local(index) || is_next_position(position, index))
{
return true;
}
if self == sequence || *self == Self::Local(index) {
return false;
}
self.children()
.into_iter()
.all(|child| child.adjacent_only(sequence, index))
}

/// The same body with each neighbour reading replaced by its element.
fn with_adjacent_elements(&self, sequence: &Self, index: u32, successor: u32) -> Self {
if let Self::Index {
sequence: indexed,
position,
} = self
&& indexed.as_ref() == sequence
{
if **position == Self::Local(index) {
return Self::Local(index);
}
if is_next_position(position, index) {
return Self::Local(successor);
}
}
self.map_children(&|child| child.with_adjacent_elements(sequence, index, successor))
}

/// Whether a body reads a sequence only by indexing it at named positions.
///
/// This is the licence to forget the index. `for i in 0..v.len()` visits
Expand Down Expand Up @@ -1289,6 +1370,7 @@ impl Display for Form {
let kind = match coverage {
Coverage::Once => "pairwise",
Coverage::BothWays => "pairwise-both-ways",
Coverage::Adjacent => "pairwise-adjacent",
};
write!(formatter, "({kind} {sequence} {left} {right} {body})")
}
Expand Down
64 changes: 64 additions & 0 deletions crates/infact-normalize/src/simplify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ impl Form {
.or_else(|| rebuilt.as_canonical_arithmetic())
.or_else(|| rebuilt.as_element_traversal())
.or_else(|| rebuilt.as_pairwise())
.or_else(|| rebuilt.as_adjacent_pairwise())
.or_else(|| rebuilt.as_recovered_escape())
.or_else(|| rebuilt.as_unfolded(fuel))
.unwrap_or(rebuilt)
Expand Down Expand Up @@ -479,6 +480,51 @@ impl Form {
.or_else(|| Self::as_enumerated_pairwise(outer, first, inner, second, inner_body))
}

/// A single loop reading each element and the one after it.
///
/// `for i in 0..v.len() - 1 { .. v[i] .. v[i + 1] .. }` is the `windows(2)`
/// walk written out, and unlike the other two coverages it is one loop
/// rather than two. The bound has to stop one short, because that is what
/// keeps `v[i + 1]` inside the sequence; a loop that ran to the end would
/// be a different walk, and in Rust a panicking one.
///
/// The second element needs a name the body is not already using, since the
/// code has only one index where the form has two elements.
fn as_adjacent_pairwise(&self) -> Option<Self> {
let Self::Traverse {
sequence,
item,
body,
direction: Direction::Forward,
} = self
else {
return None;
};
let Pattern::Binding(index) = item.as_ref() else {
return None;
};
let (start, end) = counting_span(sequence)?;
if *start != Self::Number("0".to_owned()) {
return None;
}
// `0..n - 1` reads positions up to `n`, because the last step reads its
// neighbour. That is the extent, and it is why this cannot reuse the
// bound as written.
let extent = one_more_than(end)?;
let source = body.sole_indexed_sequence(&[*index])?;
if !body.adjacent_only(source, *index) || body.writes_indexed(source) {
return None;
}
let successor = self.highest_binding().map_or(0, |highest| highest + 1);
Some(Self::Pairwise {
sequence: Box::new(walked_sequence(start, &extent, source)),
left: item.clone(),
right: Box::new(Pattern::Binding(successor)),
body: Box::new(body.with_adjacent_elements(source, *index, successor)),
coverage: Coverage::Adjacent,
})
}

/// The square spelling: two loops over the whole range, minus the diagonal.
///
/// `for i in 0..n { for j in 0..n { if i != j { .. } } }` reaches each pair
Expand Down Expand Up @@ -1020,6 +1066,24 @@ fn names_both_positions(first: &Form, second: &Form, left: u32, right: u32) -> b
|| (*first == Form::Local(right) && *second == Form::Local(left))
}

/// The bound a loop would have had if it did not stop one short.
///
/// `n - 1` reads up to `n`. A length that is already a call — `v.len() - 1` —
/// gives back `v.len()`, which is what makes the extent the whole sequence
/// rather than a slice of it.
fn one_more_than(form: &Form) -> Option<Form> {
match form {
Form::Binary {
operator,
left,
right,
} if operator == "-" && **right == Form::Number("1".to_owned()) => {
Some(left.as_ref().clone())
}
_ => None,
}
}

/// Whether a form is one less than another.
fn is_predecessor_of(form: &Form, of: &Form) -> bool {
matches!(form, Form::Binary { operator, left, right }
Expand Down
Loading