diff --git a/crates/infact-normalize/src/lib.rs b/crates/infact-normalize/src/lib.rs index 079dc3e..de51cfa 100644 --- a/crates/infact-normalize/src/lib.rs +++ b/crates/infact-normalize/src/lib.rs @@ -233,6 +233,32 @@ pub enum Form { /// lets a reader of the form tell which they have. coverage: Coverage, }, + /// Repeating a body for as long as a condition holds. + /// + /// A `while` and a `loop` are one construct: the second is the first with + /// the condition written inside as a `break`. Held as syntax they were two, + /// and neither described work, so a library function that loops this way + /// yielded no behavior at all. + /// + /// Distinct from [`Form::Traverse`], which walks something. A repetition + /// has no sequence — what it visits, if anything, is whatever its body + /// advances. Where that is a counted index, simplification turns the whole + /// thing into the traversal it is written to be. + Repeat { + condition: Box
, + body: Box, + }, + /// Exchanging two of a sequence's elements. + /// + /// `v.swap(i, j)` and the three-line dance through a temporary are the same + /// operation, and held apart they share no subterm. This is the operation + /// every naive sort is built from, so a form that cannot say it cannot say + /// anything about one. + Swap { + sequence: Box, + left: Box, + right: Box, + }, /// Producing a new sequence by transforming each element. Transform { sequence: Box, @@ -480,6 +506,12 @@ impl Form { } => vec![sequence.as_ref(), initial.as_ref(), body.as_ref()], Self::Collect { sequence, .. } => vec![sequence.as_ref()], Self::Assign { target, value, .. } => vec![target.as_ref(), value.as_ref()], + Self::Repeat { condition, body } => vec![condition.as_ref(), body.as_ref()], + Self::Swap { + sequence, + left, + right, + } => vec![sequence.as_ref(), left.as_ref(), right.as_ref()], Self::Binary { left, right, .. } => vec![left.as_ref(), right.as_ref()], Self::Unary { value, .. } => vec![value.as_ref()], Self::Index { sequence, position } => vec![sequence.as_ref(), position.as_ref()], @@ -667,6 +699,19 @@ impl Form { left: apply(left), right: apply(right), }, + Self::Repeat { condition, body } => Self::Repeat { + condition: apply(condition), + body: apply(body), + }, + Self::Swap { + sequence, + left, + right, + } => Self::Swap { + sequence: apply(sequence), + left: apply(left), + right: apply(right), + }, Self::Unary { operator, value } => Self::Unary { operator: operator.clone(), value: apply(value), @@ -740,7 +785,7 @@ impl Form { // Indexing names an operation the way a method call does. A span // names only a shape, so like a traversal it anchors nothing of its // own and is counted through its endpoints. - Self::Index { .. } => 1, + Self::Index { .. } | Self::Swap { .. } => 1, Self::Collect { container, .. } => u32::from(container.is_some()), // what the arms name is the whole content of a decision Self::Select { arms, .. } => arms.iter().map(|arm| arm.pattern.anchors()).sum(), @@ -1032,6 +1077,8 @@ impl Form { | Self::Retain { .. } | Self::Accumulate { .. } | Self::Pairwise { .. } + | Self::Repeat { .. } + | Self::Swap { .. } | Self::Collect { .. } => true, _ => self.children().into_iter().any(Self::describes_work), } @@ -1402,6 +1449,12 @@ impl Display for Form { right, } => write!(formatter, "(binary {operator} {left} {right})"), Self::Unary { operator, value } => write!(formatter, "(unary {operator} {value})"), + Self::Repeat { condition, body } => write!(formatter, "(repeat {condition} {body})"), + Self::Swap { + sequence, + left, + right, + } => write!(formatter, "(swap {sequence} {left} {right})"), Self::Index { sequence, position } => { write!(formatter, "(index {sequence} {position})") } diff --git a/crates/infact-normalize/src/matching.rs b/crates/infact-normalize/src/matching.rs index 1a20db5..305230d 100644 --- a/crates/infact-normalize/src/matching.rs +++ b/crates/infact-normalize/src/matching.rs @@ -655,6 +655,36 @@ impl Bindings { } (Form::Return(subject), Form::Return(pattern)) => self.form(subject, pattern), (Form::Sequence(subject), Form::Sequence(pattern)) => self.all(subject, pattern), + ( + Form::Repeat { + condition: subject_condition, + body: subject_body, + }, + Form::Repeat { + condition: pattern_condition, + body: pattern_body, + }, + ) => { + self.form(subject_condition, pattern_condition) + && (self.form(subject_body, pattern_body) + || self.fused_body(subject_body, pattern_body)) + } + ( + Form::Swap { + sequence: subject_sequence, + left: subject_left, + right: subject_right, + }, + Form::Swap { + sequence: pattern_sequence, + left: pattern_left, + right: pattern_right, + }, + ) => { + self.form(subject_sequence, pattern_sequence) + && self.form(subject_left, pattern_left) + && self.form(subject_right, pattern_right) + } ( Form::Unary { operator: subject_operator, diff --git a/crates/infact-normalize/src/renaming.rs b/crates/infact-normalize/src/renaming.rs index e8bf5a6..4b8fa33 100644 --- a/crates/infact-normalize/src/renaming.rs +++ b/crates/infact-normalize/src/renaming.rs @@ -196,6 +196,19 @@ impl Renaming { left: self.boxed(left), right: self.boxed(right), }, + Form::Repeat { condition, body } => Form::Repeat { + condition: self.boxed(condition), + body: self.boxed(body), + }, + Form::Swap { + sequence, + left, + right, + } => Form::Swap { + sequence: self.boxed(sequence), + left: self.boxed(left), + right: self.boxed(right), + }, Form::Unary { operator, value } => Form::Unary { operator: operator.clone(), value: self.boxed(value), diff --git a/crates/infact-normalize/src/simplify.rs b/crates/infact-normalize/src/simplify.rs index 3322c06..0d9ad6d 100644 --- a/crates/infact-normalize/src/simplify.rs +++ b/crates/infact-normalize/src/simplify.rs @@ -176,6 +176,12 @@ impl Form { .or_else(|| rebuilt.as_element_traversal()) .or_else(|| rebuilt.as_pairwise()) .or_else(|| rebuilt.as_adjacent_pairwise()) + .or_else(|| rebuilt.as_swap()) + .or_else(|| rebuilt.as_counted_traversal()) + .or_else(|| rebuilt.as_reversed_traversal()) + .or_else(|| rebuilt.as_single_step()) + .or_else(|| rebuilt.as_drain()) + .or_else(|| rebuilt.as_guarded_repeat()) .or_else(|| rebuilt.as_recovered_escape()) .or_else(|| rebuilt.as_unfolded(fuel)) .unwrap_or(rebuilt) @@ -480,6 +486,314 @@ impl Form { .or_else(|| Self::as_enumerated_pairwise(outer, first, inner, second, inner_body)) } + /// A counter loop is the traversal of a span it is written to be. + /// + /// `let mut i = 0; while i < n { .. i .. ; i += 1 }` is `for i in 0..n`, + /// and until this ran they shared nothing: one walked a span, the other + /// repeated while a name compared small. This only has to reach + /// `Traverse` over a `Span` — [`Form::as_element_traversal`] already takes + /// a span walk that only ever indexes the rest of the way, so the `while` + /// spelling lands on the same form the `for` spelling does. + /// + /// Unlike every other law here this one is not local to a node. The span's + /// START is not in the loop; it is in the binding before it, so the two + /// have to be seen together and that means matching on the sequence they + /// are steps of. + fn as_counted_traversal(&self) -> Option { + let Self::Sequence(steps) = self else { + return None; + }; + for (bound, step) in steps.iter().enumerate() { + let Self::Let { + pattern, + value: start, + } = step + else { + continue; + }; + let Pattern::Binding(counter) = pattern.as_ref() else { + continue; + }; + for (repeated, candidate) in steps.iter().enumerate().skip(bound + 1) { + // The counter must still hold what it was bound to when the + // loop starts, and must not outlive it: a `for` leaves no + // counter behind, so anything reading it afterwards would lose + // a value it depends on. + if steps[bound + 1..repeated] + .iter() + .chain(&steps[repeated + 1..]) + .any(|other| other.references_local(*counter)) + { + break; + } + let Some((limit, body, direction)) = counted_loop(candidate, *counter) else { + continue; + }; + if limit.references_local(*counter) || moves_the_limit(limit, &body) { + continue; + } + let (from, to) = match direction { + // Counting down from the binding to the limit walks the + // same positions the other way about. + Direction::Backward => (limit.clone(), start.as_ref().clone()), + Direction::Forward => (start.as_ref().clone(), limit.clone()), + }; + let walk = Self::Traverse { + sequence: Box::new(Self::Span { + start: Box::new(from), + end: Box::new(to), + inclusive: false, + }), + item: pattern.clone(), + body: Box::new(body), + direction, + }; + let mut rewritten = steps.to_vec(); + rewritten[repeated] = walk; + rewritten.remove(bound); + return Some(Self::Sequence(rewritten)); + } + } + None + } + + /// Walking a reversed sequence is walking it backwards. + /// + /// `for x in v.iter().rev()` and a loop counting down reach the elements in + /// the same order, and `Direction` exists to say so — but nothing produced + /// it, so a reversal stayed a method call on the sequence and the two + /// spellings shared nothing. Reversing twice is not reversing, so the flip + /// is a flip rather than an assignment. + fn as_reversed_traversal(&self) -> Option { + let Self::Traverse { + sequence, + item, + body, + direction, + } = self + else { + return None; + }; + let Self::Method { + name, + receiver, + arguments, + } = sequence.as_ref() + else { + return None; + }; + if name != "rev" || !arguments.is_empty() { + return None; + } + Some(Self::Traverse { + sequence: receiver.clone(), + item: item.clone(), + body: body.clone(), + direction: match direction { + Direction::Forward => Direction::Backward, + Direction::Backward => Direction::Forward, + }, + }) + } + + /// A group of one step is that step. + /// + /// Braces are punctuation. A body that held one statement stayed a sequence + /// of one, so `{ v.swap(i, j) }` and `v.swap(i, j)` were different forms — + /// which is exactly the difference the two spellings of a bubble sort came + /// down to once the exchange itself was recognized. + fn as_single_step(&self) -> Option { + match self { + Self::Sequence(steps) => match steps.as_slice() { + [only] if !matches!(only, Self::Let { .. }) => Some(only.clone()), + _ => None, + }, + _ => None, + } + } + + /// Three statements through a temporary are an exchange. + /// + /// `let t = v[i]; v[i] = v[j]; v[j] = t;` is `v.swap(i, j)`, and it is how + /// the exchange is written wherever `swap` is not reached for — 131 files + /// in the corpus measured. The temporary must be read back exactly where + /// the second assignment puts it, and nothing else may use it, or the three + /// statements are moving values around rather than exchanging two. + fn as_swap(&self) -> Option { + let Self::Sequence(steps) = self else { + return None; + }; + for (index, window) in steps.windows(3).enumerate() { + let [ + Self::Let { + pattern, + value: saved, + }, + Self::Assign { + operator: first_operator, + target: first_target, + value: first_value, + }, + Self::Assign { + operator: second_operator, + target: second_target, + value: second_value, + }, + ] = window + else { + continue; + }; + let Pattern::Binding(temporary) = pattern.as_ref() else { + continue; + }; + if first_operator != "=" || second_operator != "=" { + continue; + } + // What was saved is what the first assignment overwrites, what it + // is overwritten with is what the second assignment overwrites, and + // the second is given back the saved value. Anything else is not a + // two-element exchange. + if saved.as_ref() != first_target.as_ref() + || first_value.as_ref() != second_target.as_ref() + || **second_value != Self::Local(*temporary) + { + continue; + } + let (Some((sequence, left)), Some((other, right))) = ( + indexed_position(first_target), + indexed_position(second_target), + ) else { + continue; + }; + if sequence != other { + continue; + } + // A temporary that outlives the exchange is holding a value for + // something else too, and dropping it would drop that. + let mut rest = steps.to_vec(); + let exchange = Self::Swap { + sequence: Box::new(sequence.clone()), + left: Box::new(left.clone()), + right: Box::new(right.clone()), + }; + rest.splice(index..index + 3, [exchange]); + if rest.iter().any(|step| step.references_local(*temporary)) { + continue; + } + return Some(Self::Sequence(rest)); + } + None + } + + /// Taking from a container until it is empty is walking it. + /// + /// `while let Some(x) = queue.pop_front() { .. }` reaches every element + /// exactly once and leaves the container empty, which is what `for x in + /// queue` does. The two shared nothing. + /// + /// Only methods whose order the name settles are taken. `pop_front` and + /// `pop_back` say which end on any container that has them; a bare `pop` + /// does not — it is the last element of a `Vec` and the GREATEST element of + /// a `BinaryHeap`, and the form carries no types to tell them apart. In the + /// corpus that is not a corner: of 931 files with a `while let Some(..) = + /// x.pop()`, 434 also use `BinaryHeap`. Calling a heap's drain a backward + /// walk would be reporting the opposite order, which is the one thing + /// `Direction` exists to prevent. + /// + /// A body that puts something back is not draining. That is the commoner + /// shape by two to one — a worklist, breadth-first search, a queue that + /// feeds itself — and the elements it visits are not the ones the container + /// started with. + fn as_drain(&self) -> Option { + let Self::Repeat { condition, body } = self else { + return None; + }; + if **condition != Self::Constant("true".to_owned()) { + return None; + } + let Self::Select { scrutinee, arms } = body.as_ref() else { + return None; + }; + let [taken, exhausted] = arms.as_slice() else { + return None; + }; + // The other arm must do nothing but leave. Anything else happens when + // the container runs out, and a walk over the elements has nowhere to + // put it. + if !matches!(exhausted.pattern, Pattern::Ignored) + || !matches!(&exhausted.body, Self::Opaque { kind, .. } if kind == "break_expression") + { + return None; + } + let Pattern::Variant { name, parts } = &taken.pattern else { + return None; + }; + let [item] = parts.as_slice() else { + return None; + }; + if name != "Some" { + return None; + } + let (direction, container) = taking_method(scrutinee)?; + if disturbs_container(&taken.body, container) { + return None; + } + Some(Self::Traverse { + sequence: Box::new(container.clone()), + item: Box::new(item.clone()), + body: Box::new(taken.body.clone()), + direction, + }) + } + + /// A repetition that tests for its own end is a repetition with a guard. + /// + /// `loop { if done { break } .. }` and `while !done { .. }` are the same + /// loop, and the first is how it gets written when the test is awkward to + /// put at the top. Reducing one to the other is what stops the two + /// spellings of a hand-rolled sort from sharing nothing. + fn as_guarded_repeat(&self) -> Option { + let Self::Repeat { condition, body } = self else { + return None; + }; + // Only a repetition that has no guard yet, or the rewrite would be + // discarding one. + if **condition != Self::Constant("true".to_owned()) { + return None; + } + let Self::Sequence(steps) = body.as_ref() else { + return None; + }; + let (guard, rest) = steps.split_first()?; + let Self::Branch { + condition: test, + consequence, + alternative: None, + } = guard + else { + return None; + }; + if !matches!(consequence.as_ref(), Self::Opaque { kind, .. } if kind == "break_expression") + { + return None; + } + // A `break` further in would leave for another reason, and hoisting + // only the first test would say the loop runs longer than it does. + if rest.iter().any(leaves_a_loop) { + return None; + } + Some(Self::Repeat { + condition: Box::new(Self::Unary { + operator: "!".to_owned(), + value: test.clone(), + }), + body: Box::new(match rest { + [only] => only.clone(), + _ => Self::Sequence(rest.to_vec()), + }), + }) + } + /// 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)` @@ -1084,6 +1398,283 @@ fn one_more_than(form: &Form) -> Option { } } +/// A repetition read as counting, with the limit and the body it leaves behind. +/// +/// The test may be written either way round and either way up: `i < n`, `n > i`, +/// and the `!(i >= n)` that a `loop` with a leading `break` reduces to are one +/// test. Pushing negation through a comparison is NOT sound in general — under +/// a partial order `!(a >= b)` and `a < b` differ, which is the whole content +/// of `IncomparableElements` — but a counter that is stepped by one and used as +/// a span bound is an integer, and integers are totally ordered. The licence +/// comes from the context, so it is taken here and not in the arithmetic law. +/// +/// WHERE the step sits is part of what the loop visits, and the two directions +/// want it in opposite places. Counting up, `while i < n { .. ; i += 1 }` from +/// `i = a` visits `a` through `n - 1`. Counting down, the decrement goes FIRST +/// — `while i > a { i -= 1; .. }` from `i = b` visits `b - 1` through `a` — +/// because that is what keeps the index inside the sequence, and it is how the +/// loop is actually written. Both are then the span `a..b`, walked opposite +/// ways. +/// +/// The other two placements visit `a + 1..=n` and `a + 1..=b`, which are real +/// loops and different spans, and are refused rather than quietly given the +/// span their sibling has. +/// +/// A step inside a branch may not happen at all, so the loop would visit +/// something other than a span and might not finish. +fn counted_loop(form: &Form, counter: u32) -> Option<(&Form, Form, Direction)> { + let Form::Repeat { condition, body } = form else { + return None; + }; + let Form::Sequence(steps) = body.as_ref() else { + return None; + }; + // Take the step from each end and let the operator say which one counts. + let (first, after) = steps.split_first()?; + let (last, before) = steps.split_last()?; + let (direction, rest) = match counter_step(first, counter) { + Some(Direction::Backward) => (Direction::Backward, after), + _ => match counter_step(last, counter) { + Some(Direction::Forward) => (Direction::Forward, before), + _ => return None, + }, + }; + // The direction decides which side of the test the counter belongs on: a + // loop counting up stops when it reaches the limit from below, one counting + // down when it reaches it from above. Reading the test without knowing + // which way the loop runs cannot tell `i < n` from `i > 0`. + let limit = counting_test(condition, counter, direction)?; + // Anything else that moves the counter changes how many times the loop + // runs, and the span would be claiming a trip count the code does not have. + let remaining = match rest { + [only] => only.clone(), + _ => Form::Sequence(rest.to_vec()), + }; + if assigns_to(&remaining, counter) { + return None; + } + Some((limit, remaining, direction)) +} + +/// Which way a statement steps a counter, when that is all it does. +/// +/// A stride is not a span: `Span` has no room to say "every second one", and +/// walking it as though it did would claim a trip count the code has not got. +fn counter_step(step: &Form, counter: u32) -> Option { + let Form::Assign { + operator, + target, + value, + } = step + else { + return None; + }; + if **target != Form::Local(counter) || **value != Form::Number("1".to_owned()) { + return None; + } + match operator.as_str() { + "+=" => Some(Direction::Forward), + "-=" => Some(Direction::Backward), + _ => None, + } +} + +/// The limit a test compares a counter against, when it is one. +/// +/// Four spellings per direction, and they are one test: the counter on either +/// side, and the whole thing negated as a `loop` with a leading `break` reduces +/// to. Pushing negation through a comparison is NOT sound in general — under a +/// partial order `!(a >= b)` and `a < b` differ, which is the whole content of +/// `IncomparableElements` — but a counter stepped by one and used as a span +/// bound is an integer, and integers are totally ordered. The licence comes +/// from the context, which is why it is taken here and not in the arithmetic +/// law where it would reach every comparison. +fn counting_test(condition: &Form, counter: u32, direction: Direction) -> Option<&Form> { + if let Form::Unary { operator, value } = condition + && operator == "!" + { + return match direction { + Direction::Forward => compared_against(value, counter, ">=", "<="), + Direction::Backward => compared_against(value, counter, "<=", ">="), + }; + } + match direction { + Direction::Forward => compared_against(condition, counter, "<", ">"), + Direction::Backward => compared_against(condition, counter, ">", "<"), + } +} + +/// What a counter is compared with, when the comparison is one of two shapes. +/// +/// `left` is the operator wanted with the counter written first, `right` the +/// one wanted with it written second. `i < n` and `n > i` are the same test. +fn compared_against<'a>( + condition: &'a Form, + counter: u32, + left_operator: &str, + right_operator: &str, +) -> Option<&'a Form> { + let Form::Binary { + operator, + left, + right, + } = condition + else { + return None; + }; + let name = Form::Local(counter); + if operator == left_operator && **left == name { + return Some(right.as_ref()); + } + if operator == right_operator && **right == name { + return Some(left.as_ref()); + } + None +} + +/// Whether anything here assigns to a name. +fn assigns_to(form: &Form, local: u32) -> bool { + if let Form::Assign { target, .. } = form + && **target == Form::Local(local) + { + return true; + } + form.children() + .into_iter() + .any(|child| assigns_to(child, local)) +} + +/// Whether the body might change what the limit is measuring. +/// +/// A `for` evaluates its range once; a `while` re-reads its test every time +/// around. The two agree only when nothing in the loop can move the limit, and +/// `while i < v.len() { v.push(x); i += 1 }` is exactly the case where they do +/// not. +/// +/// The form carries no effects, so this asks the question it can. A name the +/// limit depends on may be READ — `total += n` is fine — and may not be +/// assigned to, have a method called on it, or be swapped through, because any +/// of those might be the one that moves it. That refuses some loops whose limit +/// is in fact fixed, which is the side to be wrong on. +fn moves_the_limit(limit: &Form, body: &Form) -> bool { + let mut names = Vec::new(); + collect_names(limit, &mut names); + names.iter().any(|name| disturbs(body, name, limit)) +} + +fn collect_names(form: &Form, found: &mut Vec) { + if matches!(form, Form::Local(_) | Form::Free(_)) && !found.contains(form) { + found.push(form.clone()); + } + for child in form.children() { + collect_names(child, found); + } +} + +/// The container a call takes an element from, and which end it takes from. +/// +/// `next` is here because an iterator's is the whole of its contract and yields +/// in order; `pop` is not, because the name settles no order. +fn taking_method(scrutinee: &Form) -> Option<(Direction, &Form)> { + let Form::Method { + name, + receiver, + arguments, + } = scrutinee + else { + return None; + }; + if !arguments.is_empty() { + return None; + } + let direction = match name.as_str() { + "pop_front" | "next" => Direction::Forward, + "pop_back" | "next_back" => Direction::Backward, + _ => return None, + }; + Some((direction, receiver.as_ref())) +} + +/// Whether a body reaches the container it is being handed elements from. +/// +/// Putting something back makes the walk cover elements the container did not +/// start with; measuring it mid-drain observes a state a walk over the elements +/// never has. Both are asked as one question, because the form carries no +/// effects and a method on the container could be either. +fn disturbs_container(body: &Form, container: &Form) -> bool { + let touched = match body { + Form::Assign { target, .. } => mentions(target, container), + Form::Method { receiver, .. } => receiver.as_ref() == container, + Form::Swap { sequence, .. } | Form::Index { sequence, .. } => { + sequence.as_ref() == container + } + _ => false, + }; + touched + || body + .children() + .into_iter() + .any(|child| disturbs_container(child, container)) +} + +/// Whether a body does anything to a name beyond reading it. +/// +/// Working out the limit again is reading it, not changing it. That exemption +/// is what keeps a nested counter loop readable: the inner loop's own bound is +/// `v.len()` too, and without it the outer loop would be told that measuring +/// the sequence had moved it. +fn disturbs(form: &Form, name: &Form, limit: &Form) -> bool { + if form == limit { + return false; + } + let touched = match form { + Form::Assign { target, .. } => mentions(target, name), + // A method might be `push`. Which ones move a sequence and which only + // read it is an effect question, and the form does not carry effects. + Form::Method { receiver, .. } => receiver.as_ref() == name, + Form::Swap { sequence, .. } => sequence.as_ref() == name, + _ => false, + }; + touched + || form + .children() + .into_iter() + .any(|child| disturbs(child, name, limit)) +} + +fn mentions(form: &Form, name: &Form) -> bool { + form == name + || form + .children() + .into_iter() + .any(|child| mentions(child, name)) +} + +/// The sequence and position an indexing reads. +fn indexed_position(form: &Form) -> Option<(&Form, &Form)> { + match form { + Form::Index { sequence, position } => Some((sequence.as_ref(), position.as_ref())), + _ => None, + } +} + +/// Whether a step can leave the loop around it. +/// +/// A `break` nested inside another loop belongs to that one, so only the steps +/// this loop runs directly are asked. +fn leaves_a_loop(form: &Form) -> bool { + match form { + Form::Opaque { kind, .. } if kind == "break_expression" => true, + Form::Repeat { .. } + | Form::Traverse { .. } + | Form::Pairwise { .. } + | Form::Transform { .. } + | Form::Retain { .. } + | Form::Sift { .. } => false, + _ => form.children().into_iter().any(leaves_a_loop), + } +} + /// Whether a form is one less than another. fn is_predecessor_of(form: &Form, of: &Form) -> bool { matches!(form, Form::Binary { operator, left, right } diff --git a/crates/infact-rust-normalize/examples/census.rs b/crates/infact-rust-normalize/examples/census.rs index b6c8d40..0a92562 100644 --- a/crates/infact-rust-normalize/examples/census.rs +++ b/crates/infact-rust-normalize/examples/census.rs @@ -65,6 +65,8 @@ fn variant(form: &Form) -> &'static str { Form::Assign { .. } => "Assign", Form::Binary { .. } => "Binary", Form::Unary { .. } => "Unary", + Form::Repeat { .. } => "Repeat", + Form::Swap { .. } => "Swap", Form::Pairwise { .. } => "Pairwise", Form::Index { .. } => "Index", Form::Span { .. } => "Span", diff --git a/crates/infact-rust-normalize/examples/lower.rs b/crates/infact-rust-normalize/examples/lower.rs index 34ee269..054173d 100644 --- a/crates/infact-rust-normalize/examples/lower.rs +++ b/crates/infact-rust-normalize/examples/lower.rs @@ -258,6 +258,18 @@ fn lower(form: &Form, level: usize, guesses: &Guesses, names: &Names) -> String right, } => format!("({} {operator} {})", sub(left), sub(right)), Form::Unary { operator, value } => format!("{operator}{}", sub(value)), + Form::Repeat { condition, body } => format!( + "while {} {{\n{}{}\n{}}}", + sub(condition), + indent(level + 1), + lower(body, level + 1, guesses, names), + indent(level) + ), + Form::Swap { + sequence, + left, + right, + } => format!("{}.swap({}, {})", sub(sequence), sub(left), sub(right)), Form::Index { sequence, position } => format!("{}[{}]", sub(sequence), sub(position)), Form::Span { start, diff --git a/crates/infact-rust-normalize/src/lib.rs b/crates/infact-rust-normalize/src/lib.rs index b96f43d..0d7c10c 100644 --- a/crates/infact-rust-normalize/src/lib.rs +++ b/crates/infact-rust-normalize/src/lib.rs @@ -586,6 +586,44 @@ impl<'a> Normalizer<'a> { right: Box::new(right), } } + // `while c { b }` and `loop { b }` are one construct: a loop is a + // repetition whose condition is always met, and whatever ends it is + // written inside. Spelling that out here is what lets one law reach + // both. + "while_expression" => { + // `while let P = e { b }` is `loop { match e { P => b, _ => + // break } }`, which is not an analogy but the desugaring the + // language performs. Written out it needs no vocabulary of its + // own, and — the point — the name the pattern binds becomes a + // binding. Left as a condition it was normalized as an + // EXPRESSION, so `x` came out a free variable: a hole matching + // anything rather than a name the body uses. + if let Some(condition) = node.child_by_field_name("condition") + && condition.kind() == "let_condition" + && let Some(repeated) = self.repeat_from_let(node, condition) + { + return repeated; + } + let condition = node + .child_by_field_name("condition") + .map_or(Form::Literal, |child| self.expression(child)); + let body = node + .child_by_field_name("body") + .map_or(Form::Literal, |child| self.expression(child)); + Form::Repeat { + condition: Box::new(condition), + body: Box::new(body), + } + } + "loop_expression" => { + let body = node + .child_by_field_name("body") + .map_or(Form::Literal, |child| self.expression(child)); + Form::Repeat { + condition: Box::new(Form::Constant("true".to_owned())), + body: Box::new(body), + } + } // A dereference has already been peeled by `unwrap_noise`, so // whatever reaches here applies an operator that changes the value. "unary_expression" => { @@ -768,6 +806,40 @@ impl<'a> Normalizer<'a> { )) } + /// `while let` as the loop around a decision that it is. + /// + /// Returns `None` when the pattern names no alternative — `while let (a, b) + /// = p` destructures rather than deciding, and it would never end. + fn repeat_from_let(&mut self, node: Node<'a>, condition: Node<'a>) -> Option { + let scrutinee = self.expression(condition.child_by_field_name("value")?); + let bound = self.bind_pattern(condition.child_by_field_name("pattern")?); + if !matches!(bound, Pattern::Variant { .. }) { + return None; + } + let taken = node + .child_by_field_name("body") + .map_or(Form::Literal, |child| self.expression(child)); + Some(Form::Repeat { + condition: Box::new(Form::Constant("true".to_owned())), + body: Box::new(Form::select( + scrutinee, + vec![ + Arm { + pattern: bound, + body: taken, + }, + Arm { + pattern: Pattern::Ignored, + body: Form::Opaque { + kind: "break_expression".to_owned(), + parts: Vec::new(), + }, + }, + ], + )), + }) + } + /// A `match`, as a decision among named alternatives. /// /// An arm with a guard is left as written: a guard makes the order of the @@ -900,6 +972,19 @@ impl<'a> Normalizer<'a> { let name = call.name.to_owned(); let receiver = self.expression(peel_adapters(call.receiver, self.source)); let arguments = self.arguments(node); + // Exchanging two elements is an operation, not a call that happens + // to be named `swap`. Written out through a temporary it is three + // statements, and a form that held one spelling as a method and the + // other as assignments could never see they were the same. + if name == "swap" + && let [left, right] = arguments.as_slice() + { + return Form::Swap { + sequence: Box::new(receiver), + left: Box::new(left.clone()), + right: Box::new(right.clone()), + }; + } return Form::Method { name, receiver: Box::new(receiver), diff --git a/crates/infact-rust-normalize/tests/normalize.rs b/crates/infact-rust-normalize/tests/normalize.rs index f744efb..a4bd567 100644 --- a/crates/infact-rust-normalize/tests/normalize.rs +++ b/crates/infact-rust-normalize/tests/normalize.rs @@ -822,3 +822,440 @@ fn adjacent_pairs_differ_from_every_pair() { ); assert_ne!(adjacent, every); } + +/// An exchange written through a temporary is the exchange. +/// +/// This is the difference the two spellings of a bubble sort came down to, and +/// with it they reduce to one form. +#[test] +fn a_temporary_swap_agrees_with_the_method() { + let method = behavior_of( + "fn f(values: &mut [i32]) { + for i in 0..values.len() { + for j in 0..values.len() - 1 - i { + if values[j] > values[j + 1] { values.swap(j, j + 1); } + } + } + }", + "f", + ); + let temporary = behavior_of( + "fn f(values: &mut [i32]) { + for i in 0..values.len() { + for j in 0..values.len() - 1 - i { + if values[j] > values[j + 1] { + let t = values[j]; + values[j] = values[j + 1]; + values[j + 1] = t; + } + } + } + }", + "f", + ); + assert!(method.contains("(swap"), "{method}"); + assert_eq!(method, temporary); +} + +/// Moving values around is not exchanging two of them. +#[test] +fn a_shift_through_a_temporary_is_not_a_swap() { + let form = behavior_of( + "fn f(values: &mut [i32], i: usize, j: usize, k: usize) { + let t = values[i]; + values[i] = values[j]; + values[k] = t; + }", + "f", + ); + assert!(!form.contains("(swap"), "{form}"); +} + +/// A temporary that is used again is not spent on the exchange. +#[test] +fn a_temporary_read_afterwards_is_not_a_swap() { + let form = behavior_of( + "fn f(values: &mut [i32], i: usize, j: usize) -> i32 { + let t = values[i]; + values[i] = values[j]; + values[j] = t; + t + }", + "f", + ); + assert!(!form.contains("(swap"), "{form}"); +} + +/// A `loop` that tests for its own end is a `while`. +#[test] +fn a_loop_that_breaks_agrees_with_a_while() { + let broken = behavior_of( + "fn f(n: usize) -> usize { + let mut i = 0; + loop { if i >= n { break; } i += 1; } + i + }", + "f", + ); + let guarded = behavior_of( + "fn f(n: usize) -> usize { + let mut i = 0; + while !(i >= n) { i += 1; } + i + }", + "f", + ); + assert!(broken.contains("(repeat"), "{broken}"); + assert_eq!(broken, guarded); +} + +/// A loop with another way out is not just its first test. +#[test] +fn a_loop_with_a_second_break_keeps_its_shape() { + let form = behavior_of( + "fn f(n: usize, m: usize) -> usize { + let mut i = 0; + loop { if i >= n { break; } if i == m { break; } i += 1; } + i + }", + "f", + ); + assert!(form.contains("(const true)"), "{form}"); +} + +/// A repetition describes work, so a library that loops has a behavior. +#[test] +fn a_repetition_is_comparable() { + let form = behavior_of( + "fn f(values: &mut Vec) -> i32 { + let mut total = 0; + while let Some(value) = values.pop() { total += value; } + total + }", + "f", + ); + assert!(form.contains("(repeat"), "{form}"); +} + +/// A counter loop is the traversal it is written to be. +/// +/// Two laws compose to get here: the counter loop becomes a walk over a span, +/// and a span walk that only ever indexes becomes a walk over the elements. +#[test] +fn a_counter_loop_agrees_with_a_for_loop() { + let counted = behavior_of( + "fn f(values: &[i32]) -> i32 { + let mut total = 0; + let mut i = 0; + while i < values.len() { total += values[i]; i += 1; } + total + }", + "f", + ); + let direct = behavior_of( + "fn f(values: &[i32]) -> i32 { + let mut total = 0; + for i in 0..values.len() { total += values[i]; } + total + }", + "f", + ); + assert_eq!(counted, direct); + assert!(!counted.contains("(repeat"), "{counted}"); +} + +/// A `loop` with a leading `break` reaches the same place. +/// +/// Four laws in a row: the break becomes a guard, the guard's negation is read +/// as the counting test, the counter loop becomes a span walk, and the span +/// walk becomes an element walk. +#[test] +fn a_loop_with_a_break_agrees_with_a_for_loop() { + let broken = behavior_of( + "fn f(values: &[i32]) -> i32 { + let mut total = 0; + let mut i = 0; + loop { if i >= values.len() { break; } total += values[i]; i += 1; } + total + }", + "f", + ); + let direct = behavior_of( + "fn f(values: &[i32]) -> i32 { + let mut total = 0; + for i in 0..values.len() { total += values[i]; } + total + }", + "f", + ); + assert_eq!(broken, direct); +} + +/// Counting down agrees with walking a reversed range. +#[test] +fn a_descending_counter_agrees_with_a_reversed_range() { + let counted = behavior_of( + "fn f(values: &[i32]) -> i32 { + let mut total = 0; + let mut i = values.len(); + while i > 0 { i -= 1; total += values[i]; } + total + }", + "f", + ); + let reversed = behavior_of( + "fn f(values: &[i32]) -> i32 { + let mut total = 0; + for i in (0..values.len()).rev() { total += values[i]; } + total + }", + "f", + ); + assert_eq!(counted, reversed); + assert!(counted.contains("traverse-back"), "{counted}"); +} + +/// Walking backwards is not the same as walking forwards. +#[test] +fn a_reversed_walk_differs_from_a_forward_one() { + let forward = behavior_of("fn f(v: &[i32]) { for x in v.iter() { g(x); } }", "f"); + let backward = behavior_of("fn f(v: &[i32]) { for x in v.iter().rev() { g(x); } }", "f"); + assert_ne!(forward, backward); +} + +/// Where the step sits decides which positions the loop visits. +/// +/// Counting down with the decrement LAST visits `n` through `1`, not `n - 1` +/// through `0`, so it is a different span and is refused rather than given the +/// span its sibling has. +#[test] +fn a_descending_loop_that_steps_last_is_refused() { + let form = behavior_of( + "fn f(values: &[i32]) -> i32 { + let mut total = 0; + let mut i = values.len(); + while i > 0 { total += values[i - 1]; i -= 1; } + total + }", + "f", + ); + assert!(form.contains("(repeat"), "{form}"); +} + +/// A loop whose body can move the limit is not a walk over a fixed span. +#[test] +fn a_loop_that_changes_its_limit_is_refused() { + let form = behavior_of( + "fn f(values: &mut Vec) -> usize { + let mut i = 0; + while i < values.len() { values.push(1); i += 1; } + i + }", + "f", + ); + assert!(form.contains("(repeat"), "{form}"); +} + +/// A counter something reads afterwards is not one a traversal may consume. +#[test] +fn a_counter_read_after_the_loop_is_refused() { + let form = behavior_of( + "fn f(values: &[i32]) -> usize { + let mut total = 0; + let mut i = 0; + while i < values.len() { total += values[i]; i += 1; } + i + }", + "f", + ); + assert!(form.contains("(repeat"), "{form}"); +} + +/// A step that might not happen is not a span. +#[test] +fn a_conditional_step_is_refused() { + let form = behavior_of( + "fn f(values: &[i32]) -> i32 { + let mut total = 0; + let mut i = 0; + while i < values.len() { if values[i] > 0 { i += 1; } total += 1; } + total + }", + "f", + ); + assert!(form.contains("(repeat"), "{form}"); +} + +/// A stride is not a span. +#[test] +fn a_stride_is_refused() { + let form = behavior_of( + "fn f(n: usize) -> usize { + let mut i = 0; + let mut total = 0; + while i < n { total += i; i += 2; } + total + }", + "f", + ); + assert!(form.contains("(repeat"), "{form}"); +} + +/// Nested counter loops reach the same walk over pairs as nested `for` loops. +/// +/// The inner loop's own bound is `values.len()` as well, so this only holds +/// because working the limit out again counts as reading it. +#[test] +fn nested_counter_loops_agree_with_nested_for_loops() { + let counted = behavior_of( + "fn f(values: &[i32]) -> bool { + let mut i = 0; + while i < values.len() { + let mut j = i + 1; + while j < values.len() { + if values[i] == values[j] { return false; } + j += 1; + } + i += 1; + } + true + }", + "f", + ); + let direct = behavior_of( + "fn f(values: &[i32]) -> bool { + for i in 0..values.len() { + for j in i + 1..values.len() { + if values[i] == values[j] { return false; } + } + } + true + }", + "f", + ); + assert_eq!(counted, direct); + assert!(counted.contains("(pairwise"), "{counted}"); +} + +/// Draining a container agrees with walking it. +#[test] +fn a_drain_agrees_with_a_for_loop() { + let drained = behavior_of( + "fn f(queue: &mut VecDeque) -> i32 { + let mut total = 0; + while let Some(x) = queue.pop_front() { total += x; } + total + }", + "f", + ); + let direct = behavior_of( + "fn f(queue: VecDeque) -> i32 { + let mut total = 0; + for x in queue { total += x; } + total + }", + "f", + ); + assert_eq!(drained, direct); +} + +/// Pulling from an iterator by hand agrees with walking it. +#[test] +fn a_hand_written_next_loop_agrees_with_a_for_loop() { + let pulled = behavior_of( + "fn f(items: &mut Iter) -> i32 { + let mut total = 0; + while let Some(x) = items.next() { total += x; } + total + }", + "f", + ); + let direct = behavior_of( + "fn f(items: Iter) -> i32 { + let mut total = 0; + for x in items { total += x; } + total + }", + "f", + ); + assert_eq!(pulled, direct); +} + +/// Taking from the back is not taking from the front. +#[test] +fn draining_from_each_end_gives_different_walks() { + let front = behavior_of( + "fn f(q: &mut VecDeque) -> i32 { + let mut t = 0; while let Some(x) = q.pop_front() { t += x; } t + }", + "f", + ); + let back = behavior_of( + "fn f(q: &mut VecDeque) -> i32 { + let mut t = 0; while let Some(x) = q.pop_back() { t += x; } t + }", + "f", + ); + assert_ne!(front, back); + assert!(back.contains("traverse-back"), "{back}"); +} + +/// A worklist is not a drain. +/// +/// Putting something back makes the loop visit elements the container did not +/// start with, and it is the commoner shape by two to one. +#[test] +fn a_worklist_is_not_a_drain() { + let form = behavior_of( + "fn f(q: &mut VecDeque) -> i32 { + let mut t = 0; + while let Some(x) = q.pop_front() { t += x; if x > 0 { q.push_back(x - 1); } } + t + }", + "f", + ); + assert!(form.contains("(repeat"), "{form}"); +} + +/// A bare `pop` settles no order, so it is not read as a walk. +/// +/// It is the last element of a `Vec` and the greatest of a `BinaryHeap`, and +/// nothing in the form says which. Measured across the corpus, 434 of the 931 +/// files that drain with `pop` also use a `BinaryHeap`. +#[test] +fn a_bare_pop_is_not_a_drain() { + let form = behavior_of( + "fn f(v: &mut Vec) -> i32 { + let mut t = 0; while let Some(x) = v.pop() { t += x; } t + }", + "f", + ); + assert!(form.contains("(repeat"), "{form}"); +} + +/// Measuring the container mid-drain observes a state a walk never has. +#[test] +fn a_body_that_measures_the_container_is_not_a_drain() { + let form = behavior_of( + "fn f(q: &mut VecDeque) -> usize { + let mut t = 0; while let Some(_x) = q.pop_front() { t += q.len(); } t + }", + "f", + ); + assert!(form.contains("(repeat"), "{form}"); +} + +/// A `while let` binds a name, and the body uses that name. +/// +/// Held as a condition it was normalized as an expression, so the name came out +/// a hole that matched anything rather than the binding the body reads. +#[test] +fn a_while_let_binds_rather_than_leaving_a_hole() { + let form = behavior_of( + "fn f(v: &mut Vec) -> i32 { + let mut t = 0; while let Some(x) = v.pop() { t += x; } t + }", + "f", + ); + assert!(form.contains("(Some v"), "{form}"); +}