diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000000..83d28915e6bbe --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "attribution": { + "commit": "T.E.A de Souza", + "pr": "T.E.A de Souza" + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "git config user.name \"T.E.A de Souza\" && git config user.email \"tea.desouza@gmail.com\"" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcdde9c91e8da..d9d059ebc7ea2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,9 @@ on: - automation/bors/auto - automation/bors/try - try-perf - pull_request: - branches: - - "**" + # pull_request trigger disabled — this fork uses decisive-ci.yml instead. + # The upstream CI matrix requires rust-lang/rust infrastructure (sccache, S3, bors) + # and will burn CI minutes or get cancelled on forks. permissions: contents: read diff --git a/.github/workflows/decisive-ci.yml b/.github/workflows/decisive-ci.yml new file mode 100644 index 0000000000000..96fe8584ab6ce --- /dev/null +++ b/.github/workflows/decisive-ci.yml @@ -0,0 +1,101 @@ +name: Decisive Trait CI + +on: + push: + tags: [ 'v*' ] + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + build-and-test: + name: Build stage1 compiler & run decisive tests + runs-on: ubuntu-latest + timeout-minutes: 240 + + steps: + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc* + df -h / + + - name: Checkout compiler fork + uses: actions/checkout@v4 + with: + fetch-depth: 2 + submodules: recursive + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake ninja-build python3 curl pkg-config libssl-dev + + - name: Configure compiler build + run: | + cat > bootstrap.toml << 'TOML' + change-id = 153143 + profile = "compiler" + + [llvm] + download-ci-llvm = false + assertions = false + + [rust] + debug-logging = false + debug-assertions = false + incremental = false + lto = "off" + download-rustc = false + TOML + + - name: Cache stage0 and LLVM + uses: actions/cache@v4 + with: + path: | + build/cache + build/x86_64-unknown-linux-gnu/llvm + key: stage0-llvm-${{ hashFiles('src/stage0', 'src/llvm-project/llvm/CMakeLists.txt') }} + restore-keys: stage0-llvm- + + - name: Build stage1 compiler and stdlib + run: python3 x.py build library --stage 1 + + - name: Run compiler tests (decisive trait) + run: python3 x.py test tests/ui --stage 1 --test-args "decisive" + + - name: Package stage1 toolchain + run: | + STAGE1=build/x86_64-unknown-linux-gnu/stage1 + DEST=rustc-decisive-x86_64-unknown-linux-gnu + mkdir -p "$DEST" + cp -a "$STAGE1/bin" "$DEST/" + cp -a "$STAGE1/lib" "$DEST/" + echo "decisive-stage1 ($(git rev-parse --short HEAD))" > "$DEST/VERSION" + tar czf "$DEST.tar.gz" "$DEST" + echo "TOOLCHAIN_ARCHIVE=$DEST.tar.gz" >> "$GITHUB_ENV" + echo "TOOLCHAIN_NAME=$DEST" >> "$GITHUB_ENV" + + - name: Upload toolchain artifact + uses: actions/upload-artifact@v4 + with: + name: rustc-decisive-x86_64-unknown-linux-gnu + path: ${{ env.TOOLCHAIN_ARCHIVE }} + retention-days: 90 + + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: ${{ env.TOOLCHAIN_ARCHIVE }} + body: | + ## Rust with Decisive Trait + + Custom rustc build with the `Decisive` trait, enabling user-defined + short-circuiting `&&` and `||` operators. + + ### Install + ```bash + tar xzf ${{ env.TOOLCHAIN_NAME }}.tar.gz + rustup toolchain link decisive ./${{ env.TOOLCHAIN_NAME }} + cargo +decisive build + ``` diff --git a/README.md b/README.md index ed35016eb3a23..65cebbe8b770b 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,125 @@ +> Fork of [rust-lang/rust](https://github.com/rust-lang/rust). See the [original README](https://github.com/rust-lang/rust/blob/master/README.md). +
- - - - The Rust Programming Language: A language empowering everyone to build reliable and efficient software - - -[Website][Rust] | [Getting started] | [Learn] | [Documentation] | [Contributing] + Rust - Shorted
-This is the main source code repository for [Rust]. It contains the compiler, -standard library, and documentation. +# Overloadable Short-Circuiting Operators for Rust + +This public fork `rust-lang/rust` adds a `Decisive` trait to `core::ops`, enabling user-defined types to participate in short-circuiting `&&` and `||` expressions. The mechanism is general-purpose (three-valued logic, fuzzy logic, option types, etc.). + +The key change is at [7a510a6](https://github.com/eelstork/rust/commit/7a510a641535e0c1c5acc7dfef41e04b4df9ada5) + +## Community discussions + +Making `&&` and `||` overloadable has been discussed several times within the Rust ecosystem: + +- **[rust-lang/libs-team #144](https://github.com/rust-lang/libs-team/issues/144)** — *Traits for short-circuiting `||` and `&&`*, proposing `And` and `Or` traits using `ControlFlow` for the short-circuit decision. Originally suggested by @scottmcm on Zulip. +- **[rust-lang/rfcs #2722](https://github.com/rust-lang/rfcs/pull/2722)** — @Nokel81's RFC proposing `LogicalOr` and `LogicalAnd` traits with a `ShortCircuit` enum. +- **[Rust Internals: Pre-RFC (2019)](https://internals.rust-lang.org/t/pre-rfc-overload-short-curcuits/10460)** — Early discussion exploring `BoolOr`/`BoolAnd` trait shapes and whether "truthiness" should be a separate trait. +- **[Rust Internals: A different way to override `||` and `&&` (2021)](https://internals.rust-lang.org/t/a-different-way-to-override-and/14627)** — Follow-up reacting to RFC 2722's design. + +Rust's own `std::ops` documentation [acknowledges the gap](https://doc.rust-lang.org/std/ops/index.html): `&&` and `||` are not currently overloadable because their short-circuiting semantics don't fit the standard trait-as-function-call model. + +This fork takes a simpler approach than those proposals — a single `Decisive` trait with `is_true()` / `is_false()` methods, combined with existing `BitAnd` / `BitOr` — closer to how C# solves the same problem via `operator true`, `operator false`, `operator &`, and `operator |`. + +## Re: Classic Behavior Trees and Correctness + +The design implemented here enables stateless behavior trees via Active Logic, convergent with Colledanchise and Ögren in [*Behavior Trees in Robotics and AI: An Introduction*](https://arxiv.org/abs/1709.00084) (CRC Press, 2018). The central argument is that BTs derive their formal properties — reactivity, modularity, and analyzability for safety and liveness — from re-evaluating the tree from the root at every tick. Nodes that cache a "running" status and expect to be resumed introduce stale state, which undermines exactly the reactive guarantees that motivated adopting BTs over finite state machines. + +Many popular BT implementations (Unreal's BT system, BehaviorTree.CPP, Unity Behavior) lean on persistent running state and blackboards. These are practical choices for specific domains, but they trade away the formal properties that Colledanchise and Ögren analyze. This fork targets the stateless end of the spectrum. + +## What Changed -[Rust]: https://www.rust-lang.org/ -[Getting Started]: https://www.rust-lang.org/learn/get-started -[Learn]: https://www.rust-lang.org/learn -[Documentation]: https://www.rust-lang.org/learn#learn-use -[Contributing]: CONTRIBUTING.md +The `Decisive` trait is gated behind `#![feature(decisive_trait)]`. The change touches 18 files: -## Why Rust? +**Library:** -- **Performance:** Fast and memory-efficient, suitable for critical services, embedded devices, and easily integrated with other languages. +- `library/core/src/ops/decisive.rs` — The `Decisive` trait with `is_true()` and `is_false()` methods, plus `impl Decisive for bool`. -- **Reliability:** Our rich type system and ownership model ensure memory and thread safety, reducing bugs at compile-time. +**Compiler:** -- **Productivity:** Comprehensive documentation, a compiler committed to providing great diagnostics, and advanced tooling including package manager and build tool ([Cargo]), auto-formatter ([rustfmt]), linter ([Clippy]) and editor support ([rust-analyzer]). +- `rustc_hir_typeck/src/op.rs` — When `&&`/`||` operands aren't `bool`, the type checker now accepts types implementing `Decisive + BitAnd` / `Decisive + BitOr`. +- `rustc_middle/src/thir.rs` — New `OverloadedLogicalOp` variant in the THIR. +- `rustc_mir_build/src/builder/expr/into.rs` — MIR lowering desugars to: evaluate LHS → call `is_false` / `is_true` → branch → short-circuit (return LHS) or continue (`BitAnd::bitand` / `BitOr::bitor`). -[Cargo]: https://github.com/rust-lang/cargo -[rustfmt]: https://github.com/rust-lang/rustfmt -[Clippy]: https://github.com/rust-lang/rust-clippy -[rust-analyzer]: https://github.com/rust-lang/rust-analyzer +**Tests:** -## Quick Start +- New `decisive-trait.rs` test passes — verifies short-circuiting, chaining, and `bool` backward compatibility. +- All 56 existing `binop/` tests pass. +- All 44 existing `overloaded/` tests pass. -Read ["Installation"] from [The Book]. +## Desugaring -["Installation"]: https://doc.rust-lang.org/book/ch01-01-installation.html -[The Book]: https://doc.rust-lang.org/book/index.html +```rust +// a && b desugars to: +{ + let lhs = a; + if Decisive::is_false(&lhs) { lhs } else { BitAnd::bitand(lhs, b) } +} -## Installing from Source +// a || b desugars to: +{ + let lhs = a; + if Decisive::is_true(&lhs) { lhs } else { BitOr::bitor(lhs, b) } +} +``` -If you really want to install from source (though this is not recommended), see -[INSTALL.md](INSTALL.md). +For `bool`, this is a no-op — `Decisive` for `bool` is trivially `is_true = *self`, `is_false = !*self`, and `bool` already implements `BitAnd` and `BitOr`. -## Getting Help +## Usage -See https://www.rust-lang.org/community for a list of chat platforms and forums. +```rust +#![feature(decisive_trait)] +use std::ops::{BitAnd, BitOr, Decisive}; -## Contributing +#[derive(Clone, Copy, PartialEq)] +struct Status(i8); -See [CONTRIBUTING.md](CONTRIBUTING.md). +const DONE: Status = Status(1); +const CONT: Status = Status(0); +const FAIL: Status = Status(-1); -For a detailed explanation of the compiler's architecture and how to begin contributing, see the [rustc-dev-guide](https://rustc-dev-guide.rust-lang.org/). +impl Decisive for Status { + fn is_true(&self) -> bool { self.0 != -1 } // not failing → short-circuits || + fn is_false(&self) -> bool { self.0 != 1 } // not complete → short-circuits && +} -## License +impl BitAnd for Status { + type Output = Status; + fn bitand(self, rhs: Status) -> Status { rhs } +} -Rust is primarily distributed under the terms of both the MIT license and the -Apache License (Version 2.0), with portions covered by various BSD-like -licenses. +impl BitOr for Status { + type Output = Status; + fn bitor(self, rhs: Status) -> Status { rhs } +} -See [LICENSE-APACHE](LICENSE-APACHE), [LICENSE-MIT](LICENSE-MIT), and -[COPYRIGHT](COPYRIGHT) for details. +// Sequence — short-circuits on first non-complete: +fn brew_coffee(&mut self) -> Status { + self.pour_water() + && self.heat_kettle() + && self.pour_grounds() + && self.wait_for_boil() + && self.pour_hot_water() +} -## Trademark +// Selector — short-circuits on first non-failing: +fn find_drink(&mut self) -> Status { + self.try_coffee() || self.try_tea() || self.try_water() +} +``` -[The Rust Foundation][rust-foundation] owns and protects the Rust and Cargo -trademarks and logos (the "Rust Trademarks"). +## Why Not Alternatives -If you want to use these names or brands, please read the -[Rust language trademark policy][trademark-policy]. +| Approach | Problem | +|---|---| +| `&` / `\|` operators | No short-circuiting — both sides always evaluate | +| Macros `seq!(a, b, c)` | Not native syntax, poor composability | +| Method chaining `.and_then()` | Verbose, requires closures | +| `?` operator via custom `Try` | Early-return semantics, not expression-level short-circuiting | -Third-party logos may be subject to third-party copyrights and trademarks. See -[Licenses][policies-licenses] for details. +## References -[rust-foundation]: https://rustfoundation.org/ -[trademark-policy]: https://rustfoundation.org/policy/rust-trademark-policy/ -[policies-licenses]: https://www.rust-lang.org/policies/licenses +- Colledanchise, M. and Ögren, P. (2018). *Behavior Trees in Robotics and AI: An Introduction*. CRC Press. [arXiv:1709.00084](https://arxiv.org/abs/1709.00084) +- de Souza, T. (2020). *Implementing Behavior Trees using Three-Valued Logic*. [arXiv:2011.03835](https://arxiv.org/abs/2011.03835) diff --git a/RUST-SHORTED.png b/RUST-SHORTED.png new file mode 100644 index 0000000000000..6e9a9e9d341cf Binary files /dev/null and b/RUST-SHORTED.png differ diff --git a/compiler/rustc_hir/src/lang_items.rs b/compiler/rustc_hir/src/lang_items.rs index 80f0af9d663cb..461024b6268b4 100644 --- a/compiler/rustc_hir/src/lang_items.rs +++ b/compiler/rustc_hir/src/lang_items.rs @@ -203,6 +203,7 @@ language_item_table! { BitXor, sym::bitxor, bitxor_trait, Target::Trait, GenericRequirement::Exact(1); BitAnd, sym::bitand, bitand_trait, Target::Trait, GenericRequirement::Exact(1); BitOr, sym::bitor, bitor_trait, Target::Trait, GenericRequirement::Exact(1); + Decisive, sym::decisive, decisive_trait, Target::Trait, GenericRequirement::Exact(0); Shl, sym::shl, shl_trait, Target::Trait, GenericRequirement::Exact(1); Shr, sym::shr, shr_trait, Target::Trait, GenericRequirement::Exact(1); AddAssign, sym::add_assign, add_assign_trait, Target::Trait, GenericRequirement::Exact(1); diff --git a/compiler/rustc_hir_typeck/src/op.rs b/compiler/rustc_hir_typeck/src/op.rs index cf61728f7c2a3..cb69a11654202 100644 --- a/compiler/rustc_hir_typeck/src/op.rs +++ b/compiler/rustc_hir_typeck/src/op.rs @@ -106,15 +106,107 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { match BinOpCategory::from(op.node) { BinOpCategory::Shortcircuit => { - // && and || are a simple case. - self.check_expr_coercible_to_type(lhs_expr, tcx.types.bool, None); - let lhs_diverges = self.diverges.get(); - self.check_expr_coercible_to_type(rhs_expr, tcx.types.bool, None); + // && and || work with bool by default, or with types that + // implement Decisive + BitAnd/BitOr for overloaded short-circuiting. + let lhs_ty = self.check_expr(lhs_expr); + let lhs_ty = self.resolve_vars_with_obligations(lhs_ty); + + if lhs_ty.references_error() + || lhs_ty.is_never() + || lhs_ty.is_ty_var() + || self.may_coerce(lhs_ty, tcx.types.bool) + { + // Standard bool path. + self.demand_coerce( + lhs_expr, + lhs_ty, + tcx.types.bool, + Some(rhs_expr), + AllowTwoPhase::No, + ); + let lhs_diverges = self.diverges.get(); + self.check_expr_coercible_to_type(rhs_expr, tcx.types.bool, None); - // Depending on the LHS' value, the RHS can never execute. - self.diverges.set(lhs_diverges); + // Depending on the LHS' value, the RHS can never execute. + self.diverges.set(lhs_diverges); - tcx.types.bool + tcx.types.bool + } else if let Some(decisive_did) = tcx.lang_items().decisive_trait() + && self + .type_implements_trait(decisive_did, [lhs_ty], self.param_env) + .may_apply() + { + // Overloaded Decisive path: map && to BitAnd, || to BitOr. + let lhs_diverges = self.diverges.get(); + let rhs_ty_var = self.next_ty_var(rhs_expr.span); + let (opname, trait_did) = match op.node { + hir::BinOpKind::And => (sym::bitand, tcx.lang_items().bitand_trait()), + hir::BinOpKind::Or => (sym::bitor, tcx.lang_items().bitor_trait()), + _ => unreachable!(), + }; + let result = self.lookup_op_method( + (lhs_expr, lhs_ty), + Some((rhs_expr, rhs_ty_var)), + (opname, trait_did), + op.span, + expected, + ); + let rhs_ty = self.check_expr_coercible_to_type_or_error( + rhs_expr, + rhs_ty_var, + Some(lhs_expr), + |_, _| {}, + ); + let rhs_ty = self.resolve_vars_with_obligations(rhs_ty); + + // Depending on the LHS' value, the RHS can never execute. + self.diverges.set(lhs_diverges); + + match result { + Ok(method) => { + self.write_method_call_and_enforce_effects( + expr.hir_id, + expr.span, + method, + ); + method.sig.output() + } + Err(_) if lhs_ty.references_error() || rhs_ty.references_error() => { + Ty::new_misc_error(self.tcx) + } + Err(errors) => { + let op_str = op.node.as_str(); + let mut path = None; + let lhs_str = self.tcx.short_string(lhs_ty, &mut path); + let mut err = struct_span_code_err!( + self.dcx(), + expr.span, + E0369, + "binary operation `{}` cannot be applied to type `{}`", + op_str, + lhs_str, + ); + self.note_unmet_impls_on_type(&mut err, &errors, false); + *err.long_ty_path() = path; + err.emit(); + Ty::new_misc_error(self.tcx) + } + } + } else { + // Type is not bool and does not implement Decisive. + // Fall back to original bool error. + self.demand_coerce( + lhs_expr, + lhs_ty, + tcx.types.bool, + Some(rhs_expr), + AllowTwoPhase::No, + ); + let lhs_diverges = self.diverges.get(); + self.check_expr_coercible_to_type(rhs_expr, tcx.types.bool, None); + self.diverges.set(lhs_diverges); + tcx.types.bool + } } _ => { // Otherwise, we always treat operators as if they are @@ -1077,9 +1169,8 @@ fn lang_item_for_binop(tcx: TyCtxt<'_>, op: Op) -> (Symbol, Option (sym::gt, lang.partial_ord_trait()), hir::BinOpKind::Eq => (sym::eq, lang.eq_trait()), hir::BinOpKind::Ne => (sym::ne, lang.eq_trait()), - hir::BinOpKind::And | hir::BinOpKind::Or => { - bug!("&& and || are not overloadable") - } + hir::BinOpKind::And => (sym::bitand, lang.bitand_trait()), + hir::BinOpKind::Or => (sym::bitor, lang.bitor_trait()), }, } } @@ -1108,7 +1199,7 @@ pub(crate) fn contains_let_in_chain(expr: &hir::Expr<'_>) -> bool { // with respect to the builtin operations supported. #[derive(Clone, Copy)] enum BinOpCategory { - /// &&, || -- cannot be overridden + /// &&, || -- overridable via Decisive + BitAnd/BitOr Shortcircuit, /// <<, >> -- when shifting a single integer, rhs can be any diff --git a/compiler/rustc_middle/src/thir.rs b/compiler/rustc_middle/src/thir.rs index 51f33691bb7dd..ba4bc840f70e5 100644 --- a/compiler/rustc_middle/src/thir.rs +++ b/compiler/rustc_middle/src/thir.rs @@ -324,6 +324,15 @@ pub enum ExprKind<'tcx> { lhs: ExprId, rhs: ExprId, }, + /// An overloaded logical operation (`&&` or `||`) on a non-bool type + /// that implements `Decisive` + `BitAnd`/`BitOr`. + /// `bitop_fun` is the resolved `BitAnd::bitand` or `BitOr::bitor` callee. + OverloadedLogicalOp { + op: LogicalOp, + lhs: ExprId, + rhs: ExprId, + bitop_fun: ExprId, + }, /// A *non-overloaded* unary operation. Note that here the deref (`*`) /// operator is represented by `ExprKind::Deref`. Unary { diff --git a/compiler/rustc_middle/src/thir/visit.rs b/compiler/rustc_middle/src/thir/visit.rs index aa1b6b1663bfd..97aa0ac22e3b4 100644 --- a/compiler/rustc_middle/src/thir/visit.rs +++ b/compiler/rustc_middle/src/thir/visit.rs @@ -69,6 +69,11 @@ pub fn walk_expr<'thir, 'tcx: 'thir, V: Visitor<'thir, 'tcx>>( visitor.visit_expr(&visitor.thir()[lhs]); visitor.visit_expr(&visitor.thir()[rhs]); } + OverloadedLogicalOp { lhs, rhs, op: _, bitop_fun } => { + visitor.visit_expr(&visitor.thir()[lhs]); + visitor.visit_expr(&visitor.thir()[rhs]); + visitor.visit_expr(&visitor.thir()[bitop_fun]); + } Unary { arg, op: _ } => visitor.visit_expr(&visitor.thir()[arg]), Cast { source } => visitor.visit_expr(&visitor.thir()[source]), Use { source } => visitor.visit_expr(&visitor.thir()[source]), diff --git a/compiler/rustc_mir_build/src/builder/coverageinfo.rs b/compiler/rustc_mir_build/src/builder/coverageinfo.rs index 2e29600c9339b..64782ad29b2fe 100644 --- a/compiler/rustc_mir_build/src/builder/coverageinfo.rs +++ b/compiler/rustc_mir_build/src/builder/coverageinfo.rs @@ -197,7 +197,9 @@ impl<'tcx> Builder<'_, 'tcx> { } // If the expression is a lazy logical op, it will naturally get branch // coverage as part of its normal lowering, so we can disregard it here. - if let ExprKind::LogicalOp { .. } = self.thir[expr_id].kind { + if let ExprKind::LogicalOp { .. } | ExprKind::OverloadedLogicalOp { .. } = + self.thir[expr_id].kind + { return; } diff --git a/compiler/rustc_mir_build/src/builder/expr/as_place.rs b/compiler/rustc_mir_build/src/builder/expr/as_place.rs index b95b565322f18..85ccf38ed81a3 100644 --- a/compiler/rustc_mir_build/src/builder/expr/as_place.rs +++ b/compiler/rustc_mir_build/src/builder/expr/as_place.rs @@ -551,6 +551,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { | ExprKind::Unary { .. } | ExprKind::Binary { .. } | ExprKind::LogicalOp { .. } + | ExprKind::OverloadedLogicalOp { .. } | ExprKind::Cast { .. } | ExprKind::Use { .. } | ExprKind::NeverToAny { .. } diff --git a/compiler/rustc_mir_build/src/builder/expr/as_rvalue.rs b/compiler/rustc_mir_build/src/builder/expr/as_rvalue.rs index a1b41510f1e1c..bd3e6caf37518 100644 --- a/compiler/rustc_mir_build/src/builder/expr/as_rvalue.rs +++ b/compiler/rustc_mir_build/src/builder/expr/as_rvalue.rs @@ -389,6 +389,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { | ExprKind::Loop { .. } | ExprKind::LoopMatch { .. } | ExprKind::LogicalOp { .. } + | ExprKind::OverloadedLogicalOp { .. } | ExprKind::Call { .. } | ExprKind::Field { .. } | ExprKind::Let { .. } diff --git a/compiler/rustc_mir_build/src/builder/expr/category.rs b/compiler/rustc_mir_build/src/builder/expr/category.rs index 5404d9800c3f5..f5a846f9cd336 100644 --- a/compiler/rustc_mir_build/src/builder/expr/category.rs +++ b/compiler/rustc_mir_build/src/builder/expr/category.rs @@ -46,6 +46,7 @@ impl Category { | ExprKind::ValueUnwrapUnsafeBinder { .. } => Some(Category::Place), ExprKind::LogicalOp { .. } + | ExprKind::OverloadedLogicalOp { .. } | ExprKind::Match { .. } | ExprKind::If { .. } | ExprKind::Let { .. } diff --git a/compiler/rustc_mir_build/src/builder/expr/into.rs b/compiler/rustc_mir_build/src/builder/expr/into.rs index 446b2939e3705..bd9e3da537287 100644 --- a/compiler/rustc_mir_build/src/builder/expr/into.rs +++ b/compiler/rustc_mir_build/src/builder/expr/into.rs @@ -204,6 +204,130 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { this.cfg.goto(short_circuit, source_info, target); target.unit() } + ExprKind::OverloadedLogicalOp { op, lhs, rhs, bitop_fun } => { + // Desugaring for `a && b` on type T: Decisive + BitAnd: + // let lhs_tmp: T = a; + // let is_short: bool = Decisive::is_false(&lhs_tmp); + // if is_short { lhs_tmp } else { BitAnd::bitand(lhs_tmp, b) } + // + // For `a || b` on type T: Decisive + BitOr: + // let lhs_tmp: T = a; + // let is_short: bool = Decisive::is_true(&lhs_tmp); + // if is_short { lhs_tmp } else { BitOr::bitor(lhs_tmp, b) } + + let lhs_ty = this.thir[lhs].ty; + + // Step 1: Evaluate LHS into a temp. + let lhs_place = this.temp(lhs_ty, expr_span); + unpack!(block = this.expr_into_dest(lhs_place, block, lhs)); + + // Step 2: Create a reference to the LHS temp for calling is_true/is_false. + let ref_ty = + Ty::new_imm_ref(this.tcx, this.tcx.lifetimes.re_erased, lhs_ty); + let ref_place = this.temp(ref_ty, expr_span); + this.cfg.push_assign( + block, + source_info, + ref_place, + Rvalue::Ref( + this.tcx.lifetimes.re_erased, + BorrowKind::Shared, + lhs_place, + ), + ); + + // Step 3: Call Decisive::is_false (for &&) or Decisive::is_true (for ||). + let decisive_trait = + this.tcx.require_lang_item(LangItem::Decisive, expr_span); + let decisive_items = this.tcx.associated_item_def_ids(decisive_trait); + // is_true is [0], is_false is [1] (based on definition order in decisive.rs) + let decisive_fn_def_id = match op { + LogicalOp::And => decisive_items[1], // is_false + LogicalOp::Or => decisive_items[0], // is_true + }; + let decisive_func = Operand::function_handle( + this.tcx, + decisive_fn_def_id, + [lhs_ty.into()], + expr_span, + ); + + let bool_ty = this.tcx.types.bool; + let bool_place = this.temp(bool_ty, expr_span); + let after_decisive = this.cfg.start_new_block(); + this.cfg.terminate( + block, + source_info, + TerminatorKind::Call { + func: decisive_func, + args: [Spanned { + node: Operand::Move(ref_place), + span: DUMMY_SP, + }] + .into(), + destination: bool_place, + target: Some(after_decisive), + unwind: UnwindAction::Continue, + call_source: CallSource::OverloadedOperator, + fn_span: expr_span, + }, + ); + + // Step 4: Branch on the bool result. + let short_circuit_block = this.cfg.start_new_block(); + let mut continue_block = this.cfg.start_new_block(); + let term = TerminatorKind::if_( + Operand::Move(bool_place), + short_circuit_block, + continue_block, + ); + this.cfg.terminate(after_decisive, source_info, term); + + // Step 5: Short-circuit path: copy/move LHS to destination. + this.cfg.push_assign( + short_circuit_block, + source_info, + destination, + Rvalue::Use(Operand::Move(lhs_place)), + ); + + // Step 6: Continue path: call BitAnd::bitand(lhs, rhs) or BitOr::bitor(lhs, rhs). + let bitop_operand = unpack!( + continue_block = this.as_local_operand(continue_block, bitop_fun) + ); + + // Evaluate RHS as an operand. + let rhs_operand = + unpack!(continue_block = this.as_local_operand(continue_block, rhs)); + + let after_bitop = this.cfg.start_new_block(); + this.cfg.terminate( + continue_block, + source_info, + TerminatorKind::Call { + func: bitop_operand, + args: [ + Spanned { + node: Operand::Move(lhs_place), + span: DUMMY_SP, + }, + Spanned { node: rhs_operand, span: DUMMY_SP }, + ] + .into(), + destination, + target: Some(after_bitop), + unwind: UnwindAction::Continue, + call_source: CallSource::OverloadedOperator, + fn_span: expr_span, + }, + ); + + // Step 7: Join paths. + let join_block = this.cfg.start_new_block(); + this.cfg.goto(short_circuit_block, source_info, join_block); + this.cfg.goto(after_bitop, source_info, join_block); + join_block.unit() + } ExprKind::Loop { body } => { // [block] // | diff --git a/compiler/rustc_mir_build/src/check_unsafety.rs b/compiler/rustc_mir_build/src/check_unsafety.rs index aed0c6e6085d3..92ebd591aa0ff 100644 --- a/compiler/rustc_mir_build/src/check_unsafety.rs +++ b/compiler/rustc_mir_build/src/check_unsafety.rs @@ -450,6 +450,7 @@ impl<'a, 'tcx> Visitor<'a, 'tcx> for UnsafetyVisitor<'a, 'tcx> { | ExprKind::If { .. } | ExprKind::InlineAsm { .. } | ExprKind::LogicalOp { .. } + | ExprKind::OverloadedLogicalOp { .. } | ExprKind::Use { .. } => { // We don't need to save the old value and restore it // because all the place expressions can't have more diff --git a/compiler/rustc_mir_build/src/thir/cx/expr.rs b/compiler/rustc_mir_build/src/thir/cx/expr.rs index b79bda2044058..480218a3e3489 100644 --- a/compiler/rustc_mir_build/src/thir/cx/expr.rs +++ b/compiler/rustc_mir_build/src/thir/cx/expr.rs @@ -575,23 +575,39 @@ impl<'tcx> ThirBuildCx<'tcx> { hir::ExprKind::Lit(lit) => ExprKind::Literal { lit, neg: false }, hir::ExprKind::Binary(op, lhs, rhs) => { - if self.typeck_results.is_method_call(expr) { - let lhs = self.mirror_expr(lhs); - let rhs = self.mirror_expr(rhs); - self.overloaded_operator(expr, Box::new([lhs, rhs])) - } else { - match op.node { - hir::BinOpKind::And => ExprKind::LogicalOp { - op: LogicalOp::And, - lhs: self.mirror_expr(lhs), - rhs: self.mirror_expr(rhs), - }, - hir::BinOpKind::Or => ExprKind::LogicalOp { - op: LogicalOp::Or, - lhs: self.mirror_expr(lhs), - rhs: self.mirror_expr(rhs), - }, - _ => { + match op.node { + // && and || are handled specially: they are either + // bool LogicalOp or overloaded via Decisive + BitAnd/BitOr. + hir::BinOpKind::And | hir::BinOpKind::Or => { + let logical_op = match op.node { + hir::BinOpKind::And => LogicalOp::And, + _ => LogicalOp::Or, + }; + if self.typeck_results.is_method_call(expr) { + // Overloaded: type implements Decisive + BitAnd/BitOr. + let callee = self.method_callee(expr, expr.span, None); + let bitop_fun = self.thir.exprs.push(callee); + ExprKind::OverloadedLogicalOp { + op: logical_op, + lhs: self.mirror_expr(lhs), + rhs: self.mirror_expr(rhs), + bitop_fun, + } + } else { + // Standard bool path. + ExprKind::LogicalOp { + op: logical_op, + lhs: self.mirror_expr(lhs), + rhs: self.mirror_expr(rhs), + } + } + } + _ => { + if self.typeck_results.is_method_call(expr) { + let lhs = self.mirror_expr(lhs); + let rhs = self.mirror_expr(rhs); + self.overloaded_operator(expr, Box::new([lhs, rhs])) + } else { let op = bin_op(op.node); ExprKind::Binary { op, diff --git a/compiler/rustc_mir_build/src/thir/pattern/check_match.rs b/compiler/rustc_mir_build/src/thir/pattern/check_match.rs index baabc1afe3fac..951e17272b55b 100644 --- a/compiler/rustc_mir_build/src/thir/pattern/check_match.rs +++ b/compiler/rustc_mir_build/src/thir/pattern/check_match.rs @@ -350,6 +350,7 @@ impl<'p, 'tcx> MatchVisitor<'p, 'tcx> { | If { .. } | Literal { .. } | LogicalOp { .. } + | OverloadedLogicalOp { .. } | Loop { .. } | LoopMatch { .. } | Match { .. } diff --git a/compiler/rustc_mir_build/src/thir/print.rs b/compiler/rustc_mir_build/src/thir/print.rs index ea34e5f4d97df..976480021025c 100644 --- a/compiler/rustc_mir_build/src/thir/print.rs +++ b/compiler/rustc_mir_build/src/thir/print.rs @@ -288,6 +288,15 @@ impl<'a, 'tcx> ThirPrinter<'a, 'tcx> { self.print_expr(*rhs, depth_lvl + 2); print_indented!(self, "}", depth_lvl); } + OverloadedLogicalOp { op, lhs, rhs, bitop_fun: _ } => { + print_indented!(self, "OverloadedLogicalOp {", depth_lvl); + print_indented!(self, format!("op: {:?}", op), depth_lvl + 1); + print_indented!(self, "lhs:", depth_lvl + 1); + self.print_expr(*lhs, depth_lvl + 2); + print_indented!(self, "rhs:", depth_lvl + 1); + self.print_expr(*rhs, depth_lvl + 2); + print_indented!(self, "}", depth_lvl); + } Unary { op, arg } => { print_indented!(self, "Unary {", depth_lvl); print_indented!(self, format!("op: {:?}", op), depth_lvl + 1); diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index 738c9b975fd00..0f2d8d063d189 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -759,6 +759,8 @@ symbols! { decl_macro, declare_lint_pass, decorated, + decisive, + decisive_trait, default_alloc_error_handler, default_field_values, default_fn, @@ -1102,6 +1104,8 @@ symbols! { irrefutable_let_patterns, is, is_auto, + is_false_decisive, + is_true_decisive, is_val_statically_known, isa_attribute, isize, diff --git a/compiler/rustc_ty_utils/src/consts.rs b/compiler/rustc_ty_utils/src/consts.rs index 49e0bdde37870..c3687b9b3f0c4 100644 --- a/compiler/rustc_ty_utils/src/consts.rs +++ b/compiler/rustc_ty_utils/src/consts.rs @@ -183,7 +183,7 @@ fn recurse_build<'tcx>( ExprKind::Binary { .. } => { error(GenericConstantTooComplexSub::BinaryNotSupported(node.span))? } - ExprKind::LogicalOp { .. } => { + ExprKind::LogicalOp { .. } | ExprKind::OverloadedLogicalOp { .. } => { error(GenericConstantTooComplexSub::LogicalOpNotSupported(node.span))? } ExprKind::Assign { .. } | ExprKind::AssignOp { .. } => { @@ -266,6 +266,7 @@ impl<'a, 'tcx> IsThirPolymorphic<'a, 'tcx> { | thir::ExprKind::Deref { .. } | thir::ExprKind::Binary { .. } | thir::ExprKind::LogicalOp { .. } + | thir::ExprKind::OverloadedLogicalOp { .. } | thir::ExprKind::Unary { .. } | thir::ExprKind::Cast { .. } | thir::ExprKind::Use { .. } diff --git a/library/core/src/ops/decisive.rs b/library/core/src/ops/decisive.rs new file mode 100644 index 0000000000000..d7d552c6f43f6 --- /dev/null +++ b/library/core/src/ops/decisive.rs @@ -0,0 +1,77 @@ +/// Trait for types that support short-circuiting logical operators `&&` and `||`. +/// +/// By implementing `Decisive` along with [`BitAnd`] and/or [`BitOr`], a type +/// gains the ability to be used with the `&&` and `||` operators respectively, +/// with proper short-circuit evaluation. +/// +/// For `&&`: if `is_false(&lhs)` returns `true`, the right-hand side is not +/// evaluated and the left-hand side is returned directly. Otherwise, +/// `BitAnd::bitand(lhs, rhs)` is evaluated. +/// +/// For `||`: if `is_true(&lhs)` returns `true`, the right-hand side is not +/// evaluated and the left-hand side is returned directly. Otherwise, +/// `BitOr::bitor(lhs, rhs)` is evaluated. +/// +/// # Three-valued logic example +/// +/// This enables behavior trees and other three-valued logic systems: +/// +/// ```ignore (illustrative) +/// use std::ops::{BitAnd, BitOr, Decisive}; +/// +/// #[derive(Copy, Clone)] +/// struct Status(i8); +/// +/// const DONE: Status = Status(1); +/// const CONT: Status = Status(0); +/// const FAIL: Status = Status(-1); +/// +/// impl Decisive for Status { +/// fn is_true(&self) -> bool { self.0 != -1 } +/// fn is_false(&self) -> bool { self.0 != 1 } +/// } +/// +/// impl BitAnd for Status { +/// type Output = Status; +/// fn bitand(self, rhs: Status) -> Status { rhs } +/// } +/// +/// impl BitOr for Status { +/// type Output = Status; +/// fn bitor(self, rhs: Status) -> Status { rhs } +/// } +/// +/// // Now you can write: +/// // let result = task_a() && task_b() && task_c(); // sequence +/// // let result = task_a() || task_b() || task_c(); // selector +/// ``` +#[lang = "decisive"] +#[unstable(feature = "decisive_trait", issue = "none")] +pub trait Decisive { + /// Returns `true` if this value should short-circuit the `||` operator. + /// + /// When used with `||`, if this returns `true` for the left-hand side, + /// the right-hand side is not evaluated. + #[unstable(feature = "decisive_trait", issue = "none")] + fn is_true(&self) -> bool; + + /// Returns `true` if this value should short-circuit the `&&` operator. + /// + /// When used with `&&`, if this returns `true` for the left-hand side, + /// the right-hand side is not evaluated. + #[unstable(feature = "decisive_trait", issue = "none")] + fn is_false(&self) -> bool; +} + +#[unstable(feature = "decisive_trait", issue = "none")] +impl Decisive for bool { + #[inline] + fn is_true(&self) -> bool { + *self + } + + #[inline] + fn is_false(&self) -> bool { + !*self + } +} diff --git a/library/core/src/ops/mod.rs b/library/core/src/ops/mod.rs index ab1ad407ee282..f70dab8b2a76c 100644 --- a/library/core/src/ops/mod.rs +++ b/library/core/src/ops/mod.rs @@ -17,10 +17,10 @@ //! should have some resemblance to multiplication (and share expected //! properties like associativity). //! -//! Note that the `&&` and `||` operators are currently not supported for -//! overloading. Due to their short circuiting nature, they require a different -//! design from traits for other operators like [`BitAnd`]. Designs for them are -//! under discussion. +//! The `&&` and `||` operators can be overloaded for custom types by +//! implementing the [`Decisive`] trait along with [`BitAnd`] and/or [`BitOr`]. +//! The `Decisive` trait controls short-circuit evaluation, while `BitAnd`/`BitOr` +//! provide the combining operation when short-circuiting does not apply. //! //! Many of the operators take their operands by value. In non-generic //! contexts involving built-in types, this is usually not a problem. @@ -143,6 +143,7 @@ mod async_function; mod bit; mod control_flow; mod coroutine; +mod decisive; mod deref; mod drop; mod function; @@ -161,6 +162,8 @@ pub use self::arith::{AddAssign, DivAssign, MulAssign, RemAssign, SubAssign}; pub use self::async_function::{AsyncFn, AsyncFnMut, AsyncFnOnce}; #[stable(feature = "rust1", since = "1.0.0")] pub use self::bit::{BitAnd, BitOr, BitXor, Not, Shl, Shr}; +#[unstable(feature = "decisive_trait", issue = "none")] +pub use self::decisive::Decisive; #[stable(feature = "op_assign_traits", since = "1.8.0")] pub use self::bit::{BitAndAssign, BitOrAssign, BitXorAssign, ShlAssign, ShrAssign}; #[stable(feature = "control_flow_enum_type", since = "1.55.0")] diff --git a/src/version b/src/version index 9141007a55821..af2f85503f886 100644 --- a/src/version +++ b/src/version @@ -1 +1 @@ -1.96.0 +1.97.0-decisive diff --git a/tests/ui/traits/decisive-trait.rs b/tests/ui/traits/decisive-trait.rs new file mode 100644 index 0000000000000..3e6b8072c5864 --- /dev/null +++ b/tests/ui/traits/decisive-trait.rs @@ -0,0 +1,138 @@ +//@ run-pass +//! Test that the Decisive trait enables overloading && and || with short-circuiting. + +#![feature(decisive_trait)] + +use std::ops::{BitAnd, BitOr, Decisive}; + +#[derive(Copy, Clone, Debug, PartialEq)] +struct Status(i8); + +const DONE: Status = Status(1); +const CONT: Status = Status(0); +const FAIL: Status = Status(-1); + +impl Decisive for Status { + fn is_true(&self) -> bool { + self.0 != -1 + } + fn is_false(&self) -> bool { + self.0 != 1 + } +} + +impl BitAnd for Status { + type Output = Status; + fn bitand(self, rhs: Status) -> Status { + rhs + } +} + +impl BitOr for Status { + type Output = Status; + fn bitor(self, rhs: Status) -> Status { + rhs + } +} + +static mut EVAL_COUNT: i32 = 0; + +fn done() -> Status { + unsafe { EVAL_COUNT += 1; } + DONE +} + +fn cont() -> Status { + unsafe { EVAL_COUNT += 1; } + CONT +} + +fn fail() -> Status { + unsafe { EVAL_COUNT += 1; } + FAIL +} + +fn reset_count() { + unsafe { EVAL_COUNT = 0; } +} + +fn get_count() -> i32 { + unsafe { EVAL_COUNT } +} + +fn main() { + // Test &&: short-circuits when LHS is_false (i.e., LHS is not complete) + + // DONE && DONE => evaluates both, returns DONE (from bitand) + reset_count(); + let r = done() && done(); + assert_eq!(r, DONE); + assert_eq!(get_count(), 2); + + // DONE && FAIL => evaluates both, returns FAIL (from bitand) + reset_count(); + let r = done() && fail(); + assert_eq!(r, FAIL); + assert_eq!(get_count(), 2); + + // FAIL && DONE => short-circuits, returns FAIL (LHS), RHS not evaluated + reset_count(); + let r = fail() && done(); + assert_eq!(r, FAIL); + assert_eq!(get_count(), 1); // Only LHS evaluated + + // CONT && DONE => short-circuits, returns CONT (LHS), RHS not evaluated + reset_count(); + let r = cont() && done(); + assert_eq!(r, CONT); + assert_eq!(get_count(), 1); // Only LHS evaluated + + // Test ||: short-circuits when LHS is_true (i.e., LHS is not failing) + + // FAIL || DONE => evaluates both, returns DONE (from bitor) + reset_count(); + let r = fail() || done(); + assert_eq!(r, DONE); + assert_eq!(get_count(), 2); + + // DONE || FAIL => short-circuits, returns DONE (LHS), RHS not evaluated + reset_count(); + let r = done() || fail(); + assert_eq!(r, DONE); + assert_eq!(get_count(), 1); // Only LHS evaluated + + // CONT || DONE => short-circuits, returns CONT (LHS), RHS not evaluated + reset_count(); + let r = cont() || done(); + assert_eq!(r, CONT); + assert_eq!(get_count(), 1); // Only LHS evaluated + + // Test chaining: a && b && c + reset_count(); + let r = done() && done() && fail(); + assert_eq!(r, FAIL); + assert_eq!(get_count(), 3); // All evaluated + + reset_count(); + let r = fail() && done() && done(); + assert_eq!(r, FAIL); + assert_eq!(get_count(), 1); // Only first evaluated + + // Test that bool && and || still work + assert_eq!(true && true, true); + assert_eq!(true && false, false); + assert_eq!(false || true, true); + assert_eq!(false || false, false); + + // Test behavior tree pattern: sequence + reset_count(); + let r = done() && done() && done(); + assert_eq!(r, DONE); + assert_eq!(get_count(), 3); + + // Test behavior tree pattern: selector + reset_count(); + let r = fail() || fail() || done(); + assert_eq!(r, DONE); + assert_eq!(get_count(), 3); +}