Order list-min-size tests in descending order

Some of the head-constructor tests we generate can be supersets of other tests.
Edges must be ordered so that more general tests always happen after their
specialized variants.

For example, patterns

  [1, ..] -> ...
  [2, 1, ..] -> ...

may generate the edges

  ListLen(>=1) -> <rest>
  ListLen(>=2) -> <rest>

but evaluated in exactly this order, the second edge is never reachable.
The necessary ordering is

  ListLen(>=2) -> <rest>
  ListLen(>=1) -> <rest>

Closes #4732
This commit is contained in:
Ayaz Hafiz 2022-12-14 12:58:33 -06:00
parent a295e7ac3d
commit a8693e6102
No known key found for this signature in database
GPG key ID: 0E2A37416A25EF58
3 changed files with 324 additions and 31 deletions

View file

@ -51,12 +51,14 @@ impl<'a> Guard<'a> {
}
}
type Edge<'a> = (GuardedTest<'a>, DecisionTree<'a>);
#[derive(Clone, Debug, PartialEq)]
enum DecisionTree<'a> {
Match(Label),
Decision {
path: Vec<PathInstruction>,
edges: Vec<(GuardedTest<'a>, DecisionTree<'a>)>,
edges: Vec<Edge<'a>>,
default: Option<Box<DecisionTree<'a>>>,
},
}
@ -1596,6 +1598,7 @@ fn test_to_comparison<'a>(
}
}
#[derive(Debug, Clone, Copy)]
enum Comparator {
Eq,
Geq,
@ -2168,6 +2171,69 @@ fn test_always_succeeds(test: &Test) -> bool {
}
}
fn sort_edge_tests_by_priority(edges: &mut Vec<Edge<'_>>) {
use std::cmp::{Ordering, Ordering::*};
use GuardedTest::*;
edges.sort_by(|(t1, _), (t2, _)| match (t1, t2) {
// Guarded takes priority
(GuardedNoTest { .. }, GuardedNoTest { .. }) => Equal,
(GuardedNoTest { .. }, TestNotGuarded { .. }) | (GuardedNoTest { .. }, Placeholder) => Less,
// Interesting case: what test do we pick?
(TestNotGuarded { test: t1 }, TestNotGuarded { test: t2 }) => order_tests(t1, t2),
// Otherwise we are between guarded and fall-backs
(TestNotGuarded { .. }, GuardedNoTest { .. }) => Greater,
(TestNotGuarded { .. }, Placeholder) => Less,
// Placeholder is always last
(Placeholder, Placeholder) => Equal,
(Placeholder, GuardedNoTest { .. }) | (Placeholder, TestNotGuarded { .. }) => Greater,
});
fn order_tests(t1: &Test, t2: &Test) -> Ordering {
match (t1, t2) {
(
Test::IsListLen {
bound: bound_l,
len: l,
},
Test::IsListLen {
bound: bound_m,
len: m,
},
) => {
// List tests can either check for
// - exact length (= l)
// - a size greater or equal to a given length (>= l)
// (>= l) tests can be superset of other tests
// - (>= m) where m > l
// - (= m)
// So, if m > l, we enforce the following order for list tests
// (>= m) then (= l) then (>= l)
match m.cmp(l) {
Less => Less, // (>= m) then (>= l)
Greater => Greater,
Equal => {
use ListLenBound::*;
match (bound_l, bound_m) {
(Exact, AtLeast) => Less, // (= l) then (>= l)
(AtLeast, Exact) => Greater,
(AtLeast, AtLeast) | (Exact, Exact) => Equal,
}
}
}
}
(Test::IsListLen { .. }, t) | (t, Test::IsListLen { .. }) => internal_error!(
"list-length tests should never pair with another test {t:?} at the same level"
),
// We don't care about anything other than list-length tests, since all other tests
// should be disjoint.
_ => Equal,
}
}
}
fn tree_to_decider(tree: DecisionTree) -> Decider<u64> {
use Decider::*;
use DecisionTree::*;
@ -2179,44 +2245,67 @@ fn tree_to_decider(tree: DecisionTree) -> Decider<u64> {
path,
mut edges,
default,
} => match default {
None => match edges.len() {
0 => panic!("compiler bug, somehow created an empty decision tree"),
1 => {
let (_, sub_tree) = edges.remove(0);
} => {
// Some of the head-constructor tests we generate can be supersets of other tests.
// Edges must be ordered so that more general tests always happen after their
// specialized variants.
//
// For example, patterns
//
// [1, ..] -> ...
// [2, 1, ..] -> ...
//
// may generate the edges
//
// ListLen(>=1) -> <rest>
// ListLen(>=2) -> <rest>
//
// but evaluated in exactly this order, the second edge is never reachable.
// The necessary ordering is
//
// ListLen(>=2) -> <rest>
// ListLen(>=1) -> <rest>
sort_edge_tests_by_priority(&mut edges);
tree_to_decider(sub_tree)
}
2 => {
let (_, failure_tree) = edges.remove(1);
let (guarded_test, success_tree) = edges.remove(0);
match default {
None => match edges.len() {
0 => panic!("compiler bug, somehow created an empty decision tree"),
1 => {
let (_, sub_tree) = edges.remove(0);
chain_decider(path, guarded_test, failure_tree, success_tree)
}
tree_to_decider(sub_tree)
}
2 => {
let (_, failure_tree) = edges.remove(1);
let (guarded_test, success_tree) = edges.remove(0);
_ => {
let fallback = edges.remove(edges.len() - 1).1;
chain_decider(path, guarded_test, failure_tree, success_tree)
}
fanout_decider(path, fallback, edges)
}
},
_ => {
let fallback = edges.remove(edges.len() - 1).1;
Some(last) => match edges.len() {
0 => tree_to_decider(*last),
1 => {
let failure_tree = *last;
let (guarded_test, success_tree) = edges.remove(0);
fanout_decider(path, fallback, edges)
}
},
chain_decider(path, guarded_test, failure_tree, success_tree)
}
Some(last) => match edges.len() {
0 => tree_to_decider(*last),
1 => {
let failure_tree = *last;
let (guarded_test, success_tree) = edges.remove(0);
_ => {
let fallback = *last;
chain_decider(path, guarded_test, failure_tree, success_tree)
}
fanout_decider(path, fallback, edges)
}
},
},
_ => {
let fallback = *last;
fanout_decider(path, fallback, edges)
}
},
}
}
}
}

