⭐️ A friendly language for building type-safe, scalable systems!

make static comparison smarter

authored by giacomocavalieri.me and committed by Louis Pilfold a9653d47 769f8e7d

+1 -9
compiler-core/src/ast/typed.rs
··· 913 913 } 914 914 915 915 #[must_use] 916 + // TODO)) is this the same as is_record_builder 916 917 pub fn is_record_constructor(&self) -> bool { 917 918 match self { 918 919 TypedExpr::Var { ··· 1048 1049 pub(crate) fn is_invalid(&self) -> bool { 1049 1050 match self { 1050 1051 TypedExpr::Invalid { .. } => true, 1051 - _ => false, 1052 - } 1053 - } 1054 - 1055 - pub(crate) fn is_literal_bool(&self) -> bool { 1056 - match self { 1057 - TypedExpr::Var { 1058 - constructor, name, .. 1059 - } if name == "True" || name == "False" => constructor.type_.is_bool(), 1060 1052 _ => false, 1061 1053 } 1062 1054 }
+259 -131
compiler-core/src/type_/expression.rs
··· 1761 1761 right: &TypedExpr, 1762 1762 location: SrcSpan, 1763 1763 ) { 1764 - let outcome = match (left, right) { 1765 - // Numbers are a bit trickier as we might have more kinds of 1766 - // comparisons that we can tell are never right. 1767 - (TypedExpr::Int { int_value: n, .. }, TypedExpr::Int { int_value: m, .. }) => { 1768 - match binop { 1769 - BinOp::Eq if n == m => Some(ComparisonOutcome::AlwaysSucceeds), 1770 - BinOp::Eq => Some(ComparisonOutcome::AlwaysFails), 1771 - 1772 - BinOp::NotEq if n != m => Some(ComparisonOutcome::AlwaysSucceeds), 1773 - BinOp::NotEq => Some(ComparisonOutcome::AlwaysFails), 1774 - 1775 - BinOp::LtInt if n < m => Some(ComparisonOutcome::AlwaysSucceeds), 1776 - BinOp::LtInt => Some(ComparisonOutcome::AlwaysFails), 1777 - 1778 - BinOp::LtEqInt if n <= m => Some(ComparisonOutcome::AlwaysSucceeds), 1779 - BinOp::LtEqInt => Some(ComparisonOutcome::AlwaysFails), 1780 - 1781 - BinOp::GtInt if n > m => Some(ComparisonOutcome::AlwaysSucceeds), 1782 - BinOp::GtInt => Some(ComparisonOutcome::AlwaysFails), 1783 - 1784 - BinOp::GtEqInt if n >= m => Some(ComparisonOutcome::AlwaysSucceeds), 1785 - BinOp::GtEqInt => Some(ComparisonOutcome::AlwaysFails), 1786 - 1787 - _ => None, 1788 - } 1789 - } 1790 - 1791 - // For other types of simple values we can only check for redundant 1792 - // equality checks. 1793 - (TypedExpr::String { value: one, .. }, TypedExpr::String { value: other, .. }) => { 1794 - match binop { 1795 - BinOp::Eq if one == other => Some(ComparisonOutcome::AlwaysSucceeds), 1796 - BinOp::Eq => Some(ComparisonOutcome::AlwaysFails), 1797 - BinOp::NotEq if one != other => Some(ComparisonOutcome::AlwaysSucceeds), 1798 - BinOp::NotEq => Some(ComparisonOutcome::AlwaysFails), 1799 - _ => None, 1800 - } 1801 - } 1802 - 1803 - // If two variables are the same then we know that the comparison 1804 - // will always succeed, but the opposite is not true: if the two 1805 - // variables are different we can't be sure the comparison will 1806 - // always fail. 1807 - (TypedExpr::Var { name: one, .. }, TypedExpr::Var { name: other, .. }) => { 1808 - if left.is_literal_bool() && right.is_literal_bool() { 1809 - match binop { 1810 - BinOp::Eq if one == other => Some(ComparisonOutcome::AlwaysSucceeds), 1811 - BinOp::Eq => Some(ComparisonOutcome::AlwaysFails), 1812 - BinOp::NotEq if one != other => Some(ComparisonOutcome::AlwaysSucceeds), 1813 - BinOp::NotEq => Some(ComparisonOutcome::AlwaysFails), 1814 - _ => None, 1815 - } 1816 - } else { 1817 - match binop { 1818 - BinOp::Eq if one == other => Some(ComparisonOutcome::AlwaysSucceeds), 1819 - BinOp::NotEq if one == other => Some(ComparisonOutcome::AlwaysFails), 1820 - _ => None, 1821 - } 1822 - } 1823 - } 1764 + let outcome = match (left, binop, right) { 1765 + (left, BinOp::Eq, right) => match static_compare(left, right) { 1766 + StaticComparison::CertainlyEqual => ComparisonOutcome::AlwaysSucceeds, 1767 + StaticComparison::CertainlyDifferent => ComparisonOutcome::AlwaysFails, 1768 + StaticComparison::CantTell => return, 1769 + }, 1824 1770 1825 - ( 1826 - TypedExpr::ModuleSelect { 1827 - constructor: constructor_one, 1828 - module_name: module_name_one, 1829 - .. 1830 - }, 1831 - TypedExpr::ModuleSelect { 1832 - constructor: constructor_other, 1833 - module_name: module_name_other, 1834 - .. 1835 - }, 1836 - ) => { 1837 - let equals = 1838 - module_name_one == module_name_other && constructor_one == constructor_other; 1839 - match binop { 1840 - BinOp::Eq if equals => Some(ComparisonOutcome::AlwaysSucceeds), 1841 - BinOp::NotEq if equals => Some(ComparisonOutcome::AlwaysFails), 1842 - _ => None, 1843 - } 1844 - } 1771 + (left, BinOp::NotEq, right) => match static_compare(left, right) { 1772 + StaticComparison::CertainlyEqual => ComparisonOutcome::AlwaysFails, 1773 + StaticComparison::CertainlyDifferent => ComparisonOutcome::AlwaysSucceeds, 1774 + StaticComparison::CantTell => return, 1775 + }, 1845 1776 1846 - // We can only compare two record selects if the selected record is 1847 - // exactly the same and is not obtained through a function call (that 1848 - // might be producing different records each time it's invoked). 1849 - // 1850 - // For example: 1851 - // ```gleam 1852 - // Wibble(1).wibble == Wibble(1).wibble 1853 - // // We can tell this always succeeds 1854 - // 1855 - // one.wibble != one.wibble 1856 - // // We can tell this always fails 1857 - // 1858 - // new().wibble == new().wibble 1859 - // // We're not sure if this will always succeed or fail 1860 - // ``` 1861 - // 1862 - ( 1863 - TypedExpr::RecordAccess { 1864 - record: record_one, 1865 - label: label_one, 1866 - .. 1867 - }, 1868 - TypedExpr::RecordAccess { 1869 - record: record_other, 1870 - label: label_other, 1871 - .. 1872 - }, 1873 - ) if label_one == label_other => { 1874 - match (binop, record_one.as_ref(), record_other.as_ref()) { 1875 - ( 1876 - BinOp::Eq, 1877 - TypedExpr::Var { name: one, .. }, 1878 - TypedExpr::Var { name: other, .. }, 1879 - ) if one == other => Some(ComparisonOutcome::AlwaysSucceeds), 1880 - ( 1881 - BinOp::NotEq, 1882 - TypedExpr::Var { name: one, .. }, 1883 - TypedExpr::Var { name: other, .. }, 1884 - ) if one == other => Some(ComparisonOutcome::AlwaysFails), 1885 - _ => None, 1777 + // We special handle int literals as there's other comparisons we 1778 + // might want to perform 1779 + (TypedExpr::Int { int_value: n, .. }, op, TypedExpr::Int { int_value: m, .. }) => { 1780 + match op { 1781 + BinOp::LtInt if n < m => ComparisonOutcome::AlwaysSucceeds, 1782 + BinOp::LtInt => ComparisonOutcome::AlwaysFails, 1783 + BinOp::LtEqInt if n <= m => ComparisonOutcome::AlwaysSucceeds, 1784 + BinOp::LtEqInt => ComparisonOutcome::AlwaysFails, 1785 + BinOp::GtInt if n > m => ComparisonOutcome::AlwaysSucceeds, 1786 + BinOp::GtInt => ComparisonOutcome::AlwaysFails, 1787 + BinOp::GtEqInt if n >= m => ComparisonOutcome::AlwaysSucceeds, 1788 + BinOp::GtEqInt => ComparisonOutcome::AlwaysFails, 1789 + _ => return, 1886 1790 } 1887 1791 } 1888 1792 1889 - // All other values we can't really compare them at compile time to 1890 - // make sure they're always the same or not. 1891 - // 1892 - // TODO: we could actualy do this for a wider range of expressions 1893 - // if we did constant folding! For example we could tell that 1894 - // `3 + 3 < 10` always succeeds. 1895 - _ => None, 1896 - }; 1897 - 1898 - let Some(outcome) = outcome else { 1899 - return; 1793 + _ => return, 1900 1794 }; 1901 1795 1902 1796 self.problems ··· 4925 4819 AlwaysFails, 4926 4820 AlwaysSucceeds, 4927 4821 } 4822 + 4823 + enum StaticComparison { 4824 + /// When we can statically tell that two values are going to be exactly the 4825 + /// same. 4826 + CertainlyEqual, 4827 + /// When we can statically tell that two values are not going to be the 4828 + /// same. 4829 + CertainlyDifferent, 4830 + /// When it's impossible to statically tell if two values are the same. 4831 + CantTell, 4832 + } 4833 + 4834 + fn static_compare(one: &TypedExpr, other: &TypedExpr) -> StaticComparison { 4835 + match (one, other) { 4836 + ( 4837 + TypedExpr::Var { 4838 + name: one, 4839 + constructor: constructor_one, 4840 + .. 4841 + }, 4842 + TypedExpr::Var { 4843 + name: other, 4844 + constructor: constructor_other, 4845 + .. 4846 + }, 4847 + ) => match (&constructor_one.variant, &constructor_other.variant) { 4848 + ( 4849 + ValueConstructorVariant::LocalVariable { .. }, 4850 + ValueConstructorVariant::LocalVariable { .. }, 4851 + ) 4852 + | ( 4853 + ValueConstructorVariant::ModuleConstant { .. }, 4854 + ValueConstructorVariant::ModuleConstant { .. }, 4855 + ) 4856 + | ( 4857 + ValueConstructorVariant::LocalConstant { .. }, 4858 + ValueConstructorVariant::LocalConstant { .. }, 4859 + ) 4860 + | (ValueConstructorVariant::Record { .. }, ValueConstructorVariant::Record { .. }) 4861 + if one == other => 4862 + { 4863 + StaticComparison::CertainlyEqual 4864 + } 4865 + 4866 + ( 4867 + ValueConstructorVariant::Record { 4868 + variant_index: index_one, 4869 + .. 4870 + }, 4871 + ValueConstructorVariant::Record { 4872 + variant_index: index_other, 4873 + .. 4874 + }, 4875 + ) if index_one != index_other => StaticComparison::CertainlyDifferent, 4876 + 4877 + ( 4878 + ValueConstructorVariant::LocalVariable { .. } 4879 + | ValueConstructorVariant::ModuleConstant { .. } 4880 + | ValueConstructorVariant::LocalConstant { .. } 4881 + | ValueConstructorVariant::ModuleFn { .. } 4882 + | ValueConstructorVariant::Record { .. }, 4883 + _, 4884 + ) => StaticComparison::CantTell, 4885 + }, 4886 + 4887 + (TypedExpr::Int { int_value: n, .. }, TypedExpr::Int { int_value: m, .. }) => { 4888 + if n == m { 4889 + StaticComparison::CertainlyEqual 4890 + } else { 4891 + StaticComparison::CertainlyDifferent 4892 + } 4893 + } 4894 + 4895 + (TypedExpr::Float { value: n, .. }, TypedExpr::Float { value: m, .. }) => { 4896 + if n == m { 4897 + StaticComparison::CertainlyEqual 4898 + } else { 4899 + StaticComparison::CertainlyDifferent 4900 + } 4901 + } 4902 + 4903 + (TypedExpr::String { value: one, .. }, TypedExpr::String { value: other, .. }) => { 4904 + if one == other { 4905 + StaticComparison::CertainlyEqual 4906 + } else { 4907 + StaticComparison::CertainlyDifferent 4908 + } 4909 + } 4910 + 4911 + (TypedExpr::NegateInt { value: one, .. }, TypedExpr::NegateInt { value: other, .. }) 4912 + | (TypedExpr::NegateBool { value: one, .. }, TypedExpr::NegateBool { value: other, .. }) => { 4913 + static_compare(one, other) 4914 + } 4915 + 4916 + ( 4917 + TypedExpr::List { 4918 + elements: elements_one, 4919 + tail: tail_one, 4920 + .. 4921 + }, 4922 + TypedExpr::List { 4923 + elements: elements_other, 4924 + tail: tail_other, 4925 + .. 4926 + }, 4927 + ) => { 4928 + match (tail_one, tail_other) { 4929 + (Some(one_tail), Some(other_tail)) => match static_compare(one_tail, other_tail) { 4930 + StaticComparison::CertainlyDifferent => { 4931 + return StaticComparison::CertainlyDifferent; 4932 + } 4933 + StaticComparison::CantTell => return StaticComparison::CantTell, 4934 + StaticComparison::CertainlyEqual => (), 4935 + }, 4936 + (None, Some(_)) | (Some(_), None) => return StaticComparison::CantTell, 4937 + (None, None) => (), 4938 + }; 4939 + 4940 + // If we can tell the two lists have a different number of items 4941 + // then we know it's never going to match. 4942 + if elements_one.len() != elements_other.len() { 4943 + return StaticComparison::CertainlyDifferent; 4944 + } 4945 + 4946 + let mut comparison = StaticComparison::CertainlyEqual; 4947 + for (one, other) in elements_one.iter().zip(elements_other.iter()) { 4948 + match static_compare(one, other) { 4949 + StaticComparison::CertainlyEqual => (), 4950 + StaticComparison::CertainlyDifferent => { 4951 + return StaticComparison::CertainlyDifferent; 4952 + } 4953 + StaticComparison::CantTell => comparison = StaticComparison::CantTell, 4954 + } 4955 + } 4956 + comparison 4957 + } 4958 + 4959 + ( 4960 + TypedExpr::Tuple { 4961 + elements: elements_one, 4962 + .. 4963 + }, 4964 + TypedExpr::Tuple { 4965 + elements: elements_other, 4966 + .. 4967 + }, 4968 + ) => { 4969 + let mut comparison = StaticComparison::CertainlyEqual; 4970 + for (one, other) in elements_one.iter().zip(elements_other.iter()) { 4971 + match static_compare(one, other) { 4972 + StaticComparison::CertainlyEqual => (), 4973 + StaticComparison::CertainlyDifferent => { 4974 + return StaticComparison::CertainlyDifferent; 4975 + } 4976 + StaticComparison::CantTell => comparison = StaticComparison::CantTell, 4977 + } 4978 + } 4979 + comparison 4980 + } 4981 + 4982 + ( 4983 + TypedExpr::ModuleSelect { 4984 + constructor: constructor_one, 4985 + module_name: module_name_one, 4986 + .. 4987 + }, 4988 + TypedExpr::ModuleSelect { 4989 + constructor: constructor_other, 4990 + module_name: module_name_other, 4991 + .. 4992 + }, 4993 + ) => { 4994 + if module_name_one == module_name_other && constructor_one == constructor_other { 4995 + StaticComparison::CertainlyEqual 4996 + } else { 4997 + StaticComparison::CantTell 4998 + } 4999 + } 5000 + 5001 + ( 5002 + TypedExpr::Call { 5003 + fun: fun_one, 5004 + args: args_one, 5005 + .. 5006 + }, 5007 + TypedExpr::Call { 5008 + fun: fun_other, 5009 + args: args_other, 5010 + .. 5011 + }, 5012 + ) => { 5013 + if fun_one.is_record_builder() && fun_other.is_record_builder() { 5014 + let mut comparison = StaticComparison::CertainlyEqual; 5015 + for (one, other) in args_one.iter().zip(args_other.iter()) { 5016 + match static_compare(&one.value, &other.value) { 5017 + StaticComparison::CertainlyEqual => (), 5018 + StaticComparison::CertainlyDifferent => { 5019 + return StaticComparison::CertainlyDifferent; 5020 + } 5021 + StaticComparison::CantTell => comparison = StaticComparison::CantTell, 5022 + } 5023 + } 5024 + comparison 5025 + } else { 5026 + StaticComparison::CantTell 5027 + } 5028 + } 5029 + 5030 + ( 5031 + TypedExpr::RecordAccess { 5032 + index: index_one, 5033 + record: record_one, 5034 + .. 5035 + }, 5036 + TypedExpr::RecordAccess { 5037 + index: index_other, 5038 + record: record_other, 5039 + .. 5040 + }, 5041 + ) => match static_compare(record_one, record_other) { 5042 + StaticComparison::CertainlyEqual if index_one == index_other => { 5043 + StaticComparison::CertainlyEqual 5044 + } 5045 + StaticComparison::CertainlyEqual 5046 + | StaticComparison::CertainlyDifferent 5047 + | StaticComparison::CantTell => StaticComparison::CantTell, 5048 + }, 5049 + 5050 + // TODO: For complex expressions we just give up, maybe in future we 5051 + // could be smarter and perform further comparisons but it sounds like 5052 + // there's no huge value in this. 5053 + (_, _) => StaticComparison::CantTell, 5054 + } 5055 + }
+15
compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__list_literals_redundant_comparison.snap
··· 1 + --- 2 + source: compiler-core/src/type_/tests/warnings.rs 3 + expression: "pub fn main(a, b) { [1] == [a, b(1)] }" 4 + --- 5 + ----- SOURCE CODE 6 + pub fn main(a, b) { [1] == [a, b(1)] } 7 + 8 + ----- WARNING 9 + warning: Redundant comparison 10 + ┌─ /src/warning/wrn.gleam:1:21 11 + 12 + 1 │ pub fn main(a, b) { [1] == [a, b(1)] } 13 + │ ^^^^^^^^^^^^^^^^ This is always `False` 14 + 15 + This comparison is redundant since it always fails.
+15
compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__list_literals_redundant_comparison_2.snap
··· 1 + --- 2 + source: compiler-core/src/type_/tests/warnings.rs 3 + expression: "pub fn main(a, b) { [1] != [a, b(1)] }" 4 + --- 5 + ----- SOURCE CODE 6 + pub fn main(a, b) { [1] != [a, b(1)] } 7 + 8 + ----- WARNING 9 + warning: Redundant comparison 10 + ┌─ /src/warning/wrn.gleam:1:21 11 + 12 + 1 │ pub fn main(a, b) { [1] != [a, b(1)] } 13 + │ ^^^^^^^^^^^^^^^^ This is always `True` 14 + 15 + This comparison is redundant since it always succeeds.
+15
compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__list_literals_redundant_comparison_3.snap
··· 1 + --- 2 + source: compiler-core/src/type_/tests/warnings.rs 3 + expression: "pub fn main() { [1] != [1] }" 4 + --- 5 + ----- SOURCE CODE 6 + pub fn main() { [1] != [1] } 7 + 8 + ----- WARNING 9 + warning: Redundant comparison 10 + ┌─ /src/warning/wrn.gleam:1:17 11 + 12 + 1 │ pub fn main() { [1] != [1] } 13 + │ ^^^^^^^^^^ This is always `False` 14 + 15 + This comparison is redundant since it always fails.
+15
compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__list_literals_redundant_comparison_4.snap
··· 1 + --- 2 + source: compiler-core/src/type_/tests/warnings.rs 3 + expression: "pub fn main(a) { [1, ..[1, a]] == [1, ..[1, a]] }" 4 + --- 5 + ----- SOURCE CODE 6 + pub fn main(a) { [1, ..[1, a]] == [1, ..[1, a]] } 7 + 8 + ----- WARNING 9 + warning: Redundant comparison 10 + ┌─ /src/warning/wrn.gleam:1:18 11 + 12 + 1 │ pub fn main(a) { [1, ..[1, a]] == [1, ..[1, a]] } 13 + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This is always `True` 14 + 15 + This comparison is redundant since it always succeeds.
+15
compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__list_literals_redundant_comparison_5.snap
··· 1 + --- 2 + source: compiler-core/src/type_/tests/warnings.rs 3 + expression: "pub fn main(a) { [1, ..a] == [1, ..a] }" 4 + --- 5 + ----- SOURCE CODE 6 + pub fn main(a) { [1, ..a] == [1, ..a] } 7 + 8 + ----- WARNING 9 + warning: Redundant comparison 10 + ┌─ /src/warning/wrn.gleam:1:18 11 + 12 + 1 │ pub fn main(a) { [1, ..a] == [1, ..a] } 13 + │ ^^^^^^^^^^^^^^^^^^^^ This is always `True` 14 + 15 + This comparison is redundant since it always succeeds.
+15
compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__list_literals_redundant_comparison_7.snap
··· 1 + --- 2 + source: compiler-core/src/type_/tests/warnings.rs 3 + expression: "pub fn main(a) { [a(1), 2] == [a(1), 3] }" 4 + --- 5 + ----- SOURCE CODE 6 + pub fn main(a) { [a(1), 2] == [a(1), 3] } 7 + 8 + ----- WARNING 9 + warning: Redundant comparison 10 + ┌─ /src/warning/wrn.gleam:1:18 11 + 12 + 1 │ pub fn main(a) { [a(1), 2] == [a(1), 3] } 13 + │ ^^^^^^^^^^^^^^^^^^^^^^ This is always `False` 14 + 15 + This comparison is redundant since it always fails.
+35
compiler-core/src/type_/tests/warnings.rs
··· 4014 4014 } 4015 4015 4016 4016 #[test] 4017 + fn list_literals_redundant_comparison() { 4018 + assert_warning!("pub fn main(a, b) { [1] == [a, b(1)] }"); 4019 + } 4020 + 4021 + #[test] 4022 + fn list_literals_redundant_comparison_2() { 4023 + assert_warning!("pub fn main(a, b) { [1] != [a, b(1)] }"); 4024 + } 4025 + 4026 + #[test] 4027 + fn list_literals_redundant_comparison_3() { 4028 + assert_warning!("pub fn main() { [1] != [1] }"); 4029 + } 4030 + 4031 + #[test] 4032 + fn list_literals_redundant_comparison_4() { 4033 + assert_warning!("pub fn main(a) { [1, ..[1, a]] == [1, ..[1, a]] }"); 4034 + } 4035 + 4036 + #[test] 4037 + fn list_literals_redundant_comparison_5() { 4038 + assert_warning!("pub fn main(a) { [1, ..a] == [1, ..a] }"); 4039 + } 4040 + 4041 + #[test] 4042 + fn list_literals_redundant_comparison_6() { 4043 + assert_no_warnings!("pub fn main(a) { [a(1)] == [a(1)] }"); 4044 + } 4045 + 4046 + #[test] 4047 + fn list_literals_redundant_comparison_7() { 4048 + assert_warning!("pub fn main(a) { [a(1), 2] == [a(1), 3] }"); 4049 + } 4050 + 4051 + #[test] 4017 4052 fn string_literals_redundant_comparison() { 4018 4053 assert_warning!("pub fn main() { \"wibble\" == \"wobble\" }"); 4019 4054 }