mz_expr/scalar/reduce/unary.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Post-order rewrites for `CallUnary` nodes.
11
12use mz_repr::{ReprColumnType, RowArena};
13
14use crate::scalar::func::{self, UnaryFunc, VariadicFunc};
15use crate::{Eval, MirScalarExpr};
16
17pub(super) fn reduce_call_unary(
18 e: &mut MirScalarExpr,
19 column_types: &[ReprColumnType],
20 temp_storage: &RowArena,
21) {
22 let MirScalarExpr::CallUnary { func, expr } = e else {
23 unreachable!()
24 };
25
26 // `jq` is not folded for the reason given on `contains_unfoldable`.
27 if expr.is_literal() && !matches!(func, UnaryFunc::Panic(_) | UnaryFunc::Jq(_)) {
28 *e = MirScalarExpr::literal(e.eval(&[], temp_storage), e.typ(column_types).scalar_type);
29 return;
30 }
31
32 // `f(g(x))` → `x` when `f` is a left inverse of `g`. Mind the contract:
33 // `g.inverse()` does not always return a mathematical inverse. Per its
34 // documentation it returns a left inverse — what this elimination needs —
35 // exactly when *`g` itself* preserves uniqueness; when only the *returned
36 // function* preserves uniqueness it is merely a right inverse (`g(f(y)) =
37 // y`, useful for moving casts across equalities, useless here). Hence the
38 // `preserves_uniqueness` check below is on `g`, and is load-bearing.
39 //
40 // Eliding the calls must also not change error or null behavior, so both
41 // functions must be infallible and propagate nulls. (This is what
42 // excludes e.g. `-(-x)` on integers, whose inner negation errors on the
43 // minimum value, and `int8(int4(x))`-style widenings, whose outer
44 // narrowing can error — while admitting `NOT(NOT(x))`, `~(~x)`,
45 // `reverse(reverse(x))`, numeric `-(-x)`, and infallible bijective cast
46 // roundtrips such as `bool::int4::bool`.)
47 if let MirScalarExpr::CallUnary {
48 func: inner_func,
49 expr: inner_expr,
50 } = &mut **expr
51 {
52 if inner_func.inverse().as_ref() == Some(func)
53 && inner_func.preserves_uniqueness()
54 && !inner_func.could_error()
55 && !func.could_error()
56 && inner_func.propagates_nulls()
57 && func.propagates_nulls()
58 {
59 let inner = inner_expr.take();
60 *e = inner;
61 return;
62 }
63 }
64
65 // `RecordGet(i)(RecordCreate(args))` → `args[i]`.
66 if let UnaryFunc::RecordGet(func::RecordGet(i)) = *func {
67 if let MirScalarExpr::CallVariadic {
68 func: VariadicFunc::RecordCreate(..),
69 exprs,
70 } = &mut **expr
71 {
72 *e = exprs.swap_remove(i);
73 }
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use mz_repr::ReprScalarType;
80
81 use crate::MirScalarExpr;
82 use crate::scalar::func;
83
84 #[mz_ore::test]
85 fn involution_folds() {
86 // `f(g(x))` folds to `x` when `g` names `f` as its inverse and both
87 // calls are infallible.
88 let col = || MirScalarExpr::column(0);
89
90 let bool_col = [ReprScalarType::Bool.nullable(true)];
91 let mut e = col().call_unary(func::Not).call_unary(func::Not);
92 e.reduce(&bool_col);
93 assert_eq!(e, col());
94
95 let int32 = [ReprScalarType::Int32.nullable(true)];
96 let mut e = col()
97 .call_unary(func::BitNotInt32)
98 .call_unary(func::BitNotInt32);
99 e.reduce(&int32);
100 assert_eq!(e, col());
101
102 let string = [ReprScalarType::String.nullable(true)];
103 let mut e = col().call_unary(func::Reverse).call_unary(func::Reverse);
104 e.reduce(&string);
105 assert_eq!(e, col());
106
107 // Numeric negation is infallible, so its double application folds.
108 let numeric = [ReprScalarType::Numeric {}.nullable(true)];
109 let mut e = col()
110 .call_unary(func::NegNumeric)
111 .call_unary(func::NegNumeric);
112 e.reduce(&numeric);
113 assert_eq!(e, col());
114
115 // An infallible bijective cast roundtrip folds: `int4(bool)` declares
116 // `bool(int4)` as its (left) inverse, and both directions are total.
117 let bool_col = [ReprScalarType::Bool.nullable(true)];
118 let mut e = col()
119 .call_unary(func::CastBoolToInt32)
120 .call_unary(func::CastInt32ToBool);
121 e.reduce(&bool_col);
122 assert_eq!(e, col());
123
124 // Integer negation errors on the minimum value, so eliding the double
125 // negation would suppress that error; it must not fold.
126 let int64 = [ReprScalarType::Int64.nullable(true)];
127 let mut e = col().call_unary(func::NegInt64).call_unary(func::NegInt64);
128 e.reduce(&int64);
129 assert_ne!(e, col());
130
131 // A widening cast roundtrip stays: the outer narrowing can error in
132 // general, and the gate does not reason about images.
133 let int32 = [ReprScalarType::Int32.nullable(true)];
134 let mut e = col()
135 .call_unary(func::CastInt32ToInt64)
136 .call_unary(func::CastInt64ToInt32);
137 e.reduce(&int32);
138 assert_ne!(e, col());
139 }
140}