Skip to main content

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}