View file

@ -0,0 +1,190 @@
procedure Test.0 ():
let Test.1 : List I64 = Array [];
joinpoint Test.10:
let Test.6 : Str = "Catchall";
ret Test.6;
in
joinpoint Test.9:
let Test.4 : Str = "B3";
ret Test.4;
in
joinpoint Test.8:
let Test.3 : Str = "B2";
ret Test.3;
in
joinpoint Test.7:
let Test.2 : Str = "B1";
ret Test.2;
in
let Test.73 : U64 = lowlevel ListLen Test.1;
let Test.74 : U64 = 4i64;
let Test.75 : Int1 = lowlevel NumGte Test.73 Test.74;
if Test.75 then
let Test.11 : U64 = 0i64;
let Test.12 : I64 = lowlevel ListGetUnsafe Test.1 Test.11;
switch Test.12:
case 1:
dec Test.1;
jump Test.7;
case 2:
let Test.13 : U64 = 1i64;
let Test.14 : I64 = lowlevel ListGetUnsafe Test.1 Test.13;
dec Test.1;
let Test.15 : I64 = 1i64;
let Test.16 : Int1 = lowlevel Eq Test.15 Test.14;
if Test.16 then
jump Test.8;
else
jump Test.10;
case 3:
joinpoint Test.23:
jump Test.10;
in
let Test.20 : U64 = 2i64;
let Test.21 : I64 = lowlevel ListGetUnsafe Test.1 Test.20;
let Test.22 : I64 = 1i64;
let Test.25 : Int1 = lowlevel Eq Test.22 Test.21;
if Test.25 then
let Test.17 : U64 = 1i64;
let Test.18 : I64 = lowlevel ListGetUnsafe Test.1 Test.17;
dec Test.1;
let Test.19 : I64 = 2i64;
let Test.24 : Int1 = lowlevel Eq Test.19 Test.18;
if Test.24 then
jump Test.9;
else
jump Test.23;
else
dec Test.1;
jump Test.23;
case 4:
joinpoint Test.35:
jump Test.10;
in
let Test.32 : U64 = 3i64;
let Test.33 : I64 = lowlevel ListGetUnsafe Test.1 Test.32;
let Test.34 : I64 = 1i64;
let Test.38 : Int1 = lowlevel Eq Test.34 Test.33;
if Test.38 then
let Test.29 : U64 = 2i64;
let Test.30 : I64 = lowlevel ListGetUnsafe Test.1 Test.29;
let Test.31 : I64 = 2i64;
let Test.37 : Int1 = lowlevel Eq Test.31 Test.30;
if Test.37 then
let Test.26 : U64 = 1i64;
let Test.27 : I64 = lowlevel ListGetUnsafe Test.1 Test.26;
dec Test.1;
let Test.28 : I64 = 3i64;
let Test.36 : Int1 = lowlevel Eq Test.28 Test.27;
if Test.36 then
let Test.5 : Str = "B4";
ret Test.5;
else
jump Test.35;
else
dec Test.1;
jump Test.35;
else
dec Test.1;
jump Test.35;
default:
dec Test.1;
jump Test.10;
else
let Test.70 : U64 = lowlevel ListLen Test.1;
let Test.71 : U64 = 3i64;
let Test.72 : Int1 = lowlevel NumGte Test.70 Test.71;
if Test.72 then
let Test.39 : U64 = 0i64;
let Test.40 : I64 = lowlevel ListGetUnsafe Test.1 Test.39;
switch Test.40:
case 1:
dec Test.1;
jump Test.7;
case 2:
let Test.41 : U64 = 1i64;
let Test.42 : I64 = lowlevel ListGetUnsafe Test.1 Test.41;
dec Test.1;
let Test.43 : I64 = 1i64;
let Test.44 : Int1 = lowlevel Eq Test.43 Test.42;
if Test.44 then
jump Test.8;
else
jump Test.10;
case 3:
joinpoint Test.51:
jump Test.10;
in
let Test.48 : U64 = 2i64;
let Test.49 : I64 = lowlevel ListGetUnsafe Test.1 Test.48;
let Test.50 : I64 = 1i64;
let Test.53 : Int1 = lowlevel Eq Test.50 Test.49;
if Test.53 then
let Test.45 : U64 = 1i64;
let Test.46 : I64 = lowlevel ListGetUnsafe Test.1 Test.45;
dec Test.1;
let Test.47 : I64 = 2i64;
let Test.52 : Int1 = lowlevel Eq Test.47 Test.46;
if Test.52 then
jump Test.9;
else
jump Test.51;
else
dec Test.1;
jump Test.51;
default:
dec Test.1;
jump Test.10;
else
let Test.67 : U64 = lowlevel ListLen Test.1;
let Test.68 : U64 = 2i64;
let Test.69 : Int1 = lowlevel NumGte Test.67 Test.68;
if Test.69 then
let Test.54 : U64 = 0i64;
let Test.55 : I64 = lowlevel ListGetUnsafe Test.1 Test.54;
switch Test.55:
case 1:
dec Test.1;
jump Test.7;
case 2:
let Test.56 : U64 = 1i64;
let Test.57 : I64 = lowlevel ListGetUnsafe Test.1 Test.56;
dec Test.1;
let Test.58 : I64 = 1i64;
let Test.59 : Int1 = lowlevel Eq Test.58 Test.57;
if Test.59 then
jump Test.8;
else
jump Test.10;
default:
dec Test.1;
jump Test.10;
else
let Test.64 : U64 = lowlevel ListLen Test.1;
let Test.65 : U64 = 1i64;
let Test.66 : Int1 = lowlevel NumGte Test.64 Test.65;
if Test.66 then
let Test.60 : U64 = 0i64;
let Test.61 : I64 = lowlevel ListGetUnsafe Test.1 Test.60;
dec Test.1;
let Test.62 : I64 = 1i64;
let Test.63 : Int1 = lowlevel Eq Test.62 Test.61;
if Test.63 then
jump Test.7;
else
jump Test.10;
else
dec Test.1;
jump Test.10;

View file

@ -2220,3 +2220,17 @@ fn lambda_set_with_imported_toplevels_issue_4733() {
"###
)
}
#[mono_test]
fn order_list_size_tests_issue_4732() {
indoc!(
r###"
when [] is
[1, ..] -> "B1"
[2, 1, ..] -> "B2"
[3, 2, 1, ..] -> "B3"
[4, 3, 2, 1, ..] -> "B4"
_ -> "Catchall"
"###
)
